Why we built Stift
Every JSON-based i18n stack eventually meets the same wall: the translation files are data, so the compiler can't help you. Keys are strings, params are any, and the failure mode for a typo is a raw key flashing on a user's screen in production.
We wanted the opposite: translations as code artifacts — validated at build time, compiled to binary packs, and typed so tightly that a mistyped key is a red squiggle before you save. That single decision cascades into everything else this toolkit does differently:
- Payload:
locales/**.jsoncompiles per locale×namespace into.datpacks (msgpack/cbor + brotli), hashed into acatalog.json. The browser never parses a translation JSON. - Memory: the namespace cache is bounded (
maxCachedNamespaces+ LRU) by construction — loading more locales can't grow memory without limit. - Invalidation: one catalog ping diffs content hashes for every installed locale. No version numbers to bump by hand.
- Offline: packs persist through an OPFS → IndexedDB → Cache API chain. No service worker required.
Just as important is what we didn't build. There is no runtime key discovery, no string-interpolation DSL beyond ICU, no hosted translation-management platform. The default locale file is the schema — nothing more, nothing less.
The full comparison table lives in the docs; the rest of this blog goes deeper into each design decision.