# Release v1.1.0 — Lock-Free, Zero-Allocation Translate Path
**Date:** 2026-05-20
**Compare:** `v1.0.1...v1.1.0`
## Summary
The performance release. Both `1.0.x` bottlenecks are eliminated.
- **`RwLock` contention on every translate call** → replaced by
`ArcSwap<LangState>`. Concurrent readers no longer take a lock and no
longer contend across CPU cores.
- **`String` allocation on every translate call** → replaced by
`Cow<'a, str>` returns backed by an internal interner. Every lookup
outcome — hit, fallback-chain hit, inline-fallback hit, complete miss —
borrows rather than allocates.
The only API change is the return type of `Lang::translate` and the two
`Translator::translate*` methods. `format!`, `println!`,
`assert_eq!(_, "literal")`, equality against `&str`, and any `&str`-deref
use continue to work unchanged.
## Added
- `arc-swap = "1.7"` and `rustc-hash = "2"` dependencies.
- Internal append-only string interner (private module) backing the
zero-allocation hit path.
- `tests/concurrency.rs` — 64-thread translate storm, concurrent reload
during reader churn, unload-during-reads churn.
- `bench_translate_hit_concurrent` benchmark sweeping 1, 4, 16, and 64
threads.
- This release note (`docs/release-notes/v1.1.0.md`).
## Changed
- **Read path is lock-free.** `RwLock<LangState>` replaced with
`ArcSwap<LangState>`. Writes briefly serialize against each other via a
small private mutex but never block readers.
- **Hot path is zero-allocation.** Return types changed:
- `Lang::translate(...) -> Cow<'a, str>` (was `String`)
- `Translator::translate(&self, key) -> Cow<'a, str>` (was `String`)
- `Translator::translate_with_fallback(...) -> Cow<'a, str>` (was `String`)
- **`Translator` is now `Copy`.** Locale is held as `&'static str` instead
of `String`.
- **`Lang::path` returns `&'static str`** (was `String`).
- **`Lang::locale` returns `&'static str`** (was `String`).
- **`Lang::loaded` returns `Vec<&'static str>`** (was `Vec<String>`).
- **`Lang::set_path` and `Lang::set_locale` take `impl AsRef<str>`** (were
`impl Into<String>`). Existing callers continue to compile — `&str`,
`String`, and `Cow<str>` all satisfy `AsRef<str>`.
- **Translation lookup uses `FxHashMap`** for both the locale registry
and per-locale key tables.
## Removed
- Internal `RwLock<LangState>`.
- Per-call `String` allocation on hit paths.
## Fixed
- Concurrent translate calls no longer serialize against each other.
## Performance
| Hit path lookup | Acquires `RwLock`, returns `String` (one alloc) | Lock-free `ArcSwap` snapshot, returns `Cow::Borrowed` (zero alloc) |
| Fallback-chain hit | One `String` alloc | Zero alloc |
| Inline-fallback miss | One `String` alloc | Zero alloc — borrows the caller's fallback |
| Complete miss | One `String` alloc | Zero alloc — borrows the caller's key |
| Concurrent reads | Serialize on `RwLock` read guard atomics | Scale cleanly — no shared mutable state on the read path |
The included Criterion suite (`cargo bench --bench performance`) exercises
single-thread hit, fallback-chain miss, inline-fallback miss, key-return
miss, and concurrent hit at 1, 4, 16, and 64 threads. Run on your target
hardware to capture real numbers.
## Compatibility
### Source-compatible patterns (no change needed)
```rust
format!("{}", t!("greeting")) // Display works
println!("{}", Lang::translate(...)) // Display works
assert_eq!(t!("greeting"), "Hello") // Cow == &str works
if t!("greeting") == "Hello" { ... } // Cow == &str works
let s = t!("greeting"); // s: Cow<'_, str>, derefs to &str
s.len(); s.starts_with("H"); // &str API via Deref
```
### Patterns that need a small fix
```rust
// 1.0.1: let s: String = t!("greeting");
// 1.1.0:
let s: String = t!("greeting").into_owned();
// 1.0.1: struct R { title: String }; let r = R { title: t!("title") };
// 1.1.0:
let r = R { title: t!("title").into_owned() };
// or change R to `title: Cow<'static, str>` — the borrowed variant is
// only valid for `&'static str` translations, so `.into_owned()` at the
// boundary is usually clearer.
```
### Less common, may need attention
```rust
// 1.0.1: let locales: Vec<String> = Lang::loaded();
// 1.1.0:
let locales: Vec<&'static str> = Lang::loaded();
// or, to keep Vec<String>:
let locales: Vec<String> = Lang::loaded().iter().map(ToString::to_string).collect();
// 1.0.1: let p: String = Lang::path();
// 1.1.0:
let p: &'static str = Lang::path();
// or `Lang::path().to_string()` if you really need owned.
```
## Memory Note
Translation values (and keys, locale identifiers, the configured path) are
interned at load time into a process-wide pool that hands out `&'static
str` references. **Interned strings live for the program's lifetime** —
`Lang::unload` removes the locale from the lookup table but does not
reclaim the interned bytes.
In practice this is bounded by the count of *unique* strings the
application ever loads — typically a few hundred KB even for fully
populated multi-locale apps. The `1.2.0` hot-reload milestone will
revisit this so long-running reloaders do not grow the interner without
bound.
## Migration from 1.0.1
```toml
# Cargo.toml — bump the version
[dependencies]
lang-lib = "1.1.0"
```
In most codebases no code changes are needed. If you have callers that
explicitly typed `String` for translation output, see the patterns above.
## Next
`1.2.0` — Hot reload via the `notify` crate, with event delivery wired to
`registry-io 1.0`. File changes trigger per-locale atomic reload; the
interner is revisited so reloaders do not accumulate unbounded memory.
Targeted at 4–5 days of focused work after `registry-io 1.0` ships.
---
**Full Changelog:** https://github.com/jamesgober/lang-lib/compare/v1.0.1...v1.1.0