lang-lib 1.3.0

A lightweight, high-performance localization library for Rust. Loads TOML language files, supports runtime locale switching, configurable paths, and automatic fallback chains.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
# lang-lib - API Reference

> Authoritative reference for the public API of `lang-lib 1.3.0`.
> Mirrors the rustdoc on docs.rs. The surface listed here is **stable**;
> see [Stability]#stability below for the guarantee.

## Contents

- [Crate root]#crate-root
- [`lang_lib::Lang`]#lang_liblang
  - [Path & locale configuration]#path--locale-configuration
  - [Loading & unloading]#loading--unloading
  - [Querying loaded state]#querying-loaded-state
  - [Translation]#translation
  - [Change notifications (`registry`)]#change-notifications-registry
  - [Filesystem watcher (`hot-reload`)]#filesystem-watcher-hot-reload
- [`lang_lib::Translator`]#lang_libtranslator
- [`lang_lib::LangError`]#lang_liblangerror
- [Macros]#macros
- [Request helpers]#request-helpers
- [Change events (`registry`)]#change-events-registry
- [`lang_lib::WatchError` (`hot-reload`)]#lang_libwatcherror-hot-reload
- [Feature flags]#feature-flags
- [MSRV]#msrv
- [Runtime dependencies]#runtime-dependencies
- [Stability]#stability
- [Memory model]#memory-model
- [Performance]#performance
- [Quick example]#quick-example
- [Hot-reload example]#hot-reload-example

## Crate root

| Item                                       | Kind   | Feature       | Notes                                              |
|--------------------------------------------|--------|---------------|----------------------------------------------------|
| `lang_lib::Lang`                           | struct | always        | Process-global translation store (zero-sized facade). |
| `lang_lib::Translator`                     | struct | always        | Request-scoped translation helper. `Copy`.         |
| `lang_lib::LangError`                      | enum   | always        | All errors produced by the crate.                  |
| `lang_lib::t!`                             | macro  | always        | The standard translation call.                     |
| `lang_lib::resolve_accept_language`        | fn     | always        | Parse `Accept-Language` header into one of your supported locales. |
| `lang_lib::resolve_accept_language_owned`  | fn     | always        | Same, but supported list is `&[impl AsRef<str>]`.  |
| `lang_lib::LangChangeEvent`                | struct | `registry`    | Change event emitted on load / reload / unload.    |
| `lang_lib::ChangeKind`                     | enum   | `registry`    | Kind of change: Loaded / Reloaded / Unloaded / FileMissing / ParseFailed. |
| `lang_lib::HandlerId`                      | alias  | `registry`    | Re-export of `registry_io::HandlerId`.             |
| `lang_lib::WatchError`                     | enum   | `hot-reload`  | Errors returned by `Lang::watch`.                  |

## `lang_lib::Lang`

```rust
pub struct Lang;
```

A zero-sized facade over the process-global translation state. All
methods are associated functions; there is no instance to construct.

Concurrent calls to read-side methods (`Lang::translate`,
`Lang::path`, `Lang::locale`, `Lang::loaded`, `Lang::is_loaded`) never
acquire a lock. They take an `ArcSwap` snapshot of the underlying
state and read from it. Write-side methods (`Lang::set_*`,
`Lang::load`, `Lang::load_from`, `Lang::unload`) briefly serialize
against each other via a private mutex but never block readers.

### Path & locale configuration

```rust
impl Lang {
    pub fn set_path(path: impl AsRef<str>);
    pub fn path() -> &'static str;
    pub fn set_locale(locale: impl AsRef<str>);
    pub fn locale() -> &'static str;
    pub fn set_fallbacks(chain: Vec<String>);
}
```

`set_path` and `set_locale` accept any `AsRef<str>` — `&str`,
`String`, `Cow<str>`, and `Path::display()` output all satisfy the
bound. Each call interns the value into the process-wide pool
exactly once; subsequent calls with the same string return the same
`'static` reference.

`set_fallbacks` accepts `Vec<String>` and interns each element. The
chain is checked in order when a key is missing in the requested
locale; duplicates are deduplicated at lookup time, so a chain of
`["en", "en", "en"]` behaves identically to `["en"]`.

**Example:**

```rust,no_run
use lang_lib::Lang;

Lang::set_path("assets/locales");
Lang::set_locale("en");
Lang::set_fallbacks(vec!["en".to_string()]);

assert_eq!(Lang::path(), "assets/locales");
assert_eq!(Lang::locale(), "en");
```

### Loading & unloading

```rust
impl Lang {
    pub fn load(locale: impl AsRef<str>) -> Result<(), LangError>;
    pub fn load_from(locale: impl AsRef<str>, path: &str) -> Result<(), LangError>;
    pub fn unload(locale: &str);
}
```

`load` reads `{path}/{locale}.toml` (where `{path}` is whatever was
set via `Lang::set_path`). `load_from` ignores the configured path
and reads from the supplied directory instead — useful when one
locale lives in a different tree from the rest. `unload` removes a
locale from the lookup table.

Locale identifiers must be single file stems such as `en`, `en-US`,
or `pt_BR`. Path separators and relative path components are
rejected before any file access.

Loading the same locale a second time replaces its translations
with a fresh load from disk. Under the `registry` feature this fires
`ChangeKind::Reloaded`; the first load fires `ChangeKind::Loaded`.

**Example:**

```rust,no_run
use lang_lib::Lang;

Lang::set_path("locales");
Lang::load("en")?;
Lang::load("es")?;

// One-off path for a single locale.
Lang::load_from("ja", "translations/ja-pack")?;

Lang::unload("ja");
# Ok::<(), lang_lib::LangError>(())
```

### Querying loaded state

```rust
impl Lang {
    pub fn is_loaded(locale: &str) -> bool;
    pub fn loaded() -> Vec<&'static str>;
}
```

`loaded` returns a sorted list of all currently loaded locale
identifiers. Sorting keeps diagnostics and tests deterministic.

**Example:**

```rust,no_run
use lang_lib::Lang;

Lang::load_from("es", "tests/fixtures/locales")?;
Lang::load_from("en", "tests/fixtures/locales")?;

assert!(Lang::is_loaded("en"));
assert_eq!(Lang::loaded(), vec!["en", "es"]);
# Ok::<(), lang_lib::LangError>(())
```

### Translation

```rust
impl Lang {
    pub fn translate<'a>(
        key: &'a str,
        locale: Option<&'a str>,
        fallback: Option<&'a str>,
    ) -> Cow<'a, str>;

    pub fn translator(locale: impl AsRef<str>) -> Translator;

    // hot-reload feature only
    pub fn translate_arc(
        key: &str,
        locale: Option<&str>,
        fallback: Option<&str>,
    ) -> Arc<str>;
}
```

`translate` is the underlying lookup function called by the `t!`
macro.

Lookup order:

1. Requested locale (or active locale when `locale` is `None`)
2. Each locale in the fallback chain, in order
3. The `fallback` argument if `Some`
4. The `key` itself (never returns an empty string)

Return type contract:

| Build         | Hit path                              | Fallback / miss path                    | Allocates per call?           |
|---------------|---------------------------------------|------------------------------------------|-------------------------------|
| Default       | `Cow::Borrowed(&'static str)`         | `Cow::Borrowed(&'a str)` of input        | **No.**                       |
| `hot-reload`  | `Cow::Owned(String)` from `Arc<str>`  | `Cow::Borrowed(&'a str)` of input        | Hit-path yes; miss-path no.   |

`translate_arc` (available only when `hot-reload` is enabled) avoids
the per-call `String` allocation by returning the underlying
`Arc<str>` directly — the hit path is a refcount bump (zero alloc).
Trade-off: many threads cloning the same `Arc<str>` simultaneously
contend on the refcount cache line. Use when you measured the
allocation cost as a hot spot and same-key contention is low.

**Example:**

```rust,no_run
use lang_lib::Lang;

Lang::load_from("en", "tests/fixtures/locales")?;
let text = Lang::translate("welcome", Some("en"), Some("Welcome"));
assert_eq!(text, "Welcome");

// Display / format / equality all work with Cow:
println!("{text}");
assert!(text.starts_with("Wel"));
# Ok::<(), lang_lib::LangError>(())
```

`translator` is a convenience for `Translator::new(locale)`:

```rust,no_run
use lang_lib::Lang;

let translator = Lang::translator("es");
assert_eq!(translator.locale(), "es");
```

### Change notifications (`registry`)

```rust
impl Lang {
    pub fn on_change<F>(handler: F) -> HandlerId
    where
        F: Fn(&LangChangeEvent) + Send + Sync + 'static;

    pub fn off_change(id: HandlerId) -> bool;
}
```

Available when the `registry` feature is enabled. Installs a handler
that fires whenever the translation store changes (load, reload,
unload, or — under `hot-reload` — a file-watch failure).

Handlers fire inline on the thread that produced the change. The
dispatch is lock-free and panic-isolating: a panic in one handler
does not stop sibling handlers from running and does not propagate
to the caller.

`off_change` returns `true` if a handler with the given id was
present and removed, `false` if it was already gone.

**Example:**

```rust,ignore
use lang_lib::{ChangeKind, Lang};

let id = Lang::on_change(|event| {
    match event.kind {
        ChangeKind::Loaded   => println!("loaded {}", event.locale),
        ChangeKind::Reloaded => println!("reloaded {}", event.locale),
        ChangeKind::Unloaded => println!("unloaded {}", event.locale),
        _ => {}
    }
});

Lang::load_from("en", "tests/fixtures/locales").unwrap();
let _ = Lang::off_change(id);
```

### Filesystem watcher (`hot-reload`)

```rust
impl Lang {
    pub fn watch(dir: impl AsRef<Path>) -> Result<(), WatchError>;
    pub fn unwatch();
}
```

Available when the `hot-reload` feature is enabled (which also
enables `registry`). Starts a background thread that subscribes to
filesystem events on `dir`, debounces rapid bursts (~150 ms per
file), and atomically reloads each affected `<locale>.toml`.
Reloads fire `ChangeKind::Reloaded` events through the registry.

Only one watcher may be active per process. Call `unwatch` before
starting a new one. `unwatch` is idempotent.

**Example:**

```rust,ignore
use lang_lib::Lang;

Lang::set_path("locales");
Lang::load("en").unwrap();

Lang::watch("locales").unwrap();
// application runs; edits to locales/*.toml are picked up automatically
Lang::unwatch();
```

## `lang_lib::Translator`

```rust
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Translator { /* interned locale */ }

impl Translator {
    pub fn new(locale: impl AsRef<str>) -> Self;
    pub fn locale(&self) -> &'static str;

    pub fn translate<'a>(&self, key: &'a str) -> Cow<'a, str>;
    pub fn translate_with_fallback<'a>(&self, key: &'a str, fallback: &'a str) -> Cow<'a, str>;

    // hot-reload feature only
    pub fn translate_arc(&self, key: &str) -> Arc<str>;
    pub fn translate_arc_with_fallback(&self, key: &str, fallback: &str) -> Arc<str>;
}
```

A request-scoped translation helper that binds a locale once and
forwards lookups to the global `Lang` store. The locale is held as a
`&'static str` (interned), so `Translator` is `Copy` — passing it
across function boundaries is a pointer copy.

**When to use:** request-driven services where locale is part of the
input rather than process-global state. Using `Translator` instead
of mutating `Lang::set_locale` per request keeps requests independent
and avoids cross-request coupling.

**Example:**

```rust,no_run
use lang_lib::{Lang, Translator};

Lang::load_from("en", "tests/fixtures/locales").unwrap();
Lang::load_from("es", "tests/fixtures/locales").unwrap();

fn render(locale: &str) -> String {
    let translator = Translator::new(locale);
    format!(
        "{} / {}",
        translator.translate("welcome"),
        translator.translate_with_fallback("missing_key", "Default"),
    )
}

println!("{}", render("en"));
println!("{}", render("es"));
```

## `lang_lib::LangError`

```rust
#[derive(Debug)]
pub enum LangError {
    Io           { locale: String, cause: String },
    Parse        { locale: String, cause: String },
    NotLoaded    { locale: String },
    InvalidLocale { locale: String },
}

impl std::fmt::Display for LangError { /* ... */ }
impl std::error::Error    for LangError { /* ... */ }
```

| Variant         | When                                                                                  |
|-----------------|---------------------------------------------------------------------------------------|
| `Io`            | The locale file could not be read (missing, permission denied, etc.).                 |
| `Parse`         | The file was read but is not valid TOML, or contained no string-typed top-level keys. |
| `NotLoaded`     | A locale was looked up that had never been loaded (reserved; future use).             |
| `InvalidLocale` | The locale identifier was rejected before file access (path separators, traversal).   |

**Example:**

```rust,no_run
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}");
    }
}
```

## Macros

### `t!`

```rust
t!("key");                                      // active locale
t!("key", "es");                                // specific locale
t!("key", fallback: "Default");                 // inline fallback
t!("key", "es", fallback: "Default");           // both
```

Expands to a call to `Lang::translate`. Returns whatever `translate`
returns — a `Cow<'_, str>` whose lifetime ties to the input strings.

**Example:**

```rust,no_run
use lang_lib::{Lang, t};

Lang::set_path("locales");
Lang::load("en").unwrap();
Lang::load("es").unwrap();
Lang::set_locale("en");

assert_eq!(t!("greeting"), "Hello");
assert_eq!(t!("greeting", "es"), "Hola");
assert_eq!(t!("missing", fallback: "Default"), "Default");
assert_eq!(t!("missing", "es", fallback: "Hola"), "Hola");
```

## Request helpers

### `resolve_accept_language`

```rust
pub fn resolve_accept_language<'a>(
    header: &str,
    supported_locales: &[&'a str],
    default_locale: &'a str,
) -> &'a str;
```

Parses an HTTP `Accept-Language` header and returns the best match
from `supported_locales`, or `default_locale` if nothing matches.
Quality values (`q=…`) and primary-language matches (`es-ES → es`)
are honored.

**Example:**

```rust
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");
```

### `resolve_accept_language_owned`

```rust
pub fn resolve_accept_language_owned<S>(
    header: &str,
    supported_locales: &[S],
    default_locale: &str,
) -> String
where
    S: AsRef<str>;
```

The owned variant for when your supported locale list is built at
runtime (`Vec<String>`, `&[Cow<str>]`, etc.) rather than a static
array.

**Example:**

```rust
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");
```

## Change events (`registry`)

```rust
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct LangChangeEvent {
    pub locale: &'static str,
    pub kind: ChangeKind,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum ChangeKind {
    Loaded,
    Reloaded,
    Unloaded,
    FileMissing,
    ParseFailed,
}

pub type HandlerId = registry_io::HandlerId;
```

| Variant       | Emitted when                                                                          |
|---------------|---------------------------------------------------------------------------------------|
| `Loaded`      | `Lang::load` / `Lang::load_from` added a previously unknown locale.                   |
| `Reloaded`    | `Lang::load` / `Lang::load_from` replaced an already-loaded locale's contents.        |
| `Unloaded`    | `Lang::unload` removed a previously loaded locale.                                    |
| `FileMissing` | (`hot-reload`) The watcher saw a change but the file disappeared before reload could read it. |
| `ParseFailed` | (`hot-reload`) The watcher reloaded a file but it no longer parses as valid TOML.     |

`HandlerId` is a re-export of `registry_io::HandlerId` so callers do
not need a direct dependency on `registry-io`.

## `lang_lib::WatchError` (`hot-reload`)

```rust
#[derive(Debug)]
pub enum WatchError {
    Io(notify::Error),
    AlreadyRunning,
}

impl std::fmt::Display for WatchError { /* ... */ }
impl std::error::Error    for WatchError { /* ... */ }
impl From<notify::Error>  for WatchError { /* ... */ }
```

| Variant          | When                                                                                  |
|------------------|---------------------------------------------------------------------------------------|
| `Io`             | The underlying `notify` watcher failed to subscribe to filesystem events.             |
| `AlreadyRunning` | `Lang::watch` was called while another watcher was already active. Call `Lang::unwatch` first. |

## Feature flags

| Flag                  | Default | Effect                                                                                          |
|-----------------------|---------|-------------------------------------------------------------------------------------------------|
| `registry`            | off     | Adds `Lang::on_change` / `Lang::off_change` and the `LangChangeEvent` / `ChangeKind` / `HandlerId` re-exports. Pulls in `registry-io = "1"`. |
| `hot-reload`          | off     | Implies `registry`. Adds `Lang::watch` / `Lang::unwatch` and `WatchError`. **Changes the value-storage strategy from interner (`&'static str`) to `Arc<str>`** so reloaded files do not leak. Pulls in `notify = "6"`. |
| `web-example-axum`    | off     | Compiles the `axum_server` example. Pulls in `axum` and `tokio`.                                |
| `web-example-actix`   | off     | Compiles the `actix_server` example. Pulls in `actix-web`.                                      |

## MSRV

`1.85` (required by edition 2024). Locked from `1.0.1` onward; a
bump requires a minor version increment and a CHANGELOG entry
under `### Changed`.

## Runtime dependencies

| Dependency     | Always pulled?      | Why                                                              |
|----------------|---------------------|------------------------------------------------------------------|
| `arc-swap`     | yes                 | Lock-free `LangState` snapshots on the translate read path.      |
| `rustc-hash`   | yes                 | `FxHashMap` — faster hashing for short translation keys.         |
| `toml`         | yes                 | Locale file parser.                                              |
| `registry-io`  | with `registry`     | Sub-microsecond change-event dispatch.                           |
| `notify`       | with `hot-reload`   | Cross-platform filesystem watcher (inotify / FSEvents / RDCW).   |

## Stability

From `1.0.0` forward, the public surface above is stable.

- Patch releases (`1.x.y`) ship bug fixes, doc improvements, and
  internal optimizations that do not change observable behavior.
- Minor releases (`1.x.0`) add to the surface but never remove or
  rename. New items may be feature-gated.
- A `2.0` would only be cut for a deliberate breaking change to the
  surface; there is no such release planned.

One historical breakage to be aware of when migrating from
pre-`1.1.0`: `Lang::translate` returned `String` in `1.0.x` and
returns `Cow<'a, str>` from `1.1.0` onward. `format!`, `println!`,
`assert_eq!(_, "literal")`, and `&str`-deref use continue to work.
Code that explicitly typed `String` for the return needs
`.into_owned()`.

## Memory model

`lang-lib` interns short strings (locale identifiers, translation
keys, the configured path, fallback locale names) into a global
append-only pool. Once interned, a string lives for the program's
lifetime. The interner is bounded by the number of *unique* strings
the application ever sees — typically a few hundred KB in real
multi-locale apps.

**Translation values** use one of two storage strategies depending
on the active feature set:

- **Default builds** intern values too. The translate hit path
  returns `Cow::Borrowed(&'static str)` — pure pointer copy, zero
  allocation. Values are never reclaimed; this is correct because
  the default build cannot reload locale data after startup, so the
  interner cannot grow over time.

- **`hot-reload` builds** store values as `Arc<str>` instead. The
  translate hit path returns `Cow::Owned(String)` — one allocation
  per call, but reloading a locale drops the old `Arc<str>`
  instances cleanly. No leak under reload churn.

`Lang::translate_arc` (under `hot-reload`) returns the underlying
`Arc<str>` directly — zero allocation per call at the cost of
refcount cache-line contention when many threads hit the same key
concurrently.

## Performance

The Criterion suite under `benches/performance.rs` measures:

- `resolve_accept_language``Accept-Language` header parsing
- `translate_lookup` — single-thread hit
- `translate_fallback_chain_miss` — hit via the fallback chain
- `translate_complete_miss_inline_fallback` — miss returning the inline fallback
- `translate_complete_miss_key_return` — miss returning the key itself
- `translate_hit_concurrent` — single-key hit scaled across 1, 4, 16, 64 threads

Run with:

```text
cargo bench --bench performance
```

Numbers vary by hardware; the CI workflow uploads Criterion HTML
reports as artifacts on every `main` push. See [`BENCHMARKS.md`](../BENCHMARKS.md) for methodology.

## Quick example

```rust,no_run
use lang_lib::{t, Lang};

// Configure once at startup.
Lang::set_path("locales");
Lang::load("en")?;
Lang::load("es")?;
Lang::set_fallbacks(vec!["en".to_string()]);
Lang::set_locale("en");

// Translate anywhere.
let msg     = t!("bad_password");
let msg_es  = t!("bad_password", "es");
let msg_fb  = t!("missing_key", fallback: "Default message");
let msg_lfb = t!("missing_key", "es", fallback: "Default es");

println!("{msg} / {msg_es} / {msg_fb} / {msg_lfb}");
# Ok::<(), lang_lib::LangError>(())
```

## Hot-reload example

```rust,ignore
use lang_lib::{ChangeKind, Lang, t};

Lang::set_path("locales");
Lang::load("en").unwrap();
Lang::set_locale("en");

let _handler_id = Lang::on_change(|event| {
    if event.kind == ChangeKind::Reloaded {
        println!("{} reloaded from disk", event.locale);
    }
});

Lang::watch("locales").unwrap();

// application runs; t!("greeting") returns the latest file contents
println!("{}", t!("greeting"));

Lang::unwatch();
```

---

<sub>lang-lib API reference - Copyright (c) 2026 James Gober. Apache-2.0 OR MIT.</sub>