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:
/pageserves the default locale (en)/ja/pageserves the Japanese locale
When false, all locales get prefixed:
/en/pageserves English/ja/pageserves 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.