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
Define a complete sentence under a stable semantic key.
Avoid stitching translated fragments together; grammar and word order differ.
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.
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/i18nCheck 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.
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.
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;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>;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 localeTranslate 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.
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.
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.
Display changes only the view. Copy and download include the complete Markdown reference.
# 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.