# Solid

Add Comvi i18n to a SolidJS app with @comvi/solid: the provider, t() and locale as signal-backed accessors, <T> for rich text and the per-request i18n instance for SSR.

Canonical: https://comvi.io/docs/i18n/solid

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](https://stackblitz.com/github/comvi-io/comvi-js/tree/main/examples/solid) and switch the locale: plurals update, `<T>` keeps its link and the catalog for the new locale arrives on first use.

## Install
**npm**

```bash
npm install @comvi/solid
```

**pnpm**

```bash
pnpm add @comvi/solid
```

**yarn**

```bash
yarn add @comvi/solid
```

**bun**

```bash
bun add @comvi/solid
```

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

## Setup

`src/index.tsx`

```tsx

const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: {
    en: { greeting: 'Hello, {name}!' },
    uk: { greeting: 'Привіт, {name}!' },
},
});

render(
() => (

),
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](/docs/i18n/quick-start).

## Use in components

`src/App.tsx`

```tsx

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.

### Markup inside a translation

`src/locales/en.json`

```json
{
"help": "For support, <link>visit our docs</link> or <bold>email us</bold>."
}
```

`src/Help.tsx`

```tsx

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.

## Load from Comvi

`src/index.tsx`

```tsx

const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
}).use(cdnLoader({ cdnUrl: 'https://cdn.comvi.io/<project-token>' }));

render(
() => (
    <I18nProvider i18n={i18n} fallback={<div class="spinner" />}>

),
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](/docs/i18n/connect-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](/docs/platform/cdn#url-format).
If your keys use ICU (`{count, plural, …}`), add `compiler: icuCompiler` as on
[Pluralize with ICU](/docs/i18n/pluralization). The platform and the compiler are independent:
either works without the other.
## First paint

`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.

## Server-side rendering

`<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](/docs/i18n/ssr#sveltekit-and-solidstart).

> **One i18n instance per request**
>
> Never share a module-level instance across SSR requests: `init()` races between them, and
> `ssrLocale` is written onto the instance during the render phase, so one request's locale lands in
> the instance another request is rendering with. A module-level instance is fine for a client-only
> app, where there is one visitor.

## Tracking scope

`src/Greeting.tsx`

```tsx

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.

## How it works

`<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.

## Errors

| 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](/docs/i18n/errors#a-capability-was-never-composed) |

More messages and their fixes are on [Troubleshooting](/docs/i18n/troubleshooting).

## Read next

- [Connect to Comvi](/docs/i18n/connect-comvi): the CDN URL, and a wording change that ships without a redeploy.
- [Load translations on demand](/docs/i18n/loading-translations): `loader(map)`, namespaces and `useI18nLoader()`.
- [Solid API reference](/docs/i18n/api/solid): every export, the standalone primitives and `<T>` props.
