---
title: Dynamic CFBundleLocalizations
description: 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.

<Callout type="info">
  **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.
</Callout>

---

```ts title="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`; // [!code highlight]

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

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

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

export default config;
```

<Callout type="tip">
  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.
</Callout>

---

## 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.

<Steps>

<Step>
### Create the hook script

```js title="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`; // [!code highlight]

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); // [!code highlight]
      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'] }));
});
```
</Step>

<Step>
### Register the hook and read the file

```json title="eas.json"
{
  "build": {
    "production": {
      "hooks": {
        "pre-install": "node eas-build-pre-install.js" // [!code highlight]
      },
      "env": {
        "BETTER_I18N_PROJECT": "your-org/your-project" // [!code highlight]
      }
    }
  }
}
```

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

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

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

</Steps>

<Callout type="warn">
  Add `.expo-locales.json` to your `.gitignore` — it's a build artifact, not source code.

  ```
  # .gitignore
  .expo-locales.json
  ```
</Callout>

---

## 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.
