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-storagepnpm add @airstrings/react-native @react-native-async-storage/async-storageyarn add @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',
})| Field | Type | Required | Description |
|---|---|---|---|
organizationId | string | yes | Your AirStrings organization ID. |
projectId | string | yes | Project ID. |
environmentId | string | yes | Environment ID (e.g. production, staging). |
publicKeys | readonly string[] | yes | One or more base64-encoded Ed25519 public keys. Multiple keys supported for rotation. |
locale | string | yes | Initial BCP-47 locale (e.g. "en", "fr-CA"). |
apiBaseURL | string | no | API base URL. Defaults to https://api.airstrings.com. |
logger | (level, msg, ctx) => void | no | Optional logger. Default: no-op. |
store | BundleStore | no | Inject a custom cache backend (e.g. MMKV). Defaults to AsyncStorage. |
seed | readonly unknown[] | no | Bundled 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 forkey, orkeyas 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 theAppStateforeground 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 valueReads 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
- On construction, the SDK seeds from bundled fallback (if provided), loads the cached bundle (if any), then fetches the latest bundle from the CDN.
- Every bundle is Ed25519-signed. Verification is mandatory: an invalid signature is rejected and never exposed.
- 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. - Anti-downgrade protection: a newer revision is never replaced by an older one for the same locale.
- The SDK auto-refreshes when the app returns to the foreground via
AppState, usingETag/If-None-Match(304 Not Modified) to avoid re-downloading unchanged bundles. - 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 pullCommit 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
| Symptom | Likely cause | Fix |
|---|---|---|
t('key') returns the key name | No bundle has loaded yet, or no bundle is published for this locale | Call await airstrings.whenReady() (or refresh()); confirm organizationId, projectId, and environmentId are correct. |
| Strings never update after publishing | Wrong environmentId, or the bundle was published to a different environment | Verify the environment ID matches the one you published from. |
strings:error emitted, strings don't change | publicKeys does not contain the key that signed the bundle | Copy 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 fallback | Install the peer dependency so the cache persists across launches. |