@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
@@ -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.1.3",
3
+ "version": "0.2.1",
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
  }
@@ -12,8 +12,10 @@ 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'
17
19
  import { compactStyleAssets } from './style-assets'
18
20
  import remarkSlugAndTOC from '../docs-shell/remark/slug-and-toc.js'
19
21
  import remarkMarkdownComponents from '../docs-shell/remark/markdown-components.js'
@@ -29,7 +31,8 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
29
31
  return {
30
32
  name: '@master/internal-site',
31
33
  hooks: {
32
- '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)
33
36
  const root = fileURLToPath(config.root)
34
37
  const contentRelative = relative(root, fileURLToPath(site.content)).split(sep).join('/')
35
38
  if (contentRelative.startsWith('..')) throw new Error('Site content must be inside the Astro project root')
@@ -41,7 +44,8 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
41
44
  'virtual:master-site/config': `export default ${JSON.stringify(site)}`,
42
45
  'virtual:master-site/app': moduleImport(site.app),
43
46
  'virtual:master-site/highlight': moduleImport(site.highlighter ?? new URL('../docs-shell/utils/highlight-code-core.ts', import.meta.url)),
44
- 'virtual:master-site/translations': `export default import.meta.glob(${JSON.stringify(`${contentPrefix}/**/content.*.mdx`)})`,
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)}`,
45
49
  }
46
50
  const plugin: Plugin = {
47
51
  name: '@master/internal-site:modules',
@@ -75,10 +79,10 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
75
79
  clearTimeout(restart)
76
80
  restart = setTimeout(() => { void server.restart() }, 100)
77
81
  }
78
- server.watcher.on('add', refresh).on('unlink', refresh)
82
+ server.watcher.on('add', refresh).on('unlink', refresh).on('change', refresh)
79
83
  server.httpServer?.once('close', () => {
80
84
  clearTimeout(restart)
81
- server.watcher.off('add', refresh).off('unlink', refresh)
85
+ server.watcher.off('add', refresh).off('unlink', refresh).off('change', refresh)
82
86
  })
83
87
  // Serve package-owned assets without copying them into the consumer's source tree.
84
88
  // A consumer's public asset takes precedence at the same URL.
@@ -110,7 +114,7 @@ export default function masterSite(site: SiteConfig): AstroIntegration {
110
114
  build: { format: 'file' },
111
115
  markdown: { processor: unified({ remarkPlugins: [remarkSlugAndTOC, remarkMarkdownComponents], gfm: true, smartypants: false }), syntaxHighlight: false },
112
116
  experimental: { svgOptimizer: svgoOptimizer(svgoConfig as Parameters<typeof svgoOptimizer>[0]) },
113
- 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] } } },
114
118
  integrations: [svelte(), mdx(), masterCSS({ mode: 'progressive' })]
115
119
  })
116
120
  },
@@ -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
+ }
@@ -3,6 +3,7 @@ import { mergeAssets, type PageAssets, type PropagatedModule } from '@master/int
3
3
  import { createStaticPaths, directoriesOf, localesOf, sectionOf, type ParamsModule, type RouteProps, type Section } from '@master/internal-site/astro/route-paths'
4
4
  import type { Metadata } from '@master/internal-site/types/Metadata'
5
5
  import type { Props } from '@master/internal-site/types/Props'
6
+ import { loadLocalizedContent } from '@master/internal-site/factories/create-page'
6
7
 
7
8
  export { localizePathname, sectionOf } from '@master/internal-site/astro/route-paths'
8
9
  export type { RouteProps, Section } from '@master/internal-site/astro/route-paths'
@@ -77,6 +78,8 @@ export async function loadRoute({ file, locale, params }: RouteProps): Promise<L
77
78
  const metadata = (module.generateMetadata ? await module.generateMetadata(props) : module.metadata) ?? {}
78
79
  const layers = [module.default]
79
80
  const propagated: PropagatedModule<unknown>[] = [page.propagated]
81
+ const localized = await loadLocalizedContent(locale, metadata)
82
+ if (localized) { await localized.getMod(); propagated.push(localized) }
80
83
  const directories = directoriesOf(file, contentRoot)
81
84
  for (let depth = directories.length; depth > 0; depth--) {
82
85
  const layoutFile = `${contentRoot}${directories.slice(0, depth).join('/')}/layout.astro`
@@ -1,5 +1,6 @@
1
1
  import config from 'virtual:master-site/config'
2
2
  import { createSite, type Site, type SiteContextValue } from '../docs-shell/site'
3
+ import sharedTranslations from '../docs-shell/common/translations'
3
4
 
4
5
  export function localeOfPathname(pathname: string): string {
5
6
  const segment = pathname.split('/')[1]
@@ -18,10 +19,17 @@ export function canonicalPathname(pathname: string) {
18
19
  export function siteContextOf(url: URL): SiteContextValue {
19
20
  const pathname = canonicalPathname(url.pathname)
20
21
  const locale = localeOfPathname(pathname)
21
- const { translations, ...i18n } = config.i18n
22
- return { locale, pathname, translations: translations[locale] ?? {}, i18n }
22
+ const { translations, defaultLocale, locales, hreflangOfLocale, nameOfLocale, localizablePathnameRoots, localizableOrigins } = config.i18n
23
+ // Only public navigation settings belong in the serialized island context.
24
+ const i18n = { defaultLocale, locales, hreflangOfLocale, nameOfLocale, localizablePathnameRoots, localizableOrigins }
25
+ return { locale, pathname, translations: { ...sharedTranslations[locale], ...translations[locale] }, i18n }
23
26
  }
24
27
 
25
28
  export function siteOf(astro: { url: URL }): Site {
26
29
  return createSite(siteContextOf(astro.url))
27
30
  }
31
+
32
+ /** Request-local authored figure captions and other article-specific copy. */
33
+ export function documentCopyOf(astro: { locals: object }): Record<string, any> {
34
+ return (astro.locals as Record<string, any>).masterDocument ?? {}
35
+ }
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { siteOf } from '@master/internal-site/astro/site-context'
3
+ import { localizeBenchmarkChart } from './localization'
2
4
  import '@master/internal-site/styles/system/table.css'
3
5
  import ExpandContent from '@master/internal-site/components/ExpandContent.svelte'
4
6
  import BenchmarkBars from '@master/internal-site/benchmarks/BenchmarkBars.astro'
@@ -11,7 +13,8 @@ interface Props {
11
13
  }
12
14
 
13
15
  /** Renders an authored chart model; `utils/benchmark-content` exports the same model as Markdown. */
14
- const { chart } = Astro.props
16
+ const { t } = siteOf(Astro)
17
+ const chart = localizeBenchmarkChart(Astro.props.chart, t)
15
18
  ---
16
19
 
17
20
  {chart.kind === 'bars' && <BenchmarkBars items={chart.items} max={chart.max} unit={chart.unit} valueFormatter={chart.valueFormatter} />}
@@ -16,7 +16,7 @@ const hasDetails = metrics.some((metric) => metric.detail)
16
16
  ---
17
17
 
18
18
  {/* Preserve keyboard access to wide metric tables. */}
19
- <div class="ui-table" role="region" aria-label={label} tabindex="0">
19
+ <div class="ui-table" role="region" aria-label={t(label)} tabindex="0">
20
20
  <table>
21
21
  <thead>
22
22
  <tr>
@@ -35,11 +35,11 @@ const labelPrefix = uniqueId(Astro, 'benchmark-stacked-bars')
35
35
  <div class="ui-bar" data-variant="stacked" role="img" aria-labelledby={`${labelId} ${valueId}`}>
36
36
  {item.segments.map((segment, segmentIndex) => <i
37
37
  data-tone={benchmarkTone(segment.color ?? benchmarkColors[segmentIndex % benchmarkColors.length])}
38
- title={`${segment.label}: ${segment.valueLabel ?? formatMetricValue(segment.value, unit)}`}
38
+ title={`${t(segment.label)}: ${segment.valueLabel ?? formatMetricValue(segment.value, unit)}`}
39
39
  style={{ width: `${clampPercent(segment.value, max ?? total)}%` }} />)}
40
40
  </div>
41
41
  <output id={valueId}>{totalLabel}</output>
42
- <ul class="ui-legend" role="group" aria-label={`${item.label} breakdown`}>
42
+ <ul class="ui-legend" role="group" aria-label={t('{label} breakdown').replace('{label}', t(item.label))}>
43
43
  {item.segments.map((segment, segmentIndex) => {
44
44
  const valueLabel = segment.valueLabel ?? (valueFormatter ? valueFormatter(segment.value) : formatMetricValue(segment.value, unit))
45
45
  return <li>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { siteOf } from '@master/internal-site/astro/site-context'
3
+ const { t } = siteOf(Astro)
2
4
  import '@master/internal-site/styles/system/specimens.css'
3
5
  import type { BenchmarkTone } from '@master/internal-site/benchmarks/types.ts'
4
6
  import { benchmarkStatusTones } from '@master/internal-site/benchmarks/utils.ts'
@@ -13,4 +15,4 @@ const names: Record<Exclude<BenchmarkTone, 'neutral'>, string> = { good: 'Good',
13
15
  const { tone } = Astro.props
14
16
  ---
15
17
 
16
- {tone && tone !== 'neutral' && <span class="ui-badge" data-tone={benchmarkStatusTones[tone]} data-variant="dot" data-size="sm">{names[tone]}</span>}
18
+ {tone && tone !== 'neutral' && <span class="ui-badge" data-tone={benchmarkStatusTones[tone]} data-variant="dot" data-size="sm">{t(names[tone])}</span>}
@@ -0,0 +1,22 @@
1
+ /** Reader copy only: numeric values, technical units, fixture rows and formatters stay canonical. */
2
+ const proseFields = new Set(['label', 'labelNote', 'title', 'detail', 'description', 'caption', 'headers'])
3
+
4
+ function mapCopy<T>(model: T, translate: (text: string) => string): T {
5
+ const visit = (value: unknown, field: string): unknown => {
6
+ if (typeof value === 'string') return proseFields.has(field) && value.trim() ? translate(value) : value
7
+ if (Array.isArray(value)) return value.map(item => visit(item, field))
8
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, visit(item, key)]))
9
+ return value
10
+ }
11
+ return visit(model, '') as T
12
+ }
13
+
14
+ export function benchmarkCopyKeys(model: unknown): string[] {
15
+ const keys = new Set<string>()
16
+ mapCopy(model, text => { keys.add(text); return text })
17
+ return [...keys].sort()
18
+ }
19
+
20
+ export function localizeBenchmarkChart<T>(model: T, translate: (text: string) => string): T {
21
+ return mapCopy(model, translate)
22
+ }
@@ -1,11 +1,10 @@
1
1
  ---
2
- import dayjs from 'dayjs'
2
+ import { formatDate } from '../../docs-shell/common/locales'
3
3
  import IconArrowLeft from '@tabler/icons-svelte-runes/icons/arrow-left'
4
4
  import IconArrowRight from '@tabler/icons-svelte-runes/icons/arrow-right'
5
5
  import Link from '@master/internal-site/components/Link.svelte'
6
6
  import { siteOf } from '@master/internal-site/astro/site-context'
7
7
  import TitleLines from './title-lines.astro'
8
- import { sourceLanguageOf } from './language'
9
8
  import type { BlogArticleNeighbor } from './neighbors'
10
9
 
11
10
  interface Props {
@@ -20,14 +19,12 @@ interface Props {
20
19
  * article as quiet links.
21
20
  */
22
21
  const { newer, older, showCategory = true } = Astro.props
23
- const { t: $, i18n, locale } = siteOf(Astro)
24
- const tw = locale === 'tw'
25
- const languageOf = (text?: string) => sourceLanguageOf(text, { ...i18n, locale })
22
+ const { t: $, locale } = siteOf(Astro)
26
23
  // Readers who finish a post usually continue backwards through the archive; the newest post leads with the one before it.
27
24
  const lead = older ?? newer
28
25
  const other = lead === older ? newer : undefined
29
26
  // Written out in full, as on the post's own header and on the index's featured sheet.
30
- const date = (value: string) => dayjs(value).format(tw ? 'YYYY 年 M 月 D 日' : 'MMMM D, YYYY')
27
+ const date = (value: string) => formatDate(value, locale)
31
28
  ---
32
29
 
33
30
  <nav class="blog-article-end blog-rails blog-rails-rule" aria-labelledby="blog-article-end-title">
@@ -38,8 +35,8 @@ const date = (value: string) => dayjs(value).format(tw ? 'YYYY 年 M 月 D 日'
38
35
  {showCategory && lead.category && <span>{$(lead.category)}</span>}
39
36
  {lead.date && <time datetime={lead.date}>{date(lead.date)}</time>}
40
37
  </span>
41
- <span class="blog-article-next-title" lang={languageOf(lead.title)}><TitleLines title={$(lead.title)} class="blog-article-next-title-line" /><IconArrowRight class="blog-article-next-arrow" aria-hidden="true" size={22} stroke={1.25} /></span>
42
- {lead.description && <span class="blog-article-next-description" lang={languageOf(lead.description)}>{$(lead.description)}</span>}
38
+ <span class="blog-article-next-title"><TitleLines title={$(lead.title)} class="blog-article-next-title-line" /><IconArrowRight class="blog-article-next-arrow" aria-hidden="true" size={22} stroke={1.25} /></span>
39
+ {lead.description && <span class="blog-article-next-description">{$(lead.description)}</span>}
43
40
  </Link>
44
41
  )}
45
42
  <div class="blog-article-end-links">
@@ -47,7 +44,7 @@ const date = (value: string) => dayjs(value).format(tw ? 'YYYY 年 M 月 D 日'
47
44
  {other && (
48
45
  <Link class="blog-article-end-link" href={other.pathname}>
49
46
  <span class="blog-article-end-link-label">{$('Newer')}</span>
50
- <span class="blog-article-end-link-title" lang={languageOf(other.title)}>{$(other.title)}</span>
47
+ <span class="blog-article-end-link-title">{$(other.title)}</span>
51
48
  <IconArrowRight aria-hidden="true" size={14} stroke={1.75} />
52
49
  </Link>
53
50
  )}
@@ -1,11 +1,10 @@
1
1
  ---
2
- import dayjs from 'dayjs'
2
+ import { formatDate } from '../../docs-shell/common/locales'
3
3
  import authors from '@master/internal-site/data/authors'
4
4
  import Image from '@master/internal-site/components/Image.svelte'
5
5
  import Link from '@master/internal-site/components/Link.svelte'
6
6
  import { siteOf } from '@master/internal-site/astro/site-context'
7
7
  import TitleLines from './title-lines.astro'
8
- import { sourceLanguageOf } from './language'
9
8
  import type { DefinedMetadata } from '@master/internal-site/types/Metadata'
10
9
 
11
10
  interface Props {
@@ -18,11 +17,9 @@ interface Props {
18
17
  * falls on it from above and to the left.
19
18
  */
20
19
  const { metadata } = Astro.props
21
- const { t: $, i18n, locale } = siteOf(Astro)
22
- const tw = locale === 'tw'
20
+ const { t: $, locale } = siteOf(Astro)
23
21
  const source: string = metadata.title?.absolute || metadata.title
24
22
  const title: string = $(source)
25
- const languageOf = (text?: string) => sourceLanguageOf(text, { ...i18n, locale })
26
23
  const people = (metadata.authors ?? [])
27
24
  .map(({ name }: { name: string }) => authors.find((author) => author.name === name))
28
25
  .filter(Boolean) as typeof authors
@@ -32,11 +29,11 @@ const people = (metadata.authors ?? [])
32
29
  {(metadata.category || metadata.date) && (
33
30
  <p class="blog-article-meta">
34
31
  {metadata.category && <span>{$(metadata.category)}</span>}
35
- {metadata.date && <time datetime={metadata.date}>{dayjs(metadata.date).format(tw ? 'YYYY 年 M 月 D 日' : 'MMMM D, YYYY')}</time>}
32
+ {metadata.date && <time datetime={metadata.date}>{formatDate(metadata.date, locale)}</time>}
36
33
  </p>
37
34
  )}
38
- <h1 class="blog-article-title blog-stone-text" lang={languageOf(source)}><TitleLines title={title} class="blog-article-title-line" /></h1>
39
- {metadata.description && <p class="blog-article-dek" lang={languageOf(metadata.description)}>{$(metadata.description)}</p>}
35
+ <h1 class="blog-article-title blog-stone-text"><TitleLines title={title} class="blog-article-title-line" /></h1>
36
+ {metadata.description && <p class="blog-article-dek">{$(metadata.description)}</p>}
40
37
  {people.length > 0 && (
41
38
  <div class="blog-article-byline">
42
39
  <span class="blog-article-byline-label">{$('Written by')}</span>
package/src/config.ts CHANGED
@@ -28,10 +28,13 @@ export interface SiteI18nConfig {
28
28
  hreflangOfLocale: Record<string, string>
29
29
  nameOfLocale: Record<string, string>
30
30
  localizablePathnameRoots: string[]
31
+ localizableOrigins?: Record<string, readonly string[]>
31
32
  translations: Record<string, Record<string, string>>
32
33
  sectionLocales?: Record<string, readonly string[]>
33
34
  /** These sections emit translated URLs only when translated MDX exists. */
34
35
  translatedOnly?: readonly string[]
36
+ /** Build-only resource loader and version-controlled explicit translation reviews. */
37
+ validation?: { reviews: URL; resources: URL }
35
38
  }
36
39
 
37
40
  export interface DocumentConfig {
@@ -4,7 +4,7 @@ import { configuredMarkupMarkdown } from '@master/internal-site/css/configured-e
4
4
  export function specimenFactsMarkdown(namespace: string, keys?: string[]) {
5
5
  return specimenTokens(namespace, keys).map(token => `- \`--${token.name}\`: ${token.values.map(value => `\`${value.value}\` (${value.path.join(' → ')})`).join('; ')}${tokenAdvice(namespace, token.key) ? ` — ${tokenAdvice(namespace, token.key)}` : ''}`).join('\n')
6
6
  }
7
- export function selectedSpecimensMarkdown(namespace: string, keys: string[]) {
7
+ export function selectedSpecimensMarkdown(namespace: string, keys: string[], t: (text: string) => string = text => text) {
8
8
  const scene = foundationScene(namespace, keys)
9
- return `${scene.caption}\n\n${specimenFactsMarkdown(namespace, keys)}${scene.html && namespace !== 'color' ? `\n\n${configuredMarkupMarkdown(scene.css ?? '', scene.html)}` : ''}`
9
+ return `${t(scene.caption ?? '')}\n\n${specimenFactsMarkdown(namespace, keys)}${scene.html && namespace !== 'color' ? `\n\n${configuredMarkupMarkdown(scene.css ?? '', scene.html)}` : ''}`
10
10
  }