Limitations
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().
ICU grammar
Section titled “ICU grammar”-
number,dateandtimeargument types are not implemented, and are not planned.{price, number}fails withE_ICU_SYNTAXrather 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. Dropoffset:and write the branches out. -
plural,selectordinalandselectare 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 explicitnullstill renders as an empty string. Opt out withmissingParam: 'drop'. -
ICU is off unless you name it. No constructor in the library turns ICU on by itself,
createNextI18nincluded.Without
compiler: icuCompiler, an ICU message ({count, plural, …}) fails in development at ingestion, before anything renders. Inline, it throwsE_ICU_SYNTAXat the call that carries the catalog; over a loader it fails that namespace’s load instead:E_ICU_SYNTAXreachesonLoadError, the namespace stays empty, and when that was the only namespaceinit()rejects with[@comvi/core] Failed to load all namespaces for locale "…": …(E_ALL_NAMESPACES_FAILEDin production) rather than withE_ICU_SYNTAX. In production it renders the message text literally and reports the error throughonError(console.errorwhen no handler is set).
Server-side rendering
Section titled “Server-side rendering”- SolidJS: server-side loading is not supported.
<I18nProvider>initializes from acreateEffect, 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
routingprop writes the default cookie.<I18nProvider>from@comvi/next/clientrecords a switch inNEXT_LOCALEwith the default attributes unless it is givenrouting, even whendefineRouting()renamed the cookie or setlocaleCookie: false. Pass the routing config to the provider.
Memory
Section titled “Memory”- A catalog is never evicted. Everything
addMessagesand a loader merge into an i18n instance stays in its translation cache untilclearMessages()ordestroy(). The three caches that are bounded, compiled templates,Intlformatters 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/nuxtdestroys 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 needsawait i18n.destroy(); the shared@comvi/nextserver instance holds every catalog it ever loaded for the life of the process. See Render on the server.
Rich text
Section titled “Rich text”<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/tagsis imported, and that import is process-wide and retroactive, with no per-instance opt-out.
Platform features
Section titled “Platform features”- The in-context editor needs a Comvi account.
@comvi/plugin-in-context-editorrequires 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 onwindow.__COMVI__;inContextEditor()composes it for you. cdnLoaderreads a public distribution only. Draft translations, private projects and auth-gated content need API mode:fetchLoaderfrom@comvi/plugin-fetch-loader/apiplus anapiKeyon the i18n instance. The/apientry 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()andaddActiveNamespace()fetch each locale infallbackLocalefor the namespace; a missing fallback catalog fails, is reported throughonLoadError, and is requested again only when a namespace is activated again, never by a locale switch.
Tooling
Section titled “Tooling”- Typed keys are generated, not inferred.
TranslationKeysis 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 whengreetingdeclares 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 aconstdoes not help. Closing this would need the arms’ params intersected, which costs the check ont('key', { ns }): a silently accepted wrong namespace, which is worse. Parameters<typeof t>isneveronce keys are generated. The key parameter’s type depends on a second, inferred type parameter, andParameterscannot see through that. Reach the argument list throughArgsFor<'your.key'>instead, which resolves normally. With no generated keys,Parametersstill works.tis not assignable to a(key: string) => stringcallback once keys are generated. The key is a closed union of your keys, andstringis not one of them. Hand such a helpert as (k: string) => string. The two-argument shape(key: string, params?: Record<string, unknown>) => stringnever acceptedtand still does not.
Environment
Section titled “Environment”- Node 20 or newer for the runtime packages; Node 22 or newer for
@comvi/cliand@comvi/vite-plugin. Exact ranges are each package’senginesfield. - Peer ranges are each package’s
peerDependenciesfield. Svelte is^5.3.0; Svelte 4 is not supported, because the bindings use runes. The full table is on Compatibility. - No
evaland nonew Functionin any runtime package, so every package runs under a strict CSP withoutunsafe-eval.
Not on the roadmap
Section titled “Not on the roadmap”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.
Read next
Section titled “Read next”- 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.