@master/internal-site 0.1.3 → 0.2.1

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.
Files changed (105) hide show
  1. package/README.md +37 -0
  2. package/dist/astro.mjs +92 -41
  3. package/dist/config.d.mts +6 -0
  4. package/dist/localization-LfIbIysE.mjs +356 -0
  5. package/dist/localization.d.mts +69 -0
  6. package/dist/localization.mjs +1 -0
  7. package/package.json +12 -9
  8. package/src/astro/content-pages.ts +30 -11
  9. package/src/astro/index.ts +9 -5
  10. package/src/astro/localized-mdx.ts +45 -0
  11. package/src/astro/routes.template.ts +3 -0
  12. package/src/astro/site-context.ts +10 -2
  13. package/src/benchmarks/BenchmarkChart.astro +4 -1
  14. package/src/benchmarks/BenchmarkMetricTable.astro +1 -1
  15. package/src/benchmarks/BenchmarkStackedBars.astro +2 -2
  16. package/src/benchmarks/BenchmarkStatus.astro +3 -1
  17. package/src/benchmarks/localization.ts +22 -0
  18. package/src/blog/article/BlogArticleEnd.astro +6 -9
  19. package/src/blog/article/BlogArticleMasthead.astro +5 -8
  20. package/src/config.ts +3 -0
  21. package/src/css/foundation-data/specimen-content.ts +2 -2
  22. package/src/css/markdown.ts +52 -11
  23. package/src/demo/Demo.svelte +6 -4
  24. package/src/demo/DemoAsset.astro +5 -3
  25. package/src/demo/DemoAxes.svelte +4 -2
  26. package/src/demo/DemoCatalog.astro +8 -6
  27. package/src/demo/DemoContainer.svelte +5 -3
  28. package/src/demo/DemoControls.svelte +3 -1
  29. package/src/demo/DemoCopyButton.svelte +6 -4
  30. package/src/demo/DemoCopyGroup.svelte +4 -2
  31. package/src/demo/DemoExample.astro +3 -1
  32. package/src/demo/DemoFeatureSupport.svelte +3 -1
  33. package/src/demo/DemoGeneratedCSS.svelte +3 -1
  34. package/src/demo/DemoIndex.astro +5 -3
  35. package/src/demo/DemoLegend.svelte +3 -1
  36. package/src/demo/DemoMeasure.svelte +4 -2
  37. package/src/demo/DemoMedia.svelte +3 -1
  38. package/src/demo/DemoMotion.svelte +5 -3
  39. package/src/demo/DemoPaletteGroups.svelte +4 -2
  40. package/src/demo/DemoStyleComparison.astro +5 -3
  41. package/src/demo/DemoThemeComparison.astro +4 -2
  42. package/src/demo/DemoTokenTable.astro +4 -2
  43. package/src/demo/DemoViewTransition.astro +3 -1
  44. package/src/demo/DemoViewport.svelte +13 -11
  45. package/src/demo/DemoWaterfall.astro +3 -1
  46. package/src/demo/ProjectStyleExample.astro +4 -1
  47. package/src/demo/foundations/FoundationDialog.astro +5 -2
  48. package/src/demo/foundations/FoundationTokensView.astro +16 -5
  49. package/src/demo/foundations/Palette.svelte +9 -7
  50. package/src/demo/foundations/dialog-copy.d.ts +1 -0
  51. package/src/demo/foundations/dialog-copy.js +5 -0
  52. package/src/docs-shell/common/i18n.config.d.ts +1 -1
  53. package/src/docs-shell/common/i18n.config.js +4 -12
  54. package/src/docs-shell/common/locales.d.ts +8 -0
  55. package/src/docs-shell/common/locales.js +18 -0
  56. package/src/docs-shell/common/terminology.json +32 -0
  57. package/src/docs-shell/common/translations.d.ts +3 -0
  58. package/src/docs-shell/common/translations.js +294 -0
  59. package/src/docs-shell/components/ArticleHeader.astro +3 -3
  60. package/src/docs-shell/components/CodeCopyButton.astro +3 -1
  61. package/src/docs-shell/components/CodeTabBar.astro +5 -3
  62. package/src/docs-shell/components/DocHeader.svelte +3 -4
  63. package/src/docs-shell/components/DocHeading.astro +3 -1
  64. package/src/docs-shell/components/DocMenuButton.svelte +4 -5
  65. package/src/docs-shell/components/DocSidebar.svelte +1 -2
  66. package/src/docs-shell/components/DocumentCodeTable.astro +3 -1
  67. package/src/docs-shell/components/DocumentIllustration.astro +3 -1
  68. package/src/docs-shell/components/DocumentKeyList.astro +5 -3
  69. package/src/docs-shell/components/DocumentNamespaceTable.astro +4 -2
  70. package/src/docs-shell/components/DocumentOptions.astro +3 -1
  71. package/src/docs-shell/components/DocumentationIndex.svelte +6 -7
  72. package/src/docs-shell/components/GeneratedCSS.astro +3 -1
  73. package/src/docs-shell/components/Link.svelte +3 -2
  74. package/src/docs-shell/components/NotFound.astro +3 -4
  75. package/src/docs-shell/components/PageNavs.astro +2 -3
  76. package/src/docs-shell/components/SearchDialog.svelte +13 -16
  77. package/src/docs-shell/components/TimeAgo.svelte +6 -6
  78. package/src/docs-shell/contexts/search.ts +1 -1
  79. package/src/docs-shell/factories/Page.astro +10 -5
  80. package/src/docs-shell/factories/create-page.ts +33 -6
  81. package/src/docs-shell/layouts/BlogArticle.astro +3 -11
  82. package/src/docs-shell/remark/slug-and-toc.js +5 -7
  83. package/src/docs-shell/scripts/code.ts +5 -2
  84. package/src/docs-shell/site.ts +9 -1
  85. package/src/docs-shell/utils/document-copy.d.ts +2 -0
  86. package/src/docs-shell/utils/document-copy.js +18 -0
  87. package/src/docs-shell/utils/generate-metadata.ts +2 -0
  88. package/src/docs-shell/utils/i18n-pathname.ts +13 -2
  89. package/src/docs-shell/utils/i18n.ts +4 -1
  90. package/src/docs-shell/utils/localized-metadata.ts +22 -0
  91. package/src/docs-shell/utils/markdown-links.ts +61 -0
  92. package/src/docs-shell/utils/markdown-tree.ts +19 -6
  93. package/src/docs-shell/utils/resolve-heading.d.ts +1 -0
  94. package/src/docs-shell/utils/resolve-heading.js +21 -0
  95. package/src/docs-shell/utils/search-pages.ts +75 -22
  96. package/src/home/hero.css +5 -0
  97. package/src/layouts/Document.astro +1 -1
  98. package/src/scripts/localization.ts +239 -0
  99. package/src/scripts/mdx-sample.ts +12 -0
  100. package/src/scripts/static-social.ts +17 -3
  101. package/src/scripts/style-delivery.mjs +14 -4
  102. package/src/sections/guide.astro +3 -1
  103. package/src/styles/docs-shell/prose.css +1 -1
  104. package/src/virtual.d.ts +5 -1
  105. package/src/blog/article/language.ts +0 -29
package/README.md CHANGED
@@ -68,6 +68,43 @@ Import individual component subpaths, including `.astro` and `.svelte` extension
68
68
 
69
69
  The CSS-specific subpaths require the matching optional compiler, tooling, schema, preset and language-service peers. Keep them out of browser-only imports. Shared public assets are served in development and copied into build output; consumer public files take precedence. No install hook writes consumer source files.
70
70
 
71
+ ## Local MDX translations
72
+
73
+ English uses the root URL. The shared locale definitions map `ja`, `tw`, `ko`
74
+ and `cn` to matching URL prefixes and the HTML language tags `ja`, `zh-TW`,
75
+ `ko` and `zh-CN`. Keep complete sibling editions in `content.<locale>.mdx`;
76
+ `metadata.ts` retains English identity, dates, authors and ordering. Translated
77
+ frontmatter supplies titles, summaries, image descriptions and article-specific
78
+ demonstration copy. Keep explicit heading IDs identical across editions.
79
+
80
+ `build/localization.ts` supplies filesystem discovery, localized document reads,
81
+ dependency fingerprints, explicit review operations and completeness checks.
82
+ Configure `i18n.validation` with the consumer's resource-loader and review-index
83
+ URLs. The integration validates them on every production build, including direct
84
+ `astro build` calls. Source fingerprints include metadata and imported local or
85
+ shared source files. A missing edition or changed source/translation fails the
86
+ gate until independently reviewed. Builds never refresh review records.
87
+
88
+ The resource loader also exposes local and shared UI dictionaries to the same
89
+ gate. Common controls use `common/translations.js`; product copy belongs to each
90
+ consumer. Both sites follow `common/terminology.json`. Use `sharedInterfaceKeys()`
91
+ to include newly introduced literal interface requests in the UI inventory.
92
+
93
+ The route registry imports each selected MDX edition through Astro's propagated
94
+ asset boundary. Nested MDX imports inherit their parent's language through Vite;
95
+ missing prose editions and draft includes throw. Prose-free fenced companion
96
+ examples stay shared. Do not import every edition eagerly or reimplement the
97
+ resolver in a consumer. Per-request `documentCopyOf(Astro)` and `demoCopy` let
98
+ shared demonstrations read localized prose while retaining one executable model.
99
+
100
+ Consumers derive navigation, search, Markdown, feeds and llms exports from their
101
+ public inventory and the selected local edition. Use `extractSearchNodesFromMdx`
102
+ for authored MDX and `extractSearchNodesFromMarkdown` for portable output; the
103
+ latter retains explicit anchor targets and GFM tables. `transformMarkdownLinks`
104
+ edits reader links without changing inline or fenced examples. Withdraw stale
105
+ owned exports when a source becomes a draft or is removed. Required Reference
106
+ inputs reject draft status instead of publishing a partial catalog.
107
+
71
108
  ## Development and releases
72
109
 
73
110
  From the Master repository, run `bun run build:packages`, `bun run --cwd packages/internal-site type-check` and `bun run --cwd packages/internal-site test`. Astro checks `.astro` implementations and Svelte checks interactive components separately. The root `build` builds the Node entrypoints before the site.
package/dist/astro.mjs CHANGED
@@ -1,18 +1,18 @@
1
+ import { d as validateSiteTranslations, f as isSharedMdxSample, h as createHeadingResolver, m as locales } from "./localization-LfIbIysE.mjs";
1
2
  import { svgoOptimizer } from "astro/config";
2
3
  import svelte from "@astrojs/svelte";
3
4
  import mdx from "@astrojs/mdx";
4
- import { unified } from "@astrojs/markdown-remark";
5
+ import { parseFrontmatter, unified } from "@astrojs/markdown-remark";
5
6
  import masterCSS from "@master/css-astro";
6
7
  import { fileURLToPath, pathToFileURL } from "node:url";
7
- import { dirname, extname, join, relative, resolve, sep } from "node:path";
8
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
9
  import { cp, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
9
10
  import { createReadStream, existsSync } from "node:fs";
10
11
  import { isCSSRequest, searchForWorkspaceRoot, transformWithOxc } from "vite";
11
- import { optimize } from "svgo";
12
12
  import { createHash } from "node:crypto";
13
- import Slugger from "github-slugger";
14
- import { SKIP, visit } from "unist-util-visit";
15
13
  import { parse, parseExpressionAt } from "acorn";
14
+ import { optimize } from "svgo";
15
+ import { SKIP, visit } from "unist-util-visit";
16
16
  // src/astro/module-id.ts
17
17
  /** A Vite module id without its query or hash. */
18
18
  function cleanId(id) {
@@ -57,6 +57,33 @@ async function prepareContentPages(site, root, contentPrefix) {
57
57
  const destination = join(root, ".astro/master-site/pages");
58
58
  const pages = {};
59
59
  const wanted = /* @__PURE__ */ new Set();
60
+ const localizedDocuments = {};
61
+ const localizedMetadata = {};
62
+ const availableLocales = {};
63
+ for (const file of files) {
64
+ if (!file.endsWith("/content.mdx")) continue;
65
+ const directory = dirname(file);
66
+ const route = relative(content, directory).replaceAll("\\", "/");
67
+ const pathname = "/" + route.split("/").filter((part) => !/^\(.+\)$/.test(part)).join("/");
68
+ const key = `${contentPrefix}/${route ? route + "/" : ""}page.astro`;
69
+ const section = route.split("/").find((part) => !/^\(.+\)$/.test(part)) ?? "home";
70
+ const locales = site.i18n.sectionLocales?.[section] ?? site.i18n.locales;
71
+ const metadataSource = await readFile(join(directory, "metadata.ts"), "utf8").catch(() => "");
72
+ const draft = parseFrontmatter(await readFile(file, "utf8")).frontmatter.draft === true || /\bdraft\s*:\s*true\b/.test(metadataSource);
73
+ const documents = {};
74
+ const metadata = {};
75
+ for (const locale of draft ? [] : locales) {
76
+ const variant = locale === site.i18n.defaultLocale ? file : join(directory, `content.${locale}.mdx`);
77
+ if (!files.has(variant)) continue;
78
+ const { frontmatter } = parseFrontmatter(await readFile(variant, "utf8"));
79
+ if (frontmatter.draft === true) continue;
80
+ documents[locale] = pathToFileURL(variant);
81
+ metadata[locale] = frontmatter;
82
+ }
83
+ localizedDocuments[pathname] = documents;
84
+ localizedMetadata[pathname] = metadata;
85
+ availableLocales[key] = Object.keys(documents);
86
+ }
60
87
  for (const file of files) {
61
88
  if (!file.endsWith("/content.mdx")) continue;
62
89
  const directory = dirname(file);
@@ -87,15 +114,6 @@ export const generateMetadata = createPageMetadata(metadata, dictionaries)
87
114
  pages[`${contentPrefix}/${route ? route + "/" : ""}page.astro`] = pathToFileURL(output);
88
115
  }
89
116
  for (const stale of await contentFiles(destination).catch(() => [])) if (stale.endsWith("/page.astro") && !wanted.has(stale)) await unlink(stale);
90
- const availableLocales = {};
91
- for (const file of files) {
92
- if (!file.endsWith("/content.mdx")) continue;
93
- const directory = dirname(file);
94
- const route = relative(content, directory).replaceAll("\\", "/");
95
- const section = route.split("/").find((segment) => !/^\(.+\)$/.test(segment)) ?? "home";
96
- if (!site.i18n.translatedOnly?.includes(section)) continue;
97
- availableLocales[`${contentPrefix}/${route ? route + "/" : ""}page.astro`] = site.i18n.locales.filter((locale) => locale === site.i18n.defaultLocale || files.has(join(directory, `content.${locale}.mdx`)));
98
- }
99
117
  for (const section of site.i18n.translatedOnly ?? []) {
100
118
  const key = `${contentPrefix}/${section}/page.astro`;
101
119
  if (!files.has(join(content, section, "page.astro"))) continue;
@@ -104,7 +122,9 @@ export const generateMetadata = createPageMetadata(metadata, dictionaries)
104
122
  }
105
123
  return {
106
124
  pages,
107
- availableLocales
125
+ availableLocales,
126
+ localizedDocuments,
127
+ localizedMetadata
108
128
  };
109
129
  }
110
130
  // src/astro/server-styles.ts
@@ -157,6 +177,52 @@ function svgMarkup() {
157
177
  }
158
178
  };
159
179
  }
180
+ // src/astro/localized-mdx.ts
181
+ /** Nested article MDX follows its importer; executable examples keep one source. */
182
+ function localizedMdx({ locales: locales$1 = locales, defaultLocale = "en" } = {}) {
183
+ return {
184
+ name: "@master/internal-site:localized-mdx",
185
+ enforce: "pre",
186
+ async resolveId(source, importer, options) {
187
+ if (!importer || !cleanId(importer).endsWith(".mdx")) return;
188
+ const language = cleanId(importer).match(/\.([a-z]{2})\.mdx$/)?.[1];
189
+ const locale = language && locales$1.includes(language) ? language : defaultLocale;
190
+ const resolved = await this.resolve(source, importer, {
191
+ ...options,
192
+ skipSelf: true
193
+ });
194
+ if (!resolved || resolved.external) return;
195
+ const id = cleanId(resolved.id);
196
+ if (!id.endsWith(".mdx") || id.startsWith("\0")) return;
197
+ const suffix = resolved.id.slice(id.length) || source.slice(cleanId(source).length);
198
+ const file = id.startsWith("file:") ? fileURLToPath(id) : id.startsWith("/@fs/") ? id.slice(5) : id;
199
+ if (!isAbsolute(file)) return;
200
+ const original = file.replace(/\.([a-z]{2})\.mdx$/, (match, language) => locales$1.includes(language) ? ".mdx" : match);
201
+ this.addWatchFile(original);
202
+ const originalSource = await readFile(original, "utf8");
203
+ const originalMetadata = parseFrontmatter(originalSource).frontmatter;
204
+ if (originalMetadata.draft === true || originalMetadata.disabled === true) throw new Error(`Unpublished source MDX include: ${original}`);
205
+ if (locale === defaultLocale || !originalSource.trim() || isSharedMdxSample(original, originalSource)) return {
206
+ ...resolved,
207
+ id: original + suffix
208
+ };
209
+ const translated = original.replace(/\.mdx$/, `.${locale}.mdx`);
210
+ this.addWatchFile(translated);
211
+ let translatedSource;
212
+ try {
213
+ translatedSource = await readFile(translated, "utf8");
214
+ } catch (cause) {
215
+ throw new Error(`Missing translated MDX include: ${translated}`, { cause });
216
+ }
217
+ const metadata = parseFrontmatter(translatedSource).frontmatter;
218
+ if (metadata.draft === true || metadata.disabled === true) throw new Error(`Unpublished translated MDX include: ${translated}`);
219
+ return {
220
+ ...resolved,
221
+ id: translated + suffix
222
+ };
223
+ }
224
+ };
225
+ }
160
226
  // src/astro/style-assets.ts
161
227
  /** Collapse only the compiler's unconditional, same-directory asset links.
162
228
  * This is concatenation of already compiled CSS, not a second CSS compiler.
@@ -259,41 +325,23 @@ async function compactStyleAssets(output) {
259
325
  await writeFile(file, html);
260
326
  }
261
327
  }
262
- // src/docs-shell/utils/resolve-heading.js
263
- function resolveHeading(title, id) {
264
- const explicit = typeof title === "string" && title.match(/\s+\{#([\w-]+)\}$/);
265
- if (explicit) {
266
- title = title.slice(0, explicit.index);
267
- id = explicit[1];
268
- }
269
- const words = title?.split?.(" ");
270
- if (words?.pop() === "[sr-only]") return {
271
- title: words.join(" "),
272
- id: id?.replace("-sr-only", "")?.replace("-[sr-only]", ""),
273
- className: "sr-only"
274
- };
275
- return {
276
- title,
277
- id
278
- };
279
- }
280
328
  // src/docs-shell/remark/slug-and-toc.js
281
329
  function remarkSlugAndTOC() {
282
330
  return (tree) => {
283
- const slugs = new Slugger();
331
+ const resolveHeading = createHeadingResolver();
284
332
  const toc = [];
285
333
  visit(tree, "heading", (node) => {
286
334
  const depth = node.depth;
287
- if (depth !== 2 && depth !== 3) return;
335
+ if (depth < 2 || depth > 6) return;
288
336
  const title = node.children.filter((n) => n.type === "text" || n.type === "inlineCode").map((n) => n.value).join(" ").trim();
289
- const resolved = resolveHeading(title, slugs.slug(title));
337
+ const resolved = resolveHeading(title, depth);
290
338
  const lastText = node.children.findLast((child) => child.type === "text");
291
339
  if (lastText) lastText.value = lastText.value.replace(/\s+\{#[\w-]+\}$/, "");
292
340
  node.data ??= {};
293
341
  node.data.hProperties ??= {};
294
342
  node.data.id = resolved.id;
295
343
  node.data.hProperties.id = resolved.id;
296
- toc.push({
344
+ if (depth === 2 || depth === 3) toc.push({
297
345
  id: resolved.id,
298
346
  title: resolved.title,
299
347
  level: depth
@@ -498,7 +546,8 @@ function masterSite(site) {
498
546
  return {
499
547
  name: "@master/internal-site",
500
548
  hooks: {
501
- "astro:config:setup": async ({ config, updateConfig, addRenderer, injectRoute }) => {
549
+ "astro:config:setup": async ({ config, command, updateConfig, addRenderer, injectRoute }) => {
550
+ if (command === "build") await validateSiteTranslations(site.i18n);
502
551
  const root = fileURLToPath(config.root);
503
552
  const contentRelative = relative(root, fileURLToPath(site.content)).split(sep).join("/");
504
553
  if (contentRelative.startsWith("..")) throw new Error("Site content must be inside the Astro project root");
@@ -516,7 +565,8 @@ function masterSite(site) {
516
565
  "virtual:master-site/config": `export default ${JSON.stringify(site)}`,
517
566
  "virtual:master-site/app": moduleImport(site.app),
518
567
  "virtual:master-site/highlight": moduleImport(site.highlighter ?? new URL("../docs-shell/utils/highlight-code-core.ts", new URL("../src/astro/index.ts", import.meta.url).href)),
519
- "virtual:master-site/translations": `export default import.meta.glob(${JSON.stringify(`${contentPrefix}/**/content.*.mdx`)})`
568
+ "virtual:master-site/translations": `export default {${Object.entries(prepared.localizedDocuments).map(([pathname, documents]) => `${JSON.stringify(pathname)}: {${Object.entries(documents).map(([locale, url]) => `${JSON.stringify(locale)}: ${propagated(url)}`).join(",")}}`).join(",")}}`,
569
+ "virtual:master-site/localized-metadata": `export default ${JSON.stringify(prepared.localizedMetadata)}`
520
570
  };
521
571
  const plugin = {
522
572
  name: "@master/internal-site:modules",
@@ -554,10 +604,10 @@ function masterSite(site) {
554
604
  server.restart();
555
605
  }, 100);
556
606
  };
557
- server.watcher.on("add", refresh).on("unlink", refresh);
607
+ server.watcher.on("add", refresh).on("unlink", refresh).on("change", refresh);
558
608
  server.httpServer?.once("close", () => {
559
609
  clearTimeout(restart);
560
- server.watcher.off("add", refresh).off("unlink", refresh);
610
+ server.watcher.off("add", refresh).off("unlink", refresh).off("change", refresh);
561
611
  });
562
612
  server.middlewares.use((request, response, next) => {
563
613
  let pathname;
@@ -613,6 +663,7 @@ function masterSite(site) {
613
663
  vite: {
614
664
  plugins: [
615
665
  plugin,
666
+ localizedMdx(site.i18n),
616
667
  sourceURL(),
617
668
  serverStyles([root, packageSource]),
618
669
  svgMarkup()
package/dist/config.d.mts CHANGED
@@ -31,10 +31,16 @@ export interface SiteI18nConfig {
31
31
  hreflangOfLocale: Record<string, string>;
32
32
  nameOfLocale: Record<string, string>;
33
33
  localizablePathnameRoots: string[];
34
+ localizableOrigins?: Record<string, readonly string[]>;
34
35
  translations: Record<string, Record<string, string>>;
35
36
  sectionLocales?: Record<string, readonly string[]>;
36
37
  /** These sections emit translated URLs only when translated MDX exists. */
37
38
  translatedOnly?: readonly string[];
39
+ /** Build-only resource loader and version-controlled explicit translation reviews. */
40
+ validation?: {
41
+ reviews: URL;
42
+ resources: URL;
43
+ };
38
44
  }
39
45
  export interface DocumentConfig {
40
46
  layout: URL;
@@ -0,0 +1,356 @@
1
+ import { parseFrontmatter } from "@astrojs/markdown-remark";
2
+ import { fileURLToPath } from "node:url";
3
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
4
+ import { readFile, readdir } from "node:fs/promises";
5
+ import { createHash } from "node:crypto";
6
+ import { fromMarkdown } from "mdast-util-from-markdown";
7
+ import { mdxjsEsm } from "micromark-extension-mdxjs-esm";
8
+ import { mdxjsEsmFromMarkdown } from "mdast-util-mdxjs-esm";
9
+ import { mdxJsx } from "micromark-extension-mdx-jsx";
10
+ import { mdxJsxFromMarkdown } from "mdast-util-mdx-jsx";
11
+ import * as acorn from "acorn";
12
+ import Slugger from "github-slugger";
13
+ // src/docs-shell/utils/resolve-heading.js
14
+ function resolveHeading(title, id) {
15
+ const explicit = typeof title === "string" && title.match(/\s+\{#([\w-]+)\}$/);
16
+ if (explicit) {
17
+ title = title.slice(0, explicit.index);
18
+ id = explicit[1];
19
+ }
20
+ const words = title?.split?.(" ");
21
+ if (words?.pop() === "[sr-only]") return {
22
+ title: words.join(" "),
23
+ id: id?.replace("-sr-only", "")?.replace("-[sr-only]", ""),
24
+ className: "sr-only"
25
+ };
26
+ return {
27
+ title,
28
+ id
29
+ };
30
+ }
31
+ /** Section counters stay stable when a detail heading is inserted between them. */
32
+ function createHeadingResolver() {
33
+ const sections = new Slugger();
34
+ const details = new Slugger();
35
+ const assigned = /* @__PURE__ */ new Set();
36
+ return (title, depth) => {
37
+ const explicit = /\s+\{#[\w-]+\}$/.test(title);
38
+ let heading;
39
+ if (explicit) heading = resolveHeading(title);
40
+ else {
41
+ const slugs = depth === 2 || depth === 3 ? sections : details;
42
+ do
43
+ heading = resolveHeading(title, slugs.slug(title));
44
+ while (heading.id && assigned.has(heading.id));
45
+ }
46
+ if (heading.id) assigned.add(heading.id);
47
+ return {
48
+ ...heading,
49
+ explicit
50
+ };
51
+ };
52
+ }
53
+ // src/docs-shell/common/locales.js
54
+ /** URL prefixes stay stable; document language tags use standard BCP 47 values. */
55
+ const locales = [
56
+ "en",
57
+ "tw",
58
+ "ja",
59
+ "ko",
60
+ "cn"
61
+ ];
62
+ // src/docs-shell/utils/localized-metadata.ts
63
+ /** Prose can vary by locale; publication identity and technical configuration stay with the source. */
64
+ function localizedMetadata(metadata, frontmatter, locale) {
65
+ const localized = {
66
+ ...metadata,
67
+ locale
68
+ };
69
+ for (const key of ["description", "subtitle"]) if (typeof frontmatter[key] === "string") localized[key] = frontmatter[key];
70
+ if (typeof frontmatter.title === "string") localized.title = metadata.title && typeof metadata.title === "object" ? {
71
+ ...metadata.title,
72
+ ..."absolute" in metadata.title ? { absolute: frontmatter.title } : { default: frontmatter.title }
73
+ } : frontmatter.title;
74
+ if (metadata.cover && typeof frontmatter.cover?.alt === "string") localized.cover = {
75
+ ...metadata.cover,
76
+ alt: frontmatter.cover.alt
77
+ };
78
+ for (const channel of ["openGraph", "twitter"]) {
79
+ const authored = frontmatter[channel];
80
+ if (metadata[channel] || authored) {
81
+ const prose = Object.fromEntries(["title", "description"].flatMap((key) => {
82
+ const text = authored?.[key] ?? (locale !== "en" ? frontmatter[key] : void 0);
83
+ return typeof text === "string" ? [[key, text]] : [];
84
+ }));
85
+ localized[channel] = {
86
+ ...metadata[channel],
87
+ ...prose
88
+ };
89
+ }
90
+ }
91
+ return localized;
92
+ }
93
+ // src/scripts/mdx-sample.ts
94
+ /** Only prose-free companion examples share one source across editions. */
95
+ function isSharedMdxSample(file, source) {
96
+ if (["content.mdx", "contract.mdx"].includes(basename(file))) return false;
97
+ const { content, frontmatter } = parseFrontmatter(source);
98
+ if (Object.keys(frontmatter).length) return false;
99
+ const nodes = fromMarkdown(content).children;
100
+ return nodes.length > 0 && nodes.every((node) => node.type === "code");
101
+ }
102
+ // src/scripts/localization.ts
103
+ const emptyReviews = () => ({
104
+ version: 1,
105
+ resources: {}
106
+ });
107
+ const hash = (value) => createHash("sha256").update(value.replaceAll("\r\n", "\n")).digest("hex");
108
+ const sourceHash = (resource) => hash(JSON.stringify([resource.source, Object.entries(resource.dependencies ?? {}).sort(([a], [b]) => a.localeCompare(b))]));
109
+ /** Explicit review operation. Preparation and production builds never call this function. */
110
+ function reviewTranslation(resource, locale) {
111
+ const translation = resource.translations[locale];
112
+ if (!translation?.trim()) throw new Error(`Missing translation: ${resource.id} (${locale})`);
113
+ return {
114
+ sourceHash: sourceHash(resource),
115
+ translationHash: hash(translation)
116
+ };
117
+ }
118
+ function auditTranslations(resources, reviews, { locales: locales$1 = locales, defaultLocale = "en" } = {}) {
119
+ if (reviews.version !== 1 || !reviews.resources) throw new Error("Unsupported translation review index; expected version 1");
120
+ const diagnostics = [];
121
+ const ids = /* @__PURE__ */ new Set();
122
+ for (const resource of resources) {
123
+ if (ids.has(resource.id)) throw new Error(`Duplicate translation resource: ${resource.id}`);
124
+ ids.add(resource.id);
125
+ if (resource.draft) continue;
126
+ for (const locale of resource.locales ?? locales$1) {
127
+ if (locale === defaultLocale) continue;
128
+ const report = (reason, message) => diagnostics.push({
129
+ id: resource.id,
130
+ locale,
131
+ reason,
132
+ message
133
+ });
134
+ const translation = resource.translations[locale];
135
+ if (!translation?.trim()) {
136
+ report("missing", "Translation is missing");
137
+ continue;
138
+ }
139
+ if (parseFrontmatter(translation).frontmatter.draft === true) {
140
+ report("missing", "Translation is a draft and is not available for publication");
141
+ continue;
142
+ }
143
+ const review = reviews.resources[resource.id]?.[locale];
144
+ if (!review) report("unreviewed", "Translation has not been reviewed");
145
+ else {
146
+ if (review.sourceHash !== sourceHash(resource)) report("source-changed", "English source changed after translation review");
147
+ if (review.translationHash !== hash(translation)) report("translation-changed", "Translation changed after review");
148
+ }
149
+ if (resource.checkAnchors) {
150
+ const expected = headingAnchors(resource.source);
151
+ const actual = headingAnchors(translation);
152
+ if (new Set(actual).size !== actual.length || JSON.stringify(expected) !== JSON.stringify(actual)) report("anchors", "Translated heading IDs must preserve source IDs and order");
153
+ }
154
+ if (resource.requireMetadata) {
155
+ const { frontmatter } = parseFrontmatter(translation);
156
+ if (typeof frontmatter.title !== "string" || !frontmatter.title.trim() || typeof frontmatter.description !== "string" || !frontmatter.description.trim()) report("metadata", "Localized MDX requires title and description frontmatter");
157
+ }
158
+ }
159
+ }
160
+ return diagnostics;
161
+ }
162
+ function assertTranslations(resources, reviews, options = {}) {
163
+ const diagnostics = auditTranslations(resources, reviews, options);
164
+ if (diagnostics.length) throw new Error(`Translation completeness check failed (${diagnostics.length}):\n${diagnostics.map((issue) => `${issue.id} [${issue.locale}] ${issue.reason}: ${issue.message}`).join("\n")}`);
165
+ }
166
+ /** Shared Astro build gate, also usable by a consumer's explicit audit command. */
167
+ async function validateSiteTranslations({ validation, ...options }) {
168
+ const locales$2 = options.locales ?? locales;
169
+ if (!validation) {
170
+ if (locales$2.some((locale) => locale !== (options.defaultLocale ?? "en"))) throw new Error("Multilingual production builds require i18n.validation resources and reviews");
171
+ return;
172
+ }
173
+ const [{ default: load }, raw] = await Promise.all([import(validation.resources.href), readFile(validation.reviews, "utf8")]);
174
+ const resources = typeof load === "function" ? await load() : load;
175
+ if (!Array.isArray(resources) || resources.length === 0) throw new Error("Translation resource loader must return a nonempty resource inventory");
176
+ assertTranslations(resources, JSON.parse(raw), options);
177
+ }
178
+ async function readLocalizedDocument({ sourceFile, locale, defaultLocale = "en", metadata = {} }) {
179
+ const original = sourceFile.replace(/\.([a-z]{2})\.mdx$/, ".mdx");
180
+ const file = locale === defaultLocale ? original : original.replace(/\.mdx$/, `.${locale}.mdx`);
181
+ let source;
182
+ try {
183
+ source = await readFile(file, "utf8");
184
+ } catch (cause) {
185
+ throw new Error(`Missing localized document: ${file} (${locale})`, { cause });
186
+ }
187
+ const { content, frontmatter } = parseFrontmatter(source);
188
+ return {
189
+ file,
190
+ content,
191
+ frontmatter,
192
+ metadata: localizedMetadata(metadata, frontmatter, locale)
193
+ };
194
+ }
195
+ async function filesOf(directory) {
196
+ const files = [];
197
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
198
+ if (entry.name.startsWith(".")) continue;
199
+ const file = join(directory, entry.name);
200
+ if (entry.isDirectory()) {
201
+ if (entry.name !== "drafts") files.push(...await filesOf(file));
202
+ } else files.push(file);
203
+ }
204
+ return files.sort();
205
+ }
206
+ /** Filesystem inventory, with no page imports, component execution or generated source writes. */
207
+ async function discoverDocumentResources({ contentRoot, locales: locales$3 = locales, defaultLocale = "en", exclude = [], siteRoot = dirname(contentRoot) }) {
208
+ const files = new Set(await filesOf(contentRoot));
209
+ const resources = [];
210
+ for (const file of files) {
211
+ if (!file.endsWith(".mdx") || /\.[a-z]{2}\.mdx$/.test(file)) continue;
212
+ const source = await readFile(file, "utf8");
213
+ const id = relative(contentRoot, file).replaceAll("\\", "/");
214
+ if (!source.trim() || exclude.includes(id)) continue;
215
+ if (isSharedMdxSample(file, source)) continue;
216
+ const metadata = join(dirname(file), "metadata.ts");
217
+ const sourceMetadata = files.has(metadata) ? await readFile(metadata, "utf8") : void 0;
218
+ const dependencies = await collectSourceDependencies([file, ...sourceMetadata ? [metadata] : []], { siteRoot });
219
+ delete dependencies[relative(siteRoot, file).replaceAll("\\", "/")];
220
+ const translations = {};
221
+ for (const locale of locales$3) {
222
+ if (locale === defaultLocale) continue;
223
+ const translatedFile = file.replace(/\.mdx$/, `.${locale}.mdx`);
224
+ if (files.has(translatedFile)) translations[locale] = await readFile(translatedFile, "utf8");
225
+ }
226
+ resources.push({
227
+ id,
228
+ source,
229
+ translations,
230
+ dependencies,
231
+ checkAnchors: true,
232
+ requireMetadata: basename(file) === "content.mdx",
233
+ draft: parseFrontmatter(source).frontmatter.draft === true || /\bdraft\s*:\s*true\b/.test(sourceMetadata ?? "")
234
+ });
235
+ }
236
+ return resources;
237
+ }
238
+ /** Literal interface requests must exist even when a newly added control has no dictionary entry yet. */
239
+ async function sharedInterfaceKeys(sourceRoot = join(dirname(fileURLToPath(import.meta.resolve("@master/internal-site/package.json"))), "src")) {
240
+ const keys = /* @__PURE__ */ new Set();
241
+ const visit = async (directory) => {
242
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
243
+ const file = join(directory, entry.name);
244
+ if (entry.isDirectory()) await visit(file);
245
+ else if (/\.(?:astro|svelte|ts|js)$/.test(entry.name) && !/\.(?:test|spec)\./.test(entry.name)) {
246
+ const source = await readFile(file, "utf8");
247
+ for (const match of source.matchAll(/\bt\s*\(\s*(?=['"`])/g)) {
248
+ const expression = acorn.parseExpressionAt(source, match.index + match[0].length, { ecmaVersion: "latest" });
249
+ const value = expression.type === "Literal" ? expression.value : expression.type === "TemplateLiteral" && !expression.expressions.length ? expression.quasis[0].value.cooked : void 0;
250
+ if (typeof value === "string" && value.trim()) keys.add(value);
251
+ }
252
+ }
253
+ }
254
+ };
255
+ await visit(sourceRoot);
256
+ return [...keys].sort();
257
+ }
258
+ /** Follow local prose/data modules without executing them or importing product compiler code. */
259
+ async function collectSourceDependencies(files, { siteRoot }) {
260
+ const root = resolve(siteRoot);
261
+ const sharedRoot = dirname(fileURLToPath(import.meta.resolve("@master/internal-site/package.json")));
262
+ const collected = {};
263
+ const seen = /* @__PURE__ */ new Set();
264
+ const visit = async (file) => {
265
+ file = resolve(file).replace(/\.(tw|ja|ko|cn)\.mdx$/, ".mdx");
266
+ if (!file.startsWith(root + sep) && !file.startsWith(sharedRoot + sep) || seen.has(file)) return;
267
+ if (file.startsWith(sharedRoot + sep) && /\/common\/(?:translations|locales)\.[jt]s$/.test(file.replaceAll("\\", "/"))) return;
268
+ seen.add(file);
269
+ const source = await readFile(file, "utf8").catch(() => void 0);
270
+ if (source === void 0) return;
271
+ const key = file.startsWith(sharedRoot + sep) ? `@master/internal-site/${relative(sharedRoot, file)}` : relative(root, file);
272
+ collected[key.replaceAll("\\", "/")] = source;
273
+ const imports = source.matchAll(/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?)(['"])([^'"\n]+)\1/g);
274
+ for (const [, , specifier] of imports) {
275
+ if (!specifier.startsWith(".") && !specifier.startsWith("~/site/") && !specifier.startsWith("@master/internal-site/")) continue;
276
+ let target;
277
+ try {
278
+ target = specifier.startsWith("@master/internal-site/") ? fileURLToPath(import.meta.resolve(specifier.split("?")[0])) : specifier.startsWith("~/site/") ? resolve(root, specifier.slice(7)) : resolve(dirname(file), specifier.split("?")[0]);
279
+ } catch {
280
+ continue;
281
+ }
282
+ if (extname(target) && !/\.(?:[cm]?[jt]s|[jt]sx|mdx|astro|svelte)$/.test(target)) continue;
283
+ const candidates = extname(target) ? [target, .../\.js$/.test(target) ? [target.replace(/\.js$/, ".ts")] : []] : [
284
+ "",
285
+ ".ts",
286
+ ".js",
287
+ ".mdx",
288
+ ".astro",
289
+ ".svelte",
290
+ "/index.ts",
291
+ "/index.js"
292
+ ].map((suffix) => target + suffix);
293
+ for (const candidate of candidates) {
294
+ const before = Object.keys(collected).length;
295
+ await visit(candidate);
296
+ if (Object.keys(collected).length > before) break;
297
+ }
298
+ }
299
+ };
300
+ for (const file of files) await visit(file);
301
+ return Object.fromEntries(Object.entries(collected).sort(([a], [b]) => a.localeCompare(b)));
302
+ }
303
+ function headings(source) {
304
+ const parsed = parseFrontmatter(source);
305
+ const offset = source.indexOf(parsed.content, parsed.rawFrontmatter ? parsed.rawFrontmatter.length : 0);
306
+ const tree = fromMarkdown(parsed.content, {
307
+ extensions: [
308
+ { disable: { null: [
309
+ "htmlFlow",
310
+ "htmlText",
311
+ "codeIndented"
312
+ ] } },
313
+ mdxjsEsm({
314
+ acorn,
315
+ addResult: true
316
+ }),
317
+ mdxJsx({
318
+ acorn,
319
+ addResult: true
320
+ })
321
+ ],
322
+ mdastExtensions: [mdxjsEsmFromMarkdown(), mdxJsxFromMarkdown()]
323
+ });
324
+ const resolveHeading = createHeadingResolver();
325
+ const found = [];
326
+ const visit = (node) => {
327
+ if (node.type === "heading" && node.depth >= 2 && node.depth <= 6) {
328
+ const title = node.children.filter((child) => child.type === "text" || child.type === "inlineCode").map((child) => child.value).join(" ").trim();
329
+ const resolved = resolveHeading(title, node.depth);
330
+ const raw = parsed.content.slice(node.position.start.offset, node.position.end.offset);
331
+ const end = raw.includes("\n") ? node.position.start.offset + raw.indexOf("\n") : node.position.end.offset;
332
+ found.push({
333
+ id: resolved.id ?? "",
334
+ explicit: resolved.explicit,
335
+ offset: Math.max(0, offset) + end
336
+ });
337
+ }
338
+ node.children?.forEach(visit);
339
+ };
340
+ visit(tree);
341
+ return found;
342
+ }
343
+ function headingAnchors(source) {
344
+ return headings(source).map((heading) => heading.id);
345
+ }
346
+ /** Pin the existing automatic English IDs before translation, without modifying fenced examples. */
347
+ function stabilizeHeadingAnchors(source) {
348
+ for (const heading of headings(source).reverse()) {
349
+ if (heading.explicit || !heading.id) continue;
350
+ const closing = source.slice(0, heading.offset).match(/[ \t]+#+[ \t]*$/)?.[0] ?? "";
351
+ const at = heading.offset - closing.length;
352
+ source = source.slice(0, at) + ` \\{#${heading.id}\\}` + source.slice(at);
353
+ }
354
+ return source;
355
+ }
356
+ export { emptyReviews as a, reviewTranslation as c, validateSiteTranslations as d, isSharedMdxSample as f, createHeadingResolver as h, discoverDocumentResources as i, sharedInterfaceKeys as l, locales as m, auditTranslations as n, headingAnchors as o, localizedMetadata as p, collectSourceDependencies as r, readLocalizedDocument as s, assertTranslations as t, stabilizeHeadingAnchors as u };