@transglot/runtime
v0.1.0
Published
Zero-dependency runtime i18n client for transglot: loads PUBLISHED translation bundles from the OTA/CDN endpoint with ETag/304 caching, interpolation and Intl.PluralRules plurals.
Downloads
185
Maintainers
Readme
@transglot/runtime
Zero-dependency, framework-agnostic runtime i18n client for transglot. It
loads a project's published translation bundles from the OTA/CDN endpoint,
caches them in memory with ETag/304 revalidation, interpolates params, and
selects plural forms with Intl.PluralRules, so an app fetches strings at
runtime and a published typo fix ships without a rebuild.
- Zero runtime dependencies. ESM, Node >= 20 and the browser.
- The real wire, pinned. Targets
GET /v1/cdn/{project}/{locale}(and the immutable/v{n}), theX-Transglot-Cdn-Keyauth, and the JSON-family bundle bodies, read off the server, not guessed. - Offline cold start + OTA. An optional persistent
storagelayer serves last-known-good strings before the network is reachable (React Native / offline), then revalidates;refreshIntervalMsand anonForegroundhook make the active locale update over the air.
Install
npm install @transglot/runtimeQuickstart
import { createClient } from '@transglot/runtime';
const i18n = createClient({
baseUrl: 'https://app.example.com', // your transglot origin (or a CDN in front of it)
project: 42, // numeric project id
cdnKey: 'taicdn_…', // read-only CDN key from the deploy hub
locale: 'en', // initial locale
});
// Load (or ETag-revalidate) the active locale's bundle, then translate.
await i18n.loadLocale('en');
i18n.t('home.title'); // "Welcome"
i18n.t('cart.summary', { name: 'Ada' }); // interpolates {name}
i18n.t('cart.items', { count: 3 }); // selects the plural form for the locale
// Switch locales, and subscribers re-render.
const stop = i18n.onChange((locale) => render(locale));
await i18n.setLocale('fr');API
createClient(options) → RuntimeClient
| option | required | meaning |
| --- | --- | --- |
| baseUrl | yes | origin of the app / CDN, e.g. https://app.example.com |
| project | yes | numeric project id (the CDN URL segment) |
| cdnKey | yes | read-only taicdn_… key |
| locale | yes | initial locale |
| version | no | pin an immutable …/v{n} version instead of "latest" |
| authIn | no | 'header' (default, X-Transglot-Cdn-Key) or 'query' (?key=, skips a browser CORS preflight) |
| fetchImpl | no | injected fetch (tests / non-browser) |
| dev | no | dev-mode warnings for missing keys (default on unless NODE_ENV=production) |
| storage | no | a persistent { get, set } store for last-known-good bundles (cold start). See Persistent cache |
| refreshIntervalMs | no | background-revalidate the active locale every N ms (OTA). See Background OTA refresh |
| onForeground | no | wire a "became visible / foregrounded" signal to a background revalidate |
| scheduler | no | injected { setInterval, clearInterval } (tests / non-standard runtimes) |
RuntimeClient:
t(key, params?): interpolate{name}params; select plurals viaIntl.PluralRuleswhenparams.countis a number. A missing key returns the key (plus a dev warning) and never throws.loadLocale(locale): fetch or ETag-revalidate a locale's bundle into the cache. A304 Not Modifiedkeeps the cached bundle (no re-download).setLocale(locale): load then switch the active locale; notifiesonChange.preload(locales): warm several locales without switching.hydrate(locale?): populate the cache for a locale (default: active) fromstoragewith no network call, so a cold start serves last-known-good immediately. Resolvestruewhen a bundle is available. A no-op returningfalsewhen nostoragewas configured; never throws on a corrupt value.refresh(): revalidate the active locale (the manual OTA primitive; an alias forloadLocale(getLocale())).stop(): stop therefreshIntervalMstimer and detach theonForegroundhook. Idempotent; call it on teardown/unmount.getLocale(),hasLocale(locale),onChange(listener) -> unsubscribe.
Network/HTTP failures surface as a typed RuntimeError carrying a stable slug
(cdn-key-invalid, not-published, network-error, ...) and a hint.
Persistent cache (offline cold start)
By default the client caches bundles in memory only, so a fresh process (a
React Native cold start, a new browser tab) begins empty and must hit the network
before it can translate. Pass a storage adapter and the client persists each
locale's last-known-good bundle ({ etag, version, entries }, keyed per project
and locale), so a cold start serves strings before the network is reachable,
then revalidates:
const i18n = createClient({
baseUrl: 'https://app.example.com',
project: 42,
cdnKey: 'taicdn_…',
locale: 'en',
storage: {
get: (key) => localStorage.getItem(key),
set: (key, value) => localStorage.setItem(key, value),
},
});
// Cold start: serve last-known-good with NO network call, then revalidate.
if (await i18n.hydrate()) {
render(); // already translated, offline-safe
}
await i18n.loadLocale('en'); // conditional GET: a 304 keeps the cached bundle,
// a new publish replaces it and re-persistsThe in-memory Map stays the hot layer; storage is the cold layer, hydrated into
it on a miss. get/set may be synchronous (localStorage) or return promises
(React Native AsyncStorage, IndexedDB); the client awaits either. A read or
write that throws, or a corrupt persisted value, is ignored (persistence is
best-effort and never breaks t).
Background OTA refresh
refreshIntervalMs background-revalidates the active locale on a timer, and
onForeground lets the host revalidate whenever the app becomes visible again, so
a published change reaches a running app over the air:
const i18n = createClient({
baseUrl: 'https://app.example.com',
project: 42,
cdnKey: 'taicdn_…',
locale: 'en',
refreshIntervalMs: 5 * 60_000, // revalidate every 5 minutes
onForeground: (revalidate) => {
const on = () => { if (!document.hidden) revalidate(); };
document.addEventListener('visibilitychange', on);
return () => document.removeEventListener('visibilitychange', on);
},
});
i18n.onChange(() => render()); // a landed publish re-renders subscribers
// On teardown:
i18n.stop();Both triggers revalidate in the background and never reject; a landed publish
notifies onChange subscribers exactly like a setLocale.
React Native usage
React Native has no localStorage, unpredictable connectivity, and a foreground
lifecycle via AppState. Wire AsyncStorage for offline cold start and
AppState for OTA:
import { createClient } from '@transglot/runtime';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppState } from 'react-native';
export const i18n = createClient({
baseUrl: 'https://app.example.com',
project: 42,
cdnKey: 'taicdn_…',
locale: 'en',
// AsyncStorage's getItem/setItem already match the { get, set } shape.
storage: { get: AsyncStorage.getItem, set: AsyncStorage.setItem },
refreshIntervalMs: 15 * 60_000,
onForeground: (revalidate) => {
const sub = AppState.addEventListener('change', (state) => {
if (state === 'active') revalidate();
});
return () => sub.remove();
},
});
// At app startup: paint last-known-good instantly (offline-safe), then revalidate.
await i18n.hydrate();
i18n.loadLocale('en').catch(() => {}); // fine to fail offline; the cache still servesSupported delivery formats
The runtime SDK reads the JSON-family delivery formats: json_flat
(default), json_nested, laravel_json and flutter_arb. All normalize to the
same dotted-key space (home.title), so the key you call t() with is stable
across formats. Plurals are carried by flutter_arb's ICU messages; the flat
JSON formats drop plurals at publish time, so they contain only simple strings.
