Color mode

Localization

Content localization for Riebeckite Markdown. It detects a locale and a separate translation identity for every note, prefixes non-default URLs, contributes a standard language switcher, and adds hreflang links. It does not translate Riebeckite UI strings.

日本語

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"] })],
});

Locale detection

Use README.ja.md-style filenames with a dot as the recommended form, matching this repository's convention. The plugin also accepts README-ja.md, README_ja.md, and configured BCP 47-style tags such as README.en-US.md and README.zh-CN.md.

Directory detection is not supported. Only filename suffixes and frontmatter are used to infer a language. Frontmatter works anywhere:

md
---
lang: en
translation: getting-started
---

Conventions may be mixed. Built-in precedence is frontmatter > custom detector > filename > default language. Conflicts emit L10N_LANGUAGE_CONFLICT; use strict: true to fail the build instead. A duplicate translation + lang emits L10N_DUPLICATE_TRANSLATION (and also fails in strict mode).

translation is independent of locale and filename. Use it to group files with unrelated paths or names:

text
日本語/はじめに.md       lang: ja, translation: getting-started
English/getting-started.md lang: en, translation: getting-started

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, so it works with other URL plugins rather than creating a router. Different source paths or a permalink plugin can provide different resolved URLs for translations.

No fallback pages are generated. 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.

Standard LanguageSwitcher

l10n(...) publishes the built-in, styled LanguageSwitcher in the standard article.metadata layout slot. A standard Site consumer renders that slot, so no l10n-specific article integration is needed. It is omitted when a page has fewer than two real translations.

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

Themes can override the default component through its .l10n-switcher CSS classes. ui.render is framework-neutral HTML so custom Sites can supply their own server-rendered component without coupling the plugin to a specific app.

Custom detector

For another convention, provide detect. Its language is considered after frontmatter and before filename; its translationId is explicit.

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

Before the existing Content Graph is built, WikiLink graph targets are switched to the source note's language when that translation exists; otherwise their 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. When no translation exists, the original/default target remains linked. External, fragment-only, unknown, asset, and other non-content URLs remain unchanged. The Content Graph and rendered links use the same resolution policy; no second graph is created.

Each translated entry receives one <link rel="alternate" hreflang="…"> per existing translation through Core's headTags extension point. The site shell remains responsible for rendering those tags and for selecting <html lang> for a request.

See also

History

1 changesCollapseExpand
1 + <!-- Generated from packages/plugins/l10n/README.md. Do not edit this page directly; edit the package README and run `pnpm docs:sync`. -->
2 +
3 + # Localization
4 +
5 + Content localization for Riebeckite Markdown. It detects a locale and a separate translation identity for every note, prefixes non-default URLs, contributes a standard language switcher, and adds `hreflang` links. It does not translate Riebeckite UI strings.
6 +
7 + [日本語](./l10n.md)
8 +
9 + ## Basic setup
10 +
11 + ```ts
12 + import { defineConfig } from "@riebeckite/core";
13 + import { l10n } from "@riebeckite/plugin-l10n";
14 +
15 + export default defineConfig({
16 + plugins: [l10n({ defaultLang: "ja", languages: ["ja", "en", "zh-CN"] })],
17 + });
18 + ```
19 +
20 + ## Locale detection
21 +
22 + Use `README.ja.md`-style filenames with a dot as the recommended form, matching this repository's convention. The plugin also accepts `README-ja.md`, `README_ja.md`, and configured BCP 47-style tags such as `README.en-US.md` and `README.zh-CN.md`.
23 +
24 + Directory detection is not supported. Only filename suffixes and frontmatter are used to infer a language. Frontmatter works anywhere:
25 +
26 + ```md
27 + ---
28 + lang: en
29 + translation: getting-started
30 + ---
31 + ```
32 +
33 + Conventions may be mixed. Built-in precedence is **frontmatter > custom detector > filename > default language**. Conflicts emit `L10N_LANGUAGE_CONFLICT`; use `strict: true` to fail the build instead. A duplicate `translation + lang` emits `L10N_DUPLICATE_TRANSLATION` (and also fails in strict mode).
34 +
35 + `translation` is independent of locale and filename. Use it to group files with unrelated paths or names:
36 +
37 + ```text
38 + 日本語/はじめに.md lang: ja, translation: getting-started
39 + English/getting-started.md lang: en, translation: getting-started
40 + ```
41 +
42 + ## URLs and missing translations
43 +
44 + The default language keeps Core's resolved URL (`/about`); other languages are prefixed (`/en/about`). The plugin augments the existing content-location resolver, so it works with other URL plugins rather than creating a router. Different source paths or a permalink plugin can provide different resolved URLs for translations.
45 +
46 + No fallback pages are generated. `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.
47 +
48 + ## Standard LanguageSwitcher
49 +
50 + `l10n(...)` publishes the built-in, styled LanguageSwitcher in the standard `article.metadata` layout slot. A standard Site consumer renders that slot, so no l10n-specific article integration is needed. It is omitted when a page has fewer than two real translations.
51 +
52 + ```ts
53 + // Keep URLs and metadata but do not publish UI.
54 + l10n({ defaultLang: "ja", languages: ["ja", "en"], ui: false });
55 +
56 + // Use another standard slot, or replace the server-rendered component.
57 + l10n({
58 + defaultLang: "ja",
59 + languages: ["ja", "en"],
60 + ui: {
61 + slot: "article.footer",
62 + render: ({ localization }) => `<p>${localization.lang}</p>`,
63 + },
64 + });
65 + ```
66 +
67 + Themes can override the default component through its `.l10n-switcher` CSS classes. `ui.render` is framework-neutral HTML so custom Sites can supply their own server-rendered component without coupling the plugin to a specific app.
68 +
69 + ## Custom detector
70 +
71 + For another convention, provide `detect`. Its language is considered after frontmatter and before filename; its `translationId` is explicit.
72 +
73 + ```ts
74 + l10n({
75 + defaultLang: "ja",
76 + languages: ["ja", "en", "fr"],
77 + detect: ({ path }) =>
78 + path.startsWith("French/") ? { lang: "fr", translationId: "hello" } : undefined,
79 + });
80 + ```
81 +
82 + ## Links and SEO
83 +
84 + Before the existing Content Graph is built, WikiLink graph targets are switched to the source note's language when that translation exists; otherwise their 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. When no translation exists, the original/default target remains linked. External, fragment-only, unknown, asset, and other non-content URLs remain unchanged. The Content Graph and rendered links use the same resolution policy; no second graph is created.
85 +
86 + Each translated entry receives one `<link rel="alternate" hreflang="…">` per existing translation through Core's `headTags` extension point. The site shell remains responsible for rendering those tags and for selecting `<html lang>` for a request.
87 +
88 + ## See also
89 +
90 + - [Plugin guide](../reference/plugin-api.en.md)
91 +