Skip to content

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.

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:

  • memory repositories become synchronous cache handlers (checked first),
  • storage repositories become asynchronous storage handlers (checked next),
  • network repositories become network handlers (checked last),
  • plugin instances (like the formatters plugin) are initialized and integrated into the core.

The client is cache-first. A t() call walks the chain:

  1. Memory — the MemoryRepository is an LRU cache (maxSize, default 1000 entries). A hit returns synchronously, on the same tick.
  2. Misst() immediately returns the default value you passed (or the key itself), so the UI never blocks, and triggers a background load.
  3. 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.

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).

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).

lionRapid.t('welcome'); // key only
lionRapid.t('greeting', { name: 'Ada' }); // ICU parameters
lionRapid.t('greeting', { name: 'Ada' }, 'Hello {name}!'); // + default value

t() 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.

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.

  • A key without a prefix uses the default namespace from init(): t('welcome') resolves app: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.
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.

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 at options.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 errorsTranslationNotFoundError (the key does not exist), TranslationLockedError (the translation exists but is not released yet), ServerError, and NetworkError are exported, along with the isRetryableError / isTranslationError guards.

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.

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.

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.

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).

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 other arm 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).

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-in HTMLSerializer (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.

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.