# Troubleshooting

Fixes for the common Comvi i18n errors, covering provider not found, hydration mismatch, missing keys rendering as the key string, CDN 403/CORS, locale not awaited, Next.js getI18n() not configured, and Nuxt auto-imports not resolving.

Canonical: https://comvi.io/docs/i18n/troubleshooting

Each entry is symptom, cause, fix. Runtime hooks (`onError`, `onMissingKey`, `onLoadError`) are on [Handle missing keys and errors](/docs/i18n/error-handling); every error code is on [Error codes](/docs/i18n/errors).

## A plural renders as `{count, plural, …}`, or throws `E_ICU_SYNTAX`

**Symptom:** in development, ingesting a catalog throws `[@comvi/core] "{count, plural, …}" in "…" is ICU "plural" syntax; the default compiler supports {param} only. Pass compiler: icuCompiler from "@comvi/core/icu" in the constructor options.` In production the braced segment appears on the page verbatim, and `E_ICU_SYNTAX` is reported once through `onError` (or `console.error`).

**Cause:** the i18n instance is on the default compiler, which handles `{param}` only. ICU is never enabled for you.

**Fix:** name the compiler in the constructor options. One option covers the inline catalog and every catalog a loader merges later:

`src/i18n.ts`

```ts

export const i18n = createI18n({ locale: 'en', compiler: icuCompiler }).use(
loader({ uk: () => import('./locales/uk.json') }),
);
```

The snippet imports from `@comvi/core`. An app that installed only a binding adds
`@comvi/core` to its own `dependencies` first: it arrives as a transitive dependency, and
pnpm does not hoist one into the app's `node_modules`, so the import does not resolve there.
See [Pluralize with ICU](/docs/i18n/pluralization#compose-the-icu-compiler).

## `<tag>` markup shows up literally in the output

**Symptom:** `t('legal.tos')` returns `"Read our <link>Terms</link>"` with the angle brackets.

**Cause:** plain `t()` does not parse tags on an instance with nothing composed.

**Fix:** render through `<T>`, which passes the tag grammar per call on any instance, or make the syntax ambient once per bundle with `import '@comvi/core/tags'`. That import is process-wide and retroactive. An SSR app needs it in the server graph too, or the server renders the literal string and the client renders markup. See [Tag interpolation](/docs/i18n/tag-interpolation).

## `useI18n()` (or `<T>`) throws "must be used within a provider"

**Symptom:** one of these runtime errors:

- React / Next.js: `[@comvi/react] Hooks must be used within an I18nProvider.`
- Vue: `[@comvi/vue] useI18n must be used within a Vue app with i18n plugin installed. Make sure you called app.use(i18n) before using this composable.`
- SolidJS: `[@comvi/solid] i18n context not found. Wrap your app with <I18nProvider i18n={i18n}>.`
- Svelte: `[@comvi/svelte] i18n context not found. Call setI18nContext(i18n) in your root component (e.g., App.svelte).`

**Cause:** the component is rendered outside the provider, or the provider was never mounted: the provider sits below the component, `app.use(i18n)` ran after a component rendered, a Next.js Client Component imported `useI18n` from `@comvi/next` instead of `@comvi/next/client`, or a test rendered the component without wrapping it.

**Fix:** mount the provider at the root, above everything that uses i18n:

`src/main.tsx`

```tsx

createRoot(document.getElementById('root')!).render(

</I18nProvider>,
);
```

`src/main.ts`

```ts

const i18n = createI18n({ locale: 'en', messages: { en: {} } });

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

In Next.js Client Components import the hook from `@comvi/next/client`; in Server Components use `await getI18n()` from `@comvi/next/server`. In Svelte call `setI18nContext(i18n)` once in `App.svelte` or your root layout.

## SSR / hydration mismatch: text flickers or React warns about mismatched HTML

**Symptom:** the server renders one locale, the client another; a hydration warning, or a flash of the wrong language.

**Cause:** the client i18n instance starts on a different locale than the server used, or the server-rendered translations were not handed to the client, so it re-fetches and re-renders.

**Fix:** in Next.js, render the locale layout through `<ComviProvider locale>`; on the manual setup, `await loadMessages(locale)` in the layout and pass both `locale` and `messages` to `<I18nProvider>` in your `"use client"` wrapper. In Nuxt, compose the loader in `comvi.i18n.ts`: the module pre-loads the active locale on the server and hydrates the client. The full pattern for each framework is on [Render on the server](/docs/i18n/ssr).

## A translation renders as the key string instead of the text

**Symptom:** the UI shows `home.title` literally.

**Cause:** the key is missing in the active locale and the `fallbackLocale` chain, its namespace was not loaded, or the key has a typo. A missing key renders the key string.

**Fix:** confirm the key exists for the active locale and the namespace is loaded (`createI18n({ ns: ['default', 'admin'] })` on an instance with a loader composed, or load on demand). Then set a fallback, turn on development diagnostics, or take over the missing-key render:

`src/i18n.ts`

```ts

declare function reportToSentry(message: string): void;

createI18n({
locale: 'de',
fallbackLocale: 'en', // a missing key falls back to a complete locale
strict: 'dev', // console warning the moment a key is missing
messages: { en: {}, de: {} },
onMissingKey: ({ key, locale, namespace }) => {
    reportToSentry(`Missing i18n key: ${namespace}:${key} (${locale})`);
    return ''; // render nothing instead of the key
},
});
```

The `onMissingKey` option works with nothing composed. The runtime `i18n.onMissingKey(cb)` registration is a plugin-capability member (`.use(plugins())`, or `useI18nPlugins()` in a component), takes positional `(key, locale, namespace)` and returns a cleanup function.

## A capability member is not a function

**Symptom:** `registerLoader`, `addActiveNamespace`, `reloadMessages`, `onLoadError`, `registerPlugin` or `onMissingKey` is not a function, or `useI18nLoader()` / `useI18nPlugins()` throws:

```text
// development
[@comvi/core] This i18n instance has no loader capability. Compose it: .use(attachLoader) from "@comvi/core/loader", or loader(map).
// production
[@comvi/core] missing loader capability — attach @comvi/core/loader
```

**Cause:** the i18n instance never composed the capability. Loader members come from `.use(loader(map))` or `.use(attachLoader)`, plugin members from `.use(plugins())` or a package installer.

**Fix:** compose the capability where the instance is created, before `init()`. In a component, acquire the members from `useI18nLoader()` and `useI18nPlugins()` rather than from `useI18n()`.

## CDN returns 403, 404, or CORS error when fetching translations

**Symptom:** the network tab shows `403 Forbidden`, `404 Not Found`, or a CORS failure on a request to `cdn.comvi.io/...` or your API server.

**Cause:** a wrong `cdnUrl` (it must be the whole CDN endpoint URL, such as `https://cdn.comvi.io/<project-token>`), a mismatch between the app's `defaultNs` and the namespace stored at the CDN root, a `baseUrl` or `apiKey` from another project, or a project not yet published to the CDN.

**Fix:** copy the endpoint from **Settings → Content Deploy** ([URL format](/docs/platform/cdn#url-format)) and publish the project at least once:

`src/i18n.ts`

```ts

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

Wherever the i18n instance carries an `apiKey`, use `fetchLoader` from `@comvi/plugin-fetch-loader/api`, which then requests the API at `baseUrl` instead of the CDN; without `apiKey` it requests the CDN whatever the build mode ([the two loader entries](/docs/i18n/bundle-size#the-two-loader-entries)).

The loader fetches `{cdnUrl}/{lang}.json` for `defaultNs` and `{cdnUrl}/{namespace}/{lang}.json` for other namespaces, so a 404 means a mismatched layout or an undeployed file. When a named namespace is `defaultNs` but a different one sits at the CDN root, pass `cdnLayout: { rootNamespace: 'default' }`: the loader then requests `storefront/en.json` for unqualified keys instead of the root `en.json`. Custom layouts, `fallback` imports and SSR cache options are on [Fetch loader](/docs/i18n/plugins/fetch-loader).

## `setLocale()` ran but the UI still shows the old language for a moment

**Symptom:** after a switch, old text shows briefly, or vanilla code reads the old locale on the next tick.

**Cause:** with the loader capability composed, a locale switch loads the namespaces it does not have cached asynchronously, and applies at once only on a full cache hit. The `i18n.locale = 'de'` setter fires that load and returns immediately.

**Fix:** `await i18n.setLocale('de')` before reading translations; it resolves once the new namespaces are loaded. On a plain `createI18n()` with nothing composed, `setLocale()` swaps the locale synchronously, so untranslated keys there mean a missing loader. The `setLocale` from `useI18n()` is the same method, and framework UIs re-render when loading finishes. See [Switch the locale](/docs/i18n/language-switching).

## Next.js: `getI18n()` throws "i18n not configured" or "Locale not set"

**Symptom:** one of two server errors:

```text
[@comvi/next] i18n not configured. Add withComvi({ config }) to next.config, or call setI18n(i18n) / createNextI18nFromInstance(factory, routing). In a test runner the alias does not apply: alias `@comvi/next/config` to your config module (vitest `resolve.alias`, jest `moduleNameMapper`) or call setI18n(i18n) in test setup. `serverExternalPackages` containing @comvi/next is not supported.
[@comvi/next] Locale not set. Call setRequestLocale(locale) in your layout/page first, or configure middleware.
```

**Cause:** `next.config` has no `withComvi({ config })` and nothing called `setI18n(i18n)`; or `getI18n()` ran without a locale argument outside `<ComviProvider>`, on a path the middleware does not match.

**Fix:** export the config through `withComvi({ config: './src/i18n/config.ts' })` in `next.config.ts` and restart the dev server, since `next.config` is read once. Pass the locale from `params`: `await getI18n({ locale })`, in pages and in `generateMetadata` alike. Where a helper takes no locale, render it under `<ComviProvider locale>` or call `setRequestLocale(locale)` first, and confirm `middleware.ts` exports `createMiddleware(routing)` with a `matcher` covering your routes. See [Next.js](/docs/i18n/next).

## Nuxt: `useI18n()` is undefined / auto-imports don't resolve

**Symptom:** `useI18n is not defined`, `NuxtLinkLocale` does not render, or a component throws ``[@comvi/nuxt] i18n not configured. Add "@comvi/nuxt" to `modules` in nuxt.config.ts.``

**Cause:** `@comvi/nuxt` is not in `modules`, or the dev server was not restarted after editing `nuxt.config.ts`. A build that stops with ``[@comvi/nuxt] `comvi.detectBrowserLanguage` is not an option: use `localeCookie` and `localeDetection`.`` (or the same sentence for `comvi.fallbackLanguage`, `runtimeConfig.public.comvi.cookieName` or a key inside `localeDetection`) is a `nuxt.config.ts` written for an older release: write the option the message names.

**Fix:** register the module, restart, and compose the loader in `comvi.i18n.ts`. The generated default instance has nothing composed, so it has no `registerLoader`:

`nuxt.config.ts`

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

`comvi.i18n.ts`

```ts

// Found by convention as ./comvi.i18n.ts; `comvi.i18nModule` overrides the path.
export default defineComviI18n((options) =>
createI18n(options).use(
    loader({
      en: () => import('./locales/en.json'),
      uk: () => import('./locales/uk.json'),
    }),
),
);
```

To register from `comvi.setup.ts` instead, destructure `core` (not `i18n`, a `VueI18n | core` union) and pass `registerLoader` a `LoaderFn`; a map throws:

```text
[@comvi/core] registerLoader(): argument must be a loader function. For a static import map use .use(loader(map)), or wrap it with createImportMapLoader(map, () => i18n.getDefaultNamespace()) — both from @comvi/core/loader.
```

The hook needs no type argument: the module writes the factory's return type into `.nuxt/types/`, so `core.registerLoader` compiles exactly when `comvi.i18n.ts` composed the loader and fails `nuxi typecheck` otherwise. `satisfies NuxtI18nSetup<I18n & I18nLoaderApi>` (all three types from `@comvi/nuxt`) stays supported for a factory the module cannot see. With no loader composed, `loadMessages` warns once, opening with `[@comvi/nuxt] This i18n instance has no loader capability, so SSR loaded nothing.` and naming the `i18nModule` factory, and returns what the catalog already holds. See [Nuxt](/docs/i18n/nuxt) and [Setup hook](/docs/i18n/api/nuxt#setup-hook).

## Read next

- [Handle missing keys and errors](/docs/i18n/error-handling): `onError`, `onMissingKey`, `onLoadError`, and the event hooks.
- [Error codes](/docs/i18n/errors): every error code, and which ones carry a `code` property.
- [Compatibility](/docs/i18n/compatibility): supported framework, runtime, and Node versions.
