lang-lib 1.2.0

A lightweight, high-performance localization library for Rust. Loads TOML language files, supports runtime locale switching, configurable paths, and automatic fallback chains.
# Release v1.2.0 — Hot Reload + Change-Event Registry
**Date:** 2026-05-20
**Compare:** `v1.1.0...v1.2.0`

## Summary

Two new opt-in features, both off by default — your existing `1.1.0`
dependency line continues to work unchanged.

- **`registry`** — wires `lang-lib` to `registry-io 1.0`. Install
  handlers via `Lang::on_change` that fire whenever a locale is loaded,
  reloaded, or unloaded. Sub-microsecond dispatch per handler. A panic
  in one handler does not affect siblings.
- **`hot-reload`** — implies `registry`. Adds `Lang::watch` /
  `Lang::unwatch`, which subscribe to filesystem events on the configured
  locales directory and atomically reload changed `<locale>.toml` files.
  Cross-platform via the `notify = "6"` crate (inotify / FSEvents /
  ReadDirectoryChangesW). Per-file events are debounced (~150 ms) so
  atomic-rename writes coalesce into a single reload.

## Added

### Feature flags

- `registry = ["dep:registry-io"]`
- `hot-reload = ["dep:notify", "registry"]` (implies `registry`)

### New public types

- `lang_lib::LangChangeEvent { locale: &'static str, kind: ChangeKind }`
  (`registry` feature)
- `lang_lib::ChangeKind` — variants: `Loaded`, `Reloaded`, `Unloaded`,
  `FileMissing`, `ParseFailed`
- `lang_lib::HandlerId` — re-export of `registry_io::HandlerId` so
  callers don't need a direct dep on `registry-io`
- `lang_lib::WatchError` (`hot-reload` feature) — variants: `Io`,
  `AlreadyRunning`

### New public functions on `Lang`

- `Lang::on_change<F>(handler: F) -> HandlerId` (`registry` feature)
- `Lang::off_change(id: HandlerId) -> bool` (`registry` feature)
- `Lang::watch(dir: impl AsRef<Path>) -> Result<(), WatchError>`
  (`hot-reload` feature)
- `Lang::unwatch()` (`hot-reload` feature)

### Tests & examples

- `tests/registry.rs` — five cases covering Loaded / Reloaded /
  Unloaded emission, no-op unload of an absent locale, and `off_change`
  stop semantics.
- `tests/watch.rs` — two smoke tests covering modify→reload round-trip
  and `unwatch` clean-up. Gated behind `--features hot-reload`.
- `examples/hot_reload.rs` — runnable demo of the watcher feeding live
  changes into `t!`.

### CI

- `actions/upload-artifact@v4``@v7` (Node 20 → Node 24 actions
  deprecation fix).
- Quality matrix exercises `cargo test --no-default-features`,
  `cargo test --features registry`, and `cargo check --example
  hot_reload --features hot-reload`.

## Changed

- README install snippet bumped to `1.2.0`; added an optional-features
  install snippet and feature bullets.
- CHANGELOG `[1.2.0]` section.

## Removed

- Nothing.

## Fixed

- Nothing functional from `1.1.0`. The only fix is the CI Node 20
  deprecation warning on the benchmarks workflow.

## Compatibility

No code changes are required when upgrading from `1.1.0`. The default
feature set is unchanged: the `registry` and `hot-reload` modules and
APIs are only compiled when the corresponding feature is enabled.

## Migration / Adoption

### Subscribing to change events programmatically

```toml
[dependencies]
lang-lib = { version = "1.2.0", features = ["registry"] }
```

```rust
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("en")?;
let _ = Lang::off_change(id);
```

### Watching the filesystem

```toml
[dependencies]
lang-lib = { version = "1.2.0", features = ["hot-reload"] }
```

```rust
use lang_lib::Lang;

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

// ... application runs; edits to locales/*.toml are picked up automatically ...

Lang::unwatch();
```

## Memory Note

The interner introduced in `1.1.0` is still append-only. Hot reload
inside this release does **not** add new pressure on the interner
because the value strings parsed from new file contents are deduped
against any identical strings already interned. Translation files that
churn through completely fresh values on every reload would grow the
interner over time; in practice locale files mostly tweak existing
strings rather than replace them wholesale.

A future release may add a generational interner if real-world hot
reload pressure exposes the growth as a problem.

## Next

Open backlog items (post-`1.2.0`):

- **Bench numbers committed.** Capture criterion + dhat baseline numbers
  on representative hardware and commit them under `benches/baseline/`.
- **String interpolation / placeholders.** `t!("welcome", name: "John")`
  with run-time template substitution.
- **Multiple locale directories.** Priority-ordered search path.

---

**Full Changelog:** https://github.com/jamesgober/lang-lib/compare/v1.1.0...v1.2.0