Swift SDK
Installation#
Add the package with Swift Package Manager:
dependencies: [ .package( url: "https://github.com/messagevisor/messagevisor-swift.git", from: "1.0.0" )]Add the products your application needs:
.target( name: "YourApp", dependencies: [ .product(name: "Messagevisor", package: "messagevisor-swift"), .product(name: "MessagevisorICU", package: "messagevisor-swift"), ])In Xcode, use File → Add Package Dependencies and enter:
https://github.com/messagevisor/messagevisor-swift.gitInitialization#
Create the main runtime with createMessagevisor():
import Messagevisorlet m: Messagevisor = createMessagevisor( MessagevisorOptions(datafile: datafile))print(try m.translate("auth.signin"))// → "Sign in"The SDK can start without a datafile. This is useful when your application fetches translations after launch:
let m = createMessagevisor( MessagevisorOptions(locale: "en-US"))m.setDatafile(downloadedDatafile)Datafile fetching#
The SDK does not prescribe a network layer. Fetch a target- and locale-specific datafile with URLSession, load one from your application bundle, or use your existing cache/CDN client.
import Foundationimport Messagevisorlet url = URL(string: "https://cdn.example.com/messagevisor/ios/en-US.json")!let (data, _) = try await URLSession.shared.data(from: url)let datafile = try DatafileContent.fromData(data)let m = createMessagevisor( MessagevisorOptions(datafile: datafile))For offline-first applications, bundle an initial datafile and replace or merge fresher content after a successful fetch.
Recommended modules#
Translation lookup and conditional overrides live in the core Messagevisor product. Formatting is intentionally modular.
This package includes:
MessagevisorICU: ICU MessageFormat-style interpolation, plural, select, number, date, and time formatting.MessagevisorInterpolation: lightweight{name}replacement for primitive values.MessagevisorMissingTranslations: observes and optionally deduplicates missing translation diagnostics.
Most applications should choose either ICU or simple interpolation:
import Messagevisorimport MessagevisorICUlet m = createMessagevisor( MessagevisorOptions( datafile: datafile, modules: [createICUModule()] ))Without a formatting module, translate() returns the resolved message string unchanged.
Translations#
Translate a message by key:
try m.translate("auth.signin")// → "Sign in"Translating with values#
Pass values when the selected module needs them:
try m.translate( "dashboard.welcome", values: ["name": .string("Ada")])// → "Welcome back, Ada"MessagevisorValue supports strings, integers, doubles, booleans, dates, arrays, objects, and null:
let values: MessagevisorValues = [ "name": .string("Ada"), "count": .int(3), "price": .double(12.5), "enabled": .bool(true), "createdAt": .date(Date()),]t alias#
t() is an alias for translate():
try m.t("dashboard.welcome", values: ["name": .string("Ada")])Raw translation#
Use getRawTranslation() when you need the selected string before modules format or transform it:
let raw = try m.getRawTranslation("dashboard.welcome")Arbitrary messages#
Use formatMessage() to run a string through the same module pipeline without looking up a message key:
try m.formatMessage( "Hello, {name}", values: ["name": .string("Ada")])Context#
Context drives message override and segment conditions.
Initial context#
let m = createMessagevisor( MessagevisorOptions( datafile: datafile, context: [ "platform": .string("ios"), "plan": .string("pro"), ] ))Merge context#
setContext() shallow-merges by default:
m.setContext([ "userId": .string("user-123"),])Nested objects are replaced at their top-level key; they are not deep-merged.
Replace context#
m.setContext( ["userId": .string("user-456")], replace: true)Per-call context#
Pass request- or screen-specific context without mutating instance state:
try m.translate( "checkout.title", options: TranslateOptions( context: ["checkoutType": .string("express")] ))Per-call context shallow-merges over instance context for that evaluation only.
Datafile operations#
Messagevisor can hold datafiles for multiple locales and can combine split target datafiles for one locale.
Set after initialization#
m.setDatafile(datafile)JSON strings are accepted as well:
m.setDatafile(datafileJSON)Invalid JSON or an invalid datafile emits invalid_datafile with the stable message could not parse datafile.
Merge by default#
When a datafile already exists for the incoming locale, setDatafile() shallow-merges segments, messages, and translations. Incoming identity fields and formats win, while an omitted incoming direction preserves the existing direction.
This supports loading several target datafiles for one locale on demand:
m.setDatafile(homeDatafile)m.setDatafile(checkoutDatafile)Replace explicitly#
m.setDatafile(freshDatafile, replace: true)Replacement removes entries that are absent from the incoming datafile.
Loading another locale#
The first successfully loaded datafile establishes the active locale when no locale was configured. Loading later locales does not silently switch it:
m.setDatafile(englishDatafile)m.setDatafile(dutchDatafile)try m.setLocale("nl-NL")Locales, currency, and time zones#
Active locale#
try m.setLocale("nl-NL")print(m.getLocale() ?? "")setLocale() requires a loaded datafile for that locale. A locale supplied to MessagevisorOptions can still be used with default translations and formats before a datafile arrives.
Per-call locale#
try m.translate( "checkout.title", options: TranslateOptions(locale: "fr-FR"))Per-call locale does not mutate instance state or emit locale events.
Direction#
let direction = try m.getDirection()// "ltr" or "rtl"Currency#
m.setCurrency("EUR")print(m.getCurrency() ?? "")Currency formats without an authored currency use the per-call currency, then the instance currency, then USD.
Time zone#
m.setTimeZone("Europe/Amsterdam")Per-call time zone and currency are available in both TranslateOptions and EvaluationOptions.
Formatting#
The ICU module supports common authored ICU messages:
try m.formatMessage( "{count, plural, =0 {No items} one {# item} other {# items}}", values: ["count": .int(2)])// → "2 items"It supports nested plural, selectordinal, and select, exact plural branches, offsets, apostrophe escaping, simple interpolation, and named number/date/time presets.
Timezone-qualified ISO strings, numeric Unix epoch milliseconds, and native Date values can be used for date/time arguments:
try m.formatMessage( "{when, date, long}", values: ["when": .date(Date())])The Swift string API does not model JavaScript/React rich-message callbacks. With ignoreTags: true (the default), tags remain literal text. Requesting rich tag processing emits unsupported_formatter.
Direct formatter helpers#
The core SDK exposes Foundation-backed helpers:
try m.formatNumber(1234.5, preset: "money")try m.formatDate(Date(), preset: "long")try m.formatTime(Date(), preset: "short")try m.formatDateTimeRange(start, end, preset: "appointment")try m.formatRelativeTime(-1, unit: .day)try m.formatPlural(2)try m.formatList(["iOS", "macOS", "visionOS"])try m.formatDisplayName("NL", type: "region")The ToParts methods return a simplified portable representation because Foundation does not expose the same tokenized parts contract as JavaScript Intl:
let parts = try m.formatNumberToParts(1234.5)// [MessagevisorFormatPart(type: "literal", value: "1,234.5")]Supported display-name types are language, region, and currency.
Format precedence#
Formats merge in this order:
defaultFormatssupplied to the SDK;- formats in the effective locale datafile;
- per-call
formats.
The merge extends to individual properties inside named presets.
Defaults#
Default translations#
Defaults allow startup copy or application-owned fallbacks before a datafile is available:
let m = createMessagevisor( MessagevisorOptions( locale: "en-US", defaultTranslations: [ "en-US": ["app.loading": "Loading…"] ] ))You can also pass a call-site fallback:
try m.translate( "optional.copy", options: TranslateOptions(defaultTranslation: "Fallback"))Empty strings are valid explicit translations and do not fall through.
Default formats#
let formats = FormatPresets( number: [ "money": [ "style": .string("currency"), "minimumFractionDigits": .int(2), ] ])let m = createMessagevisor( MessagevisorOptions( locale: "en-US", defaultFormats: ["en-US": formats] ))Feature and variation resolvers#
Message overrides can reference external feature flags or experiment variations. Messagevisor does not contact a feature service itself; provide resolvers:
let m = createMessagevisor( MessagevisorOptions( datafile: datafile, resolveFlag: { featureKey, context in featureKey == "newCheckout" && context["plan"] == .string("pro") }, resolveVariation: { experimentKey, _ in experimentKey == "checkoutCopy" ? "treatment" : nil } ))Resolvers can also be installed by modules. Removing that module restores the previous resolver registration.
Diagnostics#
Use onDiagnostic for observability:
let m = createMessagevisor( MessagevisorOptions( datafile: datafile, onDiagnostic: { diagnostic in logger.log("\(diagnostic.code): \(diagnostic.message)") }, logLevel: .warn ))Every diagnostic contains level, code, message, and an always-present details dictionary. Module provenance and an original Swift error can also be attached.
Stable diagnostic codes include:
sdk_initializedmissing_translationmissing_datafilemissing_localeinvalid_datafileinvalid_messageunsupported_formattermissing_formatinvalid_formatmessage_override_matcheddeprecated_messageduplicate_modulemodule_setup_errormodule_close_error
Without a handler, delivered diagnostics are written to standard error with a [Messagevisor] prefix. Change the threshold later with setLogLevel().
Error diagnostics emit the SDK error event even when the configured diagnostic threshold filters them from the handler.
Events and snapshots#
Subscribe to a specific event:
let unsubscribe = m.on(.localeSet) { event in if case .localeSet(let locale, let previousLocale) = event.details { print("\(previousLocale ?? "none") → \(locale)") }}unsubscribe()Available events are:
changeerrordatafile_setlocale_setcontext_setcurrency_settimeZone_set
subscribe() is a convenience for change notifications:
let unsubscribe = m.subscribe { render(m.getSnapshot())}Snapshots include a monotonically increasing version, active locale and direction, context, currency, time zone, loaded locales, and revisions by locale. A change event includes the specific source event and its details. Throwing event observers are isolated from state changes and other observers.
Modules#
A module can set up runtime integrations, format or transform translation output, observe/report diagnostics, and clean up resources:
let uppercase = MessagevisorModule( name: "uppercase", transform: { payload, _ in payload.translation.uppercased() })let m = createMessagevisor( MessagevisorOptions( datafile: datafile, modules: [uppercase] ))Return nil from format or transform to preserve the current value.
Setup API#
setup receives a MessagevisorModuleApi with:
setFlagResolversetVariationResolvergetRevisiononDiagnosticreportDiagnostic
let observer = MessagevisorModule( name: "observer", setup: { api in _ = api.onDiagnostic({ diagnostic in print(diagnostic.code) }, MessagevisorModuleDiagnosticOptions(logLevel: .warn)) })Duplicate named modules are rejected. Anonymous modules are allowed. Setup failures roll back resolver and diagnostic registrations, report module_setup_error, and close partially initialized resources.
Add and remove at runtime#
let remove = m.addModule(uppercase)try await remove()The returned removal function is idempotent. You can also remove all modules with a name:
try await m.removeModule("uppercase")Module ownership belongs to the root instance, not child instances.
Child instances#
Use spawn() for request-, task-, or screen-scoped state:
let child: MessagevisorChild = m.spawn( context: ["requestId": .string("request-123")], options: SpawnOptions( locale: "nl-NL", currency: "EUR", timeZone: "Europe/Amsterdam" ))let title = try child.translate("checkout.title")try await child.close()Children share parent datafiles, modules, resolver registrations, and bounded formatter caches. They isolate context, locale, currency, time zone, snapshots, event versions, and cleanup. Parent datafile and module updates become visible dynamically. A child's datafile_set listeners and generic change listeners observe parent datafile updates through child-owned events containing the child's active locale, context, snapshots, and monotonically increasing version. Listener unsubscription is local and idempotent. Closing the child removes its single parent bridge without affecting the parent.
MessagevisorChild intentionally omits setDatafile, addModule, removeModule, and spawn, keeping shared resource ownership unambiguous.
Translation lookup#
For an effective locale and message key, the SDK resolves in this order:
- The first matching override whose conditions and segments both match.
- The base datafile translation.
defaultTranslations[locale][messageKey].- A
missing_datafileormissing_translationdiagnostic. - Per-call
defaultTranslation. - The message key.
Matching overrides emit message_override_matched at debug level. Deprecated messages emit deprecated_message. The full instance context is shallow-merged with per-call context before evaluating conditions and segments.
Condition operators are type-strict. Regular expressions use the portable i, m, s, and u flags, with u mapped to Foundation's Unicode-aware regular expression behavior. Repeated flags, lookarounds, named or non-capturing groups, inline flags, backreferences, and possessive quantifiers are not portable and do not match. Character classes and escaped literal backslashes remain valid. Invalid or non-portable expressions return false for both matches and notMatches.
Closing the SDK#
Close root instances when their lifecycle ends:
try await m.close()Closing clears event and diagnostic subscriptions and closes root-owned modules in reverse registration order. Cleanup continues after individual errors; failures emit module_close_error and are returned together as MessagevisorCloseError.
Do not use an instance after closing it.
Apple platform behavior#
The SDK uses Foundation localization APIs and does not contain locale-specific output rewrites. Apple Foundation and JavaScript Intl can use different CLDR versions and formatter patterns, so punctuation, spacing, compact-number suffixes, localized zone names, and other presentation can differ while both outputs are valid.
Important platform notes:
- Formatter caches are bounded and shared with child instances.
- Public operations are synchronized for safe access from multiple threads. User callbacks should still avoid long-running work on the caller's thread.
- Foundation does not expose JavaScript-compatible tokenized formatter parts;
ToPartsmethods use a simplified representation. - Unsupported formatter capabilities are non-fatal and use
unsupported_formatterwhere the SDK can identify them. - Invalid explicit options, such as a malformed currency code or unknown time zone, emit
invalid_formatand throw aMessagevisorError. - Compact notation uses Foundation's locale data. Foundation does not expose separate short and long compact styles, so
compactDisplay: longemitsunsupported_formatterand uses the native compact form. - Rich ICU callback values are a JavaScript/framework feature; Swift translation results are strings.
Use expectedByRuntime.swift in shared project fixtures when an exact Apple-native expectation is intentional. Do not hardcode locale-specific rewrites into application or SDK code.
Project conformance CLI#
The package includes messagevisor-swift, a development runner that evaluates real Messagevisor project tests through the Swift SDK.
swift run messagevisor-swift test \ --projectDirectoryPath=/path/to/messagevisor-project \ --target=swift \ --withIcuModule \ --normalizeSpaces \ --onlyFailuresUseful commands:
swift run messagevisor-swift evaluate \ --projectDirectoryPath=/path/to/project \ --target=swift \ --locale=en-US \ --message=auth.signinswift run messagevisor-swift benchmark \ --projectDirectoryPath=/path/to/project \ --target=swift \ --locale=en-US \ --message=auth.signin \ -n 100000swift run messagevisor-swift examples \ --projectDirectoryPath=/path/to/project \ --target=swift \ --withIcuModule \ --normalizeSpacesThe runner shells out to the project's installed npx messagevisor CLI for source loading and datafile generation, then performs evaluations in Swift. Every assertion is compared; the runner never skips native formatting cases. --normalizeSpaces treats ordinary spaces, no-break spaces (U+00A0), and narrow no-break spaces (U+202F) as equivalent, matching the Java runner and keeping fixtures readable. All other Apple Foundation differences must be recorded explicitly with expectedByRuntime.swift.