Skip to content

SolidJS i18n Integration

Comvi’s SolidJS integration uses signals and context providers for fine-grained reactivity. Translations update only the DOM nodes that depend on them, with no unnecessary re-renders.

Terminal window
pnpm add @comvi/solid @comvi/core @comvi/plugin-fetch-loader
  1. Create the i18n instance

    src/i18n.ts
    import { createI18n } from '@comvi/core';
    import { FetchLoader } from '@comvi/plugin-fetch-loader';
    export const i18n = createI18n({
    locale: 'en',
    fallbackLocale: 'en',
    })
    .use(FetchLoader({
    cdnUrl: 'https://cdn.comvi.io/your-distribution-id',
    }));
  2. Wrap your app with the provider

    src/index.tsx
    import { render } from 'solid-js/web';
    import { I18nProvider } from '@comvi/solid';
    import { i18n } from './i18n';
    import App from './App';
    // SPA only: awaiting init() before render() avoids a flash of untranslated content.
    // <I18nProvider> auto-initializes anyway; you can drop this line if a brief flash is fine.
    await i18n.init();
    render(
    () => (
    <I18nProvider i18n={i18n}>
    <App />
    </I18nProvider>
    ),
    document.getElementById('root')!,
    );
  3. Use in components

    src/components/Greeting.tsx
    import { useI18n } from '@comvi/solid';
    function Greeting() {
    const { t } = useI18n();
    return (
    <div>
    <h1>{t('hello.world')}</h1>
    <p>{t('welcome.message', { name: 'Alice' })}</p>
    </div>
    );
    }

The primary hook for accessing translations and locale state. All returned values are reactive signals.

import { useI18n } from '@comvi/solid';
function MyComponent() {
const { t, locale, isLoading, setLocale } = useI18n();
return (
<Show when={!isLoading()} fallback={<p>Loading...</p>}>
<h1>{t('page.title')}</h1>
<p>Current locale: {locale()}</p>
</Show>
);
}

Returned values:

PropertyTypeDescription
t(key, params?) => stringTranslate a key with optional parameters
localeAccessor<string>Current language (reactive signal)
isLoadingAccessor<boolean>Whether translations are being loaded
setLocale(lang: string) => Promise<void>Switch to a different locale

Use the setLocale function returned from useI18n(). Because Solid uses signals, all translated strings update automatically:

import { useI18n } from '@comvi/solid';
function LanguageSwitcher() {
const { locale, setLocale } = useI18n();
return (
<select
value={locale()}
onChange={(e) => setLocale(e.target.value)}
>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Francais</option>
</select>
);
}

For translations containing HTML or nested components, use <T> for safe interpolation:

import { T } from '@comvi/solid';
function LegalNotice() {
return (
// Translation: "Read our <link>terms of service</link>"
<T
i18nKey="legal.tos"
components={{
link: (props) => <a href="/terms">{props.children}</a>,
}}
/>
);
}

Load translations from a specific namespace:

function Dashboard() {
const { t } = useI18n('dashboard');
return <h1>{t('page.title')}</h1>;
}

useI18n() exposes onMissingKey and onLoadError (bound to the core instance). Register them once near the top of your app and clean up with onCleanup. The onMissingKey callback may return a replacement (TranslationResult | void):

import { onCleanup } from 'solid-js';
import { useI18n } from '@comvi/solid';
function I18nDiagnostics() {
const { onMissingKey, onLoadError } = useI18n();
const offMissing = onMissingKey((key, locale, namespace) => {
console.warn(`Missing: ${namespace}:${key} (${locale})`);
return `[${key}]`; // optional fallback
});
const offError = onLoadError((locale, namespace, error) => {
console.error(`Failed to load ${namespace} for ${locale}:`, error);
});
onCleanup(() => {
offMissing();
offError();
});
return null;
}

You can also handle a missing key inline with <T>’s fallback prop or slotted fallback content:

<T i18nKey="optional.banner" fallback="Welcome!" />
<T i18nKey="optional.note">
<p>Default note shown when the key is missing.</p>
</T>

SolidJS signals make Comvi translations fine-grained reactive. Here are common patterns.

Use createMemo when you need a derived translation value:

import { createMemo } from 'solid-js';
import { useI18n } from '@comvi/solid';
function Greeting(props: { name: string }) {
const { t } = useI18n();
const greeting = createMemo(() =>
t('greeting', { name: props.name })
);
return <p>{greeting()}</p>;
}

Run logic whenever the locale changes with createEffect:

import { createEffect } from 'solid-js';
import { useI18n } from '@comvi/solid';
function DocumentLang() {
const { locale } = useI18n();
createEffect(() => {
document.documentElement.lang = locale();
});
return null;
}

Show loading states while translations are fetched:

import { Switch, Match } from 'solid-js';
import { useI18n } from '@comvi/solid';
function Page() {
const { t, isLoading } = useI18n();
return (
<Switch>
<Match when={isLoading()}>
<div class="skeleton">Loading...</div>
</Match>
<Match when={!isLoading()}>
<h1>{t('page.title')}</h1>
</Match>
</Switch>
);
}

Combine the fetch loader with lazy loading to fetch translations on demand:

src/i18n.ts
import { createI18n } from '@comvi/core';
import { FetchLoader } from '@comvi/plugin-fetch-loader';
export const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
})
.use(FetchLoader({
cdnUrl: 'https://cdn.comvi.io/your-distribution-id',
// Only load translations when needed (default behavior)
}));

Translations are fetched when the user switches locales. Already-loaded locales are served from cache.

@comvi/solid does not ship a SolidStart adapter. For SSR, load translations in a server function (a fresh i18n instance per request), pass them to the route, and render through <I18nProvider autoInit={false}> so hydration starts from the same cache.

src/lib/i18n.ts
import { createI18n } from '@comvi/solid';
import { FetchLoader } from '@comvi/plugin-fetch-loader';
import { query } from '@solidjs/router';
export const loadI18n = query(async (locale: string) => {
'use server';
const i18n = createI18n({ locale, fallbackLocale: 'en' }).use(
FetchLoader({ cdnUrl: 'https://cdn.comvi.io/your-distribution-id' }),
);
await i18n.init();
const namespace = i18n.getDefaultNamespace();
return {
locale,
messages: {
[`${locale}:${namespace}`]: i18n.getTranslations(locale, namespace) ?? {},
},
};
}, 'comvi-i18n');

autoInit={false} tells <I18nProvider> not to call init() again — the data is already seeded via translation. Set the HTML lang attribute from the reactive locale:

src/routes/index.tsx
import { createAsync } from '@solidjs/router';
import { Show } from 'solid-js';
import { createI18n, I18nProvider, useI18n } from '@comvi/solid';
import { loadI18n } from '~/lib/i18n';
export default function Home() {
const data = createAsync(() => loadI18n('en'));
return (
<Show when={data()}>
{(ready) => {
const i18n = createI18n({
locale: ready().locale,
fallbackLocale: 'en',
translation: ready().messages,
});
return (
<I18nProvider i18n={i18n} autoInit={false}>
<Content />
</I18nProvider>
);
}}
</Show>
);
}
function Content() {
const { t, locale } = useI18n();
return (
<html lang={locale()}>
<body>
<h1>{t('home.title')}</h1>
</body>
</html>
);
}

If you lazy-load extra namespaces, include them in the returned messages before rendering any component that needs them.

Enable type-safe translations with auto-generated types. The Comvi CLI emits a .d.ts that augments the TranslationKeys interface in @comvi/core — importing it once anywhere in your project gives t() autocomplete and parameter validation:

src/i18n.ts
import { createI18n } from '@comvi/core';
// Side-effect import — augments @comvi/core's TranslationKeys interface
import './types/i18n';
export const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
});

Now t('home.title') autocompletes keys and t('greeting', { name }) validates parameters at compile time. See Type-Safe Translations for the full setup.