@k8ordo/i18n

API

The functions and types @k8ordo/i18n exports. There is one entry point, @k8ordo/i18n, and both Server Component and Client Component modules import from it.

On this page

defineLocales

Import from @k8ordo/i18n

Defines the locale set. Everything that depends on the list of locales reads it from what this returns.

ts
defineLocales(
  definitions: Record<string, LocaleDefinition>,
  options?: LocalesOptions<D>,
): Locales<L, D>

Parameters

definitionsRecord<string, LocaleDefinition>
An object keyed by locale tag, each value that locale’s timeZone and dir. Tags are BCP 47, and the order written is the order of all.
optionsLocalesOptions<D>
default picks the default locale; without it, the locale written first is the default.

Returns

Locales<L, D> — The set: the list, and everything made from it.

Caveats

  • An empty set and a default outside the list throw a TypeError where defineLocales is called. So do a tag that is not BCP 47, a time zone the runtime does not know, a missing timeZone, and a dir other than ltr or rtl.
  • The set is registered on globalThis, where message reads the default from. The last set defined wins, so call it once per app.
  • The keys are inferred as literal types, so no as const is needed.
i18n.ts
export const locales = defineLocales(
  {
    ja: { timeZone: 'Asia/Tokyo', dir: 'ltr' },
    en: { timeZone: 'America/New_York', dir: 'ltr' },
  },
  { default: 'en' },
);

Locales

Import from @k8ordo/i18n

The set defineLocales returns. L is the union of its tags, and D the default among them.

ts
type Locales<L extends string, D extends L> =
  IntlFormats & {
    all: readonly L[];
    definitions: Readonly<Record<L, LocaleDefinition>>;
    default: D;
    is: (value: unknown) => value is L;
    negotiate: (requested: Iterable<string>) => L;
    negotiateRequest: (
      request: Request,
      options?: NegotiateRequestOptions,
    ) => L;
    localize: (pathname: string, locale: L) => string;
    delocalize: (pathname: string) => Delocalized<L>;
    paths: (patterns: readonly string[]) => string[];
    paramsSchema: LocaleParamsSchema<L>;
    getLocale: () => L;
    run: <T>(locale: L, fn: () => T) => T;
  };

Fields

allreadonly L[]
The tags, in the order written.
definitionsReadonly<Record<L, LocaleDefinition>>
Each locale’s { timeZone, dir }: the object given to defineLocales, as it is.
defaultD
The default locale, used when nothing names a locale and when negotiation matches none.
is(value: unknown) => value is L
Membership, as a type guard. It is case-sensitive: is('EN') is false.
negotiate(requested) => L
Picks one locale of the set from a list of preferences. Each tag is tried exactly, then by language; nothing matching by the end is the default.
negotiateRequest(request, options?) => L
Picks a locale for a Request: the cookie named by options.cookie first, then Accept-Language.
localize(pathname, locale) => string
Puts a locale segment in front of a pathname: '/ui' becomes '/en/ui', and '/' becomes '/en'.
delocalize(pathname) => Delocalized<L>
Takes the leading locale segment off. When the first segment is no locale, locale is null.
paths(patterns) => string[]
@k8ordo/static’s paths option: every pattern with a /:locale segment, once per locale.
paramsSchemaLocaleParamsSchema<L>
The [locale] segment’s schema. On the server, the locale it accepts becomes the locale of that page’s render.
getLocale() => L
The locale of the render in progress: on the server the one accepted, in the browser the first segment of the URL, and the default when neither names one.
run(locale, fn) => T
Runs fn, and everything it starts, under locale, and returns what fn returns. Server only.
dateTimeFormat …IntlFormats
The members that return Intl objects for the current locale; see IntlFormats below.

Caveats

  • No member uses this, so any can be taken out of the set, as in export const { getLocale } = locales.
  • Called in a browser, run throws locales.run: in the browser the URL is the locale — navigate instead.
  • On a server runtime with no AsyncLocalStorage to offer, run throws, and so does paramsSchema when it accepts a locale.

IntlFormats

Import from @k8ordo/i18n

The members of the set that return Intl objects for the current locale — the Intl objects themselves.

ts
type IntlFormats = {
  dateTimeFormat: (
    options?: LocaleDateTimeFormatOptions,
  ) => Intl.DateTimeFormat;
  numberFormat: (
    options?: Intl.NumberFormatOptions,
  ) => Intl.NumberFormat;
  relativeTimeFormat: (
    options?: Intl.RelativeTimeFormatOptions,
  ) => Intl.RelativeTimeFormat;
  pluralRules: (
    options?: Intl.PluralRulesOptions,
  ) => Intl.PluralRules;
  listFormat: (options?: Intl.ListFormatOptions) => Intl.ListFormat;
};

Fields

dateTimeFormat(options?) => Intl.DateTimeFormat
Intl.DateTimeFormat in the current locale and that locale’s timeZone.
numberFormat(options?) => Intl.NumberFormat
Intl.NumberFormat in the current locale.
relativeTimeFormat(options?) => Intl.RelativeTimeFormat
Intl.RelativeTimeFormat in the current locale.
pluralRules(options?) => Intl.PluralRules
Intl.PluralRules in the current locale.
listFormat(options?) => Intl.ListFormat
Intl.ListFormat in the current locale.

Caveats

  • One is made per locale and options and returned again after that. Options are told apart by their JSON.
  • None is a hook, so each can be called in a Server Component, a Client Component, or inside a message’s function.

message

Import from @k8ordo/i18n

Declares one message with its text in every locale. The function it returns gives the text in the locale where it is called.

ts
function message(variants: Variants<string>): Message;
function message<A extends readonly unknown[]>(
  variants: Variants<(...args: A) => string>,
): Message<A>;

Parameters

variantsVariants<string> | Variants<(...args: A) => string>
The text per locale: text in every locale, or a function of the same arguments in every locale.

Returns

Message<A> — A function that takes the arguments and returns the text in the current locale.

Caveats

  • With Register merged, a declaration missing a locale does not compile. A gap that gets past the types throws a TypeError such as message: no text for "en" in ["ja"] where the message is read.
  • When nothing names a locale, it returns the default locale’s text — or, where no set has been defined in this environment yet, the first text written.
  • The argument types come from the locale you annotate. With no annotation anywhere they are unknown, so annotate one, or pass them as a type argument: message<[name: string]>({ … }).
  • The declaration has no side effect, so a bundler drops a message nothing calls.
messages/nav.ts
export const home = message({ ja: 'ホーム', en: 'Home' });

export const greeting = message({
  ja: (name: string) => `こんにちは、${name}さん`,
  en: (name) => `Hello, ${name}`,
});

Message

Import from @k8ordo/i18n

The type of what message returns. A component that takes a message as a prop takes this type.

ts
type Message<A extends readonly unknown[] = []> = (
  ...args: A
) => string;

Caveats

  • Being a function, it cannot be a prop from a Server Component to a Client Component; pass the string you get by calling it there.

parseAcceptLanguage

Import from @k8ordo/i18n

Turns an Accept-Language header into a list of preferences for negotiate.

ts
parseAcceptLanguage(header: string | null): string[]

Parameters

headerstring | null
The header’s value; request.headers.get('accept-language') goes in as it is.

Returns

string[] — The tags, highest weight first; an empty array for a missing or blank header.

Caveats

  • Tags of equal weight keep the header’s order. Tags weighted 0 or less, and *, are dropped.
  • The tags themselves are not checked; negotiate skips the ones that are not BCP 47.
ts
parseAcceptLanguage('en-US;q=0.8, ja, en;q=0.9');
// ['ja', 'en', 'en-US']

currentLocale

Import from @k8ordo/i18n

The current locale, as the app’s set resolves it — for a library that renders inside an app it knows nothing about.

ts
currentLocale(): RegisteredLocale | null

Returns

RegisteredLocale | null — The locale named, else the set’s default; null where no set has been defined in this environment.

Caveats

  • @k8ordo/ui picks the locale of its built-in text with this.
  • App code uses locales.getLocale(), which is typed by the set.

Register

Import from @k8ordo/i18n

How the app tells the package its locales’ type. Merge it once per app.

ts
declare module '@k8ordo/i18n' {
  interface Register {
    locale: LocaleOf<typeof locales>;
  }
}

Caveats

  • It exists to be merged, so it is an interface, not a type.
  • Until it is merged, RegisteredLocale is string, and a missing locale is no type error.

RegisteredLocale

Import from @k8ordo/i18n

The union of the locales in Register; string until it is merged.

ts
type RegisteredLocale = Register extends {
  locale: infer L extends string;
}
  ? L
  : string;

Variants

Import from @k8ordo/i18n

An object with one value per locale in Register. It is also what message takes.

ts
type Variants<V> = Readonly<Record<RegisteredLocale, V>>;

Caveats

  • It also fits values other than messages that every locale must have, such as the names a language switcher lists.

LocaleOf

Import from @k8ordo/i18n

The union of a set’s tags. It is also what Register’s locale is set to.

ts
type LocaleOf<Ls> =
  Ls extends Locales<infer L, infer _D> ? L : never;

LocaleDefinition

Import from @k8ordo/i18n

What each locale states besides its tag. Neither is derived from the runtime.

ts
type LocaleDefinition = {
  timeZone: string;
  dir: 'ltr' | 'rtl';
};

Fields

timeZonestring
The IANA time zone the locale’s dates are shown in — the same on the server and in every browser, so a date reads the same on both.
dir'ltr' | 'rtl'
The direction the locale’s text runs in, for <html dir>.

LocalesOptions

Import from @k8ordo/i18n

The type of defineLocales’ second argument.

ts
type LocalesOptions<D extends string> = { default?: D };

Fields

defaultD
The default locale. It has to be one of the list; without it, the first one written.

NegotiateRequestOptions

Import from @k8ordo/i18n

The type of negotiateRequest’s second argument.

ts
type NegotiateRequestOptions = { cookie?: string };

Fields

cookiestring
The name of the cookie holding the visitor’s choice. Without it, only Accept-Language is read.

Delocalized

Import from @k8ordo/i18n

What delocalize returns.

ts
type Delocalized<L extends string> = {
  locale: L | null;
  pathname: string;
};

Fields

localeL | null
The locale the first segment names, or null when it names none.
pathnamestring
The pathname with the segment taken off; / when there was nothing else.

LocaleParamsSchema

Import from @k8ordo/i18n

The type of paramsSchema: the Standard Schema shape, declared by this package itself so it depends on no schema library.

ts
type LocaleParamsSchema<L extends string> = {
  '~standard': {
    version: 1;
    vendor: '@k8ordo/i18n';
    validate: (value: unknown) =>
      | { value: { locale: L } }
      | { issues: { message: string; path: ['locale'] }[] };
  };
};

Caveats

  • Validation is synchronous. A locale outside the list gets an issue at path: ['locale'].
  • The accepted value is { locale } alone; other params keep their strings for the schemas that follow.

LocaleDateTimeFormatOptions

Import from @k8ordo/i18n

The options of dateTimeFormat: Intl.DateTimeFormatOptions without timeZone.

ts
type LocaleDateTimeFormatOptions = Intl.DateTimeFormatOptions & {
  timeZone?: never;
};

Caveats

  • A timeZone forced through with as is overridden by the locale’s.
k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2