@riebeckite/plugin-l10n 0.0.27 → 0.0.28
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 +7 -1
- package/README_ja.md +6 -1
- package/dist/components/language-switcher.js +8 -5
- package/dist/index.js +118 -16
- package/package.json +4 -4
- package/style.css +7 -7
package/README.md
CHANGED
|
@@ -47,6 +47,12 @@ No fallback pages are generated. `getLocalization(manifest, slug)` returns `avai
|
|
|
47
47
|
|
|
48
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.
|
|
49
49
|
|
|
50
|
+
## CSS hooks
|
|
51
|
+
|
|
52
|
+
The LanguageSwitcher uses `rr-l10n-switcher`,
|
|
53
|
+
`rr-l10n-switcher__label`, `rr-l10n-switcher__list`, and
|
|
54
|
+
`rr-l10n-switcher__link`. Default CSS targets these hooks.
|
|
55
|
+
|
|
50
56
|
## Custom detector
|
|
51
57
|
|
|
52
58
|
For another convention, provide `detect`. Its language is considered after frontmatter and before filename; its `translationId` is explicit.
|
|
@@ -62,7 +68,7 @@ l10n({
|
|
|
62
68
|
|
|
63
69
|
## Links and SEO
|
|
64
70
|
|
|
65
|
-
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.
|
|
71
|
+
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. Link dependency tracking uses the same parsed Markdown URLs as rendering, including HTML character references. The Content Graph and rendered links use the same resolution policy; no second graph is created.
|
|
66
72
|
|
|
67
73
|
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.
|
|
68
74
|
|
package/README_ja.md
CHANGED
|
@@ -46,7 +46,12 @@ English/getting-started.md lang: en, translation: getting-started
|
|
|
46
46
|
|
|
47
47
|
`l10n(...)` は標準記事レイアウトへ組み込みの言語切替 UI を登録します。標準 Site consumer がこの UI を描画するため、l10n 専用の記事レイアウト実装は不要です。実在する翻訳が 2 つ未満のページには表示されません。
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
## CSS Hook
|
|
50
|
+
|
|
51
|
+
言語切替 UI には `rr-l10n-switcher`、`rr-l10n-switcher__label`、
|
|
52
|
+
`rr-l10n-switcher__list`、`rr-l10n-switcher__link` を使用します。default CSS はこれらの Hook を対象にします。
|
|
53
|
+
|
|
54
|
+
各ページには 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 は変更しません。リンク依存関係の追跡でも描画と同じ解析済み Markdown URL を使用するため、HTML 文字参照も同じように扱います。Content Graph と本文リンクは同じ解決ポリシーを使います。
|
|
50
55
|
|
|
51
56
|
## 独自検出
|
|
52
57
|
|
|
@@ -5,7 +5,10 @@ import { useArticleContext, useSiteContext } from "@riebeckite/ui";
|
|
|
5
5
|
import {
|
|
6
6
|
attachErrorPath,
|
|
7
7
|
createStyleAsset,
|
|
8
|
-
definePlugin
|
|
8
|
+
definePlugin,
|
|
9
|
+
escapeRegExp,
|
|
10
|
+
extractContentLinks,
|
|
11
|
+
extractMarkdownLinkUrls
|
|
9
12
|
} from "@riebeckite/core";
|
|
10
13
|
import { parse } from "yaml";
|
|
11
14
|
var TRANSLATION_METADATA_KEY = "l10n.translationId";
|
|
@@ -58,15 +61,15 @@ var LanguageSwitcher = ((props) => {
|
|
|
58
61
|
const article = useArticleContext();
|
|
59
62
|
const localization = props.localization ?? (site && article?.entry ? getLocalization(site.view, article.entry.slug) : null);
|
|
60
63
|
if (!localization || localization.availableLanguages.length < 2) return null;
|
|
61
|
-
return /* @__PURE__ */ jsxs("nav", { class: "l10n-switcher", "aria-label": "Language", children: [
|
|
62
|
-
/* @__PURE__ */ jsx("span", { class: "l10n-switcher__label", children: "Language" }),
|
|
63
|
-
/* @__PURE__ */ jsx("ul", { class: "l10n-switcher__list", children: localization.availableLanguages.map((lang) => {
|
|
64
|
+
return /* @__PURE__ */ jsxs("nav", { class: "rr-l10n-switcher", "aria-label": "Language", children: [
|
|
65
|
+
/* @__PURE__ */ jsx("span", { class: "rr-l10n-switcher__label", children: "Language" }),
|
|
66
|
+
/* @__PURE__ */ jsx("ul", { class: "rr-l10n-switcher__list", children: localization.availableLanguages.map((lang) => {
|
|
64
67
|
const href = localization.translations[lang];
|
|
65
68
|
if (!href) return null;
|
|
66
69
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
67
70
|
"a",
|
|
68
71
|
{
|
|
69
|
-
class: "l10n-switcher__link",
|
|
72
|
+
class: "rr-l10n-switcher__link",
|
|
70
73
|
href,
|
|
71
74
|
hreflang: lang,
|
|
72
75
|
lang,
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,10 @@ import { useArticleContext, useSiteContext } from "@riebeckite/ui";
|
|
|
5
5
|
import {
|
|
6
6
|
attachErrorPath,
|
|
7
7
|
createStyleAsset,
|
|
8
|
-
definePlugin
|
|
8
|
+
definePlugin,
|
|
9
|
+
escapeRegExp,
|
|
10
|
+
extractContentLinks,
|
|
11
|
+
extractMarkdownLinkUrls
|
|
9
12
|
} from "@riebeckite/core";
|
|
10
13
|
import { parse } from "yaml";
|
|
11
14
|
var TRANSLATION_METADATA_KEY = "l10n.translationId";
|
|
@@ -15,15 +18,15 @@ function l10n(options) {
|
|
|
15
18
|
return definePlugin({
|
|
16
19
|
name: "l10n",
|
|
17
20
|
processedContentCache: {
|
|
18
|
-
version: "l10n-
|
|
19
|
-
dependencyMode: "
|
|
21
|
+
version: "l10n-content-locations-v2",
|
|
22
|
+
dependencyMode: "tracked",
|
|
23
|
+
contentDependencyFingerprint: ({ slug }) => state.signatures.get(slug)
|
|
20
24
|
},
|
|
21
25
|
provides: ["content.localization"],
|
|
22
|
-
contentOutputDependencies: [{ type: "global" }],
|
|
23
26
|
options,
|
|
24
27
|
assets: [createStyleAsset("l10n", "@riebeckite/plugin-l10n/style.css")],
|
|
25
28
|
validateOptions: () => validateOptions(options).map((message) => ({ path: "l10n", message })),
|
|
26
|
-
extendContentLocations: ({ entries, locations }) => {
|
|
29
|
+
extendContentLocations: ({ entries, locations, contentIndex }) => {
|
|
27
30
|
state = {
|
|
28
31
|
...detectContents(entries, resolved),
|
|
29
32
|
locations,
|
|
@@ -38,10 +41,16 @@ function l10n(options) {
|
|
|
38
41
|
withLocalization(location, content, resolved)
|
|
39
42
|
);
|
|
40
43
|
}
|
|
44
|
+
const slugsByPermalink = new Map(
|
|
45
|
+
[...locations].map(([slug, location]) => [location.permalink, slug])
|
|
46
|
+
);
|
|
41
47
|
state = {
|
|
42
48
|
...state,
|
|
43
|
-
slugsByPermalink
|
|
44
|
-
|
|
49
|
+
slugsByPermalink,
|
|
50
|
+
signatures: computeLocalizationSignatures(
|
|
51
|
+
{ ...state, slugsByPermalink },
|
|
52
|
+
entries,
|
|
53
|
+
contentIndex
|
|
45
54
|
)
|
|
46
55
|
};
|
|
47
56
|
},
|
|
@@ -273,7 +282,8 @@ function detectContents(entries, options) {
|
|
|
273
282
|
diagnostics,
|
|
274
283
|
conflictedGroups,
|
|
275
284
|
locations: null,
|
|
276
|
-
slugsByPermalink: /* @__PURE__ */ new Map()
|
|
285
|
+
slugsByPermalink: /* @__PURE__ */ new Map(),
|
|
286
|
+
signatures: /* @__PURE__ */ new Map()
|
|
277
287
|
};
|
|
278
288
|
}
|
|
279
289
|
function parseFrontmatter(entry) {
|
|
@@ -357,9 +367,6 @@ function prefixLanguage(permalink, language) {
|
|
|
357
367
|
const suffix = permalink === "/" ? "" : permalink;
|
|
358
368
|
return `/${language}${suffix}`;
|
|
359
369
|
}
|
|
360
|
-
function escapeRegExp(value) {
|
|
361
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
362
|
-
}
|
|
363
370
|
function findTranslation(state, translationId, lang) {
|
|
364
371
|
const key = `${translationId}\0${lang}`;
|
|
365
372
|
if (state.conflictedGroups.has(key)) return null;
|
|
@@ -370,6 +377,100 @@ function resolveLocalizedTarget(state, source, targetSlug) {
|
|
|
370
377
|
if (!target) return null;
|
|
371
378
|
return findTranslation(state, target.translationId, source.lang) ?? target;
|
|
372
379
|
}
|
|
380
|
+
var PUBLICATION_KEYS = [
|
|
381
|
+
"visibility",
|
|
382
|
+
"publishAt",
|
|
383
|
+
"publish",
|
|
384
|
+
"private",
|
|
385
|
+
"draft"
|
|
386
|
+
];
|
|
387
|
+
function signalValue(value) {
|
|
388
|
+
if (value === void 0 || value === null) return "";
|
|
389
|
+
if (value instanceof Date) return value.toISOString();
|
|
390
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
391
|
+
return String(value);
|
|
392
|
+
}
|
|
393
|
+
function publicationSignal(frontmatter) {
|
|
394
|
+
return PUBLICATION_KEYS.map((key) => signalValue(frontmatter[key])).join(
|
|
395
|
+
""
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
function groupMemberToken(state, member) {
|
|
399
|
+
return [
|
|
400
|
+
member.lang,
|
|
401
|
+
member.slug,
|
|
402
|
+
state.locations?.get(member.slug)?.permalink ?? "",
|
|
403
|
+
publicationSignal(member.frontmatter)
|
|
404
|
+
].join("");
|
|
405
|
+
}
|
|
406
|
+
function localizedResolutionToken(state, source, targetSlug) {
|
|
407
|
+
const target = resolveLocalizedTarget(state, source, targetSlug);
|
|
408
|
+
if (!target) return "";
|
|
409
|
+
return `${target.slug}${state.locations?.get(target.slug)?.permalink ?? ""}`;
|
|
410
|
+
}
|
|
411
|
+
function referencedPermalinks(markdown, slugsByPermalink) {
|
|
412
|
+
if (slugsByPermalink.size === 0) return [];
|
|
413
|
+
const found = /* @__PURE__ */ new Set();
|
|
414
|
+
for (const url of extractMarkdownLinkUrls(markdown)) {
|
|
415
|
+
const { permalink } = splitLinkUrl(url);
|
|
416
|
+
if (slugsByPermalink.has(permalink)) found.add(permalink);
|
|
417
|
+
}
|
|
418
|
+
return [...found].sort();
|
|
419
|
+
}
|
|
420
|
+
function computeLocalizationSignatures(state, entries, contentIndex) {
|
|
421
|
+
const markdownBySlug = new Map(
|
|
422
|
+
entries.map((entry) => [entry.slug, entry.markdown])
|
|
423
|
+
);
|
|
424
|
+
const membersByTranslation = /* @__PURE__ */ new Map();
|
|
425
|
+
for (const member of state.contents.values()) {
|
|
426
|
+
const members = membersByTranslation.get(member.translationId) ?? [];
|
|
427
|
+
members.push(groupMemberToken(state, member));
|
|
428
|
+
membersByTranslation.set(member.translationId, members);
|
|
429
|
+
}
|
|
430
|
+
for (const members of membersByTranslation.values()) members.sort();
|
|
431
|
+
const collectParts = (content, trail) => {
|
|
432
|
+
const members = membersByTranslation.get(content.translationId) ?? [];
|
|
433
|
+
const parts = [
|
|
434
|
+
`own:${content.lang}${content.translationId}`,
|
|
435
|
+
...members.map((member) => `group:${member}`)
|
|
436
|
+
];
|
|
437
|
+
const markdown = markdownBySlug.get(content.slug) ?? "";
|
|
438
|
+
const links = extractContentLinks(markdown, contentIndex);
|
|
439
|
+
for (const link of links) {
|
|
440
|
+
if (link.kind === "image" || link.kind === "attachment") continue;
|
|
441
|
+
parts.push(
|
|
442
|
+
`wiki:${link.raw.toLowerCase()}=${link.slug ? localizedResolutionToken(state, content, link.slug) : ""}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
for (const permalink of referencedPermalinks(
|
|
446
|
+
markdown,
|
|
447
|
+
state.slugsByPermalink
|
|
448
|
+
)) {
|
|
449
|
+
const targetSlug = state.slugsByPermalink.get(permalink);
|
|
450
|
+
parts.push(
|
|
451
|
+
`url:${permalink}=${targetSlug ? localizedResolutionToken(state, content, targetSlug) : ""}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (trail.size < 3) {
|
|
455
|
+
for (const link of links) {
|
|
456
|
+
if (!link.embed || !link.slug || trail.has(link.slug)) continue;
|
|
457
|
+
const embedded = state.contents.get(link.slug);
|
|
458
|
+
if (!embedded) continue;
|
|
459
|
+
parts.push(
|
|
460
|
+
...collectParts(embedded, /* @__PURE__ */ new Set([...trail, embedded.slug]))
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return parts;
|
|
465
|
+
};
|
|
466
|
+
const signatures = /* @__PURE__ */ new Map();
|
|
467
|
+
for (const content of state.contents.values()) {
|
|
468
|
+
const parts = collectParts(content, /* @__PURE__ */ new Set());
|
|
469
|
+
parts.sort();
|
|
470
|
+
signatures.set(content.slug, parts.join("\n"));
|
|
471
|
+
}
|
|
472
|
+
return signatures;
|
|
473
|
+
}
|
|
373
474
|
function addLocalizationHeadTags(manifest, state) {
|
|
374
475
|
const translationsById = collectDiscoverableTranslations(manifest);
|
|
375
476
|
for (const entry of manifest.discoverableEntries) {
|
|
@@ -486,7 +587,8 @@ function emptyState() {
|
|
|
486
587
|
diagnostics: [],
|
|
487
588
|
conflictedGroups: /* @__PURE__ */ new Set(),
|
|
488
589
|
locations: null,
|
|
489
|
-
slugsByPermalink: /* @__PURE__ */ new Map()
|
|
590
|
+
slugsByPermalink: /* @__PURE__ */ new Map(),
|
|
591
|
+
signatures: /* @__PURE__ */ new Map()
|
|
490
592
|
};
|
|
491
593
|
}
|
|
492
594
|
|
|
@@ -497,15 +599,15 @@ var LanguageSwitcher = ((props) => {
|
|
|
497
599
|
const article = useArticleContext();
|
|
498
600
|
const localization = props.localization ?? (site && article?.entry ? getLocalization(site.view, article.entry.slug) : null);
|
|
499
601
|
if (!localization || localization.availableLanguages.length < 2) return null;
|
|
500
|
-
return /* @__PURE__ */ jsxs("nav", { class: "l10n-switcher", "aria-label": "Language", children: [
|
|
501
|
-
/* @__PURE__ */ jsx("span", { class: "l10n-switcher__label", children: "Language" }),
|
|
502
|
-
/* @__PURE__ */ jsx("ul", { class: "l10n-switcher__list", children: localization.availableLanguages.map((lang) => {
|
|
602
|
+
return /* @__PURE__ */ jsxs("nav", { class: "rr-l10n-switcher", "aria-label": "Language", children: [
|
|
603
|
+
/* @__PURE__ */ jsx("span", { class: "rr-l10n-switcher__label", children: "Language" }),
|
|
604
|
+
/* @__PURE__ */ jsx("ul", { class: "rr-l10n-switcher__list", children: localization.availableLanguages.map((lang) => {
|
|
503
605
|
const href = localization.translations[lang];
|
|
504
606
|
if (!href) return null;
|
|
505
607
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
506
608
|
"a",
|
|
507
609
|
{
|
|
508
|
-
class: "l10n-switcher__link",
|
|
610
|
+
class: "rr-l10n-switcher__link",
|
|
509
611
|
href,
|
|
510
612
|
hreflang: lang,
|
|
511
613
|
lang,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-l10n",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"hono": "^4.12.25",
|
|
25
25
|
"yaml": "^2.9.0",
|
|
26
|
-
"@riebeckite/core": "0.0.
|
|
27
|
-
"@riebeckite/ui": "0.0.
|
|
26
|
+
"@riebeckite/core": "0.0.28",
|
|
27
|
+
"@riebeckite/ui": "0.0.28"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsx": "^4.22.4",
|
|
31
|
-
"@riebeckite/plugin-obsidian-markdown": "0.0.
|
|
31
|
+
"@riebeckite/plugin-obsidian-markdown": "0.0.28"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
package/style.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.l10n-switcher {
|
|
1
|
+
.rr-l10n-switcher {
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-wrap: wrap;
|
|
4
4
|
align-items: baseline;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
font-size: 0.8125rem;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
.l10n-switcher__label {
|
|
10
|
+
.rr-l10n-switcher__label {
|
|
11
11
|
font-size: 0.6875rem;
|
|
12
12
|
font-weight: 700;
|
|
13
13
|
letter-spacing: 0.12em;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
text-transform: uppercase;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.l10n-switcher__list {
|
|
21
|
+
.rr-l10n-switcher__list {
|
|
22
22
|
display: flex;
|
|
23
23
|
flex-wrap: wrap;
|
|
24
24
|
gap: 0.75rem;
|
|
@@ -27,19 +27,19 @@
|
|
|
27
27
|
list-style: none;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
.l10n-switcher__link {
|
|
30
|
+
.rr-l10n-switcher__link {
|
|
31
31
|
color: inherit;
|
|
32
32
|
text-decoration: none;
|
|
33
33
|
text-underline-offset: 0.2em;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
.l10n-switcher__link:hover,
|
|
37
|
-
.l10n-switcher__link:focus-visible {
|
|
36
|
+
.rr-l10n-switcher__link:hover,
|
|
37
|
+
.rr-l10n-switcher__link:focus-visible {
|
|
38
38
|
color: var(--rb-color-accent, currentColor);
|
|
39
39
|
text-decoration: underline;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
.l10n-switcher__link[aria-current="page"] {
|
|
42
|
+
.rr-l10n-switcher__link[aria-current="page"] {
|
|
43
43
|
font-weight: 700;
|
|
44
44
|
text-decoration: underline;
|
|
45
45
|
}
|