SDKs

React Native SDK

Fetch, verify, cache, and serve Ed25519-signed string bundles in React Native apps, with AsyncStorage caching and offline bundled fallback.

The AirStrings React Native SDK fetches remotely managed localized strings, verifies every bundle's Ed25519 signature before exposing a single string, and caches verified bundles in AsyncStorage for offline use. It seeds from signed bundles shipped inside your app, so a cold offline start serves real strings instead of key names.

Requirements: React Native 0.72+. @react-native-async-storage/async-storage (1.19+) is a peer dependency used as the default cache backend.

Install

npm install @airstrings/react-native @react-native-async-storage/async-storage

@react-native-async-storage/async-storage is a peer dependency, not a hard requirement. If it is missing, the SDK silently falls back to an in-memory cache that lives only for the process lifetime; there is no build error. Install it so verified bundles survive app restarts.

Setup

Construct an AirStrings instance once at app startup and keep it alive for the app's lifetime. The instance owns its cache, refresh cycle, and locale; there is no shared singleton. Only organizationId, projectId, environmentId, publicKeys, and locale are required.

import { AirStrings } from '@airstrings/react-native'

const airstrings = new AirStrings({
  organizationId: 'org_a1b2c3d4e5f6',
  projectId: 'proj_a1b2c3d4e5f6',
  environmentId: 'env_a1b2c3d4e5f6',
  publicKeys: ['BASE64_ED25519_PUBLIC_KEY'],
  locale: 'en',
})
FieldTypeRequiredDescription
organizationIdstringyesYour AirStrings organization ID.
projectIdstringyesProject ID.
environmentIdstringyesEnvironment ID (e.g. production, staging).
publicKeysreadonly string[]yesOne or more base64-encoded Ed25519 public keys. Multiple keys supported for rotation.
localestringyesInitial BCP-47 locale (e.g. "en", "fr-CA").
apiBaseURLstringnoAPI base URL. Defaults to https://api.airstrings.com.
logger(level, msg, ctx) => voidnoOptional logger. Default: no-op.
storeBundleStorenoInject a custom cache backend (e.g. MMKV). Defaults to AsyncStorage.
seedreadonly unknown[]noBundled fallback contents supplied at build time via Metro require.

Find your IDs and public key in the dashboard under Project Settings > SDK Configuration, or in the setup snippet shown after you publish a bundle.

Usage

React Native

Create the instance in a module so a single instance is shared across the app. Bundles are cached in AsyncStorage keyed by {projectId}:{environmentId}:{locale}, and the SDK auto-refreshes when the app returns to the foreground via AppState.

import { AirStrings } from '@airstrings/react-native'

export const airstrings = new AirStrings({
  organizationId: 'org_a1b2c3d4e5f6',
  projectId: 'proj_a1b2c3d4e5f6',
  environmentId: 'env_a1b2c3d4e5f6',
  publicKeys: ['BASE64_ED25519_PUBLIC_KEY'],
  locale: 'en',
  seed: [require('./airstrings/bundles/en.json')],
})

React integration

The SDK exposes data and events, not UI bindings. useSyncExternalStore turns the strings:updated event into React re-renders; on() returns the unsubscribe function React needs.

import { useSyncExternalStore } from 'react'
import { airstrings } from './airstrings'

export const useStrings = () =>
  useSyncExternalStore(
    (cb) => airstrings.on('strings:updated', cb),
    () => airstrings.strings,
  )

Call useStrings() in a component to re-render it whenever a new bundle lands, then read values with airstrings.t(key) and airstrings.format(key, args).

Locale

Switching locale loads from cache immediately if available, then refreshes:

await airstrings.setLocale('fr-CA')

Methods & properties

  • t(key): string: raw localized string for key, or key as fallback.
  • format(key, args?): string: formats an ICU MessageFormat string.
  • refresh(): Promise<void>: forces a bundle refresh from the CDN. Honors ETag/304.
  • setLocale(bcp47): Promise<void>: switches locale.
  • whenReady(): Promise<void>: resolves once the initial load cycle completes.
  • destroy(): void: removes the AppState foreground listener. Call on teardown in long-lived roots.
  • on(event, handler): () => void: subscribe to events; returns an unsubscribe function.
  • strings, locale, revision, isReady: read-only snapshots of current state.

Events

type AirStringsEvents = {
  'strings:updated': { locale: string; revision: number }
  'strings:error':   { error: AirStringsError }
}

A tampered or unverifiable bundle emits strings:error with code SIGNATURE_VERIFICATION_FAILED and is never served; the last good bundle stays in place.

String variants

String variants are A/B experiments: each variant is selected deterministically from a stable assignment id, entirely on-device, with no server round-trip. Set an assignment id once and forward exposures to your analytics:

airstrings.setAssignmentId(userId) // or null to clear

airstrings.on('experiment:exposure', ({ key, experimentId, variant, locale, assignmentId }) => {
  analytics.track('experiment_exposure', { key, experimentId, variant, locale, assignmentId })
})

airstrings.t('cta') // returns the assigned variant's value

Reads are unchanged: airstrings.t(key) and airstrings.format(key, args) return the assigned variant's value. The experiment:exposure event fires once per unique (key, experimentId, variant, assignmentId) the first time that string is read.

Experiment content is Ed25519-verified via a separate experiments_signature and soft-fails to base values if verification fails: variants are never served unverified. Passing null to setAssignmentId() clears the assignment and returns to base values. Available since React Native SDK 0.2.0.

See String Variants for how selection, bucketing, and exposure work, and managing experiments from the CLI to create one.

ICU formatting

Every string has a format: "text" (plain) or "icu" (ICU MessageFormat). Use format(key, args) to format ICU strings via intl-messageformat:

airstrings.format('items.count', { count: 3 })

Plural formatting needs Intl.PluralRules, which the default Hermes engine omits. The SDK bundles a guarded polyfill with plural-rules data for en, fr, and es, so plurals format out of the box for those locales; for any other locale, format() falls back to the raw ICU pattern. To add a locale, install @formatjs/intl-pluralrules and import its data after the SDK:

import '@airstrings/react-native'
import '@formatjs/intl-pluralrules/locale-data/de'

text strings are returned as-is (arguments ignored). Pattern syntax: ICU by example.

Caching & offline

  1. On construction, the SDK seeds from bundled fallback (if provided), loads the cached bundle (if any), then fetches the latest bundle from the CDN.
  2. Every bundle is Ed25519-signed. Verification is mandatory: an invalid signature is rejected and never exposed.
  3. Cached bundles are re-verified on every load (defense in depth). The cache is AsyncStorage, keyed by {projectId}:{environmentId}:{locale}; without AsyncStorage the SDK uses an in-memory cache for the process lifetime.
  4. Anti-downgrade protection: a newer revision is never replaced by an older one for the same locale.
  5. The SDK auto-refreshes when the app returns to the foreground via AppState, using ETag / If-None-Match (304 Not Modified) to avoid re-downloading unchanged bundles.
  6. Signature failures never throw from the public API. The SDK keeps the previous good bundle and emits strings:error. With no cache and no network, key names are returned as a fallback.

Multiple publicKeys support rotation; see Key rotation.

Bundled fallback

Ship published, signed bundles inside your app so a cold start with no cache and no network serves real strings instead of key names.

Pull the published bundles

airstrings bundles pull

Commit and seed the bundles

Commit the generated airstrings/bundles/ directory and pass each locale through the seed option with Metro's require:

new AirStrings({
  // ...
  seed: [
    require('./airstrings/bundles/en.json'),
    require('./airstrings/bundles/ja.json'),
  ],
})

Every seed candidate runs the full Ed25519 verification pipeline plus project_id and locale checks. The highest verified revision across cache, seed, and network wins (ties go to the cache), and a winning seed is persisted to the cache. A tampered or wrong-project seed emits strings:error and is never served; entries for other locales are skipped silently, and a missing seed is a silent no-op. Run airstrings bundles pull in CI or as a pre-release step to keep seeds current.

See Offline & fallback for how seeding, caching, and key-name fallback interact.

Troubleshooting

SymptomLikely causeFix
t('key') returns the key nameNo bundle has loaded yet, or no bundle is published for this localeCall await airstrings.whenReady() (or refresh()); confirm organizationId, projectId, and environmentId are correct.
Strings never update after publishingWrong environmentId, or the bundle was published to a different environmentVerify the environment ID matches the one you published from.
strings:error emitted, strings don't changepublicKeys does not contain the key that signed the bundleCopy the environment's public key from Project Settings > SDK Configuration. After rotation, include both old and new keys.
Verified strings vanish after an app restart@react-native-async-storage/async-storage is not installed, so the SDK is using the in-memory fallbackInstall the peer dependency so the cache persists across launches.

On this page