# Connect to Comvi

Load your catalogs from the Comvi platform CDN and ship a wording change without a redeploy.

Canonical: https://comvi.io/docs/i18n/connect-comvi

With the i18n instance from the [Quick start](/docs/i18n/quick-start) rendering an inline catalog, the
next move is to take the catalog out of your source and let the Comvi platform serve it over
its CDN, so a translator's edit reaches users without a build. You need a Comvi project with at
least the `greeting` key in it; [Your first project](/docs/platform/your-first-project) gets you
one. The switcher in the header picks the framework, and every tab follows it.

## Install the loader

`Terminal`

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

The loader is one package for every binding. `pnpm`, `yarn` and `bun` take the same name.

## Point the instance at the CDN
**React**

`src/main.tsx`

```tsx
import { createRoot } from 'react-dom/client';
import { createI18n, I18nProvider } from '@comvi/react';
import { cdnLoader } from '@comvi/plugin-fetch-loader';
import App from './App';

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={<div className="spinner" />}>

    </I18nProvider>,
);
```

**Next.js**

`src/i18n/routing.ts`

```ts
import { defineRouting } from '@comvi/next/routing';

export const routing = defineRouting({ locales: ['en', 'uk'], defaultLocale: 'en' });
```

`src/i18n/config.ts`

```ts
import { createNextI18n } from '@comvi/next';
import { cdnLoader } from '@comvi/plugin-fetch-loader';
import { routing } from './routing';

export const { i18n } = createNextI18n({ ...routing }).use(
    cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }),
);

export { routing };
```

Only `src/i18n/config.ts` changes: `next.config.ts`, the layout and the page from the
[Quick start](/docs/i18n/quick-start) stay as they are.

**Vue**

`src/main.ts`

```ts
import { createApp } from 'vue';
import { createI18n } from '@comvi/vue';
import { cdnLoader } from '@comvi/plugin-fetch-loader';
import App from './App.vue';

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

createApp(App).use(i18n).mount('#app');
```

`src/App.vue`

```vue
<script setup lang="ts">
import { useI18n } from '@comvi/vue';

const { t, isInitializing } = useI18n();
</script>

<template>
    <div v-if="isInitializing" class="spinner" />
    <h1 v-else>{{ t('greeting', { name: 'Alice' }) }}</h1>
</template>
```

**Nuxt**

`comvi.i18n.ts`

```ts
import { createI18n, defineComviI18n } from '@comvi/nuxt/i18n';
import { cdnLoader } from '@comvi/plugin-fetch-loader';

export default defineComviI18n((options) =>
    createI18n(options).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' })),
);
```

`nuxt.config.ts`

```ts
import { defineNuxtConfig } from 'nuxt/config';

export default defineNuxtConfig({
    modules: ['@comvi/nuxt'],
    comvi: {
      defaultLocale: 'en',
      locales: ['en', 'uk'],
    },
});
```

`comvi.i18n.ts` sits next to `nuxt.config.ts` and the module finds it by name. `defineComviI18n()`
types the factory; the module calls it once for the client and once per server request, so it
returns a new instance from `createI18n` every time, and in development the module throws when
it gets the same one twice. `nuxt.config.ts` loses its `messages`: an inline catalog is used
as is, and the loader is never asked.

**Svelte**

`src/routes/+layout.svelte`

```svelte
<script lang="ts">
    import { createI18n, setI18nContext } from '@comvi/svelte';
    import { cdnLoader } from '@comvi/plugin-fetch-loader';
    import type { Snippet } from 'svelte';

    const { children }: { children: Snippet } = $props();

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

    setI18nContext(i18n);
</script>

{@render children()}
```

`src/routes/+page.svelte`

```svelte
<script lang="ts">
    import { useI18n } from '@comvi/svelte';

    const { isInitializing, t } = useI18n();
</script>

{#if $isInitializing}
    <div class="spinner"></div>
{:else}
    <h1>{$t('greeting', { name: 'Alice' })}</h1>
{/if}
```

**Solid**

`src/index.tsx`

```tsx
import { render } from 'solid-js/web';
import { createI18n, I18nProvider } from '@comvi/solid';
import { cdnLoader } from '@comvi/plugin-fetch-loader';
import App from './App';

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

render(
    () => (
      <I18nProvider i18n={i18n} fallback={<div class="spinner" />}>

    ),
    document.getElementById('root')!,
);
```

**Vanilla**

`src/i18n.ts`

```ts
import { createI18n } from '@comvi/core';
import { cdnLoader } from '@comvi/plugin-fetch-loader';

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

try {
    await i18n.init();
} catch {
    // every requested namespace failed to load: the app still runs, t() renders keys
}
```

Two things changed against the Quick start: `messages` is gone, and `.use(cdnLoader())`
is composed onto the instance in the same expression that creates it. The loader fetches
`{cdnUrl}/{locale}.json` for the active locale when `init()` runs, and again for each locale
you switch to. Compose it inside the `createI18n` expression because the binding calls `init()`
as soon as it has the instance, and `.use()` belongs before `init()`; only plain `@comvi/core`
awaits `init()` itself.

The catalog arrives over the network, so there is a moment when `t()` has nothing to read.
React and Solid paint the `fallback` prop for that moment, Vue and Svelte read `isInitializing`.
Next.js and Nuxt fetch on the server, so the HTML already carries the text and no gate is
visible. The components are unchanged and keep asking for `greeting` with a `{name}`
placeholder, so the project needs that key with that placeholder. Until a translator adds it,
the heading reads `greeting`.

If your keys use ICU (`{count, plural, …}`), add `compiler: icuCompiler` as on
[Pluralize with ICU](/docs/i18n/pluralization). The platform and the compiler are independent:
either works without the other.
### How it works

<Mermaid
code={`flowchart LR
    App["Components<br/>t('greeting')"]
    Loader["cdnLoader"]
    CDN["Comvi CDN<br/>cdnUrl/locale.json"]
    Catalog["Catalog in the instance"]
    Bundled["Bundled fallback<br/>import map"]
    Chain["fallbackLocale chain"]
    Key["The key itself"]

    App --> Loader --> CDN --> Catalog
    CDN -- "fetch fails" --> Bundled --> Catalog
    Catalog -- "key missing" --> Chain -- "still missing" --> Key
`}
caption="One request per locale and namespace. A failed fetch tries the bundled fallback import map; a key missing from the loaded catalog walks the fallbackLocale chain; what is left renders as the key."
/>

The loader makes one request per `locale:namespace` pair and does not retry it. When the fetch
fails, or returns something that is not a catalog, the loader looks for a `fallback` entry under
`locale:namespace`, then under the bare locale for the default namespace, and merges whatever
that import returns; a fallback that works reports through `onLoadSuccess`, not `onLoadError`.
A miss is reported through `onLoadError` and `onError` first. When at least one requested
namespace landed, `init()` resolves and the app mounts with the catalog it has; when every one
failed and no fallback covered it, `init()` rejects with `E_ALL_NAMESPACES_FAILED` after the
report, which is what the `try`/`catch` in the Vanilla tab is for. From there the per-key rules
take over. A key missing or empty in the current locale is looked up along the `fallbackLocale`
chain, and a key missing everywhere renders as itself. `fallbackLocale: 'en'` in the snippets
makes `init()` fetch `en.json` next to the active locale's file, one request per fallback locale
and namespace, so the chain has a catalog to read; a fallback file that fails to load is reported
and does not fail `init()`. [Important defaults](/docs/i18n/important-defaults) has the full
account.

## Get the CDN URL

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).
The endpoint appears once you enable Content Deploy, tick the **Locales** and **Namespaces**
to publish, and save. With every locale and namespace ticked, a locale or namespace added later
deploys too. The endpoint is public and needs no key, so it can sit in a browser bundle. A write to the project schedules a deploy about five seconds after the last write,
and at most a minute into a continuous stream of writes; **Deploy Now** pushes the current state
at once.

> **A key you can see on the platform but not in the bundle**
>
> **Filter by Status** on the same screen decides which translations reach the CDN. Empty is the
> default and deploys every status. Tick **Translated** on its own and anything still in review
> is absent from the bundle: no error, no warning, a missing key. See
> [CDN settings](/docs/platform/cdn#cdn-settings).

## Change a translation

Edit the `greeting` value in the dashboard, wait for the deploy to land, and reload the app.
The new wording is on screen, and nothing in your repository changed.

## Read next

- [Fetch loader](/docs/i18n/plugins/fetch-loader): CDN mode against API mode, namespaces, the bundled fallback and caching.
- [Add a locale end to end](/docs/i18n/guides/add-a-locale): the same path for the second locale.
- [Troubleshooting](/docs/i18n/troubleshooting): 403, 404 and CORS on the endpoint, and the empty first paint.
