Color mode

Localization

Riebeckite localizes content. It can keep several language versions of the same note side by side, prefix non-default languages in the URL, add a language switcher, and emit hreflang links. It does not translate Riebeckite's own interface strings.

Localization is provided by @riebeckite/plugin-l10n. The starter preset and above register it with seven languages; minimal does not.

Basic setup

ts
import { defineConfig } from "@riebeckite/core";
import { l10n } from "@riebeckite/plugin-l10n";
 
export default defineConfig({
  plugins: [l10n({ defaultLang: "ja", languages: ["ja", "en", "zh-CN"] })],
});

defaultLang must appear in languages.

How a note's language is detected

The plugin looks for four signals, in this precedence order:

  1. Frontmatter — lang: anywhere in the file.
  2. Custom detector — if you provide detect.
  3. Filename — note.ja.md (recommended), and also note-en.md / note_ja.md, or BCP 47 tags such as note.en-US.md, note.zh-CN.md.

The filename suffix is the only implicit signal the plugin infers from the path; a language directory such as content/en/note.md is not treated as a locale. Signals can be mixed; a conflict emits L10N_LANGUAGE_CONFLICT, which becomes a build failure with strict: true.

Grouping translations

Translation identity is independent of the file's path or name. Give the same translation value to every language version:

md
---
lang: en
translation: getting-started
---
text
guide.ja.md     lang: ja, translation: guide
handbook.en.md  lang: en, translation: guide

A duplicate translation + lang pair emits L10N_DUPLICATE_TRANSLATION (again a build failure under strict: true). Use translation when the files have unrelated paths or names.

For a convention the plugin does not know, supply detect. Its language is considered after frontmatter and before filename, and its translationId is explicit:

ts
l10n({
  defaultLang: "ja",
  languages: ["ja", "en", "fr"],
  detect: ({ path }) =>
    path.startsWith("French/") ? { lang: "fr", translationId: "hello" } : undefined,
});

URLs and missing translations

The default language keeps Core's resolved URL (/about); other languages are prefixed (/en/about). The plugin augments the existing content-location resolver rather than adding a router, so it composes with URL plugins such as @riebeckite/plugin-permalink.

No fallback pages are generated. If a translation is missing, the original target stays linked and internal helpers report the absence:

  • getLocalization(manifest, slug) returns availableLanguages and language-to-href translations only for notes that exist.
  • getLocalizedContent(manifest, slug, lang) returns null for a missing translation.

Language switcher

l10n(...) publishes a built-in, styled LanguageSwitcher in the standard article.metadata layout slot. A standard site renders that slot, so no l10n-specific integration is needed. The switcher is omitted on a page with fewer than two real translations.

ts
// Keep URLs and metadata but do not render UI.
l10n({ defaultLang: "ja", languages: ["ja", "en"], ui: false });
 
// Use another standard slot, or replace the component.
l10n({
  defaultLang: "ja",
  languages: ["ja", "en"],
  ui: {
    slot: "article.footer",
    render: ({ localization }) => `<p>${localization.lang}</p>`,
  },
});

Themes can restyle the default component through its .l10n-switcher classes. ui.render is framework-neutral HTML, so a custom site can supply its own server-rendered component.

Before the content graph is built, WikiLink targets are switched to the source note's language when that translation exists; otherwise the original target remains. The rendered article rewrites both Obsidian WikiLinks (after @riebeckite/plugin-obsidian-markdown resolves them) and Markdown links to an existing translation in the current page's language, preserving query strings and fragments. External, fragment-only, unknown, and asset URLs are left unchanged.

Each translated entry receives one <link rel="alternate" hreflang="…"> per existing translation through Core's head-tag extension point. The site shell renders those tags and selects <html lang> for a request.

Options reference

Option Purpose
defaultLang The language that keeps unprefixed URLs. Required
languages The recognized languages. Only these are treated as locales
strict Turn language conflicts and duplicate translations into build failures
ui false to disable the switcher, or { slot, render } to customize it
detect Custom detection: returns { lang, translationId } or undefined

The full option list and implementation notes are in the plugin README.

See also

History

1 changesCollapseExpand
1 + # Localization
2 +
3 + Riebeckite localizes **content**. It can keep several language versions of the same note side by side, prefix non-default languages in the URL, add a language switcher, and emit `hreflang` links. It does not translate Riebeckite's own interface strings.
4 +
5 + Localization is provided by `@riebeckite/plugin-l10n`. The `starter` preset and above register it with seven languages; `minimal` does not.
6 +
7 + ## Basic setup
8 +
9 + ```ts
10 + import { defineConfig } from "@riebeckite/core";
11 + import { l10n } from "@riebeckite/plugin-l10n";
12 +
13 + export default defineConfig({
14 + plugins: [l10n({ defaultLang: "ja", languages: ["ja", "en", "zh-CN"] })],
15 + });
16 + ```
17 +
18 + `defaultLang` must appear in `languages`.
19 +
20 + ## How a note's language is detected
21 +
22 + The plugin looks for four signals, in this precedence order:
23 +
24 + 1. **Frontmatter** — `lang:` anywhere in the file.
25 + 2. **Custom detector** — if you provide `detect`.
26 + 3. **Filename** — `note.ja.md` (recommended), and also `note-en.md` / `note_ja.md`, or BCP 47 tags such as `note.en-US.md`, `note.zh-CN.md`.
27 +
28 + The filename suffix is the only implicit signal the plugin infers from the path; a language directory such as `content/en/note.md` is not treated as a locale. Signals can be mixed; a conflict emits `L10N_LANGUAGE_CONFLICT`, which becomes a build failure with `strict: true`.
29 +
30 + ## Grouping translations
31 +
32 + Translation identity is independent of the file's path or name. Give the same `translation` value to every language version:
33 +
34 + ```md
35 + ---
36 + lang: en
37 + translation: getting-started
38 + ---
39 + ```
40 +
41 + ```text
42 + guide.ja.md lang: ja, translation: guide
43 + handbook.en.md lang: en, translation: guide
44 + ```
45 +
46 + A duplicate `translation + lang` pair emits `L10N_DUPLICATE_TRANSLATION` (again a build failure under `strict: true`). Use `translation` when the files have unrelated paths or names.
47 +
48 + For a convention the plugin does not know, supply `detect`. Its language is considered after frontmatter and before filename, and its `translationId` is explicit:
49 +
50 + ```ts
51 + l10n({
52 + defaultLang: "ja",
53 + languages: ["ja", "en", "fr"],
54 + detect: ({ path }) =>
55 + path.startsWith("French/") ? { lang: "fr", translationId: "hello" } : undefined,
56 + });
57 + ```
58 +
59 + ## URLs and missing translations
60 +
61 + The default language keeps Core's resolved URL (`/about`); other languages are prefixed (`/en/about`). The plugin augments the existing content-location resolver rather than adding a router, so it composes with URL plugins such as `@riebeckite/plugin-permalink`.
62 +
63 + No fallback pages are generated. If a translation is missing, the original target stays linked and internal helpers report the absence:
64 +
65 + - `getLocalization(manifest, slug)` returns `availableLanguages` and language-to-href `translations` only for notes that exist.
66 + - `getLocalizedContent(manifest, slug, lang)` returns `null` for a missing translation.
67 +
68 + ## Language switcher
69 +
70 + `l10n(...)` publishes a built-in, styled `LanguageSwitcher` in the standard `article.metadata` layout slot. A standard site renders that slot, so no l10n-specific integration is needed. The switcher is omitted on a page with fewer than two real translations.
71 +
72 + ```ts
73 + // Keep URLs and metadata but do not render UI.
74 + l10n({ defaultLang: "ja", languages: ["ja", "en"], ui: false });
75 +
76 + // Use another standard slot, or replace the component.
77 + l10n({
78 + defaultLang: "ja",
79 + languages: ["ja", "en"],
80 + ui: {
81 + slot: "article.footer",
82 + render: ({ localization }) => `<p>${localization.lang}</p>`,
83 + },
84 + });
85 + ```
86 +
87 + Themes can restyle the default component through its `.l10n-switcher` classes. `ui.render` is framework-neutral HTML, so a custom site can supply its own server-rendered component.
88 +
89 + ## Links and SEO
90 +
91 + Before the content graph is built, WikiLink targets are switched to the source note's language when that translation exists; otherwise the original target remains. The rendered article rewrites both Obsidian WikiLinks (after `@riebeckite/plugin-obsidian-markdown` resolves them) and Markdown links to an existing translation in the current page's language, preserving query strings and fragments. External, fragment-only, unknown, and asset URLs are left unchanged.
92 +
93 + Each translated entry receives one `<link rel="alternate" hreflang="…">` per existing translation through Core's head-tag extension point. The site shell renders those tags and selects `<html lang>` for a request.
94 +
95 + ## Options reference
96 +
97 + | Option | Purpose |
98 + | --- | --- |
99 + | `defaultLang` | The language that keeps unprefixed URLs. Required |
100 + | `languages` | The recognized languages. Only these are treated as locales |
101 + | `strict` | Turn language conflicts and duplicate translations into build failures |
102 + | `ui` | `false` to disable the switcher, or `{ slot, render }` to customize it |
103 + | `detect` | Custom detection: returns `{ lang, translationId }` or `undefined` |
104 +
105 + The full option list and implementation notes are in the plugin README.
106 +
107 + ## See also
108 +
109 + - [Configuration](../reference/configuration.en.md) — `plugins` and `content`
110 + - [Plugin API](../reference/plugin-api.en.md) — public-location resolution
111 + - [Content System](../framework/content-system.en.md) — how resolved locations reach pages
112 +
113 +
114 +
115 +