@silgi/i18n

The first-party i18n module. Add it to modules and set its options under the i18n config key. A page's locale comes from its URL only: every page is copied per locale (about___fr at /fr/a-propos), and the cookie and Accept-Language only choose where / sends a visitor.

// silgi.config.ts
export default defineConfig({
  modules: ["@silgi/i18n"],
  i18n: {
    locales: ["en", { code: "fr", language: "fr-FR" }, { code: "ar", language: "ar-EG" }],
    defaultLocale: "en",
    pages: { about: { fr: "/a-propos", ar: "/hawl" } },
  },
});

default

The module, as defineModule({ meta: { name: "i18n", configKey: "i18n" } }). Its setup checks the options (a wrong value is a build error naming the key), copies the pages, adds the app plugin that provides useApp().i18n, a global route middleware, and the root redirect on GET /.

I18nOptions, LocaleInput, ResolvedI18nOptions, I18nSetup

KeyTypeDefault
locales(string | LocaleInput)[]required
defaultLocalestringrequired
strategy"prefix_except_default" | "prefix""prefix_except_default"
baseUrlstring | { runtimeConfig: string }none
detect.cookiestring | false"silgi_locale"
detect.redirect"root" | false"root"
pagesRecord<string, Partial<Record<string, string>>>{}
timeZonestring"UTC"
fallback"default" | false"default"

LocaleInput is { code, language?, dir?, name? }. ResolvedI18nOptions is the checked form with every default filled in, and I18nSetup is (options: ResolvedI18nOptions, silgi: SilgiContext) => void | Promise<void>, the type of each of the module's internal setup steps.

LocaleInfo

One resolved locale: { code, language, dir, name }. language defaults to code, dir to the language's text direction and name to the language's own name for it ("English", "français (France)").

CurrentLocale

LocaleInfo & { locales: readonly LocaleInfo[] }: the locale of the current page and every configured locale. It is what useLocale() of @silgi/i18n/<library> gives (a ComputedRef in vue, an Accessor in solid, getters in svelte).

I18nRuntime, I18nRouteLocation

useApp().i18n. localeOf(path) is the locale of an app path, localePath(to, code) the same page in another locale (a path no page matches comes back unchanged), switchLocalePath(route, code) the current route in another locale, and routeBaseName("about___fr") is about. I18nRouteLocation is { name, params?, query?, hash? } with a base route name.

interface I18nRuntime {
  readonly locales: readonly LocaleInfo[];
  readonly defaultLocale: LocaleInfo;
  localeOf(path: string): LocaleInfo;
  load(code: SilgiLocale): Promise<void>;
  translator(code: SilgiLocale): Translator;
  localePath(to: string | I18nRouteLocation, code: SilgiLocale): string;
  switchLocalePath(route: { path: string; name?: string | null; params: Record<string, string | string[]>; query: Record<string, string | string[]>; hash: string }, code: SilgiLocale): string;
  routeBaseName(name: string): string;
}

Translator

{ locale, t(key, args?), n(value, options?), d(value, options?) } for one locale.

SilgiI18nMessages, SilgiI18nLocales, SilgiLocale, I18nKey, I18nArgsTuple

The registries typegen fills (message key → arguments, locale code → true) and the types over them. While a registry is empty, SilgiLocale and I18nKey are string and I18nArgsTuple<K> is [args?: Record<string, unknown>].

@silgi/i18n/server

useTranslation(event)

useTranslation(event: SilgiEvent): Promise<{ locale: LocaleInfo; t; n; d }>. The translator of event.context.locale, which the module's server middleware sets for every request: the first segment of the path below baseURL when it is a locale with a route copy, then ?locale=<code>, the locale cookie, Accept-Language, the default locale. For a page request the route middleware then sets it to the page's locale. An event the middleware did not see resolves by the same order. A locale's messages load once per process. The event is passed explicitly; there is no AsyncLocalStorage.

export default defineMiddleware(async (event) => {
  const { t } = await useTranslation(event);
  return { message: t("api.saved") };
});

@silgi/i18n/react, @silgi/i18n/vue, @silgi/i18n/preact, @silgi/i18n/svelte, @silgi/i18n/solid, @silgi/i18n/html

One entry per adapter. Each exports useI18n, useLocale and the type CurrentLocale, with the same names in all six. Both read useApp().i18n and useRoute() of the adapter and throw useI18n() needs the "@silgi/i18n" module: app.i18n is missing without the module. The locale is localeOf(route.path), so the route is the only store.

useI18n()

useI18n(): I18nFunctions, where I18nFunctions is { t, n, d, localePath(to), switchLocalePath(code), routeBaseName(name) }. t, n and d are those of the page locale's Translator. localePath(to: string | I18nRouteLocation) is app.i18n.localePath with the current locale, and switchLocalePath(code) is app.i18n.switchLocalePath with the current route. The functions read the route when they are called, so a template, accessor or getter that calls them runs again when a navigation changes the locale.

LibraryReturns
react, preactI18nFunctions; the same object while the locale code is the same
vueI18nFunctions & { locale: ComputedRef<CurrentLocale> }
svelteI18nFunctions & { readonly locale: CurrentLocale }, where locale is a getter
solidI18nFunctions & { locale: Accessor<CurrentLocale> }
htmlI18nFunctions for this render, with t, n and d returning text escaped for HTML (&, <, >, ", ')
const { t, n, d, localePath, switchLocalePath } = useI18n();
t("home.hello", { name: "Ada" });
localePath("/about");
switchLocalePath("ar");

useLocale()

useLocale() gives the CurrentLocale of the page: CurrentLocale in react, preact and html; ComputedRef<CurrentLocale> in vue; Accessor<CurrentLocale> in solid; and in svelte an object whose code, language, dir, name and locales are getters. The html value is not escaped.

const { code, language, dir, locales } = useLocale();