@riebeckite/plugin-l10n 0.0.9 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,95 +1,95 @@
1
- # @riebeckite/plugin-l10n
2
-
3
- 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.
4
-
5
- [日本語](./README_ja.md)
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
- ## Locale detection
19
-
20
- Use `README_ja.md`-style filenames with an underscore as the recommended form, matching this repository's convention. The plugin also accepts `README.en.md`, `README-en.md`, and configured BCP 47-style tags such as `README.en-US.md` and `README.zh-CN.md`.
21
-
22
- Directory detection is also supported:
23
-
24
- ```text
25
- content/en/README.md
26
- content/ja/README.md
27
- ```
28
-
29
- Only directories matching configured `languages` are treated as locales. Frontmatter works anywhere:
30
-
31
- ```md
32
- ---
33
- lang: en
34
- translation: getting-started
35
- ---
36
- ```
37
-
38
- Conventions may be mixed. Built-in precedence is **frontmatter > filename > directory > 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).
39
-
40
- `translation` is independent of locale and filename. Use it to group files with unrelated paths or names:
41
-
42
- ```text
43
- 日本語/はじめに.md lang: ja, translation: getting-started
44
- English/getting-started.md lang: en, translation: getting-started
45
- ```
46
-
47
- ## URLs and missing translations
48
-
49
- 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.
50
-
51
- 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.
52
-
53
- ## Standard LanguageSwitcher
54
-
55
- `l10n(...)` publishes the built-in, styled LanguageSwitcher in the standard `article.after-meta` 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.
56
-
57
- ```ts
58
- // Keep URLs and metadata but do not publish UI.
59
- l10n({ defaultLang: "ja", languages: ["ja", "en"], ui: false });
60
-
61
- // Use another standard slot, or replace the server-rendered component.
62
- l10n({
63
- defaultLang: "ja",
64
- languages: ["ja", "en"],
65
- ui: {
66
- slot: "article.footer",
67
- render: ({ localization }) => `<p>${localization.lang}</p>`,
68
- },
69
- });
70
- ```
71
-
72
- 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.
73
-
74
- ## Custom detector
75
-
76
- For another convention, provide `detect`. Its language is considered after frontmatter and before filename/directory; its `translationId` is explicit.
77
-
78
- ```ts
79
- l10n({
80
- defaultLang: "ja",
81
- languages: ["ja", "en", "fr"],
82
- detect: ({ path }) =>
83
- path.startsWith("French/") ? { lang: "fr", translationId: "hello" } : undefined,
84
- });
85
- ```
86
-
87
- ## Links and SEO
88
-
89
- 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. No second graph is created. Normal Markdown links retain their authored destination.
90
-
91
- 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.
92
-
93
- ## See also
94
-
95
- - [Plugin guide](../../../docs/en/plugin-system.md)
1
+ # @riebeckite/plugin-l10n
2
+
3
+ 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.
4
+
5
+ [日本語](./README_ja.md)
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
+ ## Locale detection
19
+
20
+ Use `README_ja.md`-style filenames with an underscore as the recommended form, matching this repository's convention. The plugin also accepts `README.en.md`, `README-en.md`, and configured BCP 47-style tags such as `README.en-US.md` and `README.zh-CN.md`.
21
+
22
+ Directory detection is also supported:
23
+
24
+ ```text
25
+ content/en/README.md
26
+ content/ja/README.md
27
+ ```
28
+
29
+ Only directories matching configured `languages` are treated as locales. Frontmatter works anywhere:
30
+
31
+ ```md
32
+ ---
33
+ lang: en
34
+ translation: getting-started
35
+ ---
36
+ ```
37
+
38
+ Conventions may be mixed. Built-in precedence is **frontmatter > filename > directory > 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).
39
+
40
+ `translation` is independent of locale and filename. Use it to group files with unrelated paths or names:
41
+
42
+ ```text
43
+ 日本語/はじめに.md lang: ja, translation: getting-started
44
+ English/getting-started.md lang: en, translation: getting-started
45
+ ```
46
+
47
+ ## URLs and missing translations
48
+
49
+ 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.
50
+
51
+ 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.
52
+
53
+ ## Standard LanguageSwitcher
54
+
55
+ `l10n(...)` publishes the built-in, styled LanguageSwitcher in the standard `article.after-meta` 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.
56
+
57
+ ```ts
58
+ // Keep URLs and metadata but do not publish UI.
59
+ l10n({ defaultLang: "ja", languages: ["ja", "en"], ui: false });
60
+
61
+ // Use another standard slot, or replace the server-rendered component.
62
+ l10n({
63
+ defaultLang: "ja",
64
+ languages: ["ja", "en"],
65
+ ui: {
66
+ slot: "article.footer",
67
+ render: ({ localization }) => `<p>${localization.lang}</p>`,
68
+ },
69
+ });
70
+ ```
71
+
72
+ 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.
73
+
74
+ ## Custom detector
75
+
76
+ For another convention, provide `detect`. Its language is considered after frontmatter and before filename/directory; its `translationId` is explicit.
77
+
78
+ ```ts
79
+ l10n({
80
+ defaultLang: "ja",
81
+ languages: ["ja", "en", "fr"],
82
+ detect: ({ path }) =>
83
+ path.startsWith("French/") ? { lang: "fr", translationId: "hello" } : undefined,
84
+ });
85
+ ```
86
+
87
+ ## Links and SEO
88
+
89
+ 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.
90
+
91
+ 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.
92
+
93
+ ## See also
94
+
95
+ - [Plugin guide](../../../docs/en/plugin-system.md)
package/README_ja.md CHANGED
@@ -1,76 +1,76 @@
1
- # @riebeckite/plugin-l10n
2
-
3
- Riebeckite の Markdown コンテンツをローカライズするプラグインです。UI 文言の翻訳は担当しませんが、標準の言語切替 UI を提供します。
4
-
5
- [English](./README.md)
6
-
7
- ## 基本設定
8
-
9
- ```ts
10
- import { l10n } from "@riebeckite/plugin-l10n";
11
-
12
- l10n({ defaultLang: "ja", languages: ["ja", "en", "zh-CN"] });
13
- ```
14
-
15
- ## ロケール検出と翻訳グループ
16
-
17
- ファイル名はこのリポジトリの慣例に合わせ、アンダースコアの `README_ja.md` 形式を推奨します。`README.ja.md`、`README-ja.md`、`README.en.md`、`README.en-US.md`、`README.zh-CN.md` も利用できます。設定済みの言語名に一致する先頭ディレクトリ(`en/README.md`)も検出します。
18
-
19
- frontmatter も利用できます。
20
-
21
- ```md
22
- ---
23
- lang: en
24
- translation: getting-started
25
- ---
26
- ```
27
-
28
- 組み合わせた場合の組み込み規則の優先順位は **frontmatter > ファイル名 > ディレクトリ > 既定言語** です。矛盾は `L10N_LANGUAGE_CONFLICT`、同一 `translation + lang` の重複は `L10N_DUPLICATE_TRANSLATION` として診断します。`strict: true` ではビルドを失敗させます。
29
-
30
- `translation` はロケール検出とは別です。異なるパス・ファイル名でも明示的な ID でグループ化できます。翻訳がない言語のページを複製・生成することはありません。
31
-
32
- ## URL・テーマ・SEO
33
-
34
- 既定言語は既存の URL(`/about`)、その他は言語プレフィックス付き(`/en/about`)です。既存の Content Location を拡張するため、別の URL 解決プラグインとも併用できます。翻訳ごとに URL を変える場合は、異なるソースパスまたは permalink プラグインを使います。
35
-
36
- テーマは `getLocalization(manifest, slug)` から現在の言語、存在する言語、各翻訳の href を取得できます。`getLocalizedContent()` は翻訳がなければ `null` を返します。
37
-
38
- ## 標準 LanguageSwitcher
39
-
40
- `l10n(...)` は標準記事レイアウトの `article.after-meta` スロットへ、組み込みの LanguageSwitcher を登録します。標準 Site consumer がこのスロットを描画するため、l10n 専用の記事レイアウト実装は不要です。実在する翻訳が 2 つ未満のページには表示されません。
41
-
42
- ```ts
43
- // URL とメタデータは維持し、UI だけを無効化します。
44
- l10n({ defaultLang: "ja", languages: ["ja", "en"], ui: false });
45
-
46
- // 配置の変更、またはサーバー描画コンポーネントの置換。
47
- l10n({
48
- defaultLang: "ja",
49
- languages: ["ja", "en"],
50
- ui: {
51
- slot: "article.footer",
52
- render: ({ localization }) => `<p>${localization.lang}</p>`,
53
- },
54
- });
55
- ```
56
-
57
- Theme は `.l10n-switcher` の CSS class を上書きできます。`ui.render` はフレームワーク非依存の HTML renderer なので、カスタム Site は任意のサーバー描画コンポーネントへ置き換えられます。
58
-
59
- 各ページには実在する翻訳だけを対象に `hreflang` の alternate link を追加します。WikiLink の Content Graph は同言語の翻訳を優先し、なければ元のリンク先を使います。通常の Markdown リンクは記述された URL を維持します。
60
-
61
- ## 独自検出
62
-
63
- ```ts
64
- l10n({
65
- defaultLang: "ja",
66
- languages: ["ja", "en", "fr"],
67
- detect: ({ path }) =>
68
- path.startsWith("French/") ? { lang: "fr", translationId: "hello" } : undefined,
69
- });
70
- ```
71
-
72
- 独自検出の言語は frontmatter より弱く、ファイル名・ディレクトリより強く扱われます。`translationId` は明示的な翻訳グループ ID です。
73
-
74
- ## ????
75
-
76
- - [?????????](../../../docs/ja/plugin-system.md)
1
+ # @riebeckite/plugin-l10n
2
+
3
+ Riebeckite の Markdown コンテンツをローカライズするプラグインです。UI 文言の翻訳は担当しませんが、標準の言語切替 UI を提供します。
4
+
5
+ [English](./README.md)
6
+
7
+ ## 基本設定
8
+
9
+ ```ts
10
+ import { l10n } from "@riebeckite/plugin-l10n";
11
+
12
+ l10n({ defaultLang: "ja", languages: ["ja", "en", "zh-CN"] });
13
+ ```
14
+
15
+ ## ロケール検出と翻訳グループ
16
+
17
+ ファイル名はこのリポジトリの慣例に合わせ、アンダースコアの `README_ja.md` 形式を推奨します。`README.ja.md`、`README-ja.md`、`README.en.md`、`README.en-US.md`、`README.zh-CN.md` も利用できます。設定済みの言語名に一致する先頭ディレクトリ(`en/README.md`)も検出します。
18
+
19
+ frontmatter も利用できます。
20
+
21
+ ```md
22
+ ---
23
+ lang: en
24
+ translation: getting-started
25
+ ---
26
+ ```
27
+
28
+ 組み合わせた場合の組み込み規則の優先順位は **frontmatter > ファイル名 > ディレクトリ > 既定言語** です。矛盾は `L10N_LANGUAGE_CONFLICT`、同一 `translation + lang` の重複は `L10N_DUPLICATE_TRANSLATION` として診断します。`strict: true` ではビルドを失敗させます。
29
+
30
+ `translation` はロケール検出とは別です。異なるパス・ファイル名でも明示的な ID でグループ化できます。翻訳がない言語のページを複製・生成することはありません。
31
+
32
+ ## URL・テーマ・SEO
33
+
34
+ 既定言語は既存の URL(`/about`)、その他は言語プレフィックス付き(`/en/about`)です。既存の Content Location を拡張するため、別の URL 解決プラグインとも併用できます。翻訳ごとに URL を変える場合は、異なるソースパスまたは permalink プラグインを使います。
35
+
36
+ テーマは `getLocalization(manifest, slug)` から現在の言語、存在する言語、各翻訳の href を取得できます。`getLocalizedContent()` は翻訳がなければ `null` を返します。
37
+
38
+ ## 標準 LanguageSwitcher
39
+
40
+ `l10n(...)` は標準記事レイアウトの `article.after-meta` スロットへ、組み込みの LanguageSwitcher を登録します。標準 Site consumer がこのスロットを描画するため、l10n 専用の記事レイアウト実装は不要です。実在する翻訳が 2 つ未満のページには表示されません。
41
+
42
+ ```ts
43
+ // URL とメタデータは維持し、UI だけを無効化します。
44
+ l10n({ defaultLang: "ja", languages: ["ja", "en"], ui: false });
45
+
46
+ // 配置の変更、またはサーバー描画コンポーネントの置換。
47
+ l10n({
48
+ defaultLang: "ja",
49
+ languages: ["ja", "en"],
50
+ ui: {
51
+ slot: "article.footer",
52
+ render: ({ localization }) => `<p>${localization.lang}</p>`,
53
+ },
54
+ });
55
+ ```
56
+
57
+ Theme は `.l10n-switcher` の CSS class を上書きできます。`ui.render` はフレームワーク非依存の HTML renderer なので、カスタム Site は任意のサーバー描画コンポーネントへ置き換えられます。
58
+
59
+ 各ページには実在する翻訳だけを対象に `hreflang` の alternate link を追加します。WikiLink の Content Graph は同言語の翻訳を優先し、なければ元のリンク先を使います。本文では Obsidian WikiLink を `@riebeckite/plugin-obsidian-markdown` が解決した後、WikiLink と Markdown の内部リンクを同じ規則で処理します。現在のページと同じ言語の翻訳があればその URL に切り替え、なければ original/default のリンク先を維持します。クエリ文字列とフラグメントは維持し、外部 URL、フラグメントのみのリンク、不明な URL、asset などコンテンツ以外の URL は変更しません。Content Graph と本文リンクは同じ解決ポリシーを使います。
60
+
61
+ ## 独自検出
62
+
63
+ ```ts
64
+ l10n({
65
+ defaultLang: "ja",
66
+ languages: ["ja", "en", "fr"],
67
+ detect: ({ path }) =>
68
+ path.startsWith("French/") ? { lang: "fr", translationId: "hello" } : undefined,
69
+ });
70
+ ```
71
+
72
+ 独自検出の言語は frontmatter より弱く、ファイル名・ディレクトリより強く扱われます。`translationId` は明示的な翻訳グループ ID です。
73
+
74
+ ## ????
75
+
76
+ - [?????????](../../../docs/ja/plugin-system.md)
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ function l10n(options) {
35
35
  assets: [createStyleAsset("l10n")],
36
36
  validateOptions: () => validateOptions(options).map((message) => ({ path: "l10n", message })),
37
37
  extendContentLocations: ({ entries, locations }) => {
38
- state = detectContents(entries, resolved);
38
+ state = { ...detectContents(entries, resolved), locations };
39
39
  throwForStrictDiagnostics(state.diagnostics, resolved.strict);
40
40
  for (const content of state.contents.values()) {
41
41
  const location = locations.get(content.slug);
@@ -46,20 +46,21 @@ function l10n(options) {
46
46
  );
47
47
  }
48
48
  },
49
+ extendMarkdownPipeline: (pipeline, context) => {
50
+ pipeline.use(remarkLocalizedLinks, {
51
+ state,
52
+ sourceSlug: context.sourceSlug,
53
+ resolvePermalink: context.resolvePermalink
54
+ });
55
+ },
49
56
  extendContentGraph: ({ entries }) => {
50
57
  for (const entry of entries) {
51
58
  const source = state.contents.get(entry.slug);
52
59
  if (!source) continue;
53
60
  entry.links = entry.links.map((link) => {
54
61
  if (!link.slug) return link;
55
- const target = state.contents.get(link.slug);
56
- if (!target) return link;
57
- const localized = findTranslation(
58
- state,
59
- target.translationId,
60
- source.lang
61
- );
62
- return localized ? { ...link, kind: "note", slug: localized.slug } : { ...link, kind: "note" };
62
+ const target = resolveLocalizedTarget(state, source, link.slug);
63
+ return target ? { ...link, kind: "note", slug: target.slug } : link;
63
64
  });
64
65
  }
65
66
  },
@@ -71,6 +72,38 @@ function l10n(options) {
71
72
  });
72
73
  }
73
74
  var l10nPlugin = l10n;
75
+ function remarkLocalizedLinks(options) {
76
+ return (tree) => {
77
+ const source = options.sourceSlug ? options.state.contents.get(options.sourceSlug) : void 0;
78
+ if (!source || !options.state.locations) return;
79
+ const slugsByPermalink = new Map(
80
+ [...options.state.locations].map(([slug, location]) => [
81
+ location.permalink,
82
+ slug
83
+ ])
84
+ );
85
+ localizeLinks(tree, source, options, slugsByPermalink);
86
+ };
87
+ }
88
+ function localizeLinks(node, source, options, slugsByPermalink) {
89
+ if (node.type === "link" && node.url) {
90
+ node.url = localizeLinkUrl(node.url, source, options, slugsByPermalink);
91
+ }
92
+ for (const child of node.children ?? []) {
93
+ localizeLinks(child, source, options, slugsByPermalink);
94
+ }
95
+ }
96
+ function localizeLinkUrl(url, source, options, slugsByPermalink) {
97
+ const { permalink, suffix } = splitLinkUrl(url);
98
+ const targetSlug = slugsByPermalink.get(permalink);
99
+ if (!targetSlug) return url;
100
+ const target = resolveLocalizedTarget(options.state, source, targetSlug);
101
+ return target ? `${options.resolvePermalink(target.slug)}${suffix}` : url;
102
+ }
103
+ function splitLinkUrl(url) {
104
+ const suffixIndex = url.search(/[?#]/);
105
+ return suffixIndex < 0 ? { permalink: url, suffix: "" } : { permalink: url.slice(0, suffixIndex), suffix: url.slice(suffixIndex) };
106
+ }
74
107
  function getLocalization(manifest, slug) {
75
108
  const entry = manifest.bySlug.get(slug);
76
109
  if (!entry) return null;
@@ -250,7 +283,7 @@ function detectContents(entries, options) {
250
283
  owners.set(key, content);
251
284
  }
252
285
  }
253
- return { contents, diagnostics, conflictedGroups };
286
+ return { contents, diagnostics, conflictedGroups, locations: null };
254
287
  }
255
288
  function parseFrontmatter(entry) {
256
289
  const match = entry.markdown.match(
@@ -346,6 +379,11 @@ function findTranslation(state, translationId, lang) {
346
379
  (content) => content.translationId === translationId && content.lang === lang
347
380
  ) ?? null;
348
381
  }
382
+ function resolveLocalizedTarget(state, source, targetSlug) {
383
+ const target = state.contents.get(targetSlug);
384
+ if (!target) return null;
385
+ return findTranslation(state, target.translationId, source.lang) ?? target;
386
+ }
349
387
  function addLocalizationHeadTags(manifest, state) {
350
388
  for (const entry of manifest.entries) {
351
389
  const content = state.contents.get(entry.slug);
@@ -441,7 +479,12 @@ function throwForStrictDiagnostics(diagnostics, strict) {
441
479
  );
442
480
  }
443
481
  function emptyState() {
444
- return { contents: /* @__PURE__ */ new Map(), diagnostics: [], conflictedGroups: /* @__PURE__ */ new Set() };
482
+ return {
483
+ contents: /* @__PURE__ */ new Map(),
484
+ diagnostics: [],
485
+ conflictedGroups: /* @__PURE__ */ new Set(),
486
+ locations: null
487
+ };
445
488
  }
446
489
  export {
447
490
  getLocalization,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-l10n",
3
- "version": "0.0.9",
3
+ "version": "0.0.12",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,10 +16,11 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "yaml": "^2.9.0",
19
- "@riebeckite/core": "0.0.9"
19
+ "@riebeckite/core": "0.0.12"
20
20
  },
21
21
  "devDependencies": {
22
- "tsx": "^4.22.4"
22
+ "tsx": "^4.22.4",
23
+ "@riebeckite/plugin-obsidian-markdown": "0.0.12"
23
24
  },
24
25
  "publishConfig": {
25
26
  "access": "public"