# Inlang I18N

> 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.

- Skill: `aidenlx/inlang-i18n` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aidenlx/inlang-i18n`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aidenlx/inlang-i18n/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: aidenlx (https://skillmd.com/u/aidenlx)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aidenlx/inlang-i18n

---


# 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:

```sh
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

1. 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.
2. Regenerate the facade when running package tools directly.
3. Import the facade and keep named-input call shapes:

```ts
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

```json
{
  "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:

```json
{
  "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:

```json
{
  "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.

