| name | inlang-i18n |
| description | Define ZotLit UI messages in the Inlang Message Format and consume them through the generated JSON Language Pack facade. Use when editing messages/*.json, adding interpolation or variants, changing locale resolution, or working with m.* calls. |
| paths | messages/*.json |
Inlang messages + JSON Language Packs
Use messages/{locale}.json as the source of truth. The Obsidian build compiles
those sources into a typed message facade and bundled English Language Pack;
the runtime interprets pack data synchronously.
Pair this skill with /i18n-ui-text when writing English UI copy.
Project layout
project.inlang/settings.json — Inlang project configuration, base locale,
and locale list.
messages/{locale}.json — shared message sources.
apps/obsidian/scripts/generate-language-packs.ts — compiler entry point.
apps/obsidian/src/lib/i18n/generated/messages.ts — generated typed facade.
apps/obsidian/src/lib/i18n/generated/en.json — bundled English pack.
apps/obsidian/src/lib/i18n/runtime.ts — eval-free interpreter.
apps/obsidian/src/lib/i18n.ts — Obsidian Locale Alias resolution and
startup.
Generated files are gitignored. Run:
pnpm --filter @zotlit/obsidian generate:language-packs
Turbo runs generation before typecheck and tests. Vite regenerates at build
start and watches the message sources, project settings, and generator.
Adding or changing a message
- Add or edit the key in
messages/en.json. Use snake_case; keys prefixed
docs_ belong only to the docs site and are excluded from plugin artifacts.
- Regenerate the facade when running package tools directly.
- Import the facade and keep named-input call shapes:
import * as m from "@/lib/i18n/generated/messages";
m.welcome_view_name();
m.annot_view_filter_count({ shown: 3, total: 8 });
Input-free messages are functions with no arguments. Input-bearing messages
take one object whose required properties come from input declarations.
Inlang Message Format
Simple messages
{
"hello_world": "Hello world",
"greeting": "Good morning {name}!"
}
Variables use {curlyBraces}. Escape literal braces with \\{ and \\} in
JSON.
Variants
Wrap structured messages in an array:
{
"item_count": [
{
"declarations": ["input count", "local category = count: plural"],
"selectors": ["category"],
"match": {
"category=one": "There is one item.",
"category=*": "There are {count} items."
}
}
]
}
Declarations run in source order:
input name declares a facade input.
local result = source: formatter derives a local value.
- Formatter options are
key=value; $inputName references another input.
Matching is evaluated in source order. Use literal matches where needed and
* as the catch-all.
Supported formatters
| Formatter | Runtime |
|---|
plural | Intl.PluralRules |
number | Intl.NumberFormat |
datetime | Intl.DateTimeFormat |
Examples:
{
"price": [
{
"declarations": [
"input amount",
"input currency",
"local formatted = amount: number style=currency currency=$currency"
],
"match": { "formatted=*": "Price: {formatted}" }
}
],
"event_date": [
{
"declarations": [
"input date",
"local formatted = date: datetime dateStyle=long timeZone=UTC"
],
"match": { "formatted=*": "Date: {formatted}" }
}
]
}
Use an explicit timeZone when output must be stable across environments.
Markup, unknown declarations, unknown expressions, and unknown formatters are
rejected by the compiler.
Locale behavior
apps/obsidian/src/lib/i18n.ts resolves Obsidian language codes through the
Locale Alias table. Canonical locale names follow Inlang (zh-CN); unmatched
codes resolve to English. Startup always installs bundled English first.
Downloaded Language Packs are pure JSON data and use the same generated
facade.