SDK: core
@lionrapid/core is the framework-agnostic heart of the SDK. It
gives you a client that resolves translation keys through a chain of
handlers — in-memory cache first, optional persistent storage second,
network last — returns synchronously from cache, loads misses in the
background, and reports missing keys back to the platform in batches.
Everything the React bindings and the
WordPress plugin do is built on this
package.
Building a client
Section titled “Building a client”LionRapidBuilder.init() takes the base configuration, use() registers
repositories and plugins, and build() assembles the handler chain and
runs eager namespace preloads:
import { LionRapidBuilder, MemoryRepository, NetworkRepository,} from '@lionrapid/core';import { coreFormattersPlugin } from '@lionrapid/formatters';
const lionRapid = await LionRapidBuilder.init({ defaultLocale: 'en', fallbackLocale: 'en', namespace: 'app',}) .use(new MemoryRepository({ enabled: true, options: { maxSize: 1000 } })) .use( new NetworkRepository({ enabled: true, options: { baseUrl: 'https://your-lionrapid-host', apiKey: process.env.LIONRAPID_INTEGRATION_KEY, }, }) ) .use(coreFormattersPlugin) .build();The init() config:
| Option | Default | Meaning |
|---|---|---|
defaultLocale |
'en' |
Locale used for lookups until you change it |
fallbackLocale |
'en' |
Locale to fall back to |
namespace |
'app' |
Namespace assumed when a key has no ns: prefix |
use() accepts repositories and plugins and sorts them by their type:
memoryrepositories become synchronous cache handlers (checked first),storagerepositories become asynchronous storage handlers (checked next),networkrepositories become network handlers (checked last),plugininstances (like the formatters plugin) are initialized and integrated into the core.
The handler chain and caching
Section titled “The handler chain and caching”The client is cache-first. A t() call walks the chain:
- Memory — the
MemoryRepositoryis an LRU cache (maxSize, default 1000 entries). A hit returns synchronously, on the same tick. - Miss —
t()immediately returns the default value you passed (or the key itself), so the UI never blocks, and triggers a background load. - Background load — the asynchronous handlers run in order: persistent
storage (if configured), then network. A successful result is written
back into the faster layers, so the next
t()for that key is a memory hit. Duplicate in-flight loads for the same key are coalesced.
Cache entries use a uniform key format shared by every layer (and by the
WordPress plugin): locale:namespace:key for individual translations and
@locale:namespace for whole-namespace payloads.
Persistent storage
Section titled “Persistent storage”There is no bundled localStorage/IndexedDB repository; persistence is
pluggable. Implement the async repository interface with type: 'storage'
(get, set, remove, clear, getNamespace, setNamespace,
getConfig) and register it with use() — the builder places it between
memory and network automatically. Storage entries are kept as
{ text, styles } JSON and honor a configurable TTL (default 30 minutes).
Inspecting and invalidating the cache
Section titled “Inspecting and invalidating the cache”lionRapid.cache is a cache manager with TanStack-Query-style
invalidation:
// Clear and refetch everything for the current locale's 'app' namespace.await lionRapid.cache.invalidate({ namespace: 'app' });
// Clear a whole locale, a single key, or by predicate.await lionRapid.cache.invalidate({ locale: 'es' });await lionRapid.cache.invalidate({ key: 'en:app:welcome' });await lionRapid.cache.invalidate({ predicate: (k) => k.includes(':errors:') });
// Invalidate without triggering a refetch.await lionRapid.cache.invalidate({ namespace: 'app', refetch: false });For debugging there are lionRapid.isCached(key) and
lionRapid.getEntries() (all cache entries grouped by handler).
Translating
Section titled “Translating”t() — plain text
Section titled “t() — plain text”lionRapid.t('welcome'); // key onlylionRapid.t('greeting', { name: 'Ada' }); // ICU parameterslionRapid.t('greeting', { name: 'Ada' }, 'Hello {name}!'); // + default valuet() always returns plain text: any markup in the stored translation
is stripped, so the result is safe to place anywhere without escaping.
With generated types the key and its
parameter object are checked at compile time.
The default value plays two roles: it is shown until the real translation is available, and — when missing-key sync is enabled — it is sent to the platform as the source text for the new key.
tHtml() — HTML output
Section titled “tHtml() — HTML output”Some translations carry inline styling (bold, links, emphasis). tHtml()
returns the translation as an HTML string with those styles applied:
element.innerHTML = lionRapid.tHtml('welcome');// → 'Hello <strong>World</strong>'Use it where you control the sink (innerHTML, server-side rendering). In
React, prefer the <Trans> component
instead.
Namespaces and nested keys
Section titled “Namespaces and nested keys”- A key without a prefix uses the default namespace from
init():t('welcome')resolvesapp:welcome. - An explicit prefix overrides it:
t('errors:required'). - Dots address nested structures:
t('user.profile.name'). Dot paths are resolved inside the namespace data, so deep translation objects work without flattening.
Changing language
Section titled “Changing language”await lionRapid.changeLanguage('fi', { clear: false, // also wipe caches from the previous locale preloadNamespaces: ['app', 'errors'], // fetch these before resolving});
lionRapid.getCurrentLocale(); // 'fi'Translations for the new locale that are not preloaded load lazily on first use, exactly like any other cache miss.
Network behavior
Section titled “Network behavior”The network handler is built for flaky networks:
- Timeout — each request has a timeout (default 5000 ms, configurable
via
options.timeout). - Retry with exponential backoff and jitter — failed requests are
retried (default 3 attempts,
options.retries). The delay starts atoptions.retryDelay(default 1000 ms), doubles per attempt, gets ±10% jitter to avoid thundering herds, and is capped at 10 seconds. Only retryable failures (network errors and server-side 5xx) are retried. - Key-then-namespace lookup — an individual key lookup that returns “not found” falls back to fetching the whole namespace and resolving the key inside it (which also warms the cache for neighboring keys).
- Typed errors —
TranslationNotFoundError(the key does not exist),TranslationLockedError(the translation exists but is not released yet),ServerError, andNetworkErrorare exported, along with theisRetryableError/isTranslationErrorguards.
A locked translation is treated like a temporary miss: the default value is shown and the key is not reported as missing, since the platform already knows it.
Custom endpoints
Section titled “Custom endpoints”The endpoint paths the network repository calls are remappable through
options.endpoints, which lets the same client talk to a non-LionRapid
backend. See the endpoint remapping recipe.
The default endpoints target the platform’s integration API, which can
read and sync translations by key, locale, and namespace, plus batch sync —
see the Platform API reference for the endpoint detail.
Missing-translation sync
Section titled “Missing-translation sync”With syncMissing enabled on the network repository, the client turns
unknown keys into work for your translators automatically:
new NetworkRepository({ enabled: true, options: { baseUrl: 'https://your-lionrapid-host', apiKey: process.env.LIONRAPID_INTEGRATION_KEY, syncMissing: { enabled: true, debounceMs: 500, // wait for quiet before sending batchSize: 50, // or flush immediately at this size }, },});How it works:
- Every key the server reports as not found is added to a queue, keyed by
locale:namespace:key, so repeats deduplicate. - The queue flushes after 500 ms of quiet, or immediately once 50 items accumulate; each flush is one batch sync request carrying the keys and their default values.
- Flushes are fire-and-forget — a failed flush is logged and never blocks rendering.
- Locked translations (they exist, just not released) are never queued.
The synced keys appear in your continuous project as new source strings.
Events
Section titled “Events”The client emits typed events on lionRapid.bus instead of taking
callbacks. Every listener receives an envelope with a payload and a
context (request id, timestamp, trace):
lionRapid.bus.on('translation:missing', (envelope) => { const { key, locale, namespace } = envelope.payload; console.warn(`Missing: ${locale}:${namespace}:${key}`);});The most useful events:
| Event | Fires when |
|---|---|
translation:request / success / fallback |
A t() call starts, resolves, or falls back |
translation:missing / not-found / locked |
A key is missing, confirmed absent, or locked |
repo:get:success / repo:get:miss |
A cache layer hit or missed |
repo:set:success |
A translation was written into a cache layer |
network:request:start / success / error, retry |
Network activity, including backoff retries |
language:change:start / success / error |
changeLanguage() lifecycle |
cache:invalidate / cache:set |
Cache manager activity |
Unsubscribe with lionRapid.bus.off(event, listener).
ICU MessageFormat (formatters plugin)
Section titled “ICU MessageFormat (formatters plugin)”Register coreFormattersPlugin from @lionrapid/formatters to
process ICU MessageFormat in translation values — interpolation, plurals,
select, and locale-aware number/date formatting:
// Translation: "Hello {name}!"lionRapid.t('greeting', { name: 'Ada' });// → "Hello Ada!"
// Translation: "{count, plural, =0 {no items} one {# item} other {# items}}"lionRapid.t('items', { count: 5 });// → "5 items"
// Translation: "{gender, select, male {He} female {She} other {They}} replied"lionRapid.t('replied', { gender: 'female' });// → "She replied"
// Translation: "Next run: {date, date, long}"lionRapid.t('nextRun', { date: new Date() });// → "Next run: July 13, 2026"
// Translation: "Total: {total, number, ::currency/EUR}"lionRapid.t('total', { total: 1234.5 });// → "Total: €1,234.50"Notes:
- Plural rules and formatting are locale-aware; the current locale is passed to every formatter.
- Compiled ICU messages are cached, so repeated calls are cheap.
- The
otherarm is required in plural/select messages — the types CLI can enforce this at generation time. - Beyond ICU processing, the plugin also registers standalone formatters used by the core (currency, date — including a relative style — number, percent, list, and uppercase/lowercase/capitalize transforms).
ContentUnit: styled translations
Section titled “ContentUnit: styled translations”LionRapid separates translatable text from its inline styling. The server stores translations as a ContentUnit:
{ "text": "Read our <1>terms</1> before continuing.", "styles": { "1": ["bold"] }}Indexed tags (<1>…</1>) mark styled spans; the styles map records what
each span means. Translators move the tags around freely without touching
real markup, and each consumer decides how to render them:
t()strips the tags — plain text out.tHtml()serializes them to HTML via the built-inHTMLSerializer(bold→<strong>, and so on).- The React
<Trans>component serializes them to JSX — see SDK: React.
For advanced use the pieces are exported directly: MarkupParser (HTML →
ContentUnit), HTMLSerializer (ContentUnit → HTML), DefaultStyleMapper
(style names → elements), and ContentValidator, plus
lionRapid.getContentUnit(key, params?, defaultValue?) to fetch the raw
unit. A default value containing HTML is parsed through the same pipeline
automatically.
Production builds
Section titled “Production builds”Set NODE_ENV=production in your build (Vite, Next.js, and most bundlers
do this for their production commands). In production mode debug and info
logging is eliminated and only warnings and errors remain.