@riebeckite/plugin-l10n 0.0.22 → 0.0.24
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 +2 -21
- package/README_ja.md +1 -20
- package/dist/components/language-switcher.d.ts +6 -0
- package/dist/components/language-switcher.js +28 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +94 -76
- package/dist/src/l10n.d.ts +2 -17
- package/package.json +10 -3
- package/dist/src/language-switcher.d.ts +0 -3
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Conventions may be mixed. Built-in precedence is **frontmatter > custom detector
|
|
|
34
34
|
|
|
35
35
|
`translation` is independent of locale and filename. Use it to group files with unrelated paths or names:
|
|
36
36
|
|
|
37
|
-
```
|
|
37
|
+
```
|
|
38
38
|
日本語/はじめに.md lang: ja, translation: getting-started
|
|
39
39
|
English/getting-started.md lang: en, translation: getting-started
|
|
40
40
|
```
|
|
@@ -45,26 +45,7 @@ The default language keeps Core's resolved URL (`/about`); other languages are p
|
|
|
45
45
|
|
|
46
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
47
|
|
|
48
|
-
|
|
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.
|
|
48
|
+
`l10n(...)` publishes the built-in, styled LanguageSwitcher in the standard article layout. A standard Site consumer renders that, so no l10n-specific article integration is needed. It is omitted when a page has fewer than two real translations.
|
|
68
49
|
|
|
69
50
|
## Custom detector
|
|
70
51
|
|
package/README_ja.md
CHANGED
|
@@ -44,26 +44,7 @@ English/getting-started.md lang: en, translation: getting-started
|
|
|
44
44
|
|
|
45
45
|
テーマは `getLocalization(manifest, slug)` から現在の言語、存在する言語、各翻訳の href を取得できます。`getLocalizedContent()` は翻訳がなければ `null` を返します。
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`l10n(...)` は標準記事レイアウトの `article.metadata` スロットへ、組み込みの LanguageSwitcher を登録します。標準 Site consumer がこのスロットを描画するため、l10n 専用の記事レイアウト実装は不要です。実在する翻訳が 2 つ未満のページには表示されません。
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
// URL とメタデータは維持し、UI だけを無効化します。
|
|
53
|
-
l10n({ defaultLang: "ja", languages: ["ja", "en"], ui: false });
|
|
54
|
-
|
|
55
|
-
// 配置の変更、またはサーバー描画コンポーネントの置換。
|
|
56
|
-
l10n({
|
|
57
|
-
defaultLang: "ja",
|
|
58
|
-
languages: ["ja", "en"],
|
|
59
|
-
ui: {
|
|
60
|
-
slot: "article.footer",
|
|
61
|
-
render: ({ localization }) => `<p>${localization.lang}</p>`,
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Theme は `.l10n-switcher` の CSS class を上書きできます。`ui.render` はフレームワーク非依存の HTML renderer なので、カスタム Site は任意のサーバー描画コンポーネントへ置き換えられます。
|
|
47
|
+
`l10n(...)` は標準記事レイアウトへ組み込みの言語切替 UI を登録します。標準 Site consumer がこの UI を描画するため、l10n 専用の記事レイアウト実装は不要です。実在する翻訳が 2 つ未満のページには表示されません。
|
|
67
48
|
|
|
68
49
|
各ページには Core の `headTags` 拡張を通じ、実在する翻訳だけを対象に `hreflang` の alternate link を追加します。サイトシェルはこれらのタグの描画と、リクエストに応じた `<html lang>` の選択を担当します。WikiLink の Content Graph は同言語の翻訳を優先し、なければ元のリンク先を使います。本文では Obsidian WikiLink を `@riebeckite/plugin-obsidian-markdown` が解決した後、WikiLink と Markdown の内部リンクを同じ規則で処理します。現在のページと同じ言語の翻訳があればその URL に切り替え、なければ original/default のリンク先を維持します。クエリ文字列とフラグメントは維持し、外部 URL、フラグメントのみのリンク、不明な URL、asset などコンテンツ以外の URL は変更しません。Content Graph と本文リンクは同じ解決ポリシーを使います。
|
|
69
50
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LocalizedContent } from "../src/l10n.js";
|
|
2
|
+
export type LanguageSwitcherProps = {
|
|
3
|
+
localization: LocalizedContent | null;
|
|
4
|
+
};
|
|
5
|
+
declare const LanguageSwitcher: (props: Readonly<LanguageSwitcherProps>) => import("hono/jsx/jsx-dev-runtime").JSX.Element;
|
|
6
|
+
export default LanguageSwitcher;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// components/language-switcher.tsx
|
|
2
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
3
|
+
var LanguageSwitcher = ((props) => {
|
|
4
|
+
const localization = props.localization;
|
|
5
|
+
if (!localization || localization.availableLanguages.length < 2) return null;
|
|
6
|
+
return /* @__PURE__ */ jsxs("nav", { class: "l10n-switcher", "aria-label": "Language", children: [
|
|
7
|
+
/* @__PURE__ */ jsx("span", { class: "l10n-switcher__label", children: "Language" }),
|
|
8
|
+
/* @__PURE__ */ jsx("ul", { class: "l10n-switcher__list", children: localization.availableLanguages.map((lang) => {
|
|
9
|
+
const href = localization.translations[lang];
|
|
10
|
+
if (!href) return null;
|
|
11
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
12
|
+
"a",
|
|
13
|
+
{
|
|
14
|
+
class: "l10n-switcher__link",
|
|
15
|
+
href,
|
|
16
|
+
hreflang: lang,
|
|
17
|
+
lang,
|
|
18
|
+
"aria-current": lang === localization.lang ? "page" : void 0,
|
|
19
|
+
children: lang
|
|
20
|
+
}
|
|
21
|
+
) }, lang);
|
|
22
|
+
}) })
|
|
23
|
+
] });
|
|
24
|
+
});
|
|
25
|
+
var language_switcher_default = LanguageSwitcher;
|
|
26
|
+
export {
|
|
27
|
+
language_switcher_default as default
|
|
28
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type { LanguageSwitcherProps } from "./components/language-switcher.js";
|
|
2
|
+
export { default as LanguageSwitcher } from "./components/language-switcher.js";
|
|
3
|
+
export { getLanguageFromPath, getLocalization, getLocalizedContent, type L10nContent, type L10nDetectorContext, type L10nDetectorResult, type L10nOptions, type LocalizedContent, l10n, l10nPlugin, } from "./src/l10n.js";
|
package/dist/index.js
CHANGED
|
@@ -1,43 +1,57 @@
|
|
|
1
|
+
// components/language-switcher.tsx
|
|
2
|
+
import { jsx, jsxs } from "hono/jsx/jsx-runtime";
|
|
3
|
+
var LanguageSwitcher = ((props) => {
|
|
4
|
+
const localization = props.localization;
|
|
5
|
+
if (!localization || localization.availableLanguages.length < 2) return null;
|
|
6
|
+
return /* @__PURE__ */ jsxs("nav", { class: "l10n-switcher", "aria-label": "Language", children: [
|
|
7
|
+
/* @__PURE__ */ jsx("span", { class: "l10n-switcher__label", children: "Language" }),
|
|
8
|
+
/* @__PURE__ */ jsx("ul", { class: "l10n-switcher__list", children: localization.availableLanguages.map((lang) => {
|
|
9
|
+
const href = localization.translations[lang];
|
|
10
|
+
if (!href) return null;
|
|
11
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
12
|
+
"a",
|
|
13
|
+
{
|
|
14
|
+
class: "l10n-switcher__link",
|
|
15
|
+
href,
|
|
16
|
+
hreflang: lang,
|
|
17
|
+
lang,
|
|
18
|
+
"aria-current": lang === localization.lang ? "page" : void 0,
|
|
19
|
+
children: lang
|
|
20
|
+
}
|
|
21
|
+
) }, lang);
|
|
22
|
+
}) })
|
|
23
|
+
] });
|
|
24
|
+
});
|
|
25
|
+
var language_switcher_default = LanguageSwitcher;
|
|
26
|
+
|
|
1
27
|
// src/l10n.ts
|
|
2
28
|
import {
|
|
3
|
-
appendContentBodySlot,
|
|
4
29
|
attachErrorPath,
|
|
5
30
|
createStyleAsset,
|
|
6
31
|
definePlugin
|
|
7
32
|
} from "@riebeckite/core";
|
|
8
33
|
import { parse } from "yaml";
|
|
9
|
-
|
|
10
|
-
// src/language-switcher.ts
|
|
11
|
-
import { escapeHtml, escapeHtmlAttribute } from "@riebeckite/core";
|
|
12
|
-
function renderLanguageSwitcher({
|
|
13
|
-
localization
|
|
14
|
-
}) {
|
|
15
|
-
if (localization.availableLanguages.length < 2) return null;
|
|
16
|
-
const links = localization.availableLanguages.map((lang) => {
|
|
17
|
-
const href = localization.translations[lang];
|
|
18
|
-
if (!href) return "";
|
|
19
|
-
const current = lang === localization.lang ? ' aria-current="page"' : "";
|
|
20
|
-
const escaped = escapeHtml(lang);
|
|
21
|
-
return `<li><a class="l10n-switcher__link" href="${escapeHtmlAttribute(href)}" hreflang="${escapeHtmlAttribute(lang)}" lang="${escapeHtmlAttribute(lang)}"${current}>${escaped}</a></li>`;
|
|
22
|
-
}).join("");
|
|
23
|
-
if (!links) return null;
|
|
24
|
-
return `<nav class="l10n-switcher" aria-label="Language"><span class="l10n-switcher__label">Language</span><ul class="l10n-switcher__list">${links}</ul></nav>`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// src/l10n.ts
|
|
28
34
|
var TRANSLATION_METADATA_KEY = "l10n.translationId";
|
|
29
35
|
function l10n(options) {
|
|
30
36
|
const resolved = resolveOptions(options);
|
|
31
37
|
let state = emptyState();
|
|
32
38
|
return definePlugin({
|
|
33
39
|
name: "l10n",
|
|
40
|
+
processedContentCache: {
|
|
41
|
+
version: "l10n-global-content-locations-v1",
|
|
42
|
+
dependencyMode: "global"
|
|
43
|
+
},
|
|
34
44
|
provides: ["content.localization"],
|
|
35
|
-
|
|
45
|
+
contentOutputDependencies: [{ type: "global" }],
|
|
36
46
|
options,
|
|
37
|
-
assets: [createStyleAsset("l10n")],
|
|
47
|
+
assets: [createStyleAsset("l10n", "@riebeckite/plugin-l10n/style.css")],
|
|
38
48
|
validateOptions: () => validateOptions(options).map((message) => ({ path: "l10n", message })),
|
|
39
49
|
extendContentLocations: ({ entries, locations }) => {
|
|
40
|
-
state = {
|
|
50
|
+
state = {
|
|
51
|
+
...detectContents(entries, resolved),
|
|
52
|
+
locations,
|
|
53
|
+
slugsByPermalink: /* @__PURE__ */ new Map()
|
|
54
|
+
};
|
|
41
55
|
throwForStrictDiagnostics(state.diagnostics, resolved.strict);
|
|
42
56
|
for (const content of state.contents.values()) {
|
|
43
57
|
const location = locations.get(content.slug);
|
|
@@ -47,6 +61,12 @@ function l10n(options) {
|
|
|
47
61
|
withLocalization(location, content, resolved)
|
|
48
62
|
);
|
|
49
63
|
}
|
|
64
|
+
state = {
|
|
65
|
+
...state,
|
|
66
|
+
slugsByPermalink: new Map(
|
|
67
|
+
[...locations].map(([slug, location]) => [location.permalink, slug])
|
|
68
|
+
)
|
|
69
|
+
};
|
|
50
70
|
},
|
|
51
71
|
extendMarkdownPipeline: (pipeline, context) => {
|
|
52
72
|
pipeline.use(remarkLocalizedLinks, {
|
|
@@ -68,7 +88,6 @@ function l10n(options) {
|
|
|
68
88
|
},
|
|
69
89
|
onManifestCreated: ({ manifest }) => {
|
|
70
90
|
addLocalizationHeadTags(manifest, state);
|
|
71
|
-
addLanguageSwitchers(manifest, resolved.ui);
|
|
72
91
|
},
|
|
73
92
|
addDiagnostics: () => state.diagnostics
|
|
74
93
|
});
|
|
@@ -78,13 +97,7 @@ function remarkLocalizedLinks(options) {
|
|
|
78
97
|
return (tree) => {
|
|
79
98
|
const source = options.sourceSlug ? options.state.contents.get(options.sourceSlug) : void 0;
|
|
80
99
|
if (!source || !options.state.locations) return;
|
|
81
|
-
|
|
82
|
-
[...options.state.locations].map(([slug, location]) => [
|
|
83
|
-
location.permalink,
|
|
84
|
-
slug
|
|
85
|
-
])
|
|
86
|
-
);
|
|
87
|
-
localizeLinks(tree, source, options, slugsByPermalink);
|
|
100
|
+
localizeLinks(tree, source, options, options.state.slugsByPermalink);
|
|
88
101
|
};
|
|
89
102
|
}
|
|
90
103
|
function localizeLinks(node, source, options, slugsByPermalink) {
|
|
@@ -106,13 +119,13 @@ function splitLinkUrl(url) {
|
|
|
106
119
|
const suffixIndex = url.search(/[?#]/);
|
|
107
120
|
return suffixIndex < 0 ? { permalink: url, suffix: "" } : { permalink: url.slice(0, suffixIndex), suffix: url.slice(suffixIndex) };
|
|
108
121
|
}
|
|
109
|
-
function getLocalization(
|
|
110
|
-
const entry =
|
|
122
|
+
function getLocalization(view, slug) {
|
|
123
|
+
const entry = findLocalizedEntry(view, slug);
|
|
111
124
|
if (!entry) return null;
|
|
112
125
|
const lang = entry.publicLocation.language;
|
|
113
126
|
const translationId = entry.publicLocation.metadata?.[TRANSLATION_METADATA_KEY];
|
|
114
127
|
if (!lang || !translationId) return null;
|
|
115
|
-
const candidates =
|
|
128
|
+
const candidates = getLocalizedDiscoverableEntries(view).filter(
|
|
116
129
|
(candidate) => candidate.publicLocation.metadata?.[TRANSLATION_METADATA_KEY] === translationId
|
|
117
130
|
);
|
|
118
131
|
const translations = Object.fromEntries(
|
|
@@ -130,6 +143,12 @@ function getLocalization(manifest, slug) {
|
|
|
130
143
|
translations
|
|
131
144
|
};
|
|
132
145
|
}
|
|
146
|
+
function findLocalizedEntry(view, slug) {
|
|
147
|
+
return "findRoutableBySlug" in view ? view.findRoutableBySlug(slug) : view.bySlug.get(slug);
|
|
148
|
+
}
|
|
149
|
+
function getLocalizedDiscoverableEntries(view) {
|
|
150
|
+
return view.discoverableEntries;
|
|
151
|
+
}
|
|
133
152
|
function getLocalizedContent(manifest, slug, lang) {
|
|
134
153
|
const localization = getLocalization(manifest, slug);
|
|
135
154
|
if (!localization) return null;
|
|
@@ -158,8 +177,7 @@ function resolveOptions(options) {
|
|
|
158
177
|
languages.map((language) => [language.toLowerCase(), language])
|
|
159
178
|
),
|
|
160
179
|
strict: options.strict ?? false,
|
|
161
|
-
detect: options.detect
|
|
162
|
-
ui: resolveUiOptions(options.ui)
|
|
180
|
+
detect: options.detect
|
|
163
181
|
};
|
|
164
182
|
}
|
|
165
183
|
function validateOptions(options) {
|
|
@@ -194,24 +212,8 @@ function validateOptions(options) {
|
|
|
194
212
|
if (options.detect !== void 0 && typeof options.detect !== "function") {
|
|
195
213
|
errors.push("l10n.detect must be a function.");
|
|
196
214
|
}
|
|
197
|
-
if (options.ui !== void 0 && options.ui !== false) {
|
|
198
|
-
if (typeof options.ui !== "object") {
|
|
199
|
-
errors.push("l10n.ui must be false or an object.");
|
|
200
|
-
} else if (options.ui.slot !== void 0 && (typeof options.ui.slot !== "string" || !options.ui.slot.trim())) {
|
|
201
|
-
errors.push("l10n.ui.slot must be a non-empty string.");
|
|
202
|
-
} else if (options.ui.render !== void 0 && typeof options.ui.render !== "function") {
|
|
203
|
-
errors.push("l10n.ui.render must be a function.");
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
215
|
return errors;
|
|
207
216
|
}
|
|
208
|
-
function resolveUiOptions(ui) {
|
|
209
|
-
if (ui === false) return false;
|
|
210
|
-
return {
|
|
211
|
-
slot: ui?.slot ?? "article.metadata",
|
|
212
|
-
render: ui?.render ?? renderLanguageSwitcher
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
217
|
function canonicalLanguage(value) {
|
|
216
218
|
if (typeof value !== "string" || !value.trim()) {
|
|
217
219
|
throw new Error("A l10n language tag must be a non-empty string.");
|
|
@@ -224,6 +226,7 @@ function canonicalLanguage(value) {
|
|
|
224
226
|
}
|
|
225
227
|
function detectContents(entries, options) {
|
|
226
228
|
const contents = /* @__PURE__ */ new Map();
|
|
229
|
+
const contentsByTranslationAndLanguage = /* @__PURE__ */ new Map();
|
|
227
230
|
const diagnostics = [];
|
|
228
231
|
const owners = /* @__PURE__ */ new Map();
|
|
229
232
|
const conflictedGroups = /* @__PURE__ */ new Set();
|
|
@@ -284,9 +287,17 @@ function detectContents(entries, options) {
|
|
|
284
287
|
diagnostics.push(duplicateDiagnostic(previous, content, options.strict));
|
|
285
288
|
} else {
|
|
286
289
|
owners.set(key, content);
|
|
290
|
+
contentsByTranslationAndLanguage.set(key, content);
|
|
287
291
|
}
|
|
288
292
|
}
|
|
289
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
contents,
|
|
295
|
+
contentsByTranslationAndLanguage,
|
|
296
|
+
diagnostics,
|
|
297
|
+
conflictedGroups,
|
|
298
|
+
locations: null,
|
|
299
|
+
slugsByPermalink: /* @__PURE__ */ new Map()
|
|
300
|
+
};
|
|
290
301
|
}
|
|
291
302
|
function parseFrontmatter(entry) {
|
|
292
303
|
const match = entry.markdown.match(
|
|
@@ -375,9 +386,7 @@ function escapeRegExp(value) {
|
|
|
375
386
|
function findTranslation(state, translationId, lang) {
|
|
376
387
|
const key = `${translationId}\0${lang}`;
|
|
377
388
|
if (state.conflictedGroups.has(key)) return null;
|
|
378
|
-
return
|
|
379
|
-
(content) => content.translationId === translationId && content.lang === lang
|
|
380
|
-
) ?? null;
|
|
389
|
+
return state.contentsByTranslationAndLanguage.get(key) ?? null;
|
|
381
390
|
}
|
|
382
391
|
function resolveLocalizedTarget(state, source, targetSlug) {
|
|
383
392
|
const target = state.contents.get(targetSlug);
|
|
@@ -385,14 +394,11 @@ function resolveLocalizedTarget(state, source, targetSlug) {
|
|
|
385
394
|
return findTranslation(state, target.translationId, source.lang) ?? target;
|
|
386
395
|
}
|
|
387
396
|
function addLocalizationHeadTags(manifest, state) {
|
|
397
|
+
const translationsById = collectDiscoverableTranslations(manifest);
|
|
388
398
|
for (const entry of manifest.discoverableEntries) {
|
|
389
399
|
const content = state.contents.get(entry.slug);
|
|
390
400
|
if (!content) continue;
|
|
391
|
-
const translations =
|
|
392
|
-
manifest.discoverableEntries.filter(
|
|
393
|
-
(candidate) => candidate.publicLocation.metadata?.[TRANSLATION_METADATA_KEY] === content.translationId
|
|
394
|
-
)
|
|
395
|
-
);
|
|
401
|
+
const translations = translationsById.get(content.translationId) ?? [];
|
|
396
402
|
const headTags = translations.map(
|
|
397
403
|
(translation) => ({
|
|
398
404
|
tag: "link",
|
|
@@ -411,25 +417,35 @@ function addLocalizationHeadTags(manifest, state) {
|
|
|
411
417
|
];
|
|
412
418
|
}
|
|
413
419
|
}
|
|
414
|
-
function
|
|
415
|
-
|
|
420
|
+
function collectDiscoverableTranslations(manifest) {
|
|
421
|
+
const candidatesById = /* @__PURE__ */ new Map();
|
|
416
422
|
for (const entry of manifest.discoverableEntries) {
|
|
417
|
-
const
|
|
418
|
-
if (!
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
+
const translationId = entry.publicLocation.metadata?.[TRANSLATION_METADATA_KEY];
|
|
424
|
+
if (!translationId) continue;
|
|
425
|
+
const candidates = candidatesById.get(translationId) ?? [];
|
|
426
|
+
candidates.push(entry);
|
|
427
|
+
candidatesById.set(translationId, candidates);
|
|
423
428
|
}
|
|
429
|
+
return new Map(
|
|
430
|
+
[...candidatesById].map(([translationId, candidates]) => [
|
|
431
|
+
translationId,
|
|
432
|
+
uniqueLanguageEntries(candidates)
|
|
433
|
+
])
|
|
434
|
+
);
|
|
424
435
|
}
|
|
425
436
|
function sameHeadTag(left, right) {
|
|
426
437
|
return left.tag === right.tag && left.attrs?.rel === right.attrs?.rel && left.attrs?.hreflang === right.attrs?.hreflang && left.attrs?.href === right.attrs?.href;
|
|
427
438
|
}
|
|
428
439
|
function uniqueLanguageEntries(entries) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
440
|
+
const entriesByLanguage = /* @__PURE__ */ new Map();
|
|
441
|
+
for (const entry of entries) {
|
|
442
|
+
const language = entry.publicLocation.language;
|
|
443
|
+
if (!language) continue;
|
|
444
|
+
const candidates = entriesByLanguage.get(language) ?? [];
|
|
445
|
+
candidates.push(entry);
|
|
446
|
+
entriesByLanguage.set(language, candidates);
|
|
447
|
+
}
|
|
448
|
+
return [...entriesByLanguage.values()].filter((candidates) => candidates.length === 1).flatMap((candidates) => candidates[0] ? [candidates[0]] : []);
|
|
433
449
|
}
|
|
434
450
|
function compactSignals(signals) {
|
|
435
451
|
return Object.fromEntries(
|
|
@@ -489,16 +505,18 @@ function throwForStrictDiagnostics(diagnostics, strict) {
|
|
|
489
505
|
function emptyState() {
|
|
490
506
|
return {
|
|
491
507
|
contents: /* @__PURE__ */ new Map(),
|
|
508
|
+
contentsByTranslationAndLanguage: /* @__PURE__ */ new Map(),
|
|
492
509
|
diagnostics: [],
|
|
493
510
|
conflictedGroups: /* @__PURE__ */ new Set(),
|
|
494
|
-
locations: null
|
|
511
|
+
locations: null,
|
|
512
|
+
slugsByPermalink: /* @__PURE__ */ new Map()
|
|
495
513
|
};
|
|
496
514
|
}
|
|
497
515
|
export {
|
|
516
|
+
language_switcher_default as LanguageSwitcher,
|
|
498
517
|
getLanguageFromPath,
|
|
499
518
|
getLocalization,
|
|
500
519
|
getLocalizedContent,
|
|
501
520
|
l10n,
|
|
502
|
-
l10nPlugin
|
|
503
|
-
renderLanguageSwitcher
|
|
521
|
+
l10nPlugin
|
|
504
522
|
};
|
package/dist/src/l10n.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ContentManifest, type ContentManifestEntry, type PostFrontmatter, type PublicContentView } from "@riebeckite/core";
|
|
2
2
|
export type L10nContent = {
|
|
3
3
|
readonly path: string;
|
|
4
4
|
readonly slug: string;
|
|
@@ -17,17 +17,6 @@ export type L10nOptions = {
|
|
|
17
17
|
languages: readonly string[];
|
|
18
18
|
strict?: boolean;
|
|
19
19
|
detect?: (content: L10nDetectorContext) => L10nDetectorResult | undefined;
|
|
20
|
-
/**
|
|
21
|
-
* Publishes the built-in switcher into a standard article layout slot.
|
|
22
|
-
* Pass `false` to keep localization metadata and URLs without UI.
|
|
23
|
-
*/
|
|
24
|
-
ui?: L10nUiOptions;
|
|
25
|
-
};
|
|
26
|
-
export type L10nUiOptions = false | {
|
|
27
|
-
/** Defaults to the standard `article.metadata` slot. */
|
|
28
|
-
slot?: ContentBodySlot;
|
|
29
|
-
/** Replaces the built-in server-rendered LanguageSwitcher. */
|
|
30
|
-
render?: (context: LanguageSwitcherRenderContext) => string | null;
|
|
31
20
|
};
|
|
32
21
|
export type LocalizedContent = {
|
|
33
22
|
readonly slug: string;
|
|
@@ -37,14 +26,10 @@ export type LocalizedContent = {
|
|
|
37
26
|
readonly availableLanguages: readonly string[];
|
|
38
27
|
readonly translations: Readonly<Record<string, string>>;
|
|
39
28
|
};
|
|
40
|
-
export type LanguageSwitcherRenderContext = {
|
|
41
|
-
readonly localization: LocalizedContent;
|
|
42
|
-
};
|
|
43
29
|
export declare function l10n(options: L10nOptions): import("@riebeckite/core").RiebeckitePlugin<L10nOptions>;
|
|
44
30
|
export declare const l10nPlugin: typeof l10n;
|
|
45
31
|
/** Returns one entry's language and available real translations. */
|
|
46
|
-
export declare function getLocalization(
|
|
32
|
+
export declare function getLocalization(view: ContentManifest | PublicContentView, slug: string): LocalizedContent | null;
|
|
47
33
|
/** Resolves an existing translation; missing translations return `null`. */
|
|
48
34
|
export declare function getLocalizedContent(manifest: ContentManifest, slug: string, lang: string): ContentManifestEntry | null;
|
|
49
35
|
export declare function getLanguageFromPath(manifest: ContentManifest, pathname: string): string | undefined;
|
|
50
|
-
export { renderLanguageSwitcher } from "./language-switcher.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-l10n",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,15 +12,22 @@
|
|
|
12
12
|
"source": "./index.ts",
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
14
|
},
|
|
15
|
+
"./components": {
|
|
16
|
+
"types": "./dist/components/language-switcher.d.ts",
|
|
17
|
+
"import": "./dist/components/language-switcher.js",
|
|
18
|
+
"source": "./components/language-switcher.tsx",
|
|
19
|
+
"default": "./dist/components/language-switcher.js"
|
|
20
|
+
},
|
|
15
21
|
"./style.css": "./style.css"
|
|
16
22
|
},
|
|
17
23
|
"dependencies": {
|
|
24
|
+
"hono": "^4.12.25",
|
|
18
25
|
"yaml": "^2.9.0",
|
|
19
|
-
"@riebeckite/core": "0.0.
|
|
26
|
+
"@riebeckite/core": "0.0.24"
|
|
20
27
|
},
|
|
21
28
|
"devDependencies": {
|
|
22
29
|
"tsx": "^4.22.4",
|
|
23
|
-
"@riebeckite/plugin-obsidian-markdown": "0.0.
|
|
30
|
+
"@riebeckite/plugin-obsidian-markdown": "0.0.24"
|
|
24
31
|
},
|
|
25
32
|
"publishConfig": {
|
|
26
33
|
"access": "public"
|