@master/internal-site 0.1.2 → 0.2.0
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 +43 -0
- package/dist/astro.mjs +233 -47
- package/dist/config.d.mts +6 -0
- package/dist/localization-LfIbIysE.mjs +356 -0
- package/dist/localization.d.mts +69 -0
- package/dist/localization.mjs +1 -0
- package/package.json +12 -9
- package/src/astro/content-pages.ts +30 -11
- package/src/astro/index.ts +11 -5
- package/src/astro/localized-mdx.ts +45 -0
- package/src/astro/routes.template.ts +4 -0
- package/src/astro/site-context.ts +10 -2
- package/src/astro/style-assets.ts +107 -0
- package/src/benchmarks/BenchmarkBarRows.astro +3 -2
- package/src/benchmarks/BenchmarkBars.astro +1 -1
- package/src/benchmarks/BenchmarkChart.astro +5 -1
- package/src/benchmarks/BenchmarkChartGroup.astro +1 -1
- package/src/benchmarks/BenchmarkDataTable.astro +1 -1
- package/src/benchmarks/BenchmarkFigure.astro +2 -1
- package/src/benchmarks/BenchmarkMetricTable.astro +2 -2
- package/src/benchmarks/BenchmarkMetrics.astro +1 -1
- package/src/benchmarks/BenchmarkSampleSummary.astro +1 -1
- package/src/benchmarks/BenchmarkScrollRegion.svelte +2 -1
- package/src/benchmarks/BenchmarkSource.astro +1 -1
- package/src/benchmarks/BenchmarkStackedBars.astro +4 -3
- package/src/benchmarks/BenchmarkStatus.astro +3 -1
- package/src/benchmarks/localization.ts +22 -0
- package/src/benchmarks/types.ts +2 -0
- package/src/blog/article/BlogArticleContents.svelte +1 -1
- package/src/blog/article/BlogArticleEnd.astro +6 -9
- package/src/blog/article/BlogArticleMasthead.astro +17 -9
- package/src/config.ts +3 -0
- package/src/css/NamespaceUtilityTable.astro +1 -1
- package/src/css/ThemeNumberVariableTable.astro +1 -1
- package/src/css/foundation-data/specimen-content.ts +2 -2
- package/src/css/markdown.ts +64 -14
- package/src/demo/Demo.svelte +10 -4
- package/src/demo/DemoAsset.astro +6 -3
- package/src/demo/DemoAxes.svelte +4 -2
- package/src/demo/DemoCatalog.astro +9 -6
- package/src/demo/DemoComparison.svelte +3 -0
- package/src/demo/DemoContainer.svelte +8 -3
- package/src/demo/DemoControls.svelte +5 -1
- package/src/demo/DemoCopyButton.svelte +9 -4
- package/src/demo/DemoCopyGroup.svelte +6 -2
- package/src/demo/DemoExample.astro +3 -1
- package/src/demo/DemoFeatureSupport.svelte +5 -1
- package/src/demo/DemoGeneratedCSS.svelte +5 -1
- package/src/demo/DemoIndex.astro +6 -3
- package/src/demo/DemoLegend.svelte +3 -1
- package/src/demo/DemoMeasure.svelte +4 -2
- package/src/demo/DemoMedia.svelte +3 -1
- package/src/demo/DemoMotion.svelte +6 -3
- package/src/demo/DemoPaletteGroups.svelte +4 -2
- package/src/demo/DemoScrollArea.svelte +2 -0
- package/src/demo/DemoStyleComparison.astro +6 -3
- package/src/demo/DemoSurface.svelte +2 -0
- package/src/demo/DemoThemeComparison.astro +5 -2
- package/src/demo/DemoTokenTable.astro +5 -3
- package/src/demo/DemoViewTransition.astro +3 -1
- package/src/demo/DemoViewport.svelte +18 -11
- package/src/demo/DemoWaterfall.astro +4 -2
- package/src/demo/ProjectStyleExample.astro +4 -1
- package/src/demo/foundations/FoundationDialog.astro +6 -2
- package/src/demo/foundations/FoundationMedia.astro +1 -0
- package/src/demo/foundations/FoundationShrink.astro +1 -0
- package/src/demo/foundations/FoundationTokensView.astro +16 -5
- package/src/demo/foundations/Palette.svelte +11 -7
- package/src/demo/foundations/dialog-copy.d.ts +1 -0
- package/src/demo/foundations/dialog-copy.js +5 -0
- package/src/demo/illustration-materials.ts +1 -1
- package/src/demo/illustration.css +218 -0
- package/src/docs-shell/common/i18n.config.d.ts +1 -1
- package/src/docs-shell/common/i18n.config.js +4 -12
- package/src/docs-shell/common/locales.d.ts +8 -0
- package/src/docs-shell/common/locales.js +18 -0
- package/src/docs-shell/common/terminology.json +32 -0
- package/src/docs-shell/common/translations.d.ts +3 -0
- package/src/docs-shell/common/translations.js +294 -0
- package/src/docs-shell/components/ArticleHeader.astro +5 -3
- package/src/docs-shell/components/Callout.astro +1 -0
- package/src/docs-shell/components/CodeCopyButton.astro +5 -1
- package/src/docs-shell/components/CodeTabBar.astro +7 -3
- package/src/docs-shell/components/CodeTabs.astro +2 -1
- package/src/docs-shell/components/CodeView.astro +1 -1
- package/src/docs-shell/components/DocHeader.svelte +13 -17
- package/src/docs-shell/components/DocHeading.astro +3 -1
- package/src/docs-shell/components/DocMenuButton.svelte +126 -61
- package/src/docs-shell/components/DocSidebar.svelte +3 -2
- package/src/docs-shell/components/DocVersionSelect.svelte +2 -0
- package/src/docs-shell/components/DocumentAPIIndex.astro +1 -1
- package/src/docs-shell/components/DocumentChoices.astro +1 -1
- package/src/docs-shell/components/DocumentCodeExampleView.astro +3 -1
- package/src/docs-shell/components/DocumentCodeTable.astro +4 -2
- package/src/docs-shell/components/DocumentCopyButton.svelte +3 -0
- package/src/docs-shell/components/DocumentDisclosure.astro +2 -0
- package/src/docs-shell/components/DocumentFileTree.astro +3 -0
- package/src/docs-shell/components/DocumentFlow.astro +4 -1
- package/src/docs-shell/components/DocumentIllustration.astro +8 -12
- package/src/docs-shell/components/DocumentKeyList.astro +6 -4
- package/src/docs-shell/components/DocumentNamespaceTable.astro +5 -3
- package/src/docs-shell/components/DocumentOptionEntry.astro +0 -1
- package/src/docs-shell/components/DocumentOptionList.astro +1 -1
- package/src/docs-shell/components/DocumentOptions.astro +3 -2
- package/src/docs-shell/components/DocumentParameters.astro +1 -1
- package/src/docs-shell/components/DocumentPrompt.astro +2 -0
- package/src/docs-shell/components/DocumentValueList.astro +1 -1
- package/src/docs-shell/components/DocumentationIndex.svelte +8 -7
- package/src/docs-shell/components/ExpandContent.svelte +3 -0
- package/src/docs-shell/components/ExplorerView.astro +1 -0
- package/src/docs-shell/components/GeneratedCSS.astro +4 -1
- package/src/docs-shell/components/Header.svelte +1 -1
- package/src/docs-shell/components/InlineCode.astro +1 -0
- package/src/docs-shell/components/InteractingIndicator.astro +1 -0
- package/src/docs-shell/components/LanguageButton.svelte +1 -0
- package/src/docs-shell/components/LanguageSelect.svelte +2 -0
- package/src/docs-shell/components/Link.svelte +3 -2
- package/src/docs-shell/components/NotFound.astro +6 -4
- package/src/docs-shell/components/PMAction.astro +1 -0
- package/src/docs-shell/components/PageContent.svelte +1 -1
- package/src/docs-shell/components/PageContentDisclosure.astro +2 -1
- package/src/docs-shell/components/PageContentList.svelte +2 -0
- package/src/docs-shell/components/PageNavs.astro +3 -3
- package/src/docs-shell/components/SearchButton.svelte +3 -0
- package/src/docs-shell/components/SearchDialog.svelte +18 -16
- package/src/docs-shell/components/Segments.svelte +2 -0
- package/src/docs-shell/components/StepSection.astro +1 -2
- package/src/docs-shell/components/Tabs.astro +1 -0
- package/src/docs-shell/components/ThemeSelect.svelte +2 -0
- package/src/docs-shell/components/TimeAgo.svelte +6 -6
- package/src/docs-shell/components/markdown/Table.astro +1 -0
- package/src/docs-shell/components/markdown-components.ts +0 -1
- package/src/docs-shell/contexts/search.ts +1 -1
- package/src/docs-shell/factories/Page.astro +10 -5
- package/src/docs-shell/factories/create-page.ts +33 -6
- package/src/docs-shell/layouts/BlogArticle.astro +3 -11
- package/src/docs-shell/remark/markdown-components.js +28 -7
- package/src/docs-shell/remark/slug-and-toc.js +5 -7
- package/src/docs-shell/scripts/code.ts +5 -2
- package/src/docs-shell/scripts/document.ts +1 -1
- package/src/docs-shell/site.ts +9 -1
- package/src/docs-shell/types/Metadata.ts +11 -0
- package/src/docs-shell/utils/document-copy.d.ts +2 -0
- package/src/docs-shell/utils/document-copy.js +18 -0
- package/src/docs-shell/utils/generate-metadata.ts +5 -1
- package/src/docs-shell/utils/head.ts +5 -2
- package/src/docs-shell/utils/i18n-pathname.ts +13 -2
- package/src/docs-shell/utils/i18n.ts +4 -1
- package/src/docs-shell/utils/localized-metadata.ts +22 -0
- package/src/docs-shell/utils/markdown-links.ts +61 -0
- package/src/docs-shell/utils/markdown-tree.ts +19 -6
- package/src/docs-shell/utils/resolve-heading.d.ts +1 -0
- package/src/docs-shell/utils/resolve-heading.js +21 -0
- package/src/docs-shell/utils/search-pages.ts +75 -22
- package/src/examples/components/ArticleTransitionScene.svelte +4 -0
- package/src/examples/layout-system/page.astro +0 -1
- package/src/home/HomeBentoFrame.astro +1 -0
- package/src/home/HomeBentoGrid.astro +1 -0
- package/src/home/bento.css +167 -0
- package/src/home/design.css +1 -1
- package/src/home/hero.css +5 -0
- package/src/home/home.css +4 -576
- package/src/home/segmented-control.css +94 -0
- package/src/layouts/Document.astro +4 -2
- package/src/public/icons/json.svg +1 -0
- package/src/scripts/atomic-class-authoring.ts +98 -0
- package/src/scripts/localization.ts +239 -0
- package/src/scripts/mdx-sample.ts +12 -0
- package/src/scripts/static-social.ts +17 -3
- package/src/scripts/style-delivery.mjs +120 -0
- package/src/sections/blog.astro +1 -1
- package/src/sections/guide.astro +4 -3
- package/src/styles/blog-article.css +10 -7
- package/src/styles/blog-materials.css +13 -0
- package/src/styles/demo.css +0 -1
- package/src/styles/docs-shell/code.css +5 -61
- package/src/styles/docs-shell/documentation-search.css +1 -0
- package/src/styles/docs-shell/header.css +2 -73
- package/src/styles/docs-shell/menu.css +37 -17
- package/src/styles/docs-shell/page-content.css +2 -37
- package/src/styles/docs-shell/prose.css +18 -9
- package/src/styles/docs-shell/steps.css +2 -39
- package/src/styles/document-comparison.css +5 -2
- package/src/styles/documentation-illustration.css +1 -9
- package/src/styles/documentation-index.css +2 -1
- package/src/styles/documents.css +2 -4
- package/src/styles/interface.css +5 -3
- package/src/styles/system/DESIGN.md +34 -14
- package/src/styles/system/browser.css +21 -0
- package/src/styles/system/button-feedback.css +13 -0
- package/src/styles/system/callout.css +14 -0
- package/src/styles/system/chart.css +18 -0
- package/src/styles/system/checks.css +18 -0
- package/src/styles/system/code-panel.css +82 -0
- package/src/styles/system/code-tabs.css +40 -0
- package/src/styles/system/code.css +8 -85
- package/src/styles/system/compare.css +7 -0
- package/src/styles/system/controls.css +10 -109
- package/src/styles/system/copy.css +32 -0
- package/src/styles/system/data.css +10 -134
- package/src/styles/system/dialog.css +16 -0
- package/src/styles/system/disclosure.css +19 -0
- package/src/styles/system/expand.css +11 -0
- package/src/styles/system/fields.css +25 -0
- package/src/styles/system/header-core.css +101 -0
- package/src/styles/system/hint.css +5 -0
- package/src/styles/system/index.css +13 -0
- package/src/styles/system/inline-code.css +4 -0
- package/src/styles/system/kbd.css +4 -0
- package/src/styles/system/logos.css +10 -0
- package/src/styles/system/menu.css +14 -0
- package/src/styles/system/metrics.css +22 -0
- package/src/styles/system/pagehead.css +13 -0
- package/src/styles/system/pager.css +13 -0
- package/src/styles/system/parameters.css +17 -0
- package/src/styles/system/prompt.css +7 -0
- package/src/styles/system/prose.css +4 -75
- package/src/styles/system/range.css +12 -0
- package/src/styles/system/reading.css +44 -0
- package/src/styles/system/scroll.css +6 -0
- package/src/styles/system/search-trigger.css +10 -0
- package/src/styles/system/segmented.css +22 -0
- package/src/styles/system/sheet.css +8 -0
- package/src/styles/system/shell.css +8 -102
- package/src/styles/system/sidebar.css +14 -0
- package/src/styles/system/source.css +5 -0
- package/src/styles/system/specimens.css +3 -0
- package/src/styles/system/stage.css +18 -0
- package/src/styles/system/steps.css +51 -0
- package/src/styles/system/surfaces.css +8 -104
- package/src/styles/system/table.css +23 -0
- package/src/styles/system/toc.css +59 -0
- package/src/styles/system/tooltip.css +6 -0
- package/src/styles/system/tray-code.css +6 -0
- package/src/styles/system/tray.css +20 -0
- package/src/styles/system/tree.css +15 -0
- package/src/styles/system/waterfall.css +14 -0
- package/src/virtual.d.ts +5 -1
- package/src/blog/article/language.ts +0 -29
|
@@ -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 };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Prose can vary by locale; publication identity and technical configuration stay with the source. */
|
|
2
|
+
export declare function localizedMetadata(metadata: Record<string, any>, frontmatter: Record<string, any>, locale: string): Record<string, any>;
|
|
3
|
+
export interface TranslationResource {
|
|
4
|
+
id: string;
|
|
5
|
+
source: string;
|
|
6
|
+
translations: Record<string, string | undefined>;
|
|
7
|
+
/** Source-only data or metadata whose changes require another translation review. */
|
|
8
|
+
dependencies?: Record<string, string>;
|
|
9
|
+
locales?: readonly string[];
|
|
10
|
+
draft?: boolean;
|
|
11
|
+
checkAnchors?: boolean;
|
|
12
|
+
requireMetadata?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface TranslationReview {
|
|
15
|
+
sourceHash: string;
|
|
16
|
+
translationHash: string;
|
|
17
|
+
}
|
|
18
|
+
export interface TranslationReviews {
|
|
19
|
+
version: 1;
|
|
20
|
+
resources: Record<string, Record<string, TranslationReview>>;
|
|
21
|
+
}
|
|
22
|
+
export interface LocalizationOptions {
|
|
23
|
+
locales?: readonly string[];
|
|
24
|
+
defaultLocale?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface TranslationDiagnostic {
|
|
27
|
+
id: string;
|
|
28
|
+
locale: string;
|
|
29
|
+
reason: 'missing' | 'unreviewed' | 'source-changed' | 'translation-changed' | 'anchors' | 'metadata';
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const emptyReviews: () => TranslationReviews;
|
|
33
|
+
/** Explicit review operation. Preparation and production builds never call this function. */
|
|
34
|
+
export declare function reviewTranslation(resource: TranslationResource, locale: string): TranslationReview;
|
|
35
|
+
export declare function auditTranslations(resources: readonly TranslationResource[], reviews: TranslationReviews, { locales, defaultLocale }?: LocalizationOptions): TranslationDiagnostic[];
|
|
36
|
+
export declare function assertTranslations(resources: readonly TranslationResource[], reviews: TranslationReviews, options?: LocalizationOptions): void;
|
|
37
|
+
/** Shared Astro build gate, also usable by a consumer's explicit audit command. */
|
|
38
|
+
export declare function validateSiteTranslations({ validation, ...options }: LocalizationOptions & {
|
|
39
|
+
validation?: {
|
|
40
|
+
reviews: URL;
|
|
41
|
+
resources: URL;
|
|
42
|
+
};
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
export declare function readLocalizedDocument({ sourceFile, locale, defaultLocale, metadata }: {
|
|
45
|
+
sourceFile: string;
|
|
46
|
+
locale: string;
|
|
47
|
+
defaultLocale?: string;
|
|
48
|
+
metadata?: Record<string, any>;
|
|
49
|
+
}): Promise<{
|
|
50
|
+
file: string;
|
|
51
|
+
content: string;
|
|
52
|
+
frontmatter: Record<string, any>;
|
|
53
|
+
metadata: Record<string, any>;
|
|
54
|
+
}>;
|
|
55
|
+
/** Filesystem inventory, with no page imports, component execution or generated source writes. */
|
|
56
|
+
export declare function discoverDocumentResources({ contentRoot, locales, defaultLocale, exclude, siteRoot }: {
|
|
57
|
+
contentRoot: string;
|
|
58
|
+
siteRoot?: string;
|
|
59
|
+
exclude?: readonly string[];
|
|
60
|
+
} & LocalizationOptions): Promise<TranslationResource[]>;
|
|
61
|
+
/** Literal interface requests must exist even when a newly added control has no dictionary entry yet. */
|
|
62
|
+
export declare function sharedInterfaceKeys(sourceRoot?: string): Promise<string[]>;
|
|
63
|
+
/** Follow local prose/data modules without executing them or importing product compiler code. */
|
|
64
|
+
export declare function collectSourceDependencies(files: readonly string[], { siteRoot }: {
|
|
65
|
+
siteRoot: string;
|
|
66
|
+
}): Promise<Record<string, string>>;
|
|
67
|
+
export declare function headingAnchors(source: string): string[];
|
|
68
|
+
/** Pin the existing automatic English IDs before translation, without modifying fenced examples. */
|
|
69
|
+
export declare function stabilizeHeadingAnchors(source: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as assertTranslations, n as auditTranslations, r as collectSourceDependencies, i as discoverDocumentResources, a as emptyReviews, o as headingAnchors, p as localizedMetadata, s as readLocalizedDocument, c as reviewTranslation, l as sharedInterfaceKeys, u as stabilizeHeadingAnchors, d as validateSiteTranslations } from "./localization-LfIbIysE.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@master/internal-site",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "First-party Astro and Svelte website framework for Master sites.",
|
|
6
6
|
"engines": {
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"./assets/*": "./src/public/*",
|
|
28
28
|
"./astro/*": "./src/astro/*",
|
|
29
29
|
"./build/*": "./src/scripts/*",
|
|
30
|
+
"./build/localization.ts": {
|
|
31
|
+
"types": "./dist/localization.d.mts",
|
|
32
|
+
"import": "./dist/localization.mjs"
|
|
33
|
+
},
|
|
30
34
|
"./utils/cloudflare-image-loader.js": "./src/utils/cloudflare-image-loader.js",
|
|
31
35
|
"./package.json": "./package.json",
|
|
32
36
|
"./components/*": "./src/docs-shell/components/*",
|
|
@@ -236,7 +240,6 @@
|
|
|
236
240
|
"./blog/*": "./src/blog/*",
|
|
237
241
|
"./demo/*": "./src/demo/*",
|
|
238
242
|
"./demo/style": "./src/demo/style.ts",
|
|
239
|
-
"./blog/article/language": "./src/blog/article/language.ts",
|
|
240
243
|
"./blog/article/neighbors": "./src/blog/article/neighbors.ts",
|
|
241
244
|
"./blog/article/title-parts": "./src/blog/article/title-parts.ts",
|
|
242
245
|
"./home/home-lamp": "./src/home/home-lamp.ts",
|
|
@@ -305,14 +308,14 @@
|
|
|
305
308
|
"peerDependencies": {
|
|
306
309
|
"astro": "7.4.2",
|
|
307
310
|
"svelte": "5.57.2",
|
|
308
|
-
"@master/css": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1",
|
|
309
|
-
"@master/css-astro": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1",
|
|
311
|
+
"@master/css": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1 || ^2.0.0",
|
|
312
|
+
"@master/css-astro": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1 || ^2.0.0",
|
|
310
313
|
"vite": "8.3.5",
|
|
311
|
-
"@master/css-compiler": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1",
|
|
312
|
-
"@master/css-tooling": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1",
|
|
313
|
-
"@master/css-schema": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1",
|
|
314
|
-
"@master/css-preset": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1",
|
|
315
|
-
"@master/css-language-service": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1"
|
|
314
|
+
"@master/css-compiler": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1 || ^2.0.0",
|
|
315
|
+
"@master/css-tooling": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1 || ^2.0.0",
|
|
316
|
+
"@master/css-schema": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1 || ^2.0.0",
|
|
317
|
+
"@master/css-preset": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1 || ^2.0.0",
|
|
318
|
+
"@master/css-language-service": "0.0.0 || >=0.0.0-site.1793646900.gdf48b35846c1 <0.0.1 || ^2.0.0"
|
|
316
319
|
},
|
|
317
320
|
"dependencies": {
|
|
318
321
|
"@astrojs/svelte": "^9.1.2",
|
|
@@ -2,6 +2,7 @@ import { readdir, readFile, mkdir, writeFile, unlink } from 'node:fs/promises'
|
|
|
2
2
|
import { join, relative, dirname } from 'node:path'
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
4
|
import type { SiteConfig } from '../config'
|
|
5
|
+
import { parseFrontmatter } from '@astrojs/markdown-remark'
|
|
5
6
|
|
|
6
7
|
export async function contentFiles(directory: string): Promise<string[]> {
|
|
7
8
|
const files: string[] = []
|
|
@@ -22,6 +23,34 @@ export async function prepareContentPages(site: SiteConfig, root: string, conten
|
|
|
22
23
|
const destination = join(root, '.astro/master-site/pages')
|
|
23
24
|
const pages: Record<string, URL> = {}
|
|
24
25
|
const wanted = new Set<string>()
|
|
26
|
+
const localizedDocuments: Record<string, Record<string, URL>> = {}
|
|
27
|
+
const localizedMetadata: Record<string, Record<string, Record<string, any>>> = {}
|
|
28
|
+
const availableLocales: Record<string, readonly string[]> = {}
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
if (!file.endsWith('/content.mdx')) continue
|
|
31
|
+
const directory = dirname(file)
|
|
32
|
+
const route = relative(content, directory).replaceAll('\\', '/')
|
|
33
|
+
const pathname = '/' + route.split('/').filter(part => !/^\(.+\)$/.test(part)).join('/')
|
|
34
|
+
const key = `${contentPrefix}/${route ? route + '/' : ''}page.astro`
|
|
35
|
+
const section = route.split('/').find(part => !/^\(.+\)$/.test(part)) ?? 'home'
|
|
36
|
+
const locales = site.i18n.sectionLocales?.[section] ?? site.i18n.locales
|
|
37
|
+
const metadataSource = await readFile(join(directory, 'metadata.ts'), 'utf8').catch(() => '')
|
|
38
|
+
const source = parseFrontmatter(await readFile(file, 'utf8')).frontmatter
|
|
39
|
+
const draft = source.draft === true || /\bdraft\s*:\s*true\b/.test(metadataSource)
|
|
40
|
+
const documents: Record<string, URL> = {}
|
|
41
|
+
const metadata: Record<string, Record<string, any>> = {}
|
|
42
|
+
for (const locale of draft ? [] : locales) {
|
|
43
|
+
const variant = locale === site.i18n.defaultLocale ? file : join(directory, `content.${locale}.mdx`)
|
|
44
|
+
if (!files.has(variant)) continue
|
|
45
|
+
const { frontmatter } = parseFrontmatter(await readFile(variant, 'utf8'))
|
|
46
|
+
if (frontmatter.draft === true) continue
|
|
47
|
+
documents[locale] = pathToFileURL(variant)
|
|
48
|
+
metadata[locale] = frontmatter
|
|
49
|
+
}
|
|
50
|
+
localizedDocuments[pathname] = documents
|
|
51
|
+
localizedMetadata[pathname] = metadata
|
|
52
|
+
availableLocales[key] = Object.keys(documents)
|
|
53
|
+
}
|
|
25
54
|
for (const file of files) {
|
|
26
55
|
if (!file.endsWith('/content.mdx')) continue
|
|
27
56
|
const directory = dirname(file)
|
|
@@ -54,16 +83,6 @@ export const generateMetadata = createPageMetadata(metadata, dictionaries)
|
|
|
54
83
|
for (const stale of await contentFiles(destination).catch(() => [])) {
|
|
55
84
|
if (stale.endsWith('/page.astro') && !wanted.has(stale)) await unlink(stale)
|
|
56
85
|
}
|
|
57
|
-
const availableLocales: Record<string, readonly string[]> = {}
|
|
58
|
-
for (const file of files) {
|
|
59
|
-
if (!file.endsWith('/content.mdx')) continue
|
|
60
|
-
const directory = dirname(file)
|
|
61
|
-
const route = relative(content, directory).replaceAll('\\', '/')
|
|
62
|
-
const section = route.split('/').find(segment => !/^\(.+\)$/.test(segment)) ?? 'home'
|
|
63
|
-
if (!site.i18n.translatedOnly?.includes(section)) continue
|
|
64
|
-
availableLocales[`${contentPrefix}/${route ? route + '/' : ''}page.astro`] = site.i18n.locales.filter(locale =>
|
|
65
|
-
locale === site.i18n.defaultLocale || files.has(join(directory, `content.${locale}.mdx`)))
|
|
66
|
-
}
|
|
67
86
|
// A translated archive exists only when at least one article is translated.
|
|
68
87
|
for (const section of site.i18n.translatedOnly ?? []) {
|
|
69
88
|
const key = `${contentPrefix}/${section}/page.astro`
|
|
@@ -71,5 +90,5 @@ export const generateMetadata = createPageMetadata(metadata, dictionaries)
|
|
|
71
90
|
const published = Object.entries(availableLocales).filter(([page]) => page.startsWith(`${contentPrefix}/${section}/`))
|
|
72
91
|
availableLocales[key] = site.i18n.locales.filter(locale => locale === site.i18n.defaultLocale || published.some(([, locales]) => locales.includes(locale)))
|
|
73
92
|
}
|
|
74
|
-
return { pages, availableLocales }
|
|
93
|
+
return { pages, availableLocales, localizedDocuments, localizedMetadata }
|
|
75
94
|
}
|
package/src/astro/index.ts
CHANGED
|
@@ -12,8 +12,11 @@ import { searchForWorkspaceRoot, transformWithOxc, type Plugin } from 'vite'
|
|
|
12
12
|
import type { SiteConfig } from '../config'
|
|
13
13
|
import sourceURL from './source-url'
|
|
14
14
|
import { prepareContentPages } from './content-pages'
|
|
15
|
+
import { validateSiteTranslations } from '../scripts/localization'
|
|
15
16
|
import serverStyles from './server-styles'
|
|
16
17
|
import svgMarkup from './svg-markup'
|
|
18
|
+
import localizedMdx from './localized-mdx'
|
|
19
|
+
import { compactStyleAssets } from './style-assets'
|
|
17
20
|
import remarkSlugAndTOC from '../docs-shell/remark/slug-and-toc.js'
|
|
18
21
|
import remarkMarkdownComponents from '../docs-shell/remark/markdown-components.js'
|
|
19
22
|
import svgoConfig from '../docs-shell/common/svgo.config.js'
|
|
@@ -28,7 +31,8 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
|
|
|
28
31
|
return {
|
|
29
32
|
name: '@master/internal-site',
|
|
30
33
|
hooks: {
|
|
31
|
-
'astro:config:setup': async ({ config, updateConfig, addRenderer, injectRoute }) => {
|
|
34
|
+
'astro:config:setup': async ({ config, command, updateConfig, addRenderer, injectRoute }) => {
|
|
35
|
+
if (command === 'build') await validateSiteTranslations(site.i18n)
|
|
32
36
|
const root = fileURLToPath(config.root)
|
|
33
37
|
const contentRelative = relative(root, fileURLToPath(site.content)).split(sep).join('/')
|
|
34
38
|
if (contentRelative.startsWith('..')) throw new Error('Site content must be inside the Astro project root')
|
|
@@ -40,7 +44,8 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
|
|
|
40
44
|
'virtual:master-site/config': `export default ${JSON.stringify(site)}`,
|
|
41
45
|
'virtual:master-site/app': moduleImport(site.app),
|
|
42
46
|
'virtual:master-site/highlight': moduleImport(site.highlighter ?? new URL('../docs-shell/utils/highlight-code-core.ts', import.meta.url)),
|
|
43
|
-
'virtual:master-site/translations': `export default
|
|
47
|
+
'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(',')}}`,
|
|
48
|
+
'virtual:master-site/localized-metadata': `export default ${JSON.stringify(prepared.localizedMetadata)}`,
|
|
44
49
|
}
|
|
45
50
|
const plugin: Plugin = {
|
|
46
51
|
name: '@master/internal-site:modules',
|
|
@@ -74,10 +79,10 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
|
|
|
74
79
|
clearTimeout(restart)
|
|
75
80
|
restart = setTimeout(() => { void server.restart() }, 100)
|
|
76
81
|
}
|
|
77
|
-
server.watcher.on('add', refresh).on('unlink', refresh)
|
|
82
|
+
server.watcher.on('add', refresh).on('unlink', refresh).on('change', refresh)
|
|
78
83
|
server.httpServer?.once('close', () => {
|
|
79
84
|
clearTimeout(restart)
|
|
80
|
-
server.watcher.off('add', refresh).off('unlink', refresh)
|
|
85
|
+
server.watcher.off('add', refresh).off('unlink', refresh).off('change', refresh)
|
|
81
86
|
})
|
|
82
87
|
// Serve package-owned assets without copying them into the consumer's source tree.
|
|
83
88
|
// A consumer's public asset takes precedence at the same URL.
|
|
@@ -109,7 +114,7 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
|
|
|
109
114
|
build: { format: 'file' },
|
|
110
115
|
markdown: { processor: unified({ remarkPlugins: [remarkSlugAndTOC, remarkMarkdownComponents], gfm: true, smartypants: false }), syntaxHighlight: false },
|
|
111
116
|
experimental: { svgOptimizer: svgoOptimizer(svgoConfig as Parameters<typeof svgoOptimizer>[0]) },
|
|
112
|
-
vite: { plugins: [plugin, sourceURL(), serverStyles([root, packageSource]), svgMarkup()], resolve: { dedupe: ['svelte'] }, server: { fs: { allow: [searchForWorkspaceRoot(root), packageSource] } } },
|
|
117
|
+
vite: { plugins: [plugin, localizedMdx(site.i18n), sourceURL(), serverStyles([root, packageSource]), svgMarkup()], resolve: { dedupe: ['svelte'] }, server: { fs: { allow: [searchForWorkspaceRoot(root), packageSource] } } },
|
|
113
118
|
integrations: [svelte(), mdx(), masterCSS({ mode: 'progressive' })]
|
|
114
119
|
})
|
|
115
120
|
},
|
|
@@ -118,6 +123,7 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
|
|
|
118
123
|
const destination = output
|
|
119
124
|
await mkdir(destination, { recursive: true })
|
|
120
125
|
await cp(new URL('../public/', import.meta.url), destination, { recursive: true, force: false })
|
|
126
|
+
await compactStyleAssets(fileURLToPath(destination))
|
|
121
127
|
}
|
|
122
128
|
}
|
|
123
129
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { isAbsolute } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { parseFrontmatter } from '@astrojs/markdown-remark'
|
|
5
|
+
import type { Plugin } from 'vite'
|
|
6
|
+
import { locales as supportedLocales, defaultLocale as sourceLocale } from '../docs-shell/common/locales.js'
|
|
7
|
+
import { isSharedMdxSample } from '../scripts/mdx-sample'
|
|
8
|
+
import { cleanId } from './module-id'
|
|
9
|
+
|
|
10
|
+
/** Nested article MDX follows its importer; executable examples keep one source. */
|
|
11
|
+
export default function localizedMdx({ locales = supportedLocales, defaultLocale = sourceLocale }: { locales?: readonly string[]; defaultLocale?: string } = {}): Plugin {
|
|
12
|
+
return {
|
|
13
|
+
name: '@master/internal-site:localized-mdx',
|
|
14
|
+
enforce: 'pre',
|
|
15
|
+
async resolveId(source, importer, options) {
|
|
16
|
+
if (!importer || !cleanId(importer).endsWith('.mdx')) return
|
|
17
|
+
const language = cleanId(importer).match(/\.([a-z]{2})\.mdx$/)?.[1]
|
|
18
|
+
const locale = language && locales.includes(language) ? language : defaultLocale
|
|
19
|
+
|
|
20
|
+
// Let Vite resolve aliases and filesystem boundaries before selecting a sibling.
|
|
21
|
+
const resolved = await this.resolve(source, importer, { ...options, skipSelf: true })
|
|
22
|
+
if (!resolved || resolved.external) return
|
|
23
|
+
const id = cleanId(resolved.id)
|
|
24
|
+
if (!id.endsWith('.mdx') || id.startsWith('\0')) return
|
|
25
|
+
const suffix = resolved.id.slice(id.length) || source.slice(cleanId(source).length)
|
|
26
|
+
const file = id.startsWith('file:') ? fileURLToPath(id) : id.startsWith('/@fs/') ? id.slice(5) : id
|
|
27
|
+
if (!isAbsolute(file)) return
|
|
28
|
+
const original = file.replace(/\.([a-z]{2})\.mdx$/, (match, language: string) => locales.includes(language) ? '.mdx' : match)
|
|
29
|
+
this.addWatchFile(original)
|
|
30
|
+
const originalSource = await readFile(original, 'utf8')
|
|
31
|
+
const originalMetadata = parseFrontmatter(originalSource).frontmatter
|
|
32
|
+
if (originalMetadata.draft === true || originalMetadata.disabled === true) throw new Error(`Unpublished source MDX include: ${original}`)
|
|
33
|
+
if (locale === defaultLocale || !originalSource.trim() || isSharedMdxSample(original, originalSource)) return { ...resolved, id: original + suffix }
|
|
34
|
+
|
|
35
|
+
const translated = original.replace(/\.mdx$/, `.${locale}.mdx`)
|
|
36
|
+
this.addWatchFile(translated)
|
|
37
|
+
let translatedSource: string
|
|
38
|
+
try { translatedSource = await readFile(translated, 'utf8') }
|
|
39
|
+
catch (cause) { throw new Error(`Missing translated MDX include: ${translated}`, { cause }) }
|
|
40
|
+
const metadata = parseFrontmatter(translatedSource).frontmatter
|
|
41
|
+
if (metadata.draft === true || metadata.disabled === true) throw new Error(`Unpublished translated MDX include: ${translated}`)
|
|
42
|
+
return { ...resolved, id: translated + suffix }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|