Skip to content
Comvi

@comvi/solid

View .md

The SolidJS binding (peer solid-js ^1.8): a provider, a useI18n() hook whose reactive members are accessors, low-level primitives and a <T> component.

  • Values: I18nProvider, T, useI18n, useI18nInstance, useI18nLoader, useI18nPlugins, createLocaleSignal, createDefaultNamespaceSignal, createDefaultNamespaceState, createDefaultParamsSignal, createDefaultParamsState, createLoadingSignal, createInitializingSignal, createInitializedSignal, createCacheRevisionSignal, createI18n, loader, attachLoader, plugins, definePlugin, icuCompiler, devtools.
  • Types: I18nProviderProps, UseI18nReturn, UseI18nLoaderReturn, UseI18nPluginsReturn, TProps, ComponentMap, DevtoolsOptions, I18n, every @comvi/core type.

I18n is a type only; @comvi/core/tags is not re-exported. TagComponentConfig comes from @comvi/core/rich-text.

src/i18n.ts
import { createI18n, loader, plugins } from '@comvi/solid';
export const i18n = createI18n({ locale: 'en', fallbackLocale: 'en' })
.use(loader({ en: () => import('./locales/en.json') }))
.use(plugins());
const I18nProvider: ParentComponent<I18nProviderProps>
Prop Type Default Meaning
i18n AnyI18nInstance required The i18n instance; any default-params type
children JSX.Element required The subtree
autoInit boolean true Call init() on mount
fallback JSX.Element none Rendered instead of children until isInitialized; also what renderToString paints. undefined does not gate; false or null gates blank
ssrLocale string none Written onto the instance in the render phase, so SSR needs a per-request instance. It paints on the first render when that locale’s catalogs are already available; otherwise the switch lands when they load
onError (error: Error) => void none Called when auto-initialization fails, after the instance’s own onError
src/App.tsx
import { I18nProvider } from '@comvi/solid';
import { i18n } from './i18n';
export function App() {
return (
<I18nProvider i18n={i18n} fallback={<p>Loading</p>}>
<h1>Hello</h1>
</I18nProvider>
);
}
function useI18n<DefaultNS extends string | undefined = undefined, D extends DefaultTranslationParams = {}>(ns?: DefaultNS): UseI18nReturn<DefaultNS, D>

Reactive members are accessors: locale(), isLoading(). t() in JSX tracks the locale and the cache revision. A bound namespace adds its bare keys to t’s key union.

Returned Type Meaning
t NamespaceBoundTranslateFn<DefaultNS, D, string> Translate, reactive
tRaw NamespaceBoundTranslateFn<DefaultNS, D, TranslationResult> Rich-text half
locale Accessor<string> Current locale
isLoading, isInitializing, isInitialized Accessor<boolean> Loading flags
cacheRevision Accessor<number> Cache and config revision
defaultParams Accessor<DefaultParamsSnapshot<D>> Shallow snapshot
dir () => 'ltr' | 'rtl' Text direction
setLocale (locale: string) => Promise<void> setLocale
addMessages, setFallbackLocale, setDefaultParams, clearMessages, hasLocale, hasTranslation, on, reportError as on the instance Bound functions
getLoadedLocales, getActiveNamespaces, getDefaultNamespace, getMessagesCache () => … Snapshots
formatNumber, formatDate, formatCurrency, formatRelativeTime bound, trailing locale? Reactive: read the locale signal at call time, like t(); a trailing locale overrides it
src/Greeting.tsx
import { useI18n } from '@comvi/solid';
export function Greeting() {
const { t, locale, setLocale } = useI18n();
return (
<button onClick={() => void setLocale(locale() === 'en' ? 'de' : 'en')}>
{t('greeting', { name: 'Alice' })}
</button>
);
}
function useI18nLoader(): UseI18nLoaderReturn // AcquiredLoaderApi
function useI18nPlugins(): UseI18nPluginsReturn // AcquiredPluginsApi

useI18nLoader() gives addActiveNamespace, addActiveNamespaces, removeActiveNamespace, removeActiveNamespaces, reloadMessages, onLoadError; useI18nPlugins() gives onMissingKey. Each throws, in development and production, when the instance lacks the capability. Cached per instance.

src/LazyNamespace.tsx
import { onMount } from 'solid-js';
import { useI18n, useI18nLoader } from '@comvi/solid';
export function LazyNamespace() {
const { t } = useI18n('dashboard');
const { addActiveNamespace } = useI18nLoader();
onMount(() => void addActiveNamespace('dashboard'));
return <h1>{t('page.title')}</h1>;
}
function useI18nInstance(): WrapperI18nInstance

The i18n instance itself, not a signal. Throws outside <I18nProvider>.

const T: Component<TProps>
type TProps = TBaseProps & Record<string, unknown>
type ComponentMap = Record<string, ComponentHandler>
// ComponentHandler and ComponentConfigTarget are the shape of one ComponentMap entry; neither is exported
type ComponentHandler =
| string
| ((props: { children?: JSX.Element }) => JSX.Element)
| (Omit<TagComponentConfig, 'props'> & { props?: Record<string, any> } & (
| { tag: ComponentConfigTarget; component?: ComponentConfigTarget }
| { component: ComponentConfigTarget; tag?: ComponentConfigTarget }
))
type ComponentConfigTarget = string | ((props: any) => JSX.Element)
Prop Type Meaning
i18nKey keyof TranslationKeys | PermissiveKey Required
params TranslationParams Interpolation params; any other prop is merged in
ns, locale, fallback string Overrides; with locale set the component does not recompute on a locale change
markers boolean false turns the in-context editor’s markers off for this element when they cause a problem; it is then not editable in context
components ComponentMap Tag name to a tag name, a component, or a { tag | component, props } config
children JSX.Element Rendered when the key is missing, no fallback was given and onMissingKey returned nothing

A handler that throws is reported as reportError(error, { source: 'translation', tagName }) and the tag renders its children, on the server and the client alike. A non-callable opaque handler renders its children silently.

src/Rich.tsx
import { T } from '@comvi/solid';
export function Rich() {
return (
<>
<T
i18nKey="rich.message"
components={{
bold: 'strong',
highlight: ({ children }) => <mark>{children}</mark>,
link: { tag: 'a', props: { href: '/about' } },
}}
/>
<T i18nKey="optional.feature">Default content</T>
</>
);
}

Each takes an i18n instance and runs inside a reactive context. A Signal reads; a State returns [accessor, setter] and the setter writes through to the instance.

function createLocaleSignal(i18n: WrapperI18nInstance): Accessor<string>
function createDefaultNamespaceSignal(i18n: WrapperI18nInstance): Accessor<string>
function createDefaultNamespaceState(i18n: WrapperI18nInstance): [Accessor<string>, (namespace: string) => void]
function createDefaultParamsSignal<D>(i18n: WrapperI18nInstance<D>): Accessor<DefaultParamsSnapshot<D>>
function createDefaultParamsState<D>(i18n: WrapperI18nInstance<D>): [Accessor<DefaultParamsSnapshot<D>>, (params: SetDefaultParamsArg<D>) => void]
function createLoadingSignal(i18n: WrapperI18nInstance): Accessor<boolean>
function createInitializingSignal(i18n: WrapperI18nInstance): Accessor<boolean>
function createInitializedSignal(i18n: WrapperI18nInstance): Accessor<boolean>
function createCacheRevisionSignal(i18n: WrapperI18nInstance): Accessor<number>
Primitive Tracks
createLocaleSignal localeChanged
createDefaultNamespaceSignal, createDefaultNamespaceState defaultNamespaceChanged; the setter calls setDefaultNamespace
createDefaultParamsSignal, createDefaultParamsState configChanged; the setter calls setDefaultParams
createLoadingSignal loadingStateChanged (isLoading)
createInitializingSignal loadingStateChanged (isInitializing)
createInitializedSignal initialized and destroyed
createCacheRevisionSignal every revision event except localeChanged and loadingStateChanged