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/.localdeclarations,.matchvariants).A build-time static checker (missing keys, unused keys, variable mismatches across locales, MF2 syntax errors).
A runtime virtual module with
t()andIntl-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.