@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 CHANGED
@@ -17,16 +17,9 @@ export default defineConfig({
17
17
 
18
18
  ## Locale detection
19
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`.
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 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:
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 > 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).
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.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.
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/directory; its `translationId` is explicit.
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/en/docs/reference/plugin-api.md)
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
- ファイル名はこのリポジトリの慣例に合わせ、アンダースコアの `README_ja.md` 形式を推奨します。`README.ja.md`、`README-ja.md`、`README.en.md`、`README.en-US.md`、`README.zh-CN.md` も利用できます。設定済みの言語名に一致する先頭ディレクトリ(`en/README.md`)も検出します。
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 > ファイル名 > ディレクトリ > 既定言語** です。矛盾は `L10N_LANGUAGE_CONFLICT`、同一 `translation + lang` の重複は `L10N_DUPLICATE_TRANSLATION` として診断します。`strict: true` ではビルドを失敗させます。
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.after-meta` スロットへ、組み込みの LanguageSwitcher を登録します。標準 Site consumer がこのスロットを描画するため、l10n 専用の記事レイアウト実装は不要です。実在する翻訳が 2 つ未満のページには表示されません。
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 より弱く、ファイル名・ディレクトリより強く扱われます。`translationId` は明示的な翻訳グループ ID です。
72
+ 独自検出の言語は frontmatter より弱く、ファイル名より強く扱われます。`translationId` は明示的な翻訳グループ ID です。
73
73
 
74
74
  ## 関連リンク
75
75
 
76
- - [プラグイン API](../../../docs/ja/docs/reference/plugin-api.md)
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.metadata?.[LANGUAGE_METADATA_KEY];
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.metadata?.[LANGUAGE_METADATA_KEY] ?? "",
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.metadata?.[LANGUAGE_METADATA_KEY] === lang
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.metadata?.[LANGUAGE_METADATA_KEY]).find(
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.after-meta",
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 ?? signals.directory ?? options.defaultLang;
276
- const translationId = readTranslationId(custom?.translationId ?? frontmatter.translation) ?? deriveTranslationId(entry.path, filenameLanguage, directoryLanguage);
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
- const value = parse(match[1] ?? "");
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, directoryLanguage) {
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.metadata?.[LANGUAGE_METADATA_KEY] ?? "",
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.metadata?.[LANGUAGE_METADATA_KEY];
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 ?? signals.directory}.`,
449
+ )}); resolved to ${signals.frontmatter ?? signals.custom ?? signals.filename}.`,
465
450
  meta: {
466
451
  detected: signals,
467
- resolved: signals.frontmatter ?? signals.custom ?? signals.filename ?? signals.directory
452
+ resolved: signals.frontmatter ?? signals.custom ?? signals.filename
468
453
  }
469
454
  };
470
455
  }
@@ -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.after-meta` slot. */
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.16",
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.16"
19
+ "@riebeckite/core": "0.0.18"
20
20
  },
21
21
  "devDependencies": {
22
22
  "tsx": "^4.22.4",
23
- "@riebeckite/plugin-obsidian-markdown": "0.0.16"
23
+ "@riebeckite/plugin-obsidian-markdown": "0.0.18"
24
24
  },
25
25
  "publishConfig": {
26
26
  "access": "public"