All posts

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/**.json compiles per locale×namespace into .dat packs (msgpack/cbor + brotli), hashed into a catalog.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.