@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
@@ -6,6 +6,10 @@ import { mdxjsEsmFromMarkdown } from 'mdast-util-mdxjs-esm'
6
6
  import { mdxJsx } from 'micromark-extension-mdx-jsx'
7
7
  import { mdxJsxFromMarkdown } from 'mdast-util-mdx-jsx'
8
8
  import * as acorn from 'acorn'
9
+ import { parseFrontmatter } from '@astrojs/markdown-remark'
10
+ import { headingAnchors } from '../scripts/localization'
11
+ import { isSharedMdxSample } from '../scripts/mdx-sample'
12
+ import { translateShell } from '../docs-shell/common/translations'
9
13
  import { generatePresetCSS } from '@master/internal-site/css/generate-preset-css.ts'
10
14
  import { selectedSpecimensMarkdown } from '@master/internal-site/css/foundation-data/specimen-content.ts'
11
15
  import { getVariableNamespacePublicKeys } from '@master/internal-site/css/manifest-utilities.ts'
@@ -33,7 +37,7 @@ export function syntaxMarkdown(rows: SyntaxRow[]) {
33
37
  }
34
38
 
35
39
  export function portableMarkdown(markdown: string) {
36
- return markdown.split(/(```[\s\S]*?```)/g).map(part => part.startsWith('```') ? part : part.replace(/<!-- \/?reference-illustration(?::[\w-]+)? -->\n*/g, '').replace(/^(#{2,3}) (.+) \{#([\w-]+)\}$/gm, '<a id="$3"></a>\n\n$1 $2')).join('')
40
+ return markdown.split(/(```[\s\S]*?```)/g).map(part => part.startsWith('```') ? part : part.replace(/<!-- \/?reference-illustration(?::[\w-]+)? -->\n*/g, '').replace(/^(#{2,6}) (.+) \{#([\w-]+)\}$/gm, '<a id="$3"></a>\n\n$1 $2')).join('')
37
41
  }
38
42
 
39
43
  /** Resolve authored literals only. Never execute MDX expressions to extract documents. */
@@ -50,9 +54,11 @@ function literal(node: any, variables: Record<string, unknown>): unknown {
50
54
  }
51
55
 
52
56
  export interface MdxContentOptions {
57
+ /** Propagates through nested MDX includes and custom text adapters. */
58
+ locale?: string
53
59
  /** Reference Overview renders syntax rows; Guide Overview may be a local MDX include. */
54
60
  overview?: 'syntax' | 'include'
55
- component?: (name: string, attributes: Record<string, any>, file: string, context: { expression: (source: string) => Promise<unknown> }) => string | undefined | Promise<string | undefined>
61
+ component?: (name: string, attributes: Record<string, any>, file: string, context: { expression: (source: string) => Promise<unknown>; locale: string }) => string | undefined | Promise<string | undefined>
56
62
  }
57
63
 
58
64
  /**
@@ -64,7 +70,18 @@ const expressionElement = /(<(Code|Class2CSS|DocumentDeclaration)\b(?:"[^"]*"|'[
64
70
  export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [], stack: string[] = [], options: MdxContentOptions = {}) {
65
71
  if (stack.includes(file)) throw new Error(`Recursive document include: ${file}`)
66
72
  const expressions: Record<string, string> = {}
67
- const source = (await readFile(file, 'utf8')).split(/(```[\s\S]*?```)/g).map(part => part.startsWith('```') ? part : part.replace(/\{?<Demo\b[\s\S]*?<\/Demo>\}?/g, '').replace(expressionElement, (_, start, _name, expression, end) => { const key = `MCSS_EXPRESSION_${Object.keys(expressions).length}`; expressions[key] = expression; return `${start}\n${key}\n${end}` })).join('')
73
+ const locale = options.locale ?? file.match(/\.(tw|ja|ko|cn)\.mdx$/)?.[1] ?? 'en'
74
+ const t = (text: string) => translateShell(locale, text)
75
+ options = { ...options, locale }
76
+ const original = file.replace(/\.(tw|ja|ko|cn)\.mdx$/, '.mdx')
77
+ const canonical = parseFrontmatter(await readFile(original, 'utf8'))
78
+ if (canonical.frontmatter.draft === true || canonical.frontmatter.disabled === true) throw new Error(`Unpublished source MDX document: ${original}`)
79
+ const document = original === file ? canonical : parseFrontmatter(await readFile(file, 'utf8'))
80
+ if (document.frontmatter.draft === true || document.frontmatter.disabled === true) throw new Error(`Unpublished translated MDX document: ${file}`)
81
+ const source = document.content.split(/(```[\s\S]*?```)/g).map(part => part.startsWith('```') ? part : part.replace(/\{?<Demo\b[\s\S]*?<\/Demo>\}?/g, '').replace(expressionElement, (_, start, _name, expression, end) => { const key = `MCSS_EXPRESSION_${Object.keys(expressions).length}`; expressions[key] = expression; return `${start}\n${key}\n${end}` })).join('')
82
+ let sourceAnchors: string[]
83
+ try { sourceAnchors = headingAnchors(source) } catch (error) { throw new Error(`Cannot read headings in ${file}`, { cause: error }) }
84
+ const headingIds = new Map<any, string>()
68
85
  const examples: ReferenceExample[] = []
69
86
  const notes: string[] = []
70
87
  const variables: Record<string, unknown> = {}
@@ -75,6 +92,12 @@ export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [],
75
92
  extensions: [{ disable: { null: ['htmlFlow', 'htmlText', 'codeIndented'] } }, mdxjsEsm({ acorn, addResult: true }), mdxJsx({ acorn, addResult: true })],
76
93
  mdastExtensions: [mdxjsEsmFromMarkdown(), mdxJsxFromMarkdown()]
77
94
  }) } catch (error) { throw new Error(`Cannot parse ${file}`, { cause: error }) }
95
+ let headingIndex = 0
96
+ const collectHeadingIds = (node: any) => {
97
+ if (node.type === 'heading' && node.depth >= 2 && node.depth <= 6) headingIds.set(node, sourceAnchors[headingIndex++])
98
+ node.children?.forEach(collectHeadingIds)
99
+ }
100
+ collectHeadingIds(tree)
78
101
  const raw = (node: any) => source.slice(node.position.start.offset, node.position.end.offset)
79
102
  const collectEsm = (node: any) => {
80
103
  if (node.type === 'mdxjsEsm') {
@@ -135,7 +158,7 @@ export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [],
135
158
  const name = node.name ?? ''
136
159
  const attrs: Record<string, any> = Object.fromEntries((node.attributes ?? []).filter((a: any) => a.name).map((a: any) => [a.name, a.value]))
137
160
  const inner = (node.children ?? []).map(raw).join('\n')
138
- const adapted = await options.component?.(name, attrs, file, { expression })
161
+ const adapted = await options.component?.(name, attrs, file, { expression, locale })
139
162
  if (adapted !== undefined) return adapted
140
163
  const attribute = (value: any) => value?.type === 'mdxJsxAttributeValueExpression' ? expression(value.value) : value
141
164
  if (name === 'DocumentPrompt') {
@@ -148,11 +171,11 @@ export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [],
148
171
  const result = await attribute(attrs.result)
149
172
  const diagnostic = await attribute(attrs.diagnostic)
150
173
  if (typeof source !== 'string' || typeof attrs.language !== 'string') throw new Error('DocumentCodeExample requires literal source and language')
151
- const sourceLabel = attrs.sourceLabel ?? (result !== undefined && attrs.resultKind !== 'generated-css' ? 'Source' : undefined)
174
+ const sourceLabel = attrs.sourceLabel ?? (result !== undefined && attrs.resultKind !== 'generated-css' ? t('Source') : undefined)
152
175
  const parts = [`**${attrs.title}**`, ...(sourceLabel ? [`**${sourceLabel}**`] : []), `\`\`\`${attrs.language}\n${source}\n\`\`\``]
153
176
  if (result !== undefined) {
154
177
  if (typeof result !== 'string') throw new Error('DocumentCodeExample requires a literal result')
155
- parts.push(`**${attrs.resultLabel ?? (attrs.resultKind === 'generated-css' ? 'Generated CSS' : 'Result')}**`, `\`\`\`${attrs.resultLanguage ?? attrs.language}\n${result}\n\`\`\``)
178
+ parts.push(`**${attrs.resultLabel ?? t(attrs.resultKind === 'generated-css' ? 'Generated CSS' : 'Result')}**`, `\`\`\`${attrs.resultLanguage ?? attrs.language}\n${result}\n\`\`\``)
156
179
  }
157
180
  if (diagnostic) parts.push(`${diagnostic.severity} (\`${diagnostic.rule}\`): ${diagnostic.message}`)
158
181
  return parts.join('\n\n')
@@ -160,7 +183,7 @@ export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [],
160
183
  if (['DocumentOptions', 'DocumentParameters'].includes(name)) {
161
184
  const values = await expression(attrs[name === 'DocumentOptions' ? 'options' : 'parameters'].value) as { name: string, type?: string, requirement?: string, defaultValue?: string, description: string }[]
162
185
  if (!Array.isArray(values) || values.some(value => typeof value.name !== 'string' || typeof value.description !== 'string')) throw new Error(`${name} requires literal names and descriptions`)
163
- return values.map(value => `**\`${value.name}\`**${value.type ? ` — \`${value.type}\`` : ''}${value.requirement ? ` (${value.requirement})` : ''}\n\n${value.defaultValue !== undefined ? `Default: \`${value.defaultValue}\`. ` : ''}${value.description}`).join('\n\n')
186
+ return values.map(value => `**\`${value.name}\`**${value.type ? ` — \`${value.type}\`` : ''}${value.requirement ? ` (${value.requirement})` : ''}\n\n${value.defaultValue !== undefined ? `${t('Default')}: \`${value.defaultValue}\`. ` : ''}${value.description}`).join('\n\n')
164
187
  }
165
188
  if (name === 'DocumentAPIIndex') {
166
189
  const values = await expression(attrs.entries.value) as { name: string, href: string, description: string }[]
@@ -178,7 +201,7 @@ export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [],
178
201
  const rows = await expression(attrs.rows.value) as { syntax: string, css: string }[]
179
202
  if (!Array.isArray(rows) || rows.some(row => typeof row.syntax !== 'string' || typeof row.css !== 'string')) throw new Error('DocumentCodeTable requires literal syntax and CSS rows')
180
203
  const code = (value: string) => `\`${value.replaceAll('|', '\\|')}\``
181
- return `| ${attrs.label ?? 'Syntax'} | CSS |\n| --- | --- |\n${rows.map(row => `| ${code(row.syntax)} | ${code(row.css)} |`).join('\n')}`
204
+ return `| ${attrs.label ?? t('Syntax')} | CSS |\n| --- | --- |\n${rows.map(row => `| ${code(row.syntax)} | ${code(row.css)} |`).join('\n')}`
182
205
  }
183
206
  if (name === 'DocumentFlow') {
184
207
  const steps = await expression(attrs.steps.value) as { title: string, description: string }[]
@@ -196,7 +219,7 @@ export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [],
196
219
  if (typeof attrs.title !== 'string' || typeof source !== 'string') throw new Error('StylesheetExample requires a literal title and source')
197
220
  return stylesheetExampleMarkdown(attrs.title, source)
198
221
  }
199
- if (name === 'FoundationTokens') return selectedSpecimensMarkdown(attrs.namespace, await expression(attrs.keys.value) as string[])
222
+ if (name === 'FoundationTokens') return selectedSpecimensMarkdown(attrs.namespace, await expression(attrs.keys.value) as string[], t)
200
223
  if (name === 'DemoPageViewport') return `[Open the resizable ${attrs.title} example](${attrs.src}). The viewport slider also supports arrow keys.`
201
224
  if (name === 'DemoConfiguredExample') {
202
225
  const configuration = typeof attrs.source === 'string' ? attrs.source : await expression(attrs.source.value) as string
@@ -255,9 +278,20 @@ export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [],
255
278
  if (/^[A-Z]/.test(name)) {
256
279
  const imported = imports[name]
257
280
  const siteRoot = file.slice(0, file.lastIndexOf('/content/'))
258
- const target = imported?.startsWith('~/site/') ? path.join(siteRoot, imported.slice(7))
281
+ let target = imported?.startsWith('~/site/') ? path.join(siteRoot, imported.slice(7))
259
282
  : imported?.startsWith('.') ? path.resolve(path.dirname(file), imported)
260
283
  : path.join(path.dirname(file), 'components', `${name}.mdx`)
284
+ target = target.replace(/\.(tw|ja|ko|cn)\.mdx$/, '.mdx')
285
+ if (target.endsWith('.mdx') && locale !== 'en') {
286
+ const original = await readFile(target, 'utf8').catch(() => '')
287
+ const metadata = parseFrontmatter(original).frontmatter
288
+ if (metadata.draft === true || metadata.disabled === true) throw new Error(`Unpublished source MDX document: ${target}`)
289
+ if (original.trim() && !isSharedMdxSample(target, original)) {
290
+ const translated = target.replace(/\.mdx$/, `.${locale}.mdx`)
291
+ if (await access(translated).then(() => true, () => false)) target = translated
292
+ else throw new Error(`Missing translated MDX include: ${translated}`)
293
+ }
294
+ }
261
295
  if (target.endsWith('.mdx') && await access(target).then(() => true, () => false)) {
262
296
  const result = await extractReferenceMdx(target, rows, [...stack, file], options)
263
297
  examples.push(...result.examples)
@@ -277,7 +311,14 @@ export async function extractReferenceMdx(file: string, rows: SyntaxRow[] = [],
277
311
  const end = child.position.end.offset - node.position.start.offset
278
312
  text = text.slice(0, start) + await render(child) + text.slice(end)
279
313
  }
280
- return node.type === 'heading' ? text.replace(/\\([{}])/g, '$1') : text
314
+ if (node.type === 'heading') {
315
+ text = text.replace(/\\([{}])/g, '$1')
316
+ if (node.depth >= 2 && node.depth <= 6) {
317
+ const id = headingIds.get(node)
318
+ if (!/\s+\{#[\w-]+\}\s*$/.test(text)) text = text.replace(/\s+#+\s*$/, '') + ` {#${id}}`
319
+ }
320
+ }
321
+ return text
281
322
  }
282
323
  const parts: string[] = []
283
324
  for (const child of tree.children) parts.push(await render(child))
@@ -36,6 +36,7 @@
36
36
 
37
37
  import '@master/internal-site/styles/demo.css'
38
38
  import { styleText } from './style'
39
+ import { getSite } from '@master/internal-site/site'
39
40
 
40
41
  /** One example is one tray: head, stage, source, caption. class and style belong to the stage; the chrome never joins the demonstrated layout. */
41
42
  let {
@@ -43,17 +44,18 @@
43
44
  frameClassName, class: className, style, children, ...props
44
45
  }: DemoProps = $props()
45
46
  const head = $derived(Boolean(title || description || controls))
47
+ const { t } = getSite()
46
48
  </script>
47
49
 
48
- {#snippet node(value: DemoNode)}{#if typeof value === 'function'}{@render value()}{:else}{value}{/if}{/snippet}
50
+ {#snippet node(value: DemoNode, translate = false)}{#if typeof value === 'function'}{@render value()}{:else}{translate ? t(value) : value}{/if}{/snippet}
49
51
 
50
52
  <figure {...props} class={['ui-tray', frameClassName]}>{#if head}<figcaption class="ui-tray-head">
51
53
  {#if title || description}<div>
52
- {#if title}<p class="ui-tray-title">{title}</p>{/if}
53
- {#if description}<p class="ui-tray-description">{@render node(description)}</p>{/if}
54
+ {#if title}<p class="ui-tray-title">{t(title)}</p>{/if}
55
+ {#if description}<p class="ui-tray-description">{@render node(description, true)}</p>{/if}
54
56
  </div>{/if}
55
57
  {#if controls}<div class="ui-tray-tools">{@render node(controls)}</div>{/if}
56
58
  </figcaption>{/if}{#if frame === 'none'}{@render children?.()}{:else}<div class={['ui-stage', className]} style={styleText(style)}
57
59
  data-background={background === 'plain' ? undefined : background}
58
60
  data-padding={padding === 'lg' ? undefined : padding}
59
- data-align={align === 'center' ? undefined : align}>{@render children?.()}</div>{/if}{#if source}{@render node(source)}{/if}{#if caption}{#if head}<div class="ui-tray-caption">{@render node(caption)}</div>{:else}<figcaption class="ui-tray-caption">{@render node(caption)}</figcaption>{/if}{/if}</figure>
61
+ data-align={align === 'center' ? undefined : align}>{@render children?.()}</div>{/if}{#if source}{@render node(source)}{/if}{#if caption}{#if head}<div class="ui-tray-caption">{@render node(caption, true)}</div>{:else}<figcaption class="ui-tray-caption">{@render node(caption, true)}</figcaption>{/if}{/if}</figure>
@@ -4,6 +4,7 @@ import '@master/internal-site/styles/system/specimens.css'
4
4
  import IconDownload from '@tabler/icons-svelte-runes/icons/download'
5
5
  import Demo from './Demo.svelte'
6
6
  import Image from '@master/internal-site/components/Image.svelte'
7
+ import { siteOf } from '../astro/site-context'
7
8
 
8
9
  export interface DemoAssetProps {
9
10
  title: string
@@ -23,17 +24,18 @@ type Props = DemoAssetProps
23
24
  * transparent artwork sits on a checkerboard. A demo container keeps the prose media perimeter off the artwork.
24
25
  */
25
26
  const { title, description, src, alt, width, height, format = 'SVG', surface = 'transparent' } = Astro.props
27
+ const { t } = siteOf(Astro)
26
28
  ---
27
29
 
28
30
  <Demo title={title} description={description} background={surface === 'transparent' ? 'checkerboard' : 'plain'} class={surface === 'transparent' ? undefined : surface} data-asset={surface}>
29
31
  <Fragment slot="caption">
30
32
  <span class="flex flex-wrap items-center justify-between gap-sm">
31
33
  <span class="ui-badge" data-size="sm">{format}</span>
32
- <a href={src} download class="ui-button" data-size="sm" aria-label={`Download ${title} (${format})`}>
34
+ <a href={src} download class="ui-button" data-size="sm" aria-label={t('Download {title} ({format})').replace('{title}', t(title)).replace('{format}', format)}>
33
35
  <IconDownload aria-hidden="true" />
34
- Download {format}
36
+ {t('Download {format}').replace('{format}', format)}
35
37
  </a>
36
38
  </span>
37
39
  </Fragment>
38
- <div class="grid (place-items:center) w-full" data-demo-container=""><Image src={src} alt={alt} width={width} height={height} class="block h-auto w-auto max-w-full (max-height:var(--spacing-4xl))" /></div>
40
+ <div class="grid (place-items:center) w-full" data-demo-container=""><Image src={src} alt={t(alt)} width={width} height={height} class="block h-auto w-auto max-w-full (max-height:var(--spacing-4xl))" /></div>
39
41
  </Demo>
@@ -1,11 +1,13 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/system/specimens.css'
3
5
  import type { Snippet } from 'svelte'
4
6
  import type { HTMLAttributes } from 'svelte/elements'
5
7
  import { styleText, type DemoStyle } from '@master/internal-site/demo/style.ts'
6
8
 
7
9
  /** `inline` and `block` take text, or a snippet (a named slot from `.astro` and `.mdx`). */
8
- let { inline = 'Main axis', block = 'Cross axis', children, class: className, style, ...props }: Omit<HTMLAttributes<HTMLDivElement>, 'style'> & {
10
+ let { inline = t('Main axis'), block = t('Cross axis'), children, class: className, style, ...props }: Omit<HTMLAttributes<HTMLDivElement>, 'style'> & {
9
11
  inline?: string | Snippet
10
12
  block?: string | Snippet
11
13
  style?: DemoStyle
@@ -13,7 +15,7 @@
13
15
  } = $props()
14
16
  </script>
15
17
 
16
- {#snippet node(value: string | Snippet)}{#if typeof value === 'function'}{@render value()}{:else}{value}{/if}{/snippet}
18
+ {#snippet node(value: string | Snippet)}{#if typeof value === 'function'}{@render value()}{:else}{t(value)}{/if}{/snippet}
17
19
 
18
20
  <div {...props} class={['ui-axes', className]} style={styleText(style)}>
19
21
  <div class="ui-axes-inline"><span>{@render node(inline)}</span><span class="ui-axes-rule" aria-hidden="true"></span></div>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { siteOf } from '@master/internal-site/astro/site-context.ts'
3
+ const { t } = siteOf(Astro)
2
4
  import '@master/internal-site/styles/system/disclosure.css'
3
5
  import '@master/internal-site/styles/demo.css'
4
6
  import RenderNode from '@master/internal-site/components/RenderNode.astro'
@@ -20,16 +22,16 @@ const { groups } = Astro.props
20
22
  ---
21
23
 
22
24
  <div class="ui-catalog">
23
- <DemoIndex label="Utility recipe categories" groups={[{
24
- title: 'Choose a behavior',
25
- links: groups.map(group => ({ href: `#${group.id}`, label: `${group.title} · ${group.entries.length}` }))
25
+ <DemoIndex label={t('Utility recipe categories')} groups={[{
26
+ title: t('Choose a behavior'),
27
+ links: groups.map(group => ({ href: `#${group.id}`, label: `${t(group.title)} · ${group.entries.length}` }))
26
28
  }]} />
27
29
  {groups.map(group => <section aria-labelledby={group.id}>
28
- <h3 id={group.id}>{group.title}<small>{group.entries.length} recipes</small></h3>
30
+ <h3 id={group.id}>{t(group.title)}<small>{t('{count} recipes').replace('{count}', String(group.entries.length))}</small></h3>
29
31
  {group.entries.map(entry => <details class="ui-disclosure" data-variant="rule">
30
- <summary>{entry.title}<span class="ui-disclosure-meta">{entry.label}</span></summary>
32
+ <summary>{t(entry.title)}<span class="ui-disclosure-meta">{t(entry.label)}</span></summary>
31
33
  <div class="ui-catalog-entry">
32
- {entry.href && <a href={entry.href}>Read related documentation ↗</a>}
34
+ {entry.href && <a href={entry.href}>{t('Read related documentation')} ↗</a>}
33
35
  <RenderNode node={entry.children} />
34
36
  </div>
35
37
  </details>)}
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/interface.css'
3
5
  import '@master/internal-site/styles/system/range.css'
4
6
 
@@ -38,12 +40,12 @@
38
40
  </script>
39
41
 
40
42
  {#snippet controls()}
41
- <label class="ui-label" data-case="as-is" for={id}>Container</label>
43
+ <label class="ui-label" data-case="as-is" for={id}>{t('Container')}</label>
42
44
  <input {id} class="ui-range" type="range" min={minimumWidth} max={maxWidth} step="1" disabled={!ready} {value}
43
45
  style:--value={`${((value - minimumWidth) / range) * 100}%`} style:width="128px" style:max-width="100%"
44
46
  oninput={event => { width = Number(event.currentTarget.value) }} />
45
- <button type="button" class="ui-button" data-size="sm" disabled={!ready} onclick={() => { width = undefined }}>Fit</button>
46
- <output class="ui-label" data-case="as-is">{ready ? `${measured} px · viewport unchanged` : 'Measuring container…'}</output>
47
+ <button type="button" class="ui-button" data-size="sm" disabled={!ready} onclick={() => { width = undefined }}>{t('Fit')}</button>
48
+ <output class="ui-label" data-case="as-is">{ready ? t('{width} px · viewport unchanged').replace('{width}', String(measured)) : t('Measuring container…')}</output>
47
49
  {/snippet}
48
50
 
49
51
  <Demo {...props} {title} {caption} padding="sm" data-ready={ready} {controls}>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/system/segmented.css'
3
5
 
4
6
  import '@master/internal-site/styles/system/specimens.css'
@@ -15,4 +17,4 @@
15
17
  } = $props()
16
18
  </script>
17
19
 
18
- <div {...props} role="group" aria-label={label} class={[variant === 'segmented' ? 'ui-segmented' : 'flex flex-wrap items-center gap-xs', className]} style={styleText(style)}>{@render children?.()}</div>
20
+ <div {...props} role="group" aria-label={t(label)} class={[variant === 'segmented' ? 'ui-segmented' : 'flex flex-wrap items-center gap-xs', className]} style={styleText(style)}>{@render children?.()}</div>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/interface.css'
3
5
  import '@master/internal-site/styles/system/copy.css'
4
6
 
@@ -28,19 +30,19 @@
28
30
  async function copy() {
29
31
  if (busy) return
30
32
  busy = true
31
- report('Copying…', undefined)
33
+ report(t('Copying…'), undefined)
32
34
  try {
33
35
  if (!window.isSecureContext || !navigator.clipboard?.writeText) throw new Error('Clipboard unavailable')
34
36
  await navigator.clipboard.writeText(value)
35
- report(feedback === 'compact' ? 'Copied' : `Copied ${value}`, 'success')
37
+ report(feedback === 'compact' ? t('Copied') : t('Copied {value}').replace('{value}', value), 'success')
36
38
  } catch {
37
- report(feedback === 'compact' ? 'Copy unavailable' : `Copy unavailable: ${value}`, 'error')
39
+ report(feedback === 'compact' ? t('Copy unavailable') : t('Copy unavailable: {value}').replace('{value}', value), 'error')
38
40
  } finally { busy = false }
39
41
  }
40
42
  </script>
41
43
 
42
44
  {#snippet button()}
43
- <button data-size={className ? undefined : 'sm'} {...props} type="button" aria-label={label} class={className ?? 'ui-button'} style={styleText(style)} aria-busy={busy} onclick={copy}>
45
+ <button data-size={className ? undefined : 'sm'} {...props} type="button" aria-label={t(label)} class={className ?? 'ui-button'} style={styleText(style)} aria-busy={busy} onclick={copy}>
44
46
  {#if icon === undefined}<IconCopy aria-hidden="true" />{:else if icon}{@render icon()}{/if}{@render children?.()}
45
47
  </button>
46
48
  {/snippet}
@@ -1,17 +1,19 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/system/copy.css'
3
5
 
4
6
  import type { Snippet } from 'svelte'
5
7
  import { setCopyFeedback, type CopyResult } from '@master/internal-site/demo/copy-feedback.ts'
6
8
 
7
9
  /** One status for a set of copy actions, such as a palette. The buttons must render in this group's Svelte tree. */
8
- let { children, instruction = 'Select a swatch to copy its variable.' }: { children?: Snippet, instruction?: string } = $props()
10
+ let { children, instruction = t('Select a swatch to copy its variable.') }: { children?: Snippet, instruction?: string } = $props()
9
11
  let state = $state<{ instruction: string, message: string, result: CopyResult }>()
10
12
  const current = $derived(state?.instruction === instruction ? state : undefined)
11
13
  setCopyFeedback((message, result) => { state = { instruction, message, result } })
12
14
  </script>
13
15
 
14
16
  <div class="grid gap-sm">
15
- <p class="m(0) ui-copy"><span class="ui-copy-status" role="status" data-result={current?.result}>{current?.message ?? instruction}</span></p>
17
+ <p class="m(0) ui-copy"><span class="ui-copy-status" role="status" data-result={current?.result}>{current?.message ?? t(instruction)}</span></p>
16
18
  {@render children?.()}
17
19
  </div>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { siteOf } from '@master/internal-site/astro/site-context.ts'
3
+ const { t } = siteOf(Astro)
2
4
  import Demo from '@master/internal-site/demo/Demo.svelte'
3
5
  import DemoViewport from '@master/internal-site/demo/DemoViewport.svelte'
4
6
  import { referenceDemoSections } from '@master/internal-site/demo/reference/source.ts'
@@ -22,6 +24,6 @@ const printable = source.classes.some(value => value.includes('@media(print)'))
22
24
  ---
23
25
 
24
26
  <Demo title={source.title} frame="none" caption={scene.caption} data-demo-case={`${page}#${section}`}>
25
- <DemoViewport client:idle={{ timeout: 500 }} title={`${page}: ${source.title}`} document={demoDocument(source, scene)} responsive={scene.responsive ?? responsive}
27
+ <DemoViewport client:idle={{ timeout: 500 }} title={`${page}: ${t(source.title)}`} document={demoDocument(source, scene)} responsive={scene.responsive ?? responsive}
26
28
  theme={scene.theme ?? themed} print={printable} motion={scene.motion} inspect={scene.inspect ?? (section === 'apply-conditionally' ? [page] : undefined)} height={scene.height} maxWidth={scene.maxWidth} sizing={scene.sizing} />
27
29
  </Demo>
@@ -2,6 +2,8 @@
2
2
  import '@master/internal-site/styles/system/inline-code.css'
3
3
 
4
4
  import '@master/internal-site/styles/system/specimens.css'
5
+ import { getSite } from '@master/internal-site/site'
6
+ const { t } = getSite()
5
7
  import { onMount } from 'svelte'
6
8
 
7
9
  /** Reports syntax support only; the example still owns its native CSS behavior. */
@@ -15,6 +17,6 @@
15
17
  <p class="flex flex-wrap items-center gap-xs m(0)" data-supported={supported ?? 'unknown'}>
16
18
  <code class="ui-inline-code">{condition}</code>
17
19
  <span class="ui-badge" data-variant="dot" data-tone={supported ? 'green' : undefined}>
18
- {supported === null ? 'Checking…' : supported ? 'Supported' : 'Not supported'}
20
+ {supported === null ? t('Checking…') : supported ? t('Supported') : t('Not supported')}
19
21
  </span>
20
22
  </p>
@@ -2,9 +2,11 @@
2
2
  import '@master/internal-site/styles/system/disclosure.css'
3
3
 
4
4
  import type { Snippet } from 'svelte'
5
+ import { getSite } from '@master/internal-site/site'
5
6
 
6
7
  /** The complete generated stylesheet stays one native, closed disclosure under the source. */
7
8
  let { children }: { children?: Snippet } = $props()
9
+ const { t } = getSite()
8
10
  </script>
9
11
 
10
- <details class="ui-disclosure"><summary>Generated CSS</summary>{@render children?.()}</details>
12
+ <details class="ui-disclosure"><summary>{t('Generated CSS')}</summary>{@render children?.()}</details>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { siteOf } from '@master/internal-site/astro/site-context.ts'
3
+ const { t } = siteOf(Astro)
2
4
  import '@master/internal-site/styles/interface.css'
3
5
  import '@master/internal-site/styles/system/specimens.css'
4
6
 
@@ -16,9 +18,9 @@ interface Props {
16
18
  const { label, groups } = Astro.props
17
19
  ---
18
20
 
19
- <nav class="grid gap-md" aria-label={label}>
21
+ <nav class="grid gap-md" aria-label={t(label)}>
20
22
  {groups.map(group => <div class="grid gap-xs">
21
- <p class="m(0) ui-label">{group.title}</p>
22
- <ul class="flex flex-wrap gap-2xs m(0) p(0) (list-style:none)">{group.links.map(link => <li><a class="ui-button" data-size="sm" href={link.href}>{link.label}</a></li>)}</ul>
23
+ <p class="m(0) ui-label">{t(group.title)}</p>
24
+ <ul class="flex flex-wrap gap-2xs m(0) p(0) (list-style:none)">{group.links.map(link => <li><a class="ui-button" data-size="sm" href={link.href}>{t(link.label)}</a></li>)}</ul>
23
25
  </div>)}
24
26
  </nav>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/system/specimens.css'
3
5
  import type { HTMLAttributes } from 'svelte/elements'
4
6
  import { styleText, type DemoStyle } from '@master/internal-site/demo/style.ts'
@@ -10,6 +12,6 @@
10
12
  } = $props()
11
13
  </script>
12
14
 
13
- <ul {...props} class={['ui-legend', className]} style={styleText(style)} aria-label="Legend">
15
+ <ul {...props} class={['ui-legend', className]} style={styleText(style)} aria-label={t('Legend')}>
14
16
  {#each items as { label, tone: value } (label)}<li><i data-tone={tone(value)} aria-hidden="true"></i>{label}</li>{/each}
15
17
  </ul>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/system/specimens.css'
3
5
  import type { Snippet } from 'svelte'
4
6
 
@@ -20,8 +22,8 @@
20
22
 
21
23
  <div class="ui-measure">
22
24
  <div class="ui-measure-head">
23
- <span>{label}</span>
24
- <output aria-label={`${label} dimensions`}>{size ? `${size.width} × ${size.height} px` : 'Measuring…'}</output>
25
+ <span>{t(label)}</span>
26
+ <output aria-label={t('{label} dimensions').replace('{label}', t(label))}>{size ? `${size.width} × ${size.height} px` : t('Measuring…')}</output>
25
27
  </div>
26
28
  <div class="ui-measure-rule" aria-hidden="true"></div>
27
29
  <div bind:this={element}>{@render children?.()}</div>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/system/specimens.css'
3
5
  import type { ClassValue } from 'svelte/elements'
4
6
  import type { SVGAttributes } from 'svelte/elements'
@@ -27,7 +29,7 @@
27
29
  {@const imageProps: SiteImageProps = { width: 320, height: 200, ...(props as Omit<ImageProps, 'src' | 'alt' | 'style'>), style: styleText(style), src, alt: alt! }}
28
30
  <Image {...imageProps} />
29
31
  {:else}
30
- <svg viewBox="0 0 320 200" role="img" aria-label="Sun above two mountain ridges" {...props as SVGAttributes<SVGSVGElement>}
32
+ <svg viewBox="0 0 320 200" role="img" aria-label={t('Sun above two mountain ridges')} {...props as SVGAttributes<SVGSVGElement>}
31
33
  style={[`color:var(--ui-tone-${value})`, styleText(style)].filter(Boolean).join(';')}>
32
34
  <rect width="320" height="200" fill="var(--ui-surface-sheet)" />
33
35
  <path d="M0 175V158l92-93 113 110H0Z" fill="currentColor" opacity=".1" />
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/interface.css'
3
5
  import type { Snippet } from 'svelte'
4
6
  import { onMount } from 'svelte'
@@ -32,11 +34,11 @@
32
34
 
33
35
  <div class="grid gap-md w-full">
34
36
  <div bind:this={element} data-playback={playing ? 'playing' : 'paused'}>{@render children?.()}</div>
35
- <div class="ui-player" role="group" aria-label="Animation controls" data-state={playing ? 'playing' : 'paused'}>
37
+ <div class="ui-player" role="group" aria-label={t('Animation controls')} data-state={playing ? 'playing' : 'paused'}>
36
38
  <button type="button" class="ui-button" data-size="sm" onclick={() => control(playing ? 'pause' : 'play')}>
37
- {#if playing}<IconPlayerPause aria-hidden="true" />{:else}<IconPlayerPlay aria-hidden="true" />{/if}{playing ? 'Pause' : 'Play'}
39
+ {#if playing}<IconPlayerPause aria-hidden="true" />{:else}<IconPlayerPlay aria-hidden="true" />{/if}{playing ? t('Pause') : t('Play')}
38
40
  </button>
39
- <button type="button" class="ui-button" data-size="sm" data-variant="quiet" onclick={() => control('restart')}><IconRefresh aria-hidden="true" />Replay</button>
41
+ <button type="button" class="ui-button" data-size="sm" data-variant="quiet" onclick={() => control('restart')}><IconRefresh aria-hidden="true" />{t('Replay')}</button>
40
42
  <span class="ui-player-time"><output>{playing ? 'Playing' : 'Paused'}</output></span>
41
43
  </div>
42
44
  </div>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { getSite } from '@master/internal-site/site'
3
+ const { t } = getSite()
2
4
  import '@master/internal-site/styles/system/specimens.css'
3
5
  import DemoCopyButton from '@master/internal-site/demo/DemoCopyButton.svelte'
4
6
  import DemoCopyGroup from '@master/internal-site/demo/DemoCopyGroup.svelte'
@@ -8,8 +10,8 @@
8
10
  </script>
9
11
 
10
12
  <div class="w-full"><DemoCopyGroup><div class="ui-palette">
11
- {#each groups as { family, colors } (family)}<div class="ui-palette-row" role="group" aria-label={`${family} palette`}>
13
+ {#each groups as { family, colors } (family)}<div class="ui-palette-row" role="group" aria-label={t('{family} palette').replace('{family}', family)}>
12
14
  <span>{family}</span>
13
- {#each colors as { name, value } (name)}<DemoCopyButton value={`var(--${name})`} label={`Copy ${name}`} icon={null} class="ui-palette-chip" style={`background-color:${value}`} title={`${name}: ${value}`} />{/each}
15
+ {#each colors as { name, value } (name)}<DemoCopyButton value={`var(--${name})`} label={t('Copy {name}').replace('{name}', name)} icon={null} class="ui-palette-chip" style={`background-color:${value}`} title={`${name}: ${value}`} />{/each}
14
16
  </div>{/each}
15
17
  </div></DemoCopyGroup></div>
@@ -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/compare.css'
3
5
  import Demo from '@master/internal-site/demo/Demo.svelte'
4
6
  import DemoViewport from '@master/internal-site/demo/DemoViewport.svelte'
@@ -18,9 +20,9 @@ const { html, css, height = 320, sizing = 'viewport', caption } = Astro.props
18
20
  const document = (styles: string) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>${styles.replaceAll('</style', '<\\/style')}</style></head><body>${html}</body></html>`
19
21
  ---
20
22
 
21
- <Demo title="Browser defaults and author CSS" description="The same HTML in two documents" frame="none" caption={caption}>
23
+ <Demo title={t("Browser defaults and author CSS")} description={t("The same HTML in two documents")} frame="none" caption={caption}>
22
24
  <div class="ui-compare">
23
- <figure><figcaption>Browser defaults</figcaption><DemoViewport client:idle={{ timeout: 500 }} title="Browser defaults" document={document('')} height={height} sizing={sizing} /></figure>
24
- <figure><figcaption>CSS applied</figcaption><DemoViewport client:idle={{ timeout: 500 }} title="CSS applied" document={document(css)} height={height} sizing={sizing} /></figure>
25
+ <figure><figcaption>{t("Browser defaults")}</figcaption><DemoViewport client:idle={{ timeout: 500 }} title={t("Browser defaults")} document={document('')} height={height} sizing={sizing} /></figure>
26
+ <figure><figcaption>{t("CSS applied")}</figcaption><DemoViewport client:idle={{ timeout: 500 }} title={t("CSS applied")} document={document(css)} height={height} sizing={sizing} /></figure>
25
27
  </div>
26
28
  </Demo>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { siteOf } from '@master/internal-site/astro/site-context.ts'
3
+ const { t } = siteOf(Astro)
2
4
  import '@master/internal-site/styles/system/compare.css'
3
5
  import Demo from '@master/internal-site/demo/Demo.svelte'
4
6
  import DemoViewport from '@master/internal-site/demo/DemoViewport.svelte'
@@ -24,8 +26,8 @@ const document = (mode: 'light' | 'dark') => demoDocument({ page: 'foundations',
24
26
  <Demo title={title} frame="none" caption={caption} data-foundation-paint={name}>
25
27
  <div class="ui-compare">
26
28
  {(['light', 'dark'] as const).map(mode => <figure data-theme-specimen={mode}>
27
- <figcaption>{mode === 'light' ? 'Light' : 'Dark'}</figcaption>
28
- <DemoViewport client:idle={{ timeout: 500 }} title={`${title} · ${mode}`} document={document(mode)} initialTheme={mode} print={print} height={height} sizing="content" />
29
+ <figcaption>{mode === 'light' ? t('Light') : t('Dark')}</figcaption>
30
+ <DemoViewport client:idle={{ timeout: 500 }} title={`${t(title)} · ${mode === 'light' ? t('Light') : t('Dark')}`} document={document(mode)} initialTheme={mode} print={print} height={height} sizing="content" />
29
31
  </figure>)}
30
32
  </div>
31
33
  </Demo>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { siteOf } from '@master/internal-site/astro/site-context.ts'
3
+ const { t } = siteOf(Astro)
2
4
  import '@master/internal-site/styles/system/table.css'
3
5
  import InlineCode from '@master/internal-site/components/InlineCode.astro'
4
6
  import RenderNode from '@master/internal-site/components/RenderNode.astro'
@@ -22,8 +24,8 @@ const { rows, descriptionTitle = 'Role' } = Astro.props
22
24
  ---
23
25
 
24
26
  {/* Narrow token inventories need native keyboard scrolling. */}
25
- <div class="ui-table" role="region" aria-label={`${descriptionTitle} token table`} tabindex="0"><table>
26
- <thead><tr><th scope="col">Token / class</th><th scope="col" class="(min-width:12rem)">{descriptionTitle}</th></tr></thead>
27
+ <div class="ui-table" role="region" aria-label={t('{label} token table').replace('{label}', t(descriptionTitle))} tabindex="0"><table>
28
+ <thead><tr><th scope="col">{t('Token / class')}</th><th scope="col" class="(min-width:12rem)">{t(descriptionTitle)}</th></tr></thead>
27
29
  <tbody>{rows.map(({ token, utilities, value, description, preview }) => <tr>
28
30
  <td>
29
31
  <div class="whitespace-nowrap">{preview != null && <RenderNode node={preview} />}<code>{token}</code></div>
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { siteOf } from '@master/internal-site/astro/site-context'
3
+ const { t } = siteOf(Astro)
2
4
  import Demo from '@master/internal-site/demo/Demo.svelte'
3
5
  import DemoViewport from '@master/internal-site/demo/DemoViewport.svelte'
4
6
 
@@ -14,5 +16,5 @@ const title = articles ? 'From collection to article' : 'A change of view'
14
16
 
15
17
  <Demo title={title} frame="none" data-view-transition={example}
16
18
  caption={articles ? 'Open an article, then return to its card. Each image and title keeps a unique snapshot name. Keyboard focus follows the view change.' : 'Choose a view. The panel and heading use named snapshots; unsupported browsers and reduced motion use the same immediate update.'}>
17
- <DemoViewport client:idle={{ timeout: 500 }} title={title} src={origin + (articles ? '/examples/article-transitions' : '/examples/view-transitions')} height={articles ? 640 : 340} sizing="viewport" />
19
+ <DemoViewport client:idle={{ timeout: 500 }} title={t(title)} src={origin + (articles ? '/examples/article-transitions' : '/examples/view-transitions')} height={articles ? 640 : 340} sizing="viewport" />
18
20
  </Demo>