Connect to Comvi
With the i18n instance from the 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 gets you
one. The switcher in the header picks the framework, and every tab follows it.
Install the loader
Section titled “Install the loader”npm install @comvi/plugin-fetch-loaderThe loader is one package for every binding. pnpm, yarn and bun take the same name.
Point the instance at the CDN
Section titled “Point the instance at the CDN”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" />}> <App /> </I18nProvider>,);import { defineRouting } from '@comvi/next/routing';
export const routing = defineRouting({ locales: ['en', 'uk'], defaultLocale: 'en' });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 stay as they are.
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');<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>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>' })),);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.
<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()}<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}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" />}> <App /> </I18nProvider> ), document.getElementById('root')!,);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. The platform and the compiler are independent:
either works without the other.
How it works
Section titled “How it works”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
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 has the full
account.
Get the CDN URL
Section titled “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.
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.
Change a translation
Section titled “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
Section titled “Read next”- Fetch loader: CDN mode against API mode, namespaces, the bundled fallback and caching.
- Add a locale end to end: the same path for the second locale.
- Troubleshooting: 403, 404 and CORS on the endpoint, and the empty first paint.