@riebeckite/plugin-l10n 0.0.16 → 0.0.18
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 +6 -13
- package/README_ja.md +5 -5
- package/dist/index.js +23 -38
- package/dist/src/l10n.d.ts +1 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,16 +17,9 @@ export default defineConfig({
|
|
|
17
17
|
|
|
18
18
|
## Locale detection
|
|
19
19
|
|
|
20
|
-
Use `
|
|
20
|
+
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`.
|
|
21
21
|
|
|
22
|
-
Directory detection is
|
|
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:
|
|
22
|
+
Directory detection is not supported. Only filename suffixes and frontmatter are used to infer a language. Frontmatter works anywhere:
|
|
30
23
|
|
|
31
24
|
```md
|
|
32
25
|
---
|
|
@@ -35,7 +28,7 @@ translation: getting-started
|
|
|
35
28
|
---
|
|
36
29
|
```
|
|
37
30
|
|
|
38
|
-
Conventions may be mixed. Built-in precedence is **frontmatter >
|
|
31
|
+
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).
|
|
39
32
|
|
|
40
33
|
`translation` is independent of locale and filename. Use it to group files with unrelated paths or names:
|
|
41
34
|
|
|
@@ -52,7 +45,7 @@ No fallback pages are generated. `getLocalization(manifest, slug)` returns `avai
|
|
|
52
45
|
|
|
53
46
|
## Standard LanguageSwitcher
|
|
54
47
|
|
|
55
|
-
`l10n(...)` publishes the built-in, styled LanguageSwitcher in the standard `article.
|
|
48
|
+
`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.
|
|
56
49
|
|
|
57
50
|
```ts
|
|
58
51
|
// Keep URLs and metadata but do not publish UI.
|
|
@@ -73,7 +66,7 @@ Themes can override the default component through its `.l10n-switcher` CSS class
|
|
|
73
66
|
|
|
74
67
|
## Custom detector
|
|
75
68
|
|
|
76
|
-
For another convention, provide `detect`. Its language is considered after frontmatter and before filename
|
|
69
|
+
For another convention, provide `detect`. Its language is considered after frontmatter and before filename; its `translationId` is explicit.
|
|
77
70
|
|
|
78
71
|
```ts
|
|
79
72
|
l10n({
|
|
@@ -92,5 +85,5 @@ Each translated entry receives one `<link rel="alternate" hreflang="…">` per e
|
|
|
92
85
|
|
|
93
86
|
## See also
|
|
94
87
|
|
|
95
|
-
- [Plugin guide](../../../docs/
|
|
88
|
+
- [Plugin guide](../../../docs/docs/reference/plugin-api.en.md)
|
|
96
89
|
|
package/README_ja.md
CHANGED
|
@@ -14,7 +14,7 @@ l10n({ defaultLang: "ja", languages: ["ja", "en", "zh-CN"] });
|
|
|
14
14
|
|
|
15
15
|
## ロケール検出と翻訳グループ
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
ファイル名はこのリポジトリの慣例に合わせ、ドットの `README.ja.md` 形式を推奨します。`README-ja.md`、`README_ja.md`、`README.en-US.md`、`README.zh-CN.md` も利用できます。ディレクトリ名による検出は行いません。
|
|
18
18
|
|
|
19
19
|
frontmatter も利用できます。
|
|
20
20
|
|
|
@@ -25,7 +25,7 @@ translation: getting-started
|
|
|
25
25
|
---
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
組み合わせた場合の組み込み規則の優先順位は **frontmatter >
|
|
28
|
+
組み合わせた場合の組み込み規則の優先順位は **frontmatter > 独自検出 > ファイル名 > 既定言語** です。矛盾は `L10N_LANGUAGE_CONFLICT`、同一 `translation + lang` の重複は `L10N_DUPLICATE_TRANSLATION` として診断します。`strict: true` ではビルドを失敗させます。
|
|
29
29
|
|
|
30
30
|
`translation` はロケール検出とは別です。異なるパス・ファイル名でも明示的な ID でグループ化できます。翻訳がない言語のページを複製・生成することはありません。
|
|
31
31
|
|
|
@@ -37,7 +37,7 @@ translation: getting-started
|
|
|
37
37
|
|
|
38
38
|
## 標準 LanguageSwitcher
|
|
39
39
|
|
|
40
|
-
`l10n(...)` は標準記事レイアウトの `article.
|
|
40
|
+
`l10n(...)` は標準記事レイアウトの `article.metadata` スロットへ、組み込みの LanguageSwitcher を登録します。標準 Site consumer がこのスロットを描画するため、l10n 専用の記事レイアウト実装は不要です。実在する翻訳が 2 つ未満のページには表示されません。
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
43
|
// URL とメタデータは維持し、UI だけを無効化します。
|
|
@@ -69,8 +69,8 @@ l10n({
|
|
|
69
69
|
});
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
独自検出の言語は frontmatter
|
|
72
|
+
独自検出の言語は frontmatter より弱く、ファイル名より強く扱われます。`translationId` は明示的な翻訳グループ ID です。
|
|
73
73
|
|
|
74
74
|
## 関連リンク
|
|
75
75
|
|
|
76
|
-
- [プラグイン API](../../../docs/
|
|
76
|
+
- [プラグイン API](../../../docs/docs/reference/plugin-api.md)
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/l10n.ts
|
|
2
2
|
import {
|
|
3
3
|
appendContentBodySlot,
|
|
4
|
+
attachErrorPath,
|
|
4
5
|
createStyleAsset,
|
|
5
6
|
definePlugin
|
|
6
7
|
} from "@riebeckite/core";
|
|
@@ -24,7 +25,6 @@ function renderLanguageSwitcher({
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// src/l10n.ts
|
|
27
|
-
var LANGUAGE_METADATA_KEY = "l10n.lang";
|
|
28
28
|
var TRANSLATION_METADATA_KEY = "l10n.translationId";
|
|
29
29
|
function l10n(options) {
|
|
30
30
|
const resolved = resolveOptions(options);
|
|
@@ -109,7 +109,7 @@ function splitLinkUrl(url) {
|
|
|
109
109
|
function getLocalization(manifest, slug) {
|
|
110
110
|
const entry = manifest.bySlug.get(slug);
|
|
111
111
|
if (!entry) return null;
|
|
112
|
-
const lang = entry.publicLocation.
|
|
112
|
+
const lang = entry.publicLocation.language;
|
|
113
113
|
const translationId = entry.publicLocation.metadata?.[TRANSLATION_METADATA_KEY];
|
|
114
114
|
if (!lang || !translationId) return null;
|
|
115
115
|
const candidates = manifest.discoverableEntries.filter(
|
|
@@ -117,7 +117,7 @@ function getLocalization(manifest, slug) {
|
|
|
117
117
|
);
|
|
118
118
|
const translations = Object.fromEntries(
|
|
119
119
|
uniqueLanguageEntries(candidates).map((candidate) => [
|
|
120
|
-
candidate.publicLocation.
|
|
120
|
+
candidate.publicLocation.language ?? "",
|
|
121
121
|
candidate.permalink
|
|
122
122
|
])
|
|
123
123
|
);
|
|
@@ -134,14 +134,14 @@ function getLocalizedContent(manifest, slug, lang) {
|
|
|
134
134
|
const localization = getLocalization(manifest, slug);
|
|
135
135
|
if (!localization) return null;
|
|
136
136
|
const candidates = manifest.publicEntries.filter(
|
|
137
|
-
(entry) => entry.publicLocation.metadata?.[TRANSLATION_METADATA_KEY] === localization.translationId && entry.publicLocation.
|
|
137
|
+
(entry) => entry.publicLocation.metadata?.[TRANSLATION_METADATA_KEY] === localization.translationId && entry.publicLocation.language === lang
|
|
138
138
|
);
|
|
139
139
|
return candidates.length === 1 ? candidates[0] ?? null : null;
|
|
140
140
|
}
|
|
141
141
|
function getLanguageFromPath(manifest, pathname) {
|
|
142
142
|
const firstSegment = pathname.split("/").filter(Boolean)[0];
|
|
143
143
|
if (!firstSegment) return void 0;
|
|
144
|
-
return manifest.discoverableEntries.map((entry) => entry.publicLocation.
|
|
144
|
+
return manifest.discoverableEntries.map((entry) => entry.publicLocation.language).find(
|
|
145
145
|
(language) => language?.toLocaleLowerCase() === firstSegment.toLocaleLowerCase()
|
|
146
146
|
);
|
|
147
147
|
}
|
|
@@ -208,7 +208,7 @@ function validateOptions(options) {
|
|
|
208
208
|
function resolveUiOptions(ui) {
|
|
209
209
|
if (ui === false) return false;
|
|
210
210
|
return {
|
|
211
|
-
slot: ui?.slot ?? "article.
|
|
211
|
+
slot: ui?.slot ?? "article.metadata",
|
|
212
212
|
render: ui?.render ?? renderLanguageSwitcher
|
|
213
213
|
};
|
|
214
214
|
}
|
|
@@ -233,16 +233,11 @@ function detectContents(entries, options) {
|
|
|
233
233
|
entry.path,
|
|
234
234
|
options.languageByLowercase
|
|
235
235
|
);
|
|
236
|
-
const directoryLanguage = detectDirectoryLanguage(
|
|
237
|
-
entry.path,
|
|
238
|
-
options.languageByLowercase
|
|
239
|
-
);
|
|
240
236
|
const detectorContext = {
|
|
241
237
|
path: entry.path,
|
|
242
238
|
slug: entry.slug,
|
|
243
239
|
frontmatter,
|
|
244
|
-
filenameLanguage
|
|
245
|
-
directoryLanguage
|
|
240
|
+
filenameLanguage
|
|
246
241
|
};
|
|
247
242
|
const custom = options.detect?.(detectorContext);
|
|
248
243
|
const frontmatterLanguage = configuredLanguage(
|
|
@@ -252,8 +247,7 @@ function detectContents(entries, options) {
|
|
|
252
247
|
const signals = compactSignals({
|
|
253
248
|
custom: configuredLanguage(custom?.lang, options.languageByLowercase),
|
|
254
249
|
frontmatter: frontmatterLanguage,
|
|
255
|
-
filename: filenameLanguage
|
|
256
|
-
directory: directoryLanguage
|
|
250
|
+
filename: filenameLanguage
|
|
257
251
|
});
|
|
258
252
|
addUnknownLanguageDiagnostic(
|
|
259
253
|
diagnostics,
|
|
@@ -272,8 +266,8 @@ function detectContents(entries, options) {
|
|
|
272
266
|
if (new Set(Object.values(signals)).size > 1) {
|
|
273
267
|
diagnostics.push(conflictDiagnostic(entry, signals, options.strict));
|
|
274
268
|
}
|
|
275
|
-
const lang = signals.frontmatter ?? signals.custom ?? signals.filename ??
|
|
276
|
-
const translationId = readTranslationId(custom?.translationId ?? frontmatter.translation) ?? deriveTranslationId(entry.path, filenameLanguage
|
|
269
|
+
const lang = signals.frontmatter ?? signals.custom ?? signals.filename ?? options.defaultLang;
|
|
270
|
+
const translationId = readTranslationId(custom?.translationId ?? frontmatter.translation) ?? deriveTranslationId(entry.path, filenameLanguage);
|
|
277
271
|
const content = {
|
|
278
272
|
path: entry.path,
|
|
279
273
|
slug: entry.slug,
|
|
@@ -299,7 +293,12 @@ function parseFrontmatter(entry) {
|
|
|
299
293
|
/^(?:\uFEFF)?---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/
|
|
300
294
|
);
|
|
301
295
|
if (!match) return {};
|
|
302
|
-
|
|
296
|
+
let value;
|
|
297
|
+
try {
|
|
298
|
+
value = parse(match[1] ?? "");
|
|
299
|
+
} catch (error) {
|
|
300
|
+
throw attachErrorPath(error, entry.path);
|
|
301
|
+
}
|
|
303
302
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
304
303
|
throw new Error(`Invalid frontmatter object: ${entry.path}`);
|
|
305
304
|
}
|
|
@@ -313,10 +312,6 @@ function detectFilenameLanguage(path, languages) {
|
|
|
313
312
|
}
|
|
314
313
|
return void 0;
|
|
315
314
|
}
|
|
316
|
-
function detectDirectoryLanguage(path, languages) {
|
|
317
|
-
const first = path.split("/")[0]?.toLowerCase();
|
|
318
|
-
return first ? languages.get(first) : void 0;
|
|
319
|
-
}
|
|
320
315
|
function configuredLanguage(value, languages) {
|
|
321
316
|
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
322
317
|
try {
|
|
@@ -325,9 +320,8 @@ function configuredLanguage(value, languages) {
|
|
|
325
320
|
return void 0;
|
|
326
321
|
}
|
|
327
322
|
}
|
|
328
|
-
function deriveTranslationId(path, filenameLanguage
|
|
323
|
+
function deriveTranslationId(path, filenameLanguage) {
|
|
329
324
|
const parts = path.replace(/\.md$/i, "").split("/");
|
|
330
|
-
if (directoryLanguage) parts.shift();
|
|
331
325
|
const last = parts.pop() ?? "";
|
|
332
326
|
const language = filenameLanguage?.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
333
327
|
parts.push(
|
|
@@ -347,13 +341,13 @@ function withLocalization(location, content, options) {
|
|
|
347
341
|
return {
|
|
348
342
|
...location,
|
|
349
343
|
permalink,
|
|
344
|
+
language: content.lang,
|
|
350
345
|
redirects: location.redirects?.map((redirect) => ({
|
|
351
346
|
...redirect,
|
|
352
347
|
path: localizePath(redirect.path, content, options)
|
|
353
348
|
})),
|
|
354
349
|
metadata: {
|
|
355
350
|
...location.metadata,
|
|
356
|
-
[LANGUAGE_METADATA_KEY]: content.lang,
|
|
357
351
|
[TRANSLATION_METADATA_KEY]: content.translationId
|
|
358
352
|
}
|
|
359
353
|
};
|
|
@@ -369,13 +363,6 @@ function removeLocalePathSegments(permalink, content) {
|
|
|
369
363
|
const suffix = new RegExp(`([._-])${escapeRegExp(filenameLanguage)}$`, "i");
|
|
370
364
|
result = result.replace(suffix, "");
|
|
371
365
|
}
|
|
372
|
-
const directoryLanguage = content.signals.directory;
|
|
373
|
-
if (directoryLanguage) {
|
|
374
|
-
result = result.replace(
|
|
375
|
-
new RegExp(`^/${escapeRegExp(directoryLanguage)}(?=/|$)`, "i"),
|
|
376
|
-
""
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
366
|
return result || "/";
|
|
380
367
|
}
|
|
381
368
|
function prefixLanguage(permalink, language) {
|
|
@@ -411,7 +398,7 @@ function addLocalizationHeadTags(manifest, state) {
|
|
|
411
398
|
tag: "link",
|
|
412
399
|
attrs: {
|
|
413
400
|
rel: "alternate",
|
|
414
|
-
hreflang: translation.publicLocation.
|
|
401
|
+
hreflang: translation.publicLocation.language ?? "",
|
|
415
402
|
href: translation.permalink
|
|
416
403
|
}
|
|
417
404
|
})
|
|
@@ -440,10 +427,8 @@ function sameHeadTag(left, right) {
|
|
|
440
427
|
}
|
|
441
428
|
function uniqueLanguageEntries(entries) {
|
|
442
429
|
return entries.filter((entry) => {
|
|
443
|
-
const lang = entry.publicLocation.
|
|
444
|
-
return lang !== void 0 && entries.filter(
|
|
445
|
-
(other) => other.publicLocation.metadata?.[LANGUAGE_METADATA_KEY] === lang
|
|
446
|
-
).length === 1;
|
|
430
|
+
const lang = entry.publicLocation.language;
|
|
431
|
+
return lang !== void 0 && entries.filter((other) => other.publicLocation.language === lang).length === 1;
|
|
447
432
|
});
|
|
448
433
|
}
|
|
449
434
|
function compactSignals(signals) {
|
|
@@ -461,10 +446,10 @@ function conflictDiagnostic(entry, signals, strict) {
|
|
|
461
446
|
slug: entry.slug,
|
|
462
447
|
message: `Conflicting localization metadata (${Object.entries(signals).map(([source, language]) => `${source}: ${language}`).join(
|
|
463
448
|
", "
|
|
464
|
-
)}); resolved to ${signals.frontmatter ?? signals.custom ?? signals.filename
|
|
449
|
+
)}); resolved to ${signals.frontmatter ?? signals.custom ?? signals.filename}.`,
|
|
465
450
|
meta: {
|
|
466
451
|
detected: signals,
|
|
467
|
-
resolved: signals.frontmatter ?? signals.custom ?? signals.filename
|
|
452
|
+
resolved: signals.frontmatter ?? signals.custom ?? signals.filename
|
|
468
453
|
}
|
|
469
454
|
};
|
|
470
455
|
}
|
package/dist/src/l10n.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ export type L10nContent = {
|
|
|
6
6
|
};
|
|
7
7
|
export type L10nDetectorContext = L10nContent & {
|
|
8
8
|
readonly filenameLanguage: string | undefined;
|
|
9
|
-
readonly directoryLanguage: string | undefined;
|
|
10
9
|
};
|
|
11
10
|
/** A detector may provide a language and/or an explicit translation identity. */
|
|
12
11
|
export type L10nDetectorResult = {
|
|
@@ -25,7 +24,7 @@ export type L10nOptions = {
|
|
|
25
24
|
ui?: L10nUiOptions;
|
|
26
25
|
};
|
|
27
26
|
export type L10nUiOptions = false | {
|
|
28
|
-
/** Defaults to the standard `article.
|
|
27
|
+
/** Defaults to the standard `article.metadata` slot. */
|
|
29
28
|
slot?: ContentBodySlot;
|
|
30
29
|
/** Replaces the built-in server-rendered LanguageSwitcher. */
|
|
31
30
|
render?: (context: LanguageSwitcherRenderContext) => string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riebeckite/plugin-l10n",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"yaml": "^2.9.0",
|
|
19
|
-
"@riebeckite/core": "0.0.
|
|
19
|
+
"@riebeckite/core": "0.0.18"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"tsx": "^4.22.4",
|
|
23
|
-
"@riebeckite/plugin-obsidian-markdown": "0.0.
|
|
23
|
+
"@riebeckite/plugin-obsidian-markdown": "0.0.18"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
26
26
|
"access": "public"
|