Guide

React Native OTA translations without a JS bundle push

Updated

To update the text in a React Native app without an App Store release or a JavaScript bundle push, load strings from a CDN at runtime and keep a verified copy on the device. With @airstrings/react-native, you publish a change and every device downloads an Ed25519-signed bundle, checks the signature, caches it and re-renders, while the JavaScript your users run stays exactly the build that passed review.

This guide shows the setup, including locale handling and an offline fallback, and how it compares with an i18next remote backend and Expo EAS Update.

Three ways to change copy after release

  • An i18next HTTP backend. i18next-http-backend loads translation JSON from any server with fetch. It is simple and free, but you host the files, decide how they are cached on the device, and nothing checks that the JSON you receive is the JSON you published.
  • Expo EAS Update. Ships a new JavaScript bundle and assets to apps using expo-updates. It can change anything in the JavaScript layer, not only copy. Each update only runs on builds with a matching runtime version, and end-to-end code signing is available on the EAS Production plan ($199/month) and Enterprise. The free plan sends updates to 1,000 monthly active users; Starter is $19/month for 3,000 (Expo pricing).
  • A strings service with an SDK. Copy lives as keys and per-locale values outside the app. Publishing changes data, not code, so there is no runtime version to match and no JavaScript bundle to rebuild.

Expo facts checked on October 6, 2026.

i18next HTTP backendEAS UpdateAirStrings
What changes over the airTranslation JSON you hostJavaScript bundle and assetsString values per key and locale
Needs a matching buildNoYes, same runtime versionNo
Content verified on deviceNoCode signing on Production and Enterprise plansEd25519 on every bundle, every plan
Offline cacheYour choice of extra pluginsLast downloaded updateVerified cache in AsyncStorage, re-verified on load
Plurals and selectsi18next plural rulesWhatever your code doesICU MessageFormat
Works in native Swift and Kotlin screensNoNoYes, with the iOS and Android SDKs

Install

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

AsyncStorage is a peer dependency used as the on-device cache. The SDK needs React Native 0.72 or later. Apart from AsyncStorage it is pure JavaScript, so it runs in Expo and bare React Native apps.

Configure the SDK

Create one instance for the whole app. The ids and public key come from your project; airstrings sdk-config --env production prints them with a ready-made initializer.

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

export const airstrings = new AirStrings({
  organizationId: 'org_xxx',
  projectId: 'proj_xxx',
  environmentId: 'env_xxx',
  publicKeys: ['BASE64_ED25519_PUBLIC_KEY'],
  locale: getLocales()[0]?.languageTag ?? 'en-US',
  seed: [
    require('./airstrings/bundles/en-US.json'),
    require('./airstrings/bundles/it.json'),
  ],
})

locale is any BCP 47 tag. Read it from expo-localization, react-native-localize or your own settings screen. To change language at runtime, call setLocale: the SDK applies the cached or bundled copy for that locale, then fetches the latest.

await airstrings.setLocale('it')

Render strings in components

The SDK emits strings:updated whenever a newer verified bundle is applied, so a small hook keeps components in sync:

import { useSyncExternalStore } from 'react'
import { Text } from 'react-native'
import { airstrings } from './airstrings'

function useStrings() {
  return useSyncExternalStore(
    (cb) => airstrings.on('strings:updated', cb),
    () => airstrings.strings,
  )
}

export function CartSummary({ count }: { count: number }) {
  useStrings()
  return (
    <>
      <Text>{airstrings.t('cart.title')}</Text>
      <Text>{airstrings.format('cart.items', { count })}</Text>
    </>
  )
}

t returns the plain value and format renders ICU MessageFormat such as {count, plural, one {# item} other {# items}}. Hermes does not ship Intl.PluralRules, so the SDK includes plural data for English, French and Spanish; for other languages, import the locale data from @formatjs/intl-pluralrules.

Bundled fallback for offline first launch

A fresh install with no network has no cache. Ship your published bundles inside the app so it still shows real copy:

airstrings bundles pull
git add airstrings/bundles

The files are passed through seed with Metro’s require, as above. Seeds are treated as untrusted: each one goes through the same Ed25519 check and project and locale checks as a download, and the highest verified revision across cache, seed and network wins. A tampered seed is rejected, never shown. Run airstrings bundles pull in CI or before each release to keep the fallback fresh.

Publishing a change

npm install -g @airstrings/cli
airstrings init <staging-write-key>
airstrings strings set cart.title en="Your bag" it="La tua borsa" --format text --push
airstrings publish
airstrings promote preview

init uses the staging write key from onboarding, so publish goes to staging, where you check it in a staging build. Production is protected: promote preview prints the diff and the dashboard link where a person promotes staging to production. Release builds use the production ids and keys from airstrings sdk-config --env production. Devices pick up the new copy on their next refresh and whenever the app returns to the foreground.

When not to use this

  • The change is behavior, layout or a bug fix. That is JavaScript. Use EAS Update or a store release; see CodePush for strings for how the two fit together.
  • You already run i18next with a few languages and rarely change copy. Bundling the JSON in the app is simpler.
  • Translators need a full workflow. Pair AirStrings with a translation platform via CSV import, or see AirStrings vs Tolgee.

The free plan includes signed over-the-air delivery for 1 project, 1,000 keys, 3 locales and 2 environments. For iOS, Android and web setup, see How to update app strings without an App Store release; for SwiftUI apps, see Remote localization in SwiftUI. The SDK source is on GitHub.

FAQ

Can I update React Native translations without EAS Update or CodePush?

Yes. Load translations at runtime from a CDN instead of bundling them with your JavaScript. With @airstrings/react-native, publishing a change sends a new signed string bundle; the JavaScript bundle and runtime version do not change.

Does it work with Expo?

Yes. Apart from the AsyncStorage peer dependency the SDK is pure JavaScript, so it runs in Expo and bare React Native apps on React Native 0.72 or later.

How is this different from i18next-http-backend?

i18next-http-backend fetches translation JSON from a server you host and does not check what it receives. AirStrings verifies an Ed25519 signature on every bundle, caches verified bundles on the device and ships a signed fallback for offline first launches.

Do ICU plurals work on Hermes?

Yes. Hermes lacks Intl.PluralRules, so the SDK includes plural data for English, French and Spanish. For other languages, import locale data from @formatjs/intl-pluralrules after the SDK.

What happens on a first launch with no network?

The SDK seeds from bundles you commit with airstrings bundles pull and pass through the seed option. Each seed is signature-checked before use. Without a cache or seed, t(key) returns the key name.