SDKs
JavaScript SDK
Messagevisor's JavaScript SDK is the primary runtime package for evaluating translations from Messagevisor datafiles. It is the same runtime model that powers CLI evaluation, examples, and most of the higher-level integrations with your projects.
The SDK is universal, and it works in both Node.js and browser environments.
Installation#
In your application, install the SDK package:
$ npm install --save @messagevisor/sdkInitialization#
The SDK can be initialized with or without a datafile:
import { createMessagevisor } from "@messagevisor/sdk";const datafile = await fetch("/datafiles/messagevisor-web-en.json") .then(res => res.json());const m = createMessagevisor({ datafile,});console.log(m.translate("auth.signin"));Datafile fetching#
The SDK itself has no opinion on how you load datafiles. You can either:
- fetch it in the runtime like shown above, or
- bundle it along with your application
import { createMessagevisor } from "@messagevisor/sdk";// Option A: import directly in the bundleimport datafile from "./datafiles/messagevisor-web-en.json";// Option B: fetch it in the runtimeconst datafile = await fetch("/datafiles/messagevisor-web-en.json") .then(res => res.json());const m = createMessagevisor({ datafile,});To make the most out of Messagevisor, it is recommended that datafiles are loaded in the runtime involving ideally a CDN. See targets and deployment guides for more information.
Recommended modules#
When dealing with translations, it is very likely that you will want to use additional modules to extend the runtime behaviour.
To show an example of using the ICU module, let's install the module first:
$ npm install --save @messagevisor/module-icuThen register it on the SDK instance:
import { createMessagevisor } from "@messagevisor/sdk";import { createICUModule } from "@messagevisor/module-icu";const m = createMessagevisor({ datafile, modules: [createICUModule()],});There is also a much simpler interpolation module that can be used to interpolate values into messages, without the complexity of the ICU module.
Translations#
Now that we have the SDK instance, we can start translating messages.
m.translate("auth.signin");// → "Sign in"This will return the translated message for the key auth.signin.
Translating with values#
If we are using either ICU or Interpolation modules, we can pass values to the translate method:
m.translate("dashboard.welcome", { name: "Ada" });// → "Welcome back, Ada"This assumes the original message translation is Welcome back, {name}.
When we pass the values object to the translate method as { name: "Ada" }, a registered formatting module replaces placeholders. Without a module, translate returns the raw source string from the datafile (placeholders unchanged).
t alias#
t is an alias for translate with the same overloads and behavior:
m.t("dashboard.welcome", { name: "Ada" });formatMessage#
Use formatMessage to format an arbitrary string (not looked up from the datafile) with the same module pipeline as translate:
m.formatMessage("Hello {name}", { name: "Ada" });getRawTranslation#
Use getRawTranslation when you need the resolved source string before modules format it (for example, to pass into formatMessage yourself):
const raw = m.getRawTranslation("dashboard.total");m.formatMessage(raw, { amount: 1200 });Context#
Contexts let you translate messages based on the current user, request, or any other runtime information.
Override conditions and segment rules are evaluated against the merged context described above. See Translation lookup for the full order of operations.
Setting initial context#
If you already have some contextual info available at SDK initialization time, you can set it as follows:
const m = createMessagevisor({ datafile, context: { platform: "web", browser: "chrome", },});Setting after initialization#
setContext shallow-merges new top-level keys into the existing instance context:
m.setContext({ userId: "user-123", plan: "pro",});If the instance already had { platform: "web" }, the context is now { platform: "web", userId: "user-123", plan: "pro" }.
Replacing the whole context#
Pass true as the second argument when you intentionally want to replace the whole instance context:
m.setContext( // new context { userId: "user-123", plan: "pro", }, // pass true here true,);The merge is top-level only. Nested objects are replaced at their key, not deep-merged. getContext() returns a shallow copy, so mutating that object does not update the SDK until you pass it back through setContext.
Per-translation context#
If you don't want to set context at SDK instance level, you can always pass it as extra option:
m.translate( // message key "dashboard.welcome", // values { name: "Ada" }, // additional options { context: { plan: "enterprise", }, },);Datafile operations#
Messagevisor SDK is designed to be able to handle multiple datafiles at once which belong to different locales.
Initial datafile#
If you already have the datafile available at initialization time, you can set it directly:
const m = createMessagevisor({ datafile,});The generated datafile already contains the locale info, so you don't need to pass it yourself manually.
Setting datafile afterwards#
You can also set the datafile later on, by calling the setDatafile method:
m.setDatafile(datafile);When no locale is active yet, the first successful setDatafile call also sets the active locale from the datafile's own locale field.
Updating datafile#
You can keep calling setDatafile as many times as you want, if you want to update the datafile for a given locale again.
If the SDK already has a datafile for the incoming locale, setDatafile merges the incoming datafile with the existing one by default. Segments, messages, and translations are merged by key, while top-level fields such as revision, target, and formats come from the incoming datafile.
Replacing datafile#
Pass true as the second argument when you intentionally want to replace the existing datafile for that locale:
m.setDatafile(freshDatafile, true);Split datafiles#
It is possible that you may want to split the translations into multiple datafiles, even though they belong to the same locale.
This is very common when you have multiple Targets in your Messagevisor project, where each target produces its own datafiles based on advanced filtering rules.
For example, you may have two targets for your web app called homepage and checkout, and you don't want to load translations for the checkout page until the user has actually navigated to it.
In those cases, call setDatafile for each datafile. Same-locale datafiles merge by default:
m.setDatafile(datafileForAnotherTarget);The datafile content already has information about the locale, so no need to pass anything else manually.
Learn more in Locales and Targets.
Changing locale#
In datafile operations section, we saw how we can set multiple datafiles (belonging to different locales) to the SDK instance.
Once we have the desired datafiles set, we can switch between locales at runtime by calling the setLocale method:
m.setLocale("nl"); // or "nl-NL", "nl-BE", etcThe SDK switches the active locale to one that already has a datafile loaded (setDatafile / initialization). setLocale throws if that locale has no datafile yet.
NOTE: setDatafile for a new locale does not change the active locale automatically. The first datafile loaded becomes active, and later setDatafile calls for other locales only register those datafiles until you call setLocale.
You can verify the active locale with:
console.log(m.getLocale());// → "en"Per-call locale#
For server-side applications, or any place where one SDK instance has multiple locale datafiles loaded, pass locale in call options to evaluate one request against a different locale without changing instance state:
const m = createMessagevisor({ datafile: enDatafile });m.setDatafile(nlDatafile);m.translate("checkout.title", undefined, { locale: "nl-NL" });m.getRawTranslation("checkout.title", { locale: "nl-NL" });m.formatNumber(1200, "decimal", { locale: "nl-NL" });Per-call locale applies only to that call. It does not emit locale_set, does not emit change, and does not change m.getLocale() or m.getSnapshot().locale. If no per-call locale is provided, the active instance locale is used. The same option is available on direct formatting APIs when one formatted value needs to use a different locale.
Per-call locale is evaluation state, not context. If a segment or condition needs a locale value as an attribute, pass it explicitly in context.
Currency#
It can often be a strategy to define the formats in locales without mentioning the currency code directly, leaving the SDK to fill it in at runtime.
This is useful in applications where the currency is not always known beforehand, and can change at runtime.
Currency at initialization#
You can set the currency code at the time of SDK initialization:
const m = createMessagevisor({ currency: "EUR",});Setting currency afterwards#
Or, you can also set it later on, by calling the setCurrency method:
m.setCurrency("EUR");Time zone#
Similar to currency, time zone info can also be filled in at runtime.
Time zone at initialization#
You can set the time zone at the time of SDK initialization:
const m = createMessagevisor({ timeZone: "Europe/Amsterdam",});Setting time zone afterwards#
Or, you can also set it later on, by calling the setTimeZone method:
m.setTimeZone("Europe/Amsterdam");Formatting values#
Next to evaluating translations, the SDK also allows you to format additional types of values, such as numbers, dates, times, relative times, lists, and display names.
To format values inside translations, please refer to ICU module guide.
Before proceeding further, you are highly recommended to read the Formats guide.
Per-call format overrides#
Direct format helpers accept an options object for per-call overrides. Use currency for currency number formats and timeZone for date, time, and date-time-range formats when a single call needs to differ from the SDK instance or authored preset.
Number formatting#
Use formatNumber to format numbers with the current locale. Pass a format preset name from the active locale's formats.number, or pass Intl.NumberFormat options directly:
m.formatNumber(1200, "money");// → "$1,200.00" (preset from locale)m.formatNumber(1200, { notation: "compact", compactDisplay: "short",});// → "1.2K"// overriding currency for this callm.formatNumber(12, "money", { currency: "EUR" });Use formatNumberToParts when you need the segmented output from Intl.NumberFormat.formatToParts (for example, to style individual tokens). It accepts the same per-call currency override:
m.formatNumberToParts(12, "money", { currency: "EUR" });Date formatting#
Use formatDate for calendar dates. Values can be a Date, timestamp, or ISO string. Presets come from formats.date in the locale's formats:
m.formatDate("2025-01-02T00:00:00Z", "short");// → "1/2/25"m.formatDate(startsAt, "weekday");// → "Wednesday, January 1, 2025"m.formatDate(startsAt, { year: "numeric", month: "long", day: "numeric",});m.formatDate(startsAt, "weekday", { timeZone: "Europe/Amsterdam",});Time zone resolution follows the same rules as Time zone: per-call timeZone in the options object, then the preset's timeZone, then the instance time zone.
Use formatDateToParts for Intl.DateTimeFormat.formatToParts output:
m.formatDateToParts(startsAt, "weekday", { timeZone: "Europe/Amsterdam",});For a start/end range, use formatDateTimeRange(start, end, presetOrOptions?, options?):
m.formatDateTimeRange(startsAt, endsAt, "event", { timeZone: "America/Los_Angeles",});Time formatting#
Use formatTime for clock times. It reads presets from formats.time:
m.formatTime(startsAt, "short");// → "12:00 PM"m.formatTime(startsAt, "event", { timeZone: "America/New_York",});// → "7:00 AM"You can pass inline Intl.DateTimeFormat options instead of a preset name. Use formatTimeToParts when you need formatToParts output:
m.formatTimeToParts(startsAt, "event", { timeZone: "America/New_York",});Relative time formatting#
Use formatRelativeTime with a numeric offset and an Intl.RelativeTimeFormat unit ("second", "minute", "hour", "day", "week", "month", "year", and their plural forms).
Presets come from formats.relative:
m.formatRelativeTime(-1, "day", "short");// → "yesterday"m.formatRelativeTime(3, "hour", { numeric: "auto" });List formatting#
Use formatList to join an array of strings with locale-aware conjunctions and separators via Intl.ListFormat:
m.formatList(["HTML", "CSS", "JavaScript"], { type: "conjunction" });// → "HTML, CSS, and JavaScript" (en-US)m.formatList(["A", "B", "C"], { type: "disjunction" });// locale-aware "or" list (exact wording depends on runtime locale)Options are passed through to Intl.ListFormat. Use formatListToParts for formatToParts output when the runtime supports it.
If Intl.ListFormat is not available, the SDK logs a diagnostic and falls back to joining values with ", ".
Display name formatting#
Use formatDisplayName to resolve localized names for regions, languages, currencies, and other codes via Intl.DisplayNames:
m.formatDisplayName("NL", { type: "region" });// → "Netherlands"m.formatDisplayName("USD", { type: "currency" });// → "US Dollar"Pass Intl.DisplayNames options as the second argument. If Intl.DisplayNames is not available, the SDK logs a diagnostic and returns the input code. Pass { fallback: "none" } to get undefined instead of the raw code.
Plural rules#
Use formatPlural to select a plural category for a number with Intl.PluralRules:
m.formatPlural(1);// → "one"m.formatPlural(2, { type: "ordinal" });// → "two"Learn more in Formats guide.
Default translations#
There can be cases where you want default translations that are not present in the datafile for a given locale.
The SDK option is defaultTranslations (a map of locale keys to message-key dictionaries). When a lookup in the active datafile is missing, the SDK falls back to these defaults before per-call defaultTranslation and the message-key fallback. See Translation lookup for the full order.
Use getDefaultTranslations(locale?) to read the defaults currently registered for a locale.
Instance-level translations#
They can be set at the time of SDK initialization:
const m = createMessagevisor({ defaultTranslations: { "nl": { "dashboard.welcome": "Hallo {name}", }, },});Per-call fallback#
defaultTranslation is separate from defaultTranslations and is only available per call (not as a locale map at initialization):
m.translate("dashboard.welcome", { name: "Ada" }, { defaultTranslation: "Hallo {name}",});Default formats#
If datafile is not providing any desired format, they can be set at the time of SDK initialization as a fallback:
const m = createMessagevisor({ defaultFormats: { "nl": { "number": { "money": { style: "currency", currency: "EUR" }, }, }, },});Learn more in Formats guide.
Resolvers#
Overrides can reference feature flags and experiments (see feature and experiment conditions). The SDK does not call an external service itself. You provide resolvers that answer those conditions at runtime.
Evaluation context is the instance context merged with any per-call context in translate options.
Feature flags#
Register a flag resolver at initialization:
const m = createMessagevisor({ datafile, resolveFlag: (featureKey, context) => { return true; // or false },});// or laterm.setFlagResolver((featureKey, context) => { return true; // or false});context above is Messagevisor's own context from the instance.
Overrides use feature with isEnabled or isDisabled. If no resolver is set, feature conditions do not match.
Experiment variations#
Register a variation resolver the same way for your a/b tests experiment:
// at initializationconst m = createMessagevisor({ datafile, resolveVariation: (experimentKey, context) => { return "control"; // or any other variation string },});// or laterm.setVariationResolver((experimentKey, context) => { return "control"; // or any other variation string});Overrides use experiment with hasVariation and an expected variation string. Return null when the user is not in the experiment, or omit a resolver entirely. hasVariation will not match in either case.
For Featurevisor-backed projects, use @messagevisor/module-featurevisor instead of wiring resolvers manually.
Diagnostics#
The SDK reports structured diagnostics for missing translations, invalid datafiles, deprecated messages, unsupported formatters, module errors, and more.
Pass onDiagnostic at initialization to handle them yourself. Otherwise the SDK logs to the console with a [Messagevisor] prefix.
const m = createMessagevisor({ datafile, logLevel: "warn", onDiagnostic(diagnostic) { console.log(diagnostic.code, diagnostic.message, diagnostic); },});Log level#
logLevel filters which diagnostics reach onDiagnostic (or the console fallback). Levels are ordered from most to least severe:
fatalerrorwarninfodebug
With logLevel: "warn", you receive fatal, error, and warn diagnostics but not info or debug.
Use logLevel: "fatal" in tests when you want a quiet instance.
Modules can subscribe to diagnostics separately with their own threshold via the modules API.
Diagnostic events#
Each diagnostic is a MessagevisorDiagnostic object:
| Field | Description |
|---|---|
level | fatal, error, warn, info, or debug |
code | Stable code such as missing_translation, deprecated_message, invalid_datafile |
message | Human-readable description |
locale, messageKey, overrideKey | Present when relevant to a translation lookup |
module, moduleName | Present when reported by or about a module |
source | translation or formatMessage when relevant |
originalError | Underlying error for parse or format failures |
Diagnostic codes#
Common built-in codes include:
sdk_initialized: instance constructedmissing_translation: key missing from datafile anddefaultTranslations(emitted before per-calldefaultTranslationis applied)missing_datafile/missing_locale: locale or datafile not availableinvalid_datafile: JSON parse or shape failureinvalid_message: module formatting threwdeprecated_message: message hasdeprecatedmetadatamessage_override_matched: debug-level override matchunsupported_formatter:Intl.ListFormatorIntl.DisplayNamesunavailableduplicate_module: two modules share the samename
Diagnostics with level: "error" also emit an error event.
Events#
Subscribe to SDK state changes with on or the convenience subscribe helper (equivalent to on("change", callback)).
const unsubscribe = m.subscribe(() => { const snapshot = m.getSnapshot(); console.log(snapshot.locale, snapshot.context, snapshot.datafileLocales);});m.setLocale("nl-NL");unsubscribe();Event types#
List of event types:
| Event | When it fires |
|---|---|
datafile_set | setDatafile updates a locale |
locale_set | setLocale changes the active locale |
context_set | setContext updates instance context |
currency_set | setCurrency runs |
timeZone_set | setTimeZone runs |
change | After any of the above (same payload, type: "change") |
error | An error-level diagnostic was reported |
Each callback receives a MessagevisorEvent with:
snapshotpreviousSnapshotversion- type-specific fields (for example
locale/previousLocaleonlocale_set)
Modules API#
Register modules at initialization or add them later with addModule. Remove by name with removeModule.
import { createICUModule } from "@messagevisor/module-icu";const m = createMessagevisor({ datafile });m.addModule(createICUModule());During translate / formatMessage, the SDK runs modules in registration order:
format: interpolate or format the message (MessagevisorFormatPayloadincludestranslation,values,locale,formats,messageKey,meta,moduleOptions)transform: post-process the result (MessagevisorTransformPayload)
Returning undefined from a hook leaves the previous value unchanged. Format errors emit invalid_message diagnostics and rethrow.
Module setup API#
setup(api) runs once when the module is registered. The API exposes:
setFlagResolver/setVariationResolver: same as instance methodsgetRevision(locale?): read the active or locale-specific datafile revisiononDiagnostic(handler, { logLevel }): module-scoped diagnostic subscription (does not receive that module's ownreportDiagnosticcalls)reportDiagnostic({ level, code, message, ... }): emit diagnostics attributed to the module
Pass moduleOptions on translate, formatMessage, or formatter calls to pass arbitrary per-call options into module hooks.
Creating custom modules#
Implement the MessagevisorModule shape (name, setup, format, transform, close). See the Custom modules guide for patterns, diagnostics, and resolver integration.
export function createUppercaseModule() { return { name: "uppercase", format({ translation }) { return typeof translation === "string" ? translation.toUpperCase() : translation; }, };}Duplicate name values are rejected with a duplicate_module diagnostic.
Closing the SDK#
Call close() when tearing down the instance (for example in tests or SSR). It runs each module's optional close hook, clears modules and listeners, and ignores further operations.
await m.close();Formatter cache#
Pass a shared cache object at initialization to reuse Intl.* formatter instances across hot paths. The SDK creates one via createMessagevisorCache() when omitted.
Translation lookup#
translate (and getRawTranslation) resolve a source string first, then run registered modules on that string. Understanding lookup helps when debugging overrides, fallbacks, and missing-translation diagnostics.
Resolving the source string#
For a message key, the SDK uses the per-call locale when provided, otherwise the active locale. It then builds an evaluation context by merging instance context with any per-call context from translate options.
Lookup proceeds in this order:
- Overrides: for the message, each override is tried in datafile order. The first override whose conditions or segments match wins:
- Conditions: direct rules on context attributes, plus optional
feature/experimentrules when resolvers are configured. See Feature and experiment conditions. - Segments: reusable segment references from the datafile (
override.segments). Omitted or"*"segment groups always match. - On match, the SDK returns that override’s
translationand emits amessage_override_matcheddiagnostic at debug level.
- Conditions: direct rules on context attributes, plus optional
- Base translation: if no override matches, the SDK uses
translations[messageKey]from the datafile. defaultTranslations: if the result is still missing, the SDK checksdefaultTranslationsfor the evaluation locale.missing_translationdiagnostic: if the key is still unresolved after steps 1 to 3, the SDK emits amissing_translationerror diagnostic. This happens even when a per-calldefaultTranslationin the next step will supply the final string.- Per-call
defaultTranslation: if provided, thedefaultTranslationoption ontranslate/getRawTranslationis used. - Message key: if nothing else matched, the message key string is returned.
Empty strings are explicit translations. If an override, base translation, defaultTranslations entry, or per-call defaultTranslation resolves to "", the SDK returns "" and does not continue fallback lookup.
When a resolved message has deprecated metadata in the datafile, the SDK emits a deprecated_message warning before formatting (including when the match came from an override).
// Override wins when context matches authored conditionsm.setContext({ plan: "pro" });m.translate("pricing.cta");// → override translation for pro, or base translation otherwisem.getRawTranslation("missing.key", { defaultTranslation: "Fallback copy",});// → "Fallback copy" if the key is not in the datafile or defaultsFormatting the result#
After the source string is resolved, translate runs modules in registration order:
formathooks: interpolation, ICU, and other formatters (usingvalues, locale formats, and optionalmoduleOptions).transformhooks: post-processing on the formatted output.
getRawTranslation stops after source resolution and does not run modules. Use it when you need the matched override or base string before formatting.

