# Fetch loader

Load translations over HTTP from the Comvi CDN, with a bundled fallback for offline use, or from the platform API on the server. Options and the CDN namespace layout.

Canonical: https://comvi.io/docs/i18n/plugins/fetch-loader

With an i18n instance from the [Quick start](/docs/i18n/quick-start) and a Comvi project with
Content Deploy enabled as on [Connect to Comvi](/docs/i18n/connect-comvi), fetch each
locale's JSON over HTTP with `@comvi/plugin-fetch-loader`. The root entry reads the public
CDN and is the smaller one; the `/api` entry also talks to the platform API.

## Install
**npm**

```bash
npm install @comvi/plugin-fetch-loader
```

**pnpm**

```bash
pnpm add @comvi/plugin-fetch-loader
```

**yarn**

```bash
yarn add @comvi/plugin-fetch-loader
```

**bun**

```bash
bun add @comvi/plugin-fetch-loader
```

## Load from the CDN

`src/main.tsx`

```tsx

const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
}).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }));

createRoot(document.getElementById('root')!).render(
<I18nProvider i18n={i18n} fallback={<p>Loading…</p>}>

</I18nProvider>,
);
```

`.use(cdnLoader())` adds HTTP loading to the i18n instance. `fallback`
paints until the first catalog is in. Omitting `cdnUrl` throws at composition time with
`[@comvi/plugin-fetch-loader] cdnUrl is required`. On Vue the same `.use()` goes on the
wrapper `createI18n` returns; on Nuxt it goes in the `comvi.i18n.ts` factory.

The CDN endpoint URL is under **Settings → Content Deploy** in your Comvi project, once Content
Deploy is enabled and saved; pass it as `cdnUrl`. The URL format is on
[CDN Deployment](/docs/platform/cdn#url-format).
`cdnLoader(options)` returns an installer: it takes the instance, composes the loader and
plugin capabilities onto it, queues `cdnLoaderPlugin(options)` and returns the instance widened
with `registerLoader`, `reloadMessages` and the rest of the loader surface. The plugin itself
runs inside `init()`: it registers a load function for `(locale, namespace)` pairs, and core
then loads the active namespaces through it, for the active locale and for each locale in
`fallbackLocale`. `setLocale()` loads the target locale only. Compose the loader before
`init()`; a plugin queued afterwards does not run until the next `init()`.

A catalog with ICU messages (`{count, plural, …}`) needs `compiler: icuCompiler` on the
instance; see [Pluralize with ICU](/docs/i18n/pluralization).

## Bundled fallback

`src/i18n.ts`

```ts

export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en' }).use(
cdnLoader({
    cdnUrl: 'https://cdn.comvi.io/<project-token>',
    fallback: {
      en: () => import('./locales/en.json'),
      de: () => import('./locales/de.json'),
      'en:admin': () => import('./locales/en/admin.json'),
    },
}),
);

await i18n.init();
```

When a network load fails, the loader serves the matching import: the exact
`'locale:namespace'` key first, then the `'locale'` shorthand for the default namespace. A
fallback that resolves reports through `onLoadSuccess`, not `onLoadError`, because from the
instance's point of view the catalog arrived.

## CDN mode vs API mode

`src/i18n.server.ts`

```ts

export const i18n = createI18n({
locale: 'en',
apiKey: process.env.COMVI_API_KEY,
}).use(
fetchLoader({
    cdnUrl: 'https://cdn.comvi.io/<project-token>',
    baseUrl: process.env.COMVI_API_URL,
}),
);

await i18n.init();
```

`fetchLoader()` from the `/api` entry reads the CDN, and switches to the platform API when the
instance carries an `apiKey`: requests go out with `Authorization: Bearer <apiKey>`, one
request carries every active namespace, and draft translations and private projects are
reachable. The key needs project read and translation read access. `cdnLoader()` never builds
an API URL, so an `apiKey` changes nothing there. The `/api` entry ships the API request path
either way, which is why the root entry is smaller
([Bundle size](/docs/i18n/bundle-size#the-two-loader-entries)).

> **API mode is server-side only**
>
> The `apiKey` is a credential. In a browser bundle either the variable is not inlined, so the
> plugin silently takes the CDN branch and serves published text where you expected drafts, or
> it is inlined through `VITE_…` or `NEXT_PUBLIC_…` and shipped to every visitor. Compose
> `fetchLoader` with a key only in a server render, a prerender, a CLI or a script; in the
> browser use `cdnLoader`.

## Options

| Option | Default | Entry | Behaviour |
| --- | --- | --- | --- |
| `cdnUrl` | required | both | The CDN base URL from **Settings → Content Deploy**. |
| `baseUrl` | `https://api.comvi.io` | `/api` | The API origin. Precedence: `baseUrl`, then `NEXT_PUBLIC_COMVI_API_URL`, then the preset. The variable is read from `process.env` on the server; the name follows Next's convention, and only Next can inline it into a browser bundle. Read only when the instance has an `apiKey`. Unrelated to Nuxt's `useLocaleHead({ baseUrl })`. |
| `fallback` | none | both | Import map served when a network load fails. |
| `onLoadError` | none | both | `(locale, namespace, error)` when a pair fails and no fallback produced data. The instance's `loadError` event fires too. |
| `onLoadSuccess` | none | both | `(locale, namespace)` when a pair loads, including from `fallback`. |
| `timeout` | `10000` | both | Per-request budget in milliseconds, covering the body. |
| `retry` | `true` | `/api` | Retries the project-info request after a 429, 5xx or transport failure, twice at most. Timeouts and translation loads, CDN or API, are never retried; the root entry has no retry at all. |
| `loadOnInit` | `true` | both | Whether the plugin loads at install time. Inside `init()` the instance's own load is the only one, so the flag matters only when the factory runs by hand. Pass `ns: []` to `createI18n` to start with no request. |
| `cdnLayout` | `{ rootNamespace: defaultNs }` | both | Which namespace lives at `{cdnUrl}/{locale}.json`. |
| `cache` | none | both | `{ revalidate, tags }` forwarded to `fetch()` as Next.js's `next` option, which Next's patched `fetch` honours; Nitro ignores it. `revalidate: 0` always refetches, `false` caches indefinitely. |

Both callbacks are read once, at install time, and neither fires after the plugin's cleanup.
Naming an `/api`-only option on the root entry is a compile error. Concurrent requests for one
`locale:namespace` are deduplicated, `clearMessages()` aborts the loads it covers, and
`destroy()` aborts everything in flight.

A failed pair emits `loadError` and calls your `onLoadError`. When other namespaces for the
same locale loaded, the miss is reported through `onError` and `t()` keeps rendering from the
cache, walking `fallbackLocale` for the keys that never arrived. When every requested
namespace fails, the load rejects, so `await i18n.init()` or `setLocale()` throws. A failed
load of a `fallbackLocale` catalog is reported the same way and never rejects.

## CDN layout

`src/i18n.ts`

```ts

export const i18n = createI18n({
locale: 'en',
defaultNs: 'storefront',
ns: ['storefront', 'default'],
}).use(
cdnLoader({
    cdnUrl: 'https://cdn.comvi.io/<project-token>',
    cdnLayout: { rootNamespace: 'default' },
}),
);

await i18n.init();
```

| Requested namespace | URL |
| --- | --- |
| `default` | `{cdnUrl}/en.json` |
| `storefront` | `{cdnUrl}/storefront/en.json` |

By default the instance's `defaultNs` loads from `{cdnUrl}/{locale}.json` and every other
namespace from `{cdnUrl}/{namespace}/{locale}.json`. Set `rootNamespace` when the namespace
stored at the CDN root differs from your `defaultNs`, or `rootNamespace: false` to put every
namespace in its own folder. API mode always names the namespaces it requests.

## Read next

- [Connect to Comvi](/docs/i18n/connect-comvi): the CDN URL, the status filter and the first wording change.
- [Load translations on demand](/docs/i18n/loading-translations): import maps, custom loaders and namespaces.
- [Handle missing keys and errors](/docs/i18n/error-handling): `loadError`, `onLoadError` and `onError`.
