Frameworks
Frameworks
Framework guides show how to load Messagevisor datafiles, create SDK instances, pass runtime context, and render translated output in common application environments.
Start with the guide that matches your application:
Shared model#
Every framework integration follows the same shape:
- build target-specific datafiles with
npx messagevisor build - publish or bundle those datafiles
- load the right datafile or datafiles for the current target
- create a
@messagevisor/sdkinstance with modules such as ICU - pass locale and user/request context at evaluation time on servers
- render strings through the SDK or React hooks
Framework code should not know how messages are authored. It should only know which datafile to load and what context to pass.
Server-side requests#
Servers usually handle many users at the same time. Keep one shared Messagevisor instance with the locale datafiles your server needs, then pass the request locale and context per evaluation:
const m = await getMessagevisor(); // use the shared loader belowm.translate( "dashboard.welcome", { name: user.name }, { locale: requestLocale, context: { plan: user.plan, platform: "web" }, });Do not call setLocale() or setContext() inside request handlers on a shared server instance.
Those methods mutate instance state. Per-call locale and context keep concurrent requests isolated while still reusing loaded datafiles and formatter caches.
Shared datafile loader#
Copy this application helper into your server code. It is a recipe, not an SDK export. The framework guides wrap the same helper so loading and refresh behaviour stay consistent.
import { createMessagevisor } from "@messagevisor/sdk";import { createICUModule } from "@messagevisor/module-icu";export function createMessagevisorLoader({ baseUrl = "https://cdn.yoursite.com/datafiles", target = "web", locales = ["en-US", "nl-NL"], maxAgeMs = 60_000, retryDelayMs = 5_000, timeoutMs = 10_000, onError = (error) => console.error("Messagevisor refresh failed", error),} = {}) { if (locales.length === 0) throw new Error("Configure at least one locale"); /** @type {import("@messagevisor/sdk").Messagevisor | undefined} */ let current; /** @type {Promise<import("@messagevisor/sdk").Messagevisor> | undefined} */ let pending; let nextRefreshAt = 0; async function loadLocale(locale) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch( `${baseUrl}/messagevisor-${target}-${locale}.json`, { cache: "no-store", signal: controller.signal } ); if (!response.ok) { throw new Error(`Datafile ${locale}: HTTP ${response.status}`); } const datafile = await response.json(); if (datafile?.locale !== locale || datafile?.target !== target) { throw new Error(`Unexpected datafile identity for ${target}/${locale}`); } return datafile; } finally { clearTimeout(timer); } } function refresh() { if (pending) return pending; pending = (async () => { const datafiles = await Promise.all(locales.map(loadLocale)); const candidate = createMessagevisor({ datafile: datafiles[0], modules: [createICUModule({ ignoreTags: false })], }); try { datafiles.slice(1).forEach((datafile) => candidate.setDatafile(datafile)); // Invalid inputs can be reported without being stored by the SDK. locales.forEach((locale) => candidate.getDatafile(locale)); } catch (error) { await candidate.close(); throw error; } // Publish only after every locale has loaded and passed SDK validation. current = candidate; nextRefreshAt = Date.now() + maxAgeMs; return candidate; })() .catch((error) => { nextRefreshAt = Date.now() + retryDelayMs; throw error; }) .finally(() => { pending = undefined; }); return pending; } async function get() { if (!current) return refresh(); if (!pending && Date.now() >= nextRefreshAt) { // Observe background failures without rejecting a request using good data. void refresh().catch(onError); } return current; } return { get, refresh };}For CommonJS applications, change the two imports to require() calls, remove export from the function declaration, and append module.exports = { createMessagevisorLoader };. TypeScript projects can keep this as a JavaScript helper with allowJs enabled.
Concurrent initial calls share one load. An initial HTTP error, invalid JSON, timeout, or invalid datafile rejects that load; finally clears the pending promise so the next call can retry. Handle that rejection in the framework error boundary or return a temporary service error. Do not cache it forever or create an empty SDK as a successful fallback.
Once loaded, requests receive the last known good instance. The first request after maxAgeMs starts a real fetch in the background. A failed refresh leaves the published instance untouched and delays the next automatic attempt by retryDelayMs. onError must report the failure without throwing. Monitor failure count and data age, and choose a maximum acceptable stale age for your application.
Explicit refresh#
Wrap the helper once per server process or edge isolate:
import { createMessagevisorLoader } from "./messagevisor-loader.js";const loader = createMessagevisorLoader();export const getMessagevisor = loader.get;export const refreshMessagevisor = loader.refresh;A deployment hook or revision poller can await refreshMessagevisor(). This function always starts a fetch unless a load is already running, even when the cached instance is fresh:
try { await refreshMessagevisor();} catch (error) { console.error("Keeping the previous Messagevisor datafiles", error);}The age check runs on requests, not on a timer. Framework fetch revalidation settings alone cannot refresh an SDK hidden behind a permanently resolved promise. This helper uses cache: "no-store"; ensure your CDN also serves current data or select an immutable release URL from a deployment manifest. Publish all locales together if requests must see one release across locales.
Each request should call getMessagevisor() again, then retain that returned instance for its own work. Refresh publishes a new instance, so existing requests can finish with their previous snapshot. Do not close the previous instance while requests or spawned children still use it. This recipe uses only the ICU module; if custom modules own resources, add request lifetime tracking and close retired instances after their consumers finish.
Background work may be suspended after an edge response. On those platforms, await refreshMessagevisor() from a scheduled handler or use the platform's request lifetime mechanism. Caches are local to each process or isolate, not shared across the deployment.
This recipe loads one target per locale. For several targets in one locale, follow the merge constraints and rebuild the full intended locale snapshot before publishing it.
Client-side apps#
Client-side apps usually serve one user at a time. Keep that simple: create the SDK for the active datafile/locale, and use setLocale() when the user changes language after loading the new datafile.
const m = createMessagevisor({ datafile });m.translate("dashboard.welcome", { name: user.name });You normally do not need to pass locale on every client-side translation call.