kamod-i18n · Typed keys · Zero runtime depsInstall · Compose · Explore

Ship multilingual Preact apps with a tiny, typed i18n core

Type-safe translation lookup, pluralization via Intl.PluralRules, and formatting through native Intl APIs — with an optional Preact adapter and SSR-safe instances.

Build around one default locale and a clear message schema. Translate complete messages, keep formatting locale-aware, and plan how the selected language reaches both your server-rendered document and your Preact tree. Pair it with the component library when building your interface.

In this guide 9 sections

What the package brings

Treat language as part of the application model. Start with complete messages, give every locale the same structure, and keep translation lookup separate from presentation and locale selection.

Choose the right approach

A visible message

Define a complete sentence under a stable semantic key.

Avoid stitching translated fragments together; grammar and word order differ.

Dates, numbers and currencies

Format data with the intended locale using Intl or the documented formatting API.

Keep machine values separate from display strings; a translated number is not a storage format.

A localized Preact tree

Use I18nProvider and useI18n from the Preact entry point.

Resolve the initial locale once and keep server output and hydration consistent.

Default-locale-as-schema

Nested translation keys are inferred from your default locale so typos fail at compile time.

Group messages around a screen or task, such as common actions or checkout errors. Add a key to the default locale first, then supply the corresponding translations so changes remain easy to review together.

Preact-native adapter

I18nProvider and useI18n live in @kamod-ch/i18n/preact without React compatibility layers.

Place the provider around the tree that shares a language. Translate the visible label and its accessible name together; switching languages should not leave controls with mixed-language instructions.

SSR-safe core

Create one i18n instance per request — no global mutable locale state leaking across users.

Resolve the locale at the request boundary and reuse that choice during hydration. Test two requests with different languages to catch accidental shared state before deploying.

Installation

Install @kamod-ch/i18n for the framework-independent core. Add Preact when using @kamod-ch/i18n/preact.

pnpm add @kamod-ch/i18n

Check your environment

Use your existing package manager and keep the peer dependencies aligned with your application. Compare package.json and your lockfile with the documentation for the version you install; add the package once at the workspace boundary that uses it.

Keep the first change small. Start with the usage example, run your project’s typecheck and build, then connect it to a real screen. The package overview helps you decide how companion libraries fit together.

Usage

Define a default locale object as your schema, add locales with satisfies Messages<typeof en>, then call t(), setLocale(), and Intl formatters on a createI18n instance.

src/example.tsx
import { createI18n, type Messages } from "@kamod-ch/i18n";

const en = { common: { save: "Save" } } as const;
const de = { common: { save: "Speichern" } } satisfies Messages<typeof en>;

const i18n = createI18n({ locale: "en", fallbackLocale: "en", messages: { en, de } });
i18n.t("common.save");

Read the example

Follow the value from its definition to the interface. Each part below explains a decision to keep when adapting the example.

Let the default locale define the shape

Keep keys stable and organize them by meaning. A key such as common.save can be shared where the action means the same thing; two English labels that happen to match do not always need the same translation key.

const en = { common: { save: "Save" } } as const;
Check translations while editing

The satisfies check catches structural mistakes in this object. It cannot judge translation quality or whether a sentence fits the screen. Review the full message with a speaker of the target language and test it in context.

const de = { common: { save: "Speichern" } } satisfies Messages<typeof en>;
Resolve text at the point of use

The selected locale determines the lookup. In a reactive interface, use the package's Preact integration so changing language updates consumers; do not cache translated labels once at module initialization.

i18n.t("common.save"); // "Save" for the starting English locale

Translate complete messages with named values

Let a translator control the whole sentence instead of joining a greeting, name and punctuation in the view. Named interpolation keeps the data separate from the message and gives another language room to reorder the words.

src/i18n/messages.ts
import { createI18n, type Messages } from "@kamod-ch/i18n";

const en = { dashboard: { welcome: "Welcome {name}" } } as const;
const de = { dashboard: { welcome: "Willkommen {name}" } } satisfies Messages<typeof en>;

export function createMessages(locale: "en" | "de") {
  return createI18n({ locale, fallbackLocale: "en", messages: { en, de } });
}

const messages = createMessages("de");
messages.t("dashboard.welcome", { name: "Alex" });

Try the boundaries. Create the instance for the relevant app or request, then use the same locale for dates, numbers and the document language. Render the result as text. Translation content should not become raw HTML merely because it came from a message file.

Integrate with your application

Organize messages by the task they describe. Group labels, hints and validation messages together so a form can be translated as a coherent experience. Keep user-facing strings out of rendering branches where they are easy to miss.

Allow room for longer translations in buttons, navigation and empty states. Prefer complete messages over concatenated fragments, and use locale-aware formatting for dates and numbers instead of building display strings manually.

Create request-specific instances on the server and pass the same starting locale to the client. When language changes, review document lang, translated accessible names and any region whose reading direction changes.

Choose the owner and lifetime

Choose one locale owner for the relevant application tree. A language menu, route parameter and saved preference can all contribute to that choice, but they should not compete as independent writable sources. Define their precedence explicitly. Keep a user's language preference separate from assumptions about currency, country or time zone.

Account for the environment

For server-rendered pages, create request-specific translation state and carry the resolved locale into the client. Test simultaneous requests in different languages. Use the correct document lang and, when applicable, dir; those attributes affect pronunciation and layout beyond the strings returned by the translator.

Connect the pieces. Forms and validation — Include validation errors, field instructions and submit feedback in your message schema, not only navigation labels.

When something behaves differently

Start with the smallest failing interaction. Compare a fresh page with the same page after a change or reload, and keep the package version in your reproduction.

Some labels stay in the old language
Check whether they were translated once outside a reactive consumer. Also inspect accessible names, validation messages and loading text; visible headings alone are not the entire interface.
The layout breaks in another locale
Try longer translations, narrow widths and larger text. Allow buttons and navigation labels to wrap where appropriate; truncating an essential action can conceal its meaning.
The server shows another visitor's language
Look for a mutable module-level instance shared across requests. Move locale resolution and instance creation to the request boundary and add a two-locale isolation test.

If the behavior still differs from the documented API, check existing issues before opening a report. Include the expected result, actual result, package version and a small reproduction without private application data.

API Reference

This page is a Kamod UI overview. Full API docs, lazy locale loading, plural rules, and SSR guides live on the dedicated kamod-i18n docs.

Use this page to understand the integration, then consult the Full API reference on live docs for exact signatures and supported options. Compare those details with the version in your lockfile before adapting an example.

Browse guides, API, and Preact examples

Open the kamod-i18n docs for getting started, lazy locales, pluralization, and SSR patterns.

  • Before choosing an API: read its input types, return values and default behavior.
  • Before shipping: review lifecycle or server-rendering notes for the features you use.
  • When behavior differs: reduce the case to a small example and include your package version in the report.
Continue with the dedicated package documentation.Open live docs

Accessibility Notes

When switching locale, update lang on the document or region root and keep translated strings in accessible names, labels, and live regions.

Before you ship

  • Check a missing translation and confirm that the intended fallback is understandable.
  • Try long labels, plural messages and locale-specific dates and numbers.
  • Reload a translated route and verify that the first render and accessible labels use the selected language.

Review the result with real content and keyboard input. Keep visible labels, loading and error feedback, and focus behavior in sync with the state your application exposes.

Take the guide with you

Keep the examples, integration decisions and troubleshooting notes together in your project, or share them with a coding assistant. This reference works with any assistant; no model-specific setup is needed. Check the installed version and your project’s conventions before applying a suggestion.

Download Markdown reference

Display changes only the view. Copy and download include the complete Markdown reference.

i18n-package-reference.md
# i18n — integration reference

Type-safe translation lookup, pluralization via Intl.PluralRules, and formatting through native Intl APIs — with an optional Preact adapter and SSR-safe instances.

Package: @kamod-ch/i18n

## Choose an approach

Treat language as part of the application model. Start with complete messages, give every locale the same structure, and keep translation lookup separate from presentation and locale selection.

### A visible message

Define a complete sentence under a stable semantic key.

Avoid stitching translated fragments together; grammar and word order differ.

### Dates, numbers and currencies

Format data with the intended locale using Intl or the documented formatting API.

Keep machine values separate from display strings; a translated number is not a storage format.

### A localized Preact tree

Use I18nProvider and useI18n from the Preact entry point.

Resolve the initial locale once and keep server output and hydration consistent.

## Installation

Install @kamod-ch/i18n for the framework-independent core. Add Preact when using @kamod-ch/i18n/preact.

```bash
pnpm add @kamod-ch/i18n
```

## Starting example

```tsx
import { createI18n, type Messages } from "@kamod-ch/i18n";

const en = { common: { save: "Save" } } as const;
const de = { common: { save: "Speichern" } } satisfies Messages<typeof en>;

const i18n = createI18n({ locale: "en", fallbackLocale: "en", messages: { en, de } });
i18n.t("common.save");
```

### Let the default locale define the shape

Keep keys stable and organize them by meaning. A key such as common.save can be shared where the action means the same thing; two English labels that happen to match do not always need the same translation key.

```tsx
const en = { common: { save: "Save" } } as const;
```

### Check translations while editing

The satisfies check catches structural mistakes in this object. It cannot judge translation quality or whether a sentence fits the screen. Review the full message with a speaker of the target language and test it in context.

```tsx
const de = { common: { save: "Speichern" } } satisfies Messages<typeof en>;
```

### Resolve text at the point of use

The selected locale determines the lookup. In a reactive interface, use the package's Preact integration so changing language updates consumers; do not cache translated labels once at module initialization.

```tsx
i18n.t("common.save"); // "Save" for the starting English locale
```

## Translate complete messages with named values

Let a translator control the whole sentence instead of joining a greeting, name and punctuation in the view. Named interpolation keeps the data separate from the message and gives another language room to reorder the words.

File: src/i18n/messages.ts

```tsx
import { createI18n, type Messages } from "@kamod-ch/i18n";

const en = { dashboard: { welcome: "Welcome {name}" } } as const;
const de = { dashboard: { welcome: "Willkommen {name}" } } satisfies Messages<typeof en>;

export function createMessages(locale: "en" | "de") {
  return createI18n({ locale, fallbackLocale: "en", messages: { en, de } });
}

const messages = createMessages("de");
messages.t("dashboard.welcome", { name: "Alex" });
```

Create the instance for the relevant app or request, then use the same locale for dates, numbers and the document language. Render the result as text. Translation content should not become raw HTML merely because it came from a message file.

## Ownership and environment

Choose one locale owner for the relevant application tree. A language menu, route parameter and saved preference can all contribute to that choice, but they should not compete as independent writable sources. Define their precedence explicitly. Keep a user's language preference separate from assumptions about currency, country or time zone.

For server-rendered pages, create request-specific translation state and carry the resolved locale into the client. Test simultaneous requests in different languages. Use the correct document lang and, when applicable, dir; those attributes affect pronunciation and layout beyond the strings returned by the translator.

A language switch is complete when visible text, accessible labels, formatting and document language agree. Leave room for longer translations instead of fixing controls to English label widths.

## Troubleshooting

### Some labels stay in the old language

Check whether they were translated once outside a reactive consumer. Also inspect accessible names, validation messages and loading text; visible headings alone are not the entire interface.

### The layout breaks in another locale

Try longer translations, narrow widths and larger text. Allow buttons and navigation labels to wrap where appropriate; truncating an essential action can conceal its meaning.

### The server shows another visitor's language

Look for a mutable module-level instance shared across requests. Move locale resolution and instance creation to the request boundary and add a two-locale isolation test.

## Resources

- [Documentation](https://kamod-ch.github.io/kamod-i18n/)
- [Source](https://github.com/kamod-ch/kamod-i18n)
- [npm](https://www.npmjs.com/package/@kamod-ch/i18n)

Sources & attribution

This integration guide accompanies @kamod-ch/i18n, maintained in the Kamod ecosystem. The dedicated documentation is the reference for package APIs; examples here show how those APIs fit into a Kamod UI application.

When copying or distributing source, retain the applicable license and attribution notices from the version you use. Check the published package alongside your lockfile when comparing an example with a newer release.