Skip to content
Comvi

Limitations

View .md

Every gap in one list, each with what happens instead. The i18n instance is the object createI18n() returns: text with {param} interpolation, the cache, events and default params, plus whatever you composed with .use().

  • number, date and time argument types are not implemented, and are not planned. {price, number} fails with E_ICU_SYNTAX rather than rendering something wrong. Format the value in code and interpolate it as a plain {param}, or quote the braces ('{'…'}') to keep the text literal. See Format numbers, dates and currency.

  • The plural offset: form is not implemented. Drop offset: and write the branches out.

  • plural, selectordinal and select are the whole of what the compiler implements, each with a choice list. See ICU reference.

  • A missing interpolation parameter renders as its own placeholder. t('greet') on "Hello, {name}!" renders "Hello, {name}!", plus one development warning per template and parameter. An explicit null still renders as an empty string. Opt out with missingParam: 'drop'.

  • ICU is off unless you name it. No constructor in the library turns ICU on by itself, createNextI18n included.

    Without compiler: icuCompiler, an ICU message ({count, plural, …}) fails in development at ingestion, before anything renders. Inline, it throws E_ICU_SYNTAX at the call that carries the catalog; over a loader it fails that namespace’s load instead: E_ICU_SYNTAX reaches onLoadError, the namespace stays empty, and when that was the only namespace init() rejects with [@comvi/core] Failed to load all namespaces for locale "…": … (E_ALL_NAMESPACES_FAILED in production) rather than with E_ICU_SYNTAX. In production it renders the message text literally and reports the error through onError (console.error when no handler is set).

  • SolidJS: server-side loading is not supported. <I18nProvider> initializes from a createEffect, and effects do not run during Solid’s server render, so a catalog a loader would have fetched is absent server-side. An inline catalog renders fine.
  • The module-scope rule is not the same in every framework. Next.js requires one shared i18n instance; Solid, Svelte, Vue and Nuxt each forbid it. The four rules sit side by side on Render on the server.
  • Ambient string-API tags must be imported in both bundles. One import "@comvi/core/tags" registers the grammar per bundle, and an SSR app has two. See Tag interpolation.
  • Next.js: a client-side locale switch without a routing prop writes the default cookie. <I18nProvider> from @comvi/next/client records a switch in NEXT_LOCALE with the default attributes unless it is given routing, even when defineRouting() renamed the cookie or set localeCookie: false. Pass the routing config to the provider.
  • A catalog is never evicted. Everything addMessages and a loader merge into an i18n instance stays in its translation cache until clearMessages() or destroy(). The three caches that are bounded, compiled templates, Intl formatters and plural rules, hold 1000 entries each, FIFO, and are shared by every instance in the process; that is an entry limit, not a byte limit, so 1000 long messages cost more than 1000 short ones. A client app with a fixed set of locales has a bounded model and nothing here to tune.
  • A long-lived server that loads many locales or namespaces into one instance needs a lifecycle instead of a limit, and which code provides that lifecycle differs by framework: @comvi/nuxt destroys its per-request instances for you; a per-request instance you build yourself (Vue SSR, Solid, Svelte) is garbage-collected, and collection runs no plugin cleanup, so an instance with a plugin that holds a timer or a socket needs await i18n.destroy(); the shared @comvi/next server instance holds every catalog it ever loaded for the life of the process. See Render on the server.
  • <T> maps only the tags you give it. A tag with no mapping falls back to its inner text. There is no HTML passthrough, because there is no HTML sink anywhere in the render path.
  • t() does not parse tags unless @comvi/core/tags is imported, and that import is process-wide and retroactive, with no per-instance opt-out.
  • The in-context editor needs a Comvi account. @comvi/plugin-in-context-editor requires the Comvi platform and does not work standalone. Its production entry is a no-op installer, so a production build that composes it carries no editor bytes.
  • The browser extension sees only an instance that composed devtools. Discovery is opt-in, through .use(devtools()), which publishes the instance on window.__COMVI__; inContextEditor() composes it for you.
  • cdnLoader reads a public distribution only. Draft translations, private projects and auth-gated content need API mode: fetchLoader from @comvi/plugin-fetch-loader/api plus an apiKey on the i18n instance. The /api entry carries the API request path in the bundle either way, which is why the root entry is CDN-only. See Fetch loader.
  • A fallback catalog that does not exist is requested once per namespace activation. With a loader, init() and addActiveNamespace() fetch each locale in fallbackLocale for the namespace; a missing fallback catalog fails, is reported through onLoadError, and is requested again only when a namespace is activated again, never by a locale switch.
  • Typed keys are generated, not inferred. TranslationKeys is a declaration-merging interface, and @comvi/cli (from the platform) or @comvi/vite-plugin (from local JSON) writes it. Nothing infers key types from a runtime catalog. See Type-check translation keys.

Typed keys: three shapes t() does not check

Section titled “Typed keys: three shapes t() does not check”

t() has one call signature, which is what makes a wrong call report the real problem on its first line (Read the type errors). Three shapes fall outside it, each a consequence of that one signature.

  • A key the call site could not narrow to one literal gets loose params. t(cond ? 'greeting' : 'farewell') compiles even when greeting declares required params, and so does a params object valid for neither arm. The key itself is still checked: an arm that is not a key is rejected. When the params differ between arms, branch the call; hoisting the key into a const does not help. Closing this would need the arms’ params intersected, which costs the check on t('key', { ns }): a silently accepted wrong namespace, which is worse.
  • Parameters<typeof t> is never once keys are generated. The key parameter’s type depends on a second, inferred type parameter, and Parameters cannot see through that. Reach the argument list through ArgsFor<'your.key'> instead, which resolves normally. With no generated keys, Parameters still works.
  • t is not assignable to a (key: string) => string callback once keys are generated. The key is a closed union of your keys, and string is not one of them. Hand such a helper t as (k: string) => string. The two-argument shape (key: string, params?: Record<string, unknown>) => string never accepted t and still does not.
  • Node 20 or newer for the runtime packages; Node 22 or newer for @comvi/cli and @comvi/vite-plugin. Exact ranges are each package’s engines field.
  • Peer ranges are each package’s peerDependencies field. Svelte is ^5.3.0; Svelte 4 is not supported, because the bindings use runes. The full table is on Compatibility.
  • No eval and no new Function in any runtime package, so every package runs under a strict CSP without unsafe-eval.

An ICU format grammar, a runtime translation-memory or fuzzy-match layer, and automatic key extraction from source. Key extraction is the platform’s job, not the library’s.

  • Troubleshooting: the errors you are most likely to hit, and the fix for each.
  • Error codes: every error code the library raises, and what it means.
  • Compatibility: peer ranges and Node floors for every package.