Better I18NBetter I18N
Expo

Dynamic CFBundleLocalizations

Automatically sync iOS locale support with your Better i18n CDN languages

When you add a new language in the Better i18n dashboard, iOS needs to know about it at build time via CFBundleLocalizations in Info.plist. Without it, iOS system features like Siri locale and input mode may fall back incorrectly.

The problem: app.config.ts does not support async/await. Expo evaluates the config file synchronously at build time, so you can't fetch() the manifest there. Languages end up hardcoded — and get out of sync every time the dashboard changes.

CDN manifest already has what you need. The endpoint https://cdn.better-i18n.com/{org}/{project}/manifest.json returns a languages array with every locale in your project — no new API needed.


app.config.ts
import { ExpoConfig } from 'expo/config';
import { execFileSync } from 'child_process';

const PROJECT = 'your-org/your-project';
const url = `https://cdn.better-i18n.com/${PROJECT}/manifest.json`; 

let locales: string[] = ['en'];

try {
  // Array args — no shell injection risk
  const raw = execFileSync('curl', ['-sf', '--max-time', '5', url], { 
    encoding: 'utf-8',
  });
  const manifest = JSON.parse(raw);
  locales = manifest.languages.map((l: { code: string }) => l.code); 
} catch {
  console.warn('[better-i18n] Could not fetch manifest — using fallback locales');
}

const config: ExpoConfig = {
  name: 'my-app',
  // ...
  ios: {
    infoPlist: {
      CFBundleLocalizations: locales, 
      CFBundleAllowMixedLocalizations: true,
    },
  },
};

export default config;

This runs only during expo prebuild or eas build — not at runtime. Your users never see the CDN call; it only affects the native build output.


For CI / EAS Builds

EAS Build Hooks run before the native build — they're async, isolated, and the right place for network calls. Use this approach when you need to avoid a curl dependency in CI.

Create the hook script

eas-build-pre-install.js
const https = require('https');
const fs = require('fs');

// Set BETTER_I18N_PROJECT=org/project in eas.json env
const project = process.env.BETTER_I18N_PROJECT;

if (!project) {
  console.warn('[better-i18n] BETTER_I18N_PROJECT not set — writing fallback locales');
  fs.writeFileSync('.expo-locales.json', JSON.stringify({ locales: ['en'] }));
  process.exit(0);
}

const url = `https://cdn.better-i18n.com/${project}/manifest.json`; 

https.get(url, (res) => {
  let data = '';
  res.on('data', (chunk) => (data += chunk));
  res.on('end', () => {
    try {
      const manifest = JSON.parse(data);
      const locales = manifest.languages.map((l) => l.code); 
      fs.writeFileSync('.expo-locales.json', JSON.stringify({ locales }));
      console.log(`[better-i18n] Wrote ${locales.length} locales:`, locales);
    } catch (e) {
      console.warn('[better-i18n] Failed to parse manifest — writing fallback:', e.message);
      fs.writeFileSync('.expo-locales.json', JSON.stringify({ locales: ['en'] }));
    }
  });
}).on('error', (e) => {
  console.warn('[better-i18n] Network error — writing fallback:', e.message);
  fs.writeFileSync('.expo-locales.json', JSON.stringify({ locales: ['en'] }));
});

Register the hook and read the file

eas.json
{
  "build": {
    "production": {
      "hooks": {
        "pre-install": "node eas-build-pre-install.js"
      },
      "env": {
        "BETTER_I18N_PROJECT": "your-org/your-project"
      }
    }
  }
}

Then in app.config.ts, replace the execFileSync block with:

app.config.ts (EAS variant)
let locales: string[] = ['en'];

try {
  const { locales: saved } = require('./.expo-locales.json'); 
  if (Array.isArray(saved) && saved.length > 0) locales = saved;
} catch {
  // File doesn't exist yet (local dev without running the hook)
}

Add .expo-locales.json to your .gitignore — it's a build artifact, not source code.

# .gitignore
.expo-locales.json

Verification

After running expo prebuild (or eas build), open ios/<YourApp>/Info.plist and confirm the CFBundleLocalizations array contains the same locales as your Better i18n project.

On this page