Internationalization (i18n)

Ox Content ships a complete i18n toolkit built around ICU MessageFormat 2 (MF2):

  • Multi-locale dictionaries loaded from JSON/YAML with nested keys.

  • A hand-written MF2 parser (simple messages, .input/.local declarations, .match variants).

  • A build-time static checker (missing keys, unused keys, variable mismatches across locales, MF2 syntax errors).

  • A runtime virtual module with t() and Intl-backed formatters.

  • A CLI (ox-content-i18n) and an LSP for editor integration.

Enable

// vite.config.ts
import { oxContent } from "@ox-content/vite-plugin";

export default {
  plugins: [
    oxContent({
      i18n: {
        enabled: true,
        dir: "content/i18n",
        defaultLocale: "en",
        locales: [
          { code: "en", name: "English" },
          { code: "ja", name: "日本語" },
          { code: "ar", name: "العربية", dir: "rtl" },
        ],
      },
    }),
  ],
};

Options

Option Default Description
enabled false Enable i18n.
dir 'content/i18n' Dictionary directory, relative to the project root.
defaultLocale 'en' Default locale tag (BCP 47).
locales Available locales: { code, name, dir? } (dir is 'ltr'/'rtl').
hideDefaultLocale true When true, /page serves the default locale and /ja/page is prefixed; when false every locale is prefixed.
check true Run the static checker during build.
functionNames ['t', '$t'] Translation function names to detect when scanning source for used keys.

Dictionaries

Each locale is a JSON or YAML file under dir. Nested keys are flattened with dots, so the dictionary and your call sites share one key space:

# content/i18n/en.yaml
nav:
  home: "Home"
  docs: "Documentation"
cart:
  items: "{$count :number} items in your cart"
// content/i18n/ja.json
{
  "nav": { "home": "ホーム", "docs": "ドキュメント" },
  "cart": { "items": "カートに {$count :number} 件" },
}

Missing keys in a non-default locale fall back to the default locale at runtime.

MessageFormat 2 messages

Values are MF2 messages. Beyond plain text:

# Interpolation with a formatting function
{$count :number} items

# Declarations + a match (pluralization / selection)
.input {$count :number}
.match $count
  one  {{You have {$count} item.}}
  *    {{You have {$count} items.}}

Validate a message from the CLI:

ox-content-i18n validate "{$count :number} items"
ox-content-i18n validate ".match {$n}\n one {{1}}\n * {{many}}" --ast

Static checking

With check: true, the build scans your source for used keys and compares them against the dictionaries, reporting four classes of problem:

  • Missing keys — used in code but absent from a locale.

  • Unused keys — present in dictionaries but never referenced.

  • Type mismatches — a placeholder/variable set differs across locales for the same key.

  • Syntax errors — invalid MF2 in a dictionary value.

Run it standalone (also handy in CI — it exits non-zero on findings):

ox-content-i18n check --dict-dir content/i18n --src src
ox-content-i18n check --dict-dir content/i18n --src src --format json
ox-content-i18n check --dict-dir content/i18n --src src --default-locale en

Keys are collected from TS/JS call sites (t(...), $t(...), this.t(...), i18n.t(...)) via the OXC parser and from Markdown ({{t(...)}}); customize the detected function names with functionNames.

Runtime module

Importing the virtual module gives you translation and formatting helpers backed by the loaded dictionaries and the platform Intl APIs:

import {
  t,
  localePath,
  getLocaleFromPath,
  formatDate,
  formatNumber,
  formatRelativeTime,
  formatList,
  formatDisplayName,
  i18nConfig,
} from "virtual:ox-content/i18n";

t("cart.items", { count: 3 }); // "3 items in your cart"
t("nav.home", {}, "ja"); // force a locale → "ホーム"

localePath("/docs", "ja"); // "/ja/docs"
getLocaleFromPath("/ja/docs"); // "ja"

formatNumber(1234.5, "ja"); // "1,234.5"
formatRelativeTime(-2, "day", "en"); // "2 days ago"

Intl formatters are cached per locale, and locale metadata carries the optional text direction so RTL locales render correctly.

Editor tooling

The bundled language server (ox-content-lsp) provides dictionary-key completion, hover previews of every locale's translation, go-to-definition into the dictionary file, inlay hints showing the default-locale value, and the same missing/unused-key diagnostics as the checker. It runs over stdio:

cargo run -p ox_content_lsp --bin ox-content-lsp

and integrates with VS Code, Zed, and Neovim — see the editor tooling section of the project README for wiring instructions.