Primate LogoPrimate

I18N

Internationalization (i18n) localizes your app.

Setup

Enable i18n in your app config and import your catalogs.

TypeScript JavaScriptconfig/app.tsconfig/app.ts
import config from "primate/config";
import de from "../locales/de-DE.ts";
import en from "../locales/en-US.ts";

export default config({
  i18n: {
    defaultLocale: "en-US",
    locales: {
      "en-US": en,
      "de-DE": de,
    },
  }
})

Locales

Locales are TypeScript modules in the locales directory that export a catalog via i18n.locale(...).

File Purpose
<default>.ts catalog matching defaultLocale (drives types)
<locale>.ts any other catalog mapping keys -> translated strings

Example

In this example, defaultLocale is "en-US", so en-US.ts is the default catalog. If you set defaultLocale: "de-DE", then de-DE.ts becomes the default and drives type inference.

English Germanlocales/en-US.tslocales/de-DE.ts
import i18n from "primate/i18n";

export default i18n.locale({
  switch_language: "Switch language",
  english: "English",
  german: "German",
  all_posts: "All posts",
  counter: "Counter: {count:n}",
  title: "Title",

  greet_user: "Hello, {name}",
  added_n_items: "{n:n|one item added|{n} items added}",
  price_line: "Total: {amount:currency}",
  last_seen: "Last seen {secs:ago}",
  when: "It happened on {d:date}",
  distance: "Distance: {km:unit(kilometer)}",
  list_example: "Tags: {tags:list}",
});

Key style is up to you (flat, dotted, underscored, nested). Flat keys with underscores make object access ergonomic in TS.

Frontend

Each frontend requires a bridge file that adapts Primate's headless translator to the frontend's reactivity model. Create it once per project — conventionally at lib/i18n.ts:

// lib/i18n.ts
import app from "#app";
import i18n from "@primate/svelte/i18n";

export default i18n(app.i18n);

The bridge import follows the same pattern for every frontend — @primate/FRONTEND/i18n. Views import t from wherever you place this file.

Each frontend adapter exposes a translator t and a locale setter:

Frontend Translator Locale getter Locale setter Notes
React t t.locale.get() t.locale.set(l) Call t() anywhere
Angular t t.locale.get() t.locale.set(l) Injectable or import as needed
Vue t t.locale.get() t.locale.set(l) Works in setup/options
Svelte $t t.locale.get() t.locale.set(l) t is a store; use $t in markup
Solid t t.locale.get() t.locale.set(l) Signal-like; t.subscribe()
Marko t t.locale.get() t.locale.set(l)

React

import t from "#lib/i18n";

export function Profile({ user }: { user: { name: string; since: number } }) {
  return (
    <section>
      <h2>{t("profile_title")}</h2>
      <p>{t("greeting", { name: user.name })}</p>
      <button onClick={() => t.locale.set("de-DE")}>
        {t("switch_to_german")}
      </button>
    </section>
  );
}

Svelte

<script lang="ts">
  import t from "#lib/i18n";
  const change = () => t.locale.set("en-US");
</script>

<h1>{$t("dashboard_title")}</h1>
<p>{$t("notifications", { count: 3 })}</p>
<button onclick={change}>{$t("switch_to_english")}</button>

Vue

<script setup lang="ts">
import t from "#lib/i18n";
const switchLocale = () => t.locale.set("fr-FR");
</script>

<template>
  <h1>{{ t("home_title") }}</h1>
  <p>{{ t("items", { count: 1 }) }}</p>
  <button @click="switchLocale">{{ t("switch_to_french") }}</button>
</template>

Backend

In route handlers, use app.i18n from the app facade. The locale is automatically resolved from the user's request cookie.

// routes/api/greeting.ts
import app from "#app";
import route from "primate/route";

export default route({
  get() {
    return { message: app.i18n("greeting", { name: "World" }) };
  },
});

A German user visiting /api/greeting receives { message: "Hallo, Welt!" }.

Limitations

app.i18n is read-only on the server — locale switching is client-only.

Use cases

Message syntax

Messages are simple strings with {placeholders}. Each placeholder can specify a format spec after a colon.

Spec Meaning Param type Example
(no spec) string string {name}
n or number numbers number {count:n}
d or date date Date | number {created:d}
c or currency currency number {total:c}
o or ordinal ordinals number {rank:o}
a or ago relative time number {delta:a}
l or list item list string[] {tags:l}
u(<u>) or unit(<u>) units number {size:u(MB)}

Plural selection

{n:n|...} optionally takes 2 or 3 choices:

Inside choices you can backreference the formatted number using the same key.

// en: "You have 1 item" / "You have 3 items"
"You have {count:n|{count} item|{count} items}";

// de: "Du hast keine Artikel" / "Du hast 1 Artikel" / "Du hast 7 Artikel"
"Du hast {num:n|keine Artikel|{num} Artikel|{num} Artikel}";

Units

u(...) accepts many aliases mapped to Intl units (length, area, volume, mass, temperature, speed, time, digital, energy, power, pressure, angle, frequency, concentration, electric, force, luminous). Examples:

Alias Normalized Intl unit
km/h kph kmh kilometer-per-hour
MB mb megabyte/megabit
°C celsius celsius
kWh kilowatt-hour
psi pound-force-per-square-inch

Locale switching

Use t.locale.set(locale) to switch on the client. The active locale is reactive; subscribers are notified and the UI updates automatically.

// read current
const current = t.locale.get();

// switch and persist
t.locale.set("de-DE");

Persistence

Mode Behavior
"cookie" (default) Sends a fetch("/") and persists in cookie
"localStorage" Writes __primate_locale
"sessionStorage" Writes __primate_locale
false No persistence

Persistence only runs in the browser. t.loading is true while a cookie persistence request is inflight.

Type inference

Keys and parameter types are inferred from the default catalog:

// locales/en-US.ts
import i18n from "primate/i18n";

export default i18n.locale({
  greeting: "Hello {name}",
  added: "Added {n:n|{n} item|{n} items}",
  created_at: "Created on {date:d}",
});
t("greeting", { name: "Ada" });           // ok
t("added", { n: 3 });                     // ok
t("created_at", { date: Date.now() });    // ok (number as epoch)

// t("greeting", {});                     // TS error: missing "name"
// t("created_at", { date: "2024-01-01" });// TS error: wrong type

FAQ

Previous
Stores
Next
Modules