Skip to content
Comvi

Error codes

View .md

Branch on the E_… code an error carries, report the rest by message, and look each code up here. Every development message starts with the package that produced it: [@comvi/core] for the instance and both compilers, [@comvi/plugin-fetch-loader] for the loader, and [@comvi/react], [@comvi/vue], [@comvi/solid], [@comvi/svelte], [@comvi/next] or [@comvi/nuxt] for a binding’s own checks. A production build of the instance throws the bare E_… token instead of the sentence, with no prefix.

src/i18n.ts
import { createI18n } from '@comvi/core';
// Your error tracker: Sentry, Rollbar, a POST to your own endpoint.
declare function reportToSentry(error: unknown, context: unknown): void;
const i18n = createI18n({
locale: 'en',
onError(error, context) {
// E_ICU_SYNTAX is the one reported error with a `.code`; the two E_USE_* codes
// are thrown synchronously from .use() and never reach this handler.
const code = (error as { code?: string }).code;
if (code === 'E_ICU_SYNTAX') {
// context: { source: "compile", key, namespace, locale }
reportToSentry(error, { ...context, code });
return;
}
console.error(`${context?.source}: ${error.message}`, context);
},
});

Three codes carry a code own-property, so catch can branch without matching text: E_ICU_SYNTAX, E_USE_INSTANCE_IDENTITY and E_USE_NOT_INSTALLER. Everything else is matched by message or allowed to propagate. E_ICU_SYNTAX carries a second own-property, argumentType: "plural", "select", "selectordinal", or the parsed token such as "number" or "date".

An E_… token is a production error identity, not a message. Each one is written in the source as IS_DEV ? "a long explanatory sentence" : "E_SOMETHING", and the development export condition picks the arm at build time. A development build throws the sentence and the token never appears; a production build throws the token and the sentence is not in the bundle. Every code has a distinct development sentence; six are quoted in both arms and the rest are summarized in the table. The capability-missing pair and the @comvi/next and @comvi/nuxt server errors are not build-conditional and throw the same text in both builds.

Development: one of five sentences, depending on the compiler and the failure. On the default compiler, for a plural or select the ICU compiler would handle: [@comvi/core] "{…}" in "…" is ICU "plural" syntax; the default compiler supports {param} only. Pass compiler: icuCompiler from "@comvi/core/icu" in the constructor options. On the same compiler, for anything else: [@comvi/core] "{…}" in "…" is ICU "number" syntax; the default compiler supports {param} only. "number" is not a shipped ICU argument type: quote the literal ('{…}') or pass your own compiler. On the ICU compiler, for an argument it does not implement: [@comvi/core] ICU argument `{…}` is not supported: this compiler implements `plural`, `selectordinal` and `select`, each with a choice list like `other {…}`. Quote the braces ('{'…'}') or format the value in code and pass it as a plain {param}. For a choice list it cannot parse: [@comvi/core] The `plural` choice list `…` is malformed; every selector needs a braced sub-message, as in `other {…}`. For an exact selector that is not a number: [@comvi/core] The exact selector `=one` in the `plural` choice list `…` is not a number; an ICU exact selector is `=` followed by a number, as in `=1 {…}`.

Production: E_ICU_SYNTAX, with code and argumentType own-properties. Production does not throw at ingestion: the braced segment renders literally and the error is reported once through onError with source: 'compile', or printed with console.error when no handler is set. Development walks every string as the catalog is ingested.

Cause: a catalog uses ICU the active compiler cannot compile. Fix: for plural, select and selectordinal, pass compiler: icuCompiler from @comvi/core/icu. For number, date and time, which neither compiler implements, format the value in code and interpolate it as a plain {param}, or quote the braces. The plural offset: form reads as a selector with no braced sub-message, so it produces the malformed choice list message; drop the offset and do the arithmetic in your own parameter. See ICU reference.

Development: [@comvi/core] .use(f) expects an installer that returns the i18n instance it was given, but f returned …. A raw plugin goes through .use(definePlugin(f)) or registerPlugin(f); a plugin package's installer is the name WITHOUT the Plugin suffix (localeDetector(), not localeDetectorPlugin()). Production: E_USE_NOT_INSTALLER, with code.

Cause: .use(f) was handed a function that returned something other than the i18n instance, which is what a raw plugin, an async installer and a Plugin-suffixed factory all do. Fix: wrap your own plugin in definePlugin(fn), or call the package’s bare-name installer.

Development:

[@comvi/core] localeDetector() is a .use installer, not a plugin, and it ran during plugin initialization — it was registered with registerPlugin(localeDetector(…)) or definePlugin(localeDetector(…)). Compose it instead: createI18n(…).use(localeDetector(…)). Nothing was installed.

Production: E_INSTALLER_AS_PLUGIN.

Cause: an installer went to registerPlugin() or definePlugin() instead of .use(). Fix: compose it: createI18n(…).use(installer(…)). It throws before anything attaches, so the instance is unchanged.

Development: [@comvi/vue] .use(installer) must return the instance it was given. The Vue text continues A raw plugin goes through .use(definePlugin(f)) or registerPlugin(f); …, and the Next.js text opens with [@comvi/next] and offers .use(nextPlugin(f, { runtime })) instead of registerPlugin(f). Production: E_USE_INSTANCE_IDENTITY, with code.

Cause: an installer handed to a wrapper’s .use() returned a different object than the core instance it was given, which would leave the wrapper’s bound proxies driving an instance your app never sees. Fix: widen the instance in place and return that same instance. Raised by @comvi/vue and @comvi/next.

Development: [@comvi/core] A plugin returned a value. A plugin may only return nothing (`undefined`) or a cleanup function, so nothing was registered. If this was a bare-name installer, compose it with `.use(installer(…))` … If it was your own plugin, use a statement body: `(i18n) => { flag = true; }`. Production: E_PLUGIN_INIT_RETURN.

Cause: a plugin returned something that is neither undefined nor a cleanup function, usually an expression-bodied arrow leaking its last value. Fix: use a statement body.

Development:

[@comvi/core] loader(importMap) requires the import map. A static map of import functions goes here: .use(loader({ uk: () => import('./uk.json') })). A plain loader FUNCTION goes to .use(attachLoader) followed by i18n.registerLoader(fn) — both from @comvi/core/loader.

Production: E_LOADER_IMPORT_MAP.

Cause: loader() was called with no argument. TypeScript rejects that first, so it is reachable only from JavaScript. Fix: pass the map, or use attachLoader plus registerLoader(fn) for a plain loader function.

These two are not E_ codes: they carry no code own-property and are thrown in both build conditions at the acquisition call.

// loader, development
[@comvi/core] This i18n instance has no loader capability. Compose it: .use(attachLoader) from "@comvi/core/loader", or loader(map).
// loader, production
[@comvi/core] missing loader capability — attach @comvi/core/loader
// plugins, development
[@comvi/core] This i18n instance has no plugins capability. Compose it: .use(plugins()) from "@comvi/core/plugins", or the lower-level attachPlugins.
// plugins, production
[@comvi/core] missing plugins capability — attach @comvi/core/plugins

The same omission also shows up as “is not a function” failures. For the loader members there is a development-only exception: attachPlugins installs a marked stand-in for each of them, so reloadMessages() on an instance with only plugins() composed raises the capability message instead of a bare TypeError. Nothing stands in for a missing plugin capability, so onMissingKey is a bare TypeError in both builds.

symptom fix
addActiveNamespace / removeActiveNamespace / reloadMessages / onLoadError is not a function useI18nLoader()
onMissingKey is not a function useI18nPlugins()
vue: i18n.registerLoader / i18n.registerPlugin is not a function i18n.core.*
vue: i18n.core.reloadMessages will not type-check core is typed as the bare instance; use useI18nLoader()

The thrown messages have no IS_DEV branch, so the text is the same in both builds; the two warnings at the end are development only.

[@comvi/next] Locale not set. Call setRequestLocale(locale) in your layout/page first, or configure middleware. Cause: getI18n() ran without an explicit locale, and nothing had named one: neither setRequestLocale() filling the per-request cache nor the middleware setting the x-comvi-locale header. A request that cannot name a locale never triggers init(). Fix: pass the locale, getI18n({ locale }); or render under <ComviProvider locale>, call setRequestLocale(locale) first, or configure the middleware. Calling getLocale() directly gives [@comvi/next] Unable to determine locale. Make sure to call setRequestLocale() in your layout/page or configure middleware.

[@comvi/next] i18n already configured by createNextI18nFromInstance(); setI18n() is a second source. Configure it once — only a same-instance setI18n() repeats.

Either source can appear on either side of the sentence. Cause: server i18n was configured twice from different sources, or setI18n() was called twice with different instances. A repeat call with the same instance is a no-op, because setup files commonly run twice. Fix: configure once. If it appears in development after editing the setup module, restart the dev server. When nothing configured it at all:

[@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] <ComviProvider> needs withComvi({ config }) in next.config: the client reads the i18n instance from that module. Add it, or keep your own "use client" wrapper around <I18nProvider> from @comvi/next/client (the manual setup, also the one for a config whose loader needs server-only code).

Cause: the layout renders the server <ComviProvider> but next.config does not export through withComvi, so the client graph has no instance to hand it. The client half of the same check, a client graph built without the alias, throws the first sentence alone: [@comvi/next] <ComviProvider> needs withComvi({ config }) in next.config: the client reads the i18n instance from that module. Fix: add the plugin and restart the dev server, or stay on the manual setup with your own client wrapper.

withComvi itself refuses a path it cannot find while next.config loads: [@comvi/next] withComvi: the config module <absolute path> does not exist (config: "<what you wrote>", resolved against <cwd>)., followed by the process.cwd() rule and the absolute-path form.

[@comvi/next] Two i18n instances: one from setI18n(), another from the withComvi({ config }) module. Using the first; keep one source. Development only, once. Cause: a setI18n() or createNextI18nFromInstance() registration and the withComvi module export different objects. Fix: delete the registration, or point withComvi at the module that builds the registered instance.

The module’s server utilities have no IS_DEV branch either. The fresh-instance guard is the one development-only check, emitted into the module’s generated code and absent from production builds.

[@comvi/nuxt] i18n not configured. Add "@comvi/nuxt" to `modules` in nuxt.config.ts. Cause: useI18n() ran in an app whose nuxt.config.ts does not list the module, so no plugin provided the instance. Fix: add '@comvi/nuxt' to modules and restart the dev server.

[@comvi/nuxt] `comvi.<key>` is not an option

Section titled “[@comvi/nuxt] `comvi.<key>` is not an option”
[@comvi/nuxt] `comvi.detectBrowserLanguage` is not an option: use `localeCookie` and `localeDetection`.
[@comvi/nuxt] `comvi.fallbackLanguage` is not an option: use `fallbackLocale`.
[@comvi/nuxt] `runtimeConfig.public.comvi.cookieName` is not an option: use `localeCookie`.
[@comvi/nuxt] `comvi.localeDetection.cookieName` is not an option: use `comvi.localeCookie.name`.
[@comvi/nuxt] `comvi.localeDetection.redirectOnFirstVisit` is not an option.

Thrown at build time, one per key, with the option to write instead where one exists; a key under runtimeConfig.public.comvi is reported with that prefix. Cause: a comvi key, or a key inside localeCookie or localeDetection, that the routing schema shared with @comvi/next does not know. Fix: write the option the message names; every one is on Module options. Two more build-time refusals guard localeDetection.resolveAcceptLanguage, which the module copies into the build as source: [@comvi/nuxt] `localeDetection.resolveAcceptLanguage` must be a function. and [@comvi/nuxt] `localeDetection.resolveAcceptLanguage` could not be copied into the build. Write it as a self-contained function expression: it cannot be a bound or native function.

[@comvi/nuxt] comvi i18nModule returned an instance it already returned

Section titled “[@comvi/nuxt] comvi i18nModule returned an instance it already returned”
[@comvi/nuxt] comvi i18nModule returned an instance it already returned. Every call builds a separate instance (the client, and each server request), so return a new instance from createI18n on every call.

Cause: the comvi.i18n.ts factory handed back an instance the module had already received, which happens when the file builds one instance at module scope and returns it from the function. One shared instance would carry one locale and one catalog across every request. Fix: call createI18n(options) inside the factory so each call builds its own instance, as on Nuxt.

[@comvi/nuxt] useTranslation()/loadMessages() after the response ended

Section titled “[@comvi/nuxt] useTranslation()/loadMessages() after the response ended”
[@comvi/nuxt] useTranslation()/loadMessages() after the response ended: the request instance is already destroyed. Translate before responding, or create your own instance.

Cause: the per-request instance was already torn down. The module destroys every instance a request built once Nitro’s afterResponse (or error) hook fires and the request’s event.waitUntil background work has settled; a later call is refused rather than handed an instance nothing will tear down. Fix: call useTranslation() / loadMessages() before the handler returns, or build and own your own instance. See Render on the server and Server utilities.

A refusal from comvi typegen and @comvi/vite-plugin, thrown before either tool writes a file. Text (first line): Generated types have to augment a Comvi module, and <path-to-package.json> declares none. The rest lists the packages it accepts and mentions the augment override.

Cause: the app’s package.json declares neither @comvi/core nor a framework binding (@comvi/react, @comvi/next, @comvi/vue, @comvi/nuxt, @comvi/svelte, @comvi/solid). The rule reads the manifest, not node_modules, so a transitive install does not count. Fix: add the package the app imports t() from, or @comvi/core, to its dependencies. A private wrapper package can be named with augment in .comvirc.json or the Vite plugin’s augment option. See Generated type structure.

Columns: what you did, which part of the library owns the check, the sentence a development build throws in the token’s place, and whether the failure reaches you as a throw (a rejected promise, for the async entry points) or as a report through onError / onLoadError. Reported, then thrown means both happen.

code happens when owner development text reaches you as
E_LOCALE_NOT_SET createI18n() was called without a locale the i18n instance, constructor [@comvi/core] Locale is not set thrown synchronously
E_MESSAGES_NOT_OBJECT the messages option is not a plain object (a production build checks the top level only; development additionally rejects a non-object value inside it) the i18n instance, constructor [@comvi/core] `messages` is not an object thrown synchronously
E_INSTANCE_DESTROYED init() was called after destroy() the i18n instance, init() [@comvi/core] Cannot call init() after destroy(). Create a new i18n instance. reported (source: 'init'), then thrown; init() rejects
E_RESERVED_DEFAULT_PARAMS defaultParams contains a call-control key: locale, ns, fallback, markers, tagInterpolation the i18n instance, constructor and setDefaultParams() [@comvi/core] defaultParams cannot contain call-control keys: locale, ns, fallback, markers, tagInterpolation thrown synchronously
E_NULLISH_DEFAULT_PARAMS a defaultParams value is null or undefined the i18n instance, constructor and setDefaultParams() [@comvi/core] defaultParams values cannot be null or undefined thrown synchronously
E_DEFAULT_PARAMS_GUARANTEED_KEY setDefaultParams() dropped a key the constructor’s type guarantees the i18n instance, setDefaultParams() [@comvi/core] defaultParams must preserve constructor-guaranteed key "…" thrown synchronously
E_ICU_SYNTAX a message uses ICU the active compiler does not implement compiler (@comvi/core default, or @comvi/core/icu) one of the five sentences quoted above thrown at compile time with .code; in production the segment renders literally and is reported (source: 'compile')
E_MISSING_TAG_HANDLER a message contains a tag with no entry in the components map two producers: the strict: true throw in @comvi/core/tags, and the constructor’s default onTagWarning [@comvi/core] Missing handler for tag: <name> depends on tagInterpolation.strict: thrown on true; reported (source: 'translation', tagName set) on 'warn' by the default onTagWarning; silent on the default false, where the tag’s inner text is rendered
E_LOADER_IMPORT_MAP loader() was called with no import map loader capability, loader() quoted above thrown synchronously
E_NO_LOADER_REGISTERED reloadMessages() ran with no loader registered loader capability [@comvi/core] No loader registered. Cannot reload messages. thrown; the promise rejects
E_REGISTER_LOADER_ARG registerLoader() was handed something that is not a function loader capability [@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()), then names the subpath both come from; quoted in full on Troubleshooting thrown synchronously
E_REGISTER_LOADER_ENTRY the import-map loader has no entry for a locale:namespace pair createImportMapLoader / .use(loader(map)) [@comvi/core] registerLoader: no entry for "fr:default" thrown out of the loader, so it becomes a namespace-load failure: loadError / onLoadError
E_PARTIAL_NAMESPACE_LOAD some, but not all, namespaces failed for one locale loader capability [@comvi/core] Partial namespace load failure for "fr": 1/2 failed (admin) reported only (source: 'namespace-load'); never thrown, the load resolves
E_ALL_NAMESPACES_FAILED every namespace failed for one locale loader capability [@comvi/core] Failed to load all namespaces for locale "fr": default, admin reported (source: 'namespace-load'), then thrown; rejects init(), setLocale(), addActiveNamespace()
E_FAILED_RELOAD_TRANSLATIONS every locale in a reloadMessages() call failed loader capability [@comvi/core] Failed to reload messages reported (source: 'namespace-load'), then thrown
E_PLUGIN_INIT_TIMEOUT a plugin’s init did not settle inside its timeout, 10000 ms unless set plugins capability [@comvi/core] Plugin initialization timed out after 10000ms the plugin’s own onError, then reported (source: 'plugin'); rethrown only if the plugin is required
E_PLUGIN_INIT_FAILED a plugin threw something that is not an Error plugins capability [@comvi/core] Plugin initialization failed: … same route as the timeout
E_PLUGIN_INIT_RETURN a plugin returned a value that is neither undefined nor a cleanup function plugins capability quoted above same route as the timeout
E_INSTALLER_AS_PLUGIN an installer was passed to registerPlugin(…) or definePlugin(…) instead of .use(…) plugins capability, during the plugin drain quoted above same route as the timeout
E_REGISTER_LOCALE_DETECTOR registerLocaleDetector() was handed something that is not a function plugins capability [@comvi/core] registerLocaleDetector(): argument must be a function. thrown synchronously
E_REGISTER_POST_PROCESSOR registerPostProcessor() was handed something that is not a function plugins capability [@comvi/core] registerPostProcessor(): argument must be a function. Received: string thrown synchronously
E_USE_INSTANCE_IDENTITY an installer returned something other than the instance .use() handed it the @comvi/vue and @comvi/next wrappers, use() quoted above thrown synchronously, with .code
E_USE_NOT_INSTALLER the function handed to .use() returned something other than the instance it was given the i18n instance, use() quoted above thrown synchronously, with .code

Two @comvi/next diagnostics reach the console rather than a catch, in both builds. Four more name a locale outside routing.locales. Two are development-only: [@comvi/next] getI18n({ locale: "fr" }): "fr" is not in routing.locales (en, de). Call notFound() for an unknown locale before rendering., once per locale, and [@comvi/next] Ignored the x-comvi-locale header "xx": it is not in routing.locales (en, de)., once per process when getLocale() ignores the request header, ending it is not a locale code. when there is no routing export. The other two fire in both builds: [@comvi/next] getPathname: unknown locale "fr". Expected one of: en, de on every such call, and, through i18n.reportError, [@comvi/next] <I18nProvider>: locale "fr" is not in routing.locales (en, de). Skipping locale sync.

[@comvi/next] No loader registered, so SSR loaded nothing. Register one via i18n.registerLoader(...), or compose one on the createNextI18n(...) result: .use(cdnLoader({ cdnUrl })) or .use(loader(map)).

Warned once per instance when a server helper asks for a namespace and the instance has no loader; nothing throws and nothing loads, so the page renders keys or fallbacks. The second, [@comvi/next] Failed to load ${locale}:${namespace}:, precedes the underlying load error for one locale:namespace pair, and the render continues without it; the pair is attempted once per request and reported through loadError, never thrown.

The @comvi/nuxt server utilities warn with the same two stems. With no loader on the instance, once per instance, the message depends on what the instance has:

[@comvi/nuxt] No loader registered, so SSR loaded nothing. This i18n instance has the loader capability: pass the import map where you compose it — createI18n(options).use(loader(map)) in your `i18nModule` factory — or, on an instance composed with .use(attachLoader), register a LoaderFn from comvi.setup: (({ core }) => { core.registerLoader(fn); }).
[@comvi/nuxt] This i18n instance has no loader capability, so SSR loaded nothing. The generated default instance is core's base instance — compose the loader in comvi.i18n.ts (or the file `comvi.i18nModule` names): createI18n(options).use(loader(map)), both imported from "@comvi/nuxt/i18n".

A pair that fails to load logs [@comvi/nuxt] Failed to load ${locale}:${namespace}: before the underlying error, and the call resolves without it.

Each binding throws one message when a hook or component runs outside its provider; none is build-conditional.

message when
[@comvi/react] Hooks must be used within an I18nProvider. any @comvi/react hook but useLocale outside the provider
[@comvi/react] useLocale must be used within an I18nProvider. useLocale() outside the provider
[@comvi/react] Initialization failed: followed by the rejection init() rejected and the provider has no onError; printed with console.error, the default
[@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. useI18n() without app.use(i18n)
[@comvi/vue] <T> component must be used within a Vue app with i18n plugin installed <T> without app.use(i18n)
[@comvi/solid] i18n context not found. Wrap your app with <I18nProvider i18n={i18n}>. useI18n() outside the provider
[@comvi/svelte] i18n context not found. Call setI18nContext(i18n) in your root component (e.g., App.svelte). useI18n() with no setI18nContext() ancestor
<T> renders a tag handler as a component, so a handler that throws is caught by the tag’s error boundary and reported with source: 'translation'; there is no “must return an element” check.

Every message carries [@comvi/plugin-fetch-loader] and the same text in both builds. One is thrown synchronously at .use(); the rest are load failures, so they reach onLoadError and the loadError event with source: 'namespace-load', and init() or setLocale() rejects only when every requested namespace failed.

message entry when
cdnUrl is required both the factory was called without cdnUrl; thrown at .use()
HTTP 404: Not Found both the CDN answered with a status outside 2xx; the status and text are the response’s
Request timeout after 10000ms both the request, body included, outran timeout; the error’s name is TimeoutError
Request aborted: <url> both clearMessages(), destroy() or a superseded setLocale() cancelled the request; the error’s name is AbortError. On the API path the suffix is API translations or API translation fallback instead of a URL
Invalid JSON response from <url> both the body did not parse
Invalid CDN response: "fr:default" must be an object both the body parsed to something that is not a catalog object; on the API path it reads Invalid API response: "namespaces.default.fr" must be an object
Invalid locale: "<value>". Only alphanumeric, underscore, hyphen, dot, and @ characters are allowed; dot-only path segments are rejected. both a locale or namespace cannot be a URL segment, refused before any request is sent; the label is locale, namespace or CDN root namespace
Failed to fetch project info: 500 Internal Server Error /api the project-info request failed after its retries; 404 <url> when both endpoint spellings answered 404
Invalid API response from <url>: "id" must be a number or a path-safe string /api the project-info body has no usable id
API error: 500 Internal Server Error /api an export request answered outside 2xx
No translations found for fr:default /api neither the API nor the fallback map produced a catalog for the pair
  • The context errors of each binding, listed under Binding checks, with their fixes on Troubleshooting.
  • [@comvi/next] Invalid lazy plugin module. Expected a plugin function or { default: pluginFunction }.: thrown by nextPlugin() when a lazily imported module is neither a plugin function nor a module whose default is one. Import the plugin function itself, or export it as default.
  • The Comvi REST API has its own codes, returned in an HTTP response body: Error Reference.