Skip to content

Internationalization (i18n)

PreffX uses a single root-level language signal that drives all translations. The language is stored in the <html lang="..."> attribute and synchronized with the lang signal, so changing it updates every dictionary automatically.

Reading the language

The lang utility is a read-only signal holding the current language code:

tsx
import type { PC } from 'preffx';

export const CurrentLang: PC = (_props, { lang }) => {
    return <span>Current language: {lang}</span>;
};

Inside computed blocks the value is tracked reactively:

tsx
import type { PC } from 'preffx';

export const Caption: PC = (_props, { computed, lang }) => {
    const greeting = computed(() =>
        lang.value === 'ru' ? 'Привет' : 'Hello'
    );
    return <p>{greeting}</p>;
};

Switching the language

Call setLang to change the language. It writes to the <html lang> attribute and, in turn, updates the lang signal used by all dictionaries.

tsx
import type { PC } from 'preffx';

export const LangSwitch: PC = (_props, { setLang }) => {
    return (
        <div>
            <button onClick={() => setLang('en')}>EN</button>
            <button onClick={() => setLang('ru')}>RU</button>
        </div>
    );
};

Simple dictionaries

Pass a plain object keyed by language, then select the active one through computed:

tsx
import type { PC } from 'preffx';

const dictionary = {
    en: { openProfile: 'Open profile', showStats: 'Show statistics' },
    ru: { openProfile: 'Открыть профиль', showStats: 'Показать статистику' }
};

export const App: PC = (_props, { computed, lang }) => {
    const captions = computed(() => dictionary[lang.value] || dictionary.en);
    return (
        <div>
            <button>{captions.openProfile}</button>
            <button>{captions.showStats}</button>
        </div>
    );
};

Lazy dictionaries with dict

When a locale is large or loads asynchronously, use the dict utility. It takes a map of language code → resolver (sync or async) and returns a proxy whose fields are reactive signals. Access a field as a value or call it as a function.

tsx
import type { APC } from 'preffx';

const getRuDictionary = async () => {
    return {
        openProfile(name: string) {
            return 'Открыть профиль № ' + name;
        },
        showStats: 'Показать статистику'
    };
};

const getEnDictionary = async () => {
    return {
        openProfile(name: string) {
            return 'Open profile № ' + name;
        },
        showStats: 'Show stats'
    };
};

export const App: APC = async (_props, { dict, setLang }) => {
    const initialDict = await getEnDictionary();
    const captions = dict({ ru: getRuDictionary, en: getEnDictionary }, initialDict);

    return (
        <div>
            <button onClick={() => setLang('en')}>EN</button>
            <button onClick={() => setLang('ru')}>RU</button>
            <span>
                <button>{captions.openProfile('42')}</button>
                <button>{captions.showStats}</button>
            </span>
        </div>
    );
};
  • dict(resolvers) — first argument: Record<lang, () => Dict | Promise<Dict>>.
  • dict(resolvers, initial) — second optional argument provides the initial object used before a resolver resolves.
  • The resolver for the current lang is re-run automatically when the language changes.
  • Function fields become callables that return computed signals: captions.openProfile('42') is reactive.
  • Access .peek() on a field to read it without reactive tracking.

Setting the initial language

The initial lang is read from the <html lang> attribute at createRoot time, or can be set explicitly:

tsx
import { createRoot } from 'preffx';

createRoot({ defaultLang: 'en' }).mount(App);

Utils summary

OptionTypeDescription
defaultLangstringFallback language when none is set/declared
setLang(value) => voidSwitch the current language
langReadonlySignalCurrent language (reactive)
dict(...) => DictProxyReactive, per-language dictionaries

Released under the Apache-2.0 License.