Ox Content

i18n (Internationalization)

Built-in internationalization support for Ox Content with ICU MessageFormat 2, build-time checking, and locale-aware routing.

Setup

Enable i18n in your vite.config.ts:

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

export default defineConfig({
  plugins: [
    oxContent({
      i18n: {
        enabled: true,
        defaultLocale: "en",
        locales: [
          { code: "en", name: "English" },
          { code: "ja", name: "日本語" },
        ],
      },
    }),
  ],
});

Options

Option Type Default Description
enabled boolean false Enable/disable i18n
dir string 'content/i18n' Path to dictionary directory (relative to project root)
defaultLocale string 'en' Default locale tag
locales LocaleConfig[] [] Available locales
hideDefaultLocale boolean true Hide default locale prefix in URLs
check boolean true Run i18n checks during build
functionNames string[] ['t', '$t'] Translation function names to detect in source code

hideDefaultLocale

When true (default), the default locale does not have a URL prefix:

  • /page serves the default locale (en)

  • /ja/page serves the Japanese locale

When false, all locales get prefixed:

  • /en/page serves English

  • /ja/page serves Japanese

LocaleConfig

interface LocaleConfig {
  /** BCP 47 locale tag (e.g., 'en', 'ja', 'zh-Hans') */
  code: string;
  /** Display name for this locale (e.g., 'English', '日本語') */
  name: string;
  /** Text direction. @default 'ltr' */
  dir?: "ltr" | "rtl";
}

Example with RTL support:

locales: [
  { code: "en", name: "English" },
  { code: "ja", name: "日本語" },
  { code: "ar", name: "العربية", dir: "rtl" },
];

Dictionary Structure

Dictionaries are organized by locale in the dir directory:

content/i18n/
  en/
    common.json
    navigation.json
    messages.yaml
  ja/
    common.json
    navigation.json
    messages.yaml

Each file becomes a namespace. For example, common.json produces keys prefixed with common.:

JSON Format

{
  "greeting": "Hello {$name}",
  "farewell": "Goodbye",
  "nav": {
    "home": "Home",
    "about": "About"
  }
}

This produces flattened keys: common.greeting, common.farewell, common.nav.home, common.nav.about.

YAML Format

greeting: "Hello {$name}"
farewell: "Goodbye"
nav:
  home: "Home"
  about: "About"

ICU MessageFormat 2

Dictionary values support ICU MessageFormat 2 syntax.

Simple Variables

Hello {$name}

Plural / Match

.input {$count :number}
.match $count
one {{You have {$count} item.}}
* {{You have {$count} items.}}

Local Declarations

.local $host = {$name}
.local $guest = {$other}
{{Welcome {$host} and {$guest}!}}

Virtual Module

The plugin provides a virtual:ox-content/i18n module with translation utilities.

import {
  t,
  getLocaleFromPath,
  localePath,
  i18nConfig,
  dictionaries,
} from "virtual:ox-content/i18n";

t(key, params?, locale?)

Translates a key with optional parameter substitution.

t("common.greeting", { name: "World" }); // "Hello World"
t("common.greeting", { name: "World" }, "ja"); // "こんにちは World"

getLocaleFromPath(pathname)

Extracts the locale code from a URL pathname.

getLocaleFromPath("/ja/about"); // 'ja'
getLocaleFromPath("/about"); // 'en' (default locale)

localePath(pathname, locale)

Constructs a localized path for a given locale. Respects hideDefaultLocale.

localePath("/about", "ja"); // '/ja/about'
localePath("/about", "en"); // '/about' (when hideDefaultLocale is true)

i18nConfig

The resolved i18n configuration object.

const { enabled, defaultLocale, locales, hideDefaultLocale } = i18nConfig;

dictionaries

All loaded dictionaries as a flat key-value map per locale.

// Record<string, Record<string, string>>
const { en, ja } = dictionaries;
console.log(en["common.greeting"]); // "Hello {$name}"

Build-Time Checking

When check is enabled (default), the plugin performs static analysis at build time and reports:

Check Severity Description
Missing keys Error Keys used in source code but missing from dictionaries
Unused keys Warning Keys defined in dictionaries but not used in source code
Type mismatch Error MF2 placeholder variables differ across locales for the same key
Syntax errors Error Invalid MF2 syntax in dictionary values

Example Output

[ox-content:i18n] error: Missing key 'common.title' in locale 'ja'
[ox-content:i18n] warning: Unused key 'common.legacy' in locale 'en'
[ox-content:i18n] error: Type mismatch for key 'common.greeting': locale 'en' uses {$name, $count}, locale 'ja' uses {$name}

Translation Key Extraction

The checker automatically scans source files for translation key usage.

TypeScript / JavaScript

t("common.greeting");
$t("common.greeting");
this.t("common.greeting");
i18n.t("common.greeting");

Markdown

{{t('common.greeting')}}
{{ $t('nav.home') }}

The scanner searches src/ for .ts, .tsx, .js, .jsx files and content/ for .md, .mdx files.

NAPI API

The following functions are available from @ox-content/napi for programmatic use:

loadDictionaries(dir)

Loads dictionaries from a directory and returns metadata.

import { loadDictionaries } from "@ox-content/napi";

const result = loadDictionaries("content/i18n");
// { localeCount: 2, locales: ['en', 'ja'], errors: [] }

loadDictionariesFlat(dir)

Loads dictionaries and returns flattened key-value maps per locale.

import { loadDictionariesFlat } from "@ox-content/napi";

const dicts = loadDictionariesFlat("content/i18n");
// { en: { 'common.greeting': 'Hello {$name}', ... }, ja: { ... } }

validateMf2(message)

Validates an ICU MessageFormat 2 string.

import { validateMf2 } from "@ox-content/napi";

const result = validateMf2("Hello {$name}");
// { valid: true, errors: [], astJson: '...' }

const invalid = validateMf2("Hello {$name");
// { valid: false, errors: ['...'], astJson: null }

checkI18n(dictDir, usedKeys)

Runs all i18n checks against the given dictionary directory and used keys.

import { checkI18n } from "@ox-content/napi";

const result = checkI18n("content/i18n", ["common.greeting", "nav.home"]);
// { diagnostics: [...], errorCount: 0, warningCount: 1 }

extractTranslationKeys(source, filePath, functionNames?)

Extracts translation keys from TypeScript/JavaScript source code.

import { extractTranslationKeys } from "@ox-content/napi";

const keys = extractTranslationKeys(`const msg = t('common.greeting');`, "src/App.tsx", [
  "t",
  "$t",
]);
// [{ key: 'common.greeting', filePath: 'src/App.tsx', line: 1, column: 18, endColumn: 35 }]

CLI & LSP

CLI

The ox-content-i18n CLI provides standalone i18n checking:

ox-content-i18n check --dict content/i18n --src src

LSP Server

An LSP server is available for editor integration, providing translation key completion inside t() calls.