Skip to content
Comvi

Solid

View .md

With a SolidJS app, @comvi/solid is <I18nProvider> at the root, useI18n() in components, and <T> for translations that carry markup. t() and locale() are signal-backed, so they update exactly where Solid is tracking them.

Open the Solid example in StackBlitz and switch the locale: plurals update, <T> keeps its link and the catalog for the new locale arrives on first use.

Terminal window
npm install @comvi/solid

@comvi/solid peer-depends on solid-js (^1.8) and brings @comvi/core.

src/index.tsx
import { render } from 'solid-js/web';
import { createI18n, I18nProvider } from '@comvi/solid';
import App from './App';
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: {
en: { greeting: 'Hello, {name}!' },
uk: { greeting: 'Привіт, {name}!' },
},
});
render(
() => (
<I18nProvider i18n={i18n}>
<App />
</I18nProvider>
),
document.getElementById('root')!,
);

createI18n() returns the instance and <I18nProvider> initializes it from a createEffect, so there is no init() to write. Its other props are autoInit, onError, fallback and ssrLocale. The catalog options are on the Quick start.

src/App.tsx
import { useI18n } from '@comvi/solid';
export default function App() {
const { t, locale, setLocale } = useI18n();
return (
<>
<h1>{t('greeting', { name: 'Alice' })}</h1>
<select value={locale()} onChange={(e) => setLocale(e.currentTarget.value)}>
<option value="en">English</option>
<option value="uk">Українська</option>
</select>
</>
);
}

locale is an Accessor<string>, so it is locale() in JSX. setLocale() resolves once the new locale’s catalog has loaded, and every t() Solid is tracking re-runs with the new text. useI18n('admin') binds a namespace at the type level too. The formatNumber, formatDate, formatCurrency and formatRelativeTime members follow the locale like t(), and a trailing locale argument overrides it.

src/locales/en.json
{
"help": "For support, <link>visit our docs</link> or <bold>email us</bold>."
}
src/Help.tsx
import { T } from '@comvi/solid';
export function Help() {
return (
<T
i18nKey="help"
components={{
link: (props) => <a href="/docs">{props.children}</a>,
bold: (props) => <strong>{props.children}</strong>,
}}
/>
);
}

Each tag maps to a plain function or an element name such as { policy: 'a' }. Every prop other than i18nKey, components, params, ns, locale, fallback, markers and children is an interpolation param. Plain t() does not parse tags.

src/index.tsx
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')!,
);

Install @comvi/plugin-fetch-loader and compose .use(cdnLoader()) inside the createI18n expression, because the provider’s effect calls init() and init() is what runs the CDN plugin. The rest of the walkthrough is on Connect to Comvi.

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.

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.

fallback renders in place of the children until the instance is initialized, so raw keys never paint. The gate reads the provider’s isInitialized signal, not isInitializing: auto-init starts from an effect after the tree is built, so a gate on “is it initializing” would be open for exactly the paint the raw keys would appear in. Leave the prop off when the catalog is inline: isInitialized() is false during a server render, so the fallback is what the server would paint.

<I18nProvider> initializes from a createEffect, and effects do not run during Solid’s server render, so a loader fetches nothing there and the instance must already hold its catalog. Load the messages in a server function with a fresh instance per request, render through <I18nProvider autoInit={false}>, and pass ssrLocale on both sides so the server render and the hydrating client agree on the locale. The complete SolidStart example is on Render on the server.

src/Greeting.tsx
import { useI18n } from '@comvi/solid';
export function Greeting() {
const { t } = useI18n();
return <h1>{t('greeting', { name: 'Alice' })}</h1>;
}

t() and tRaw() read the locale, cache-revision and default-namespace signals at call time, so they are reactive only inside JSX, a createMemo or a createEffect. Solid does not re-render a component: const greeting = t('greeting') in the component body is computed once and never updates. tRaw('key', { locale: 'de' }) pins a locale and skips the global locale signal, so a switch does not recompute it.

<I18nProvider> does two things in its body, during the render phase. It applies ssrLocale to the instance when given, before anything reads the locale, so t() and the signal cannot disagree on the first paint. That holds when the locale’s catalogs are already on the instance, inline or loaded earlier; when they still have to load, the first render is in the instance’s current locale and the switch lands when the load completes. And it creates the signals: locale from localeChanged, isLoading and isInitializing from loadingStateChanged, isInitialized from initialized and destroyed, a default-namespace signal, and a cache-revision counter over every other revision event. They are created with Solid’s from() inside a memo over the i18n prop, so swapping the instance rebuilds them and disposes the old subscriptions.

useI18n() returns functions, not values. Its tRaw() reads the locale signal (unless the call pins a locale), the cache-revision signal, and the default-namespace signal (unless useI18n(ns) bound one), then calls the instance. Those reads are the dependencies, which is the entire reactivity model: a t() inside JSX, a createMemo or a createEffect re-runs when one of those signals changes, and a t() in a component body runs once because nothing is tracking it. The four formatters read the locale signal the same way, unless the call passes a trailing locale. Solid updates the text node, not the component.

init() runs from createEffect once the tree is built, on the client only. It raises isInitializing, runs the plugins, loads the default namespace, in the current locale and along the fallback chain, then sets isInitialized and emits initialized, which flips the <Show> the fallback prop gates on. A failed fallback catalog is reported and nothing more. With an inline catalog the load is a cache hit; with a loader this is where the first fetch happens, and the fallback covers it.

setLocale() is the instance’s setLocale(). When a loader is composed, the new locale’s active namespaces load before the locale is assigned, and localeChanged fires only then, so the locale signal moves once, after the catalog is complete. When several switches overlap, the latest wins and the superseded ones apply nothing.

A missing key returns the key itself and emits missingKey, so a raw key on screen means a missing translation rather than a broken provider; strict: 'dev' adds a console warning in development. On the server the instance is a mutable object, and the ssrLocale write makes a module-level one unsafe to share: build it inside the request handler.

what happened message
useI18n() outside <I18nProvider> [@comvi/solid] i18n context not found. Wrap your app with <I18nProvider i18n={i18n}>.
useI18nLoader() on an instance with no loader composed development: [@comvi/core] This i18n instance has no loader capability. Compose it: .use(attachLoader) from "@comvi/core/loader", or loader(map).; a production build throws the short form quoted on Error codes

More messages and their fixes are on Troubleshooting.