@master/internal-site 0.1.3 → 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 +37 -0
- package/dist/astro.mjs +92 -41
- 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 +9 -5
- package/src/astro/localized-mdx.ts +45 -0
- package/src/astro/routes.template.ts +3 -0
- package/src/astro/site-context.ts +10 -2
- package/src/benchmarks/BenchmarkChart.astro +4 -1
- package/src/benchmarks/BenchmarkMetricTable.astro +1 -1
- package/src/benchmarks/BenchmarkStackedBars.astro +2 -2
- package/src/benchmarks/BenchmarkStatus.astro +3 -1
- package/src/benchmarks/localization.ts +22 -0
- package/src/blog/article/BlogArticleEnd.astro +6 -9
- package/src/blog/article/BlogArticleMasthead.astro +5 -8
- package/src/config.ts +3 -0
- package/src/css/foundation-data/specimen-content.ts +2 -2
- package/src/css/markdown.ts +52 -11
- package/src/demo/Demo.svelte +6 -4
- package/src/demo/DemoAsset.astro +5 -3
- package/src/demo/DemoAxes.svelte +4 -2
- package/src/demo/DemoCatalog.astro +8 -6
- package/src/demo/DemoContainer.svelte +5 -3
- package/src/demo/DemoControls.svelte +3 -1
- package/src/demo/DemoCopyButton.svelte +6 -4
- package/src/demo/DemoCopyGroup.svelte +4 -2
- package/src/demo/DemoExample.astro +3 -1
- package/src/demo/DemoFeatureSupport.svelte +3 -1
- package/src/demo/DemoGeneratedCSS.svelte +3 -1
- package/src/demo/DemoIndex.astro +5 -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 +5 -3
- package/src/demo/DemoPaletteGroups.svelte +4 -2
- package/src/demo/DemoStyleComparison.astro +5 -3
- package/src/demo/DemoThemeComparison.astro +4 -2
- package/src/demo/DemoTokenTable.astro +4 -2
- package/src/demo/DemoViewTransition.astro +3 -1
- package/src/demo/DemoViewport.svelte +13 -11
- package/src/demo/DemoWaterfall.astro +3 -1
- package/src/demo/ProjectStyleExample.astro +4 -1
- package/src/demo/foundations/FoundationDialog.astro +5 -2
- package/src/demo/foundations/FoundationTokensView.astro +16 -5
- package/src/demo/foundations/Palette.svelte +9 -7
- package/src/demo/foundations/dialog-copy.d.ts +1 -0
- package/src/demo/foundations/dialog-copy.js +5 -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 +3 -3
- package/src/docs-shell/components/CodeCopyButton.astro +3 -1
- package/src/docs-shell/components/CodeTabBar.astro +5 -3
- package/src/docs-shell/components/DocHeader.svelte +3 -4
- package/src/docs-shell/components/DocHeading.astro +3 -1
- package/src/docs-shell/components/DocMenuButton.svelte +4 -5
- package/src/docs-shell/components/DocSidebar.svelte +1 -2
- package/src/docs-shell/components/DocumentCodeTable.astro +3 -1
- package/src/docs-shell/components/DocumentIllustration.astro +3 -1
- package/src/docs-shell/components/DocumentKeyList.astro +5 -3
- package/src/docs-shell/components/DocumentNamespaceTable.astro +4 -2
- package/src/docs-shell/components/DocumentOptions.astro +3 -1
- package/src/docs-shell/components/DocumentationIndex.svelte +6 -7
- package/src/docs-shell/components/GeneratedCSS.astro +3 -1
- package/src/docs-shell/components/Link.svelte +3 -2
- package/src/docs-shell/components/NotFound.astro +3 -4
- package/src/docs-shell/components/PageNavs.astro +2 -3
- package/src/docs-shell/components/SearchDialog.svelte +13 -16
- package/src/docs-shell/components/TimeAgo.svelte +6 -6
- 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/slug-and-toc.js +5 -7
- package/src/docs-shell/scripts/code.ts +5 -2
- package/src/docs-shell/site.ts +9 -1
- 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 +2 -0
- 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/home/hero.css +5 -0
- package/src/layouts/Document.astro +1 -1
- 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 +14 -4
- package/src/sections/guide.astro +3 -1
- package/src/virtual.d.ts +5 -1
- package/src/blog/article/language.ts +0 -29
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '@master/internal-site/styles/system/disclosure.css'
|
|
3
|
+
import { siteOf } from '../../astro/site-context'
|
|
4
|
+
const { t } = siteOf(Astro)
|
|
3
5
|
|
|
4
6
|
interface Props {
|
|
5
7
|
keys: string[]
|
|
@@ -11,12 +13,12 @@ const { keys, previewCount = keys.length } = Astro.props
|
|
|
11
13
|
const lists = [keys.slice(0, previewCount), keys.slice(previewCount)]
|
|
12
14
|
---
|
|
13
15
|
|
|
14
|
-
<ul aria-label=
|
|
16
|
+
<ul aria-label={t('Utility keys')}>
|
|
15
17
|
{lists[0].map(key => <li><code>{key}</code></li>)}
|
|
16
18
|
</ul>
|
|
17
19
|
{keys.length > previewCount && <details class="ui-disclosure">
|
|
18
|
-
<summary>Show {keys.length - previewCount}
|
|
19
|
-
<ul aria-label=
|
|
20
|
+
<summary>{t('Show {count} more keys').replace('{count}', String(keys.length - previewCount))}</summary>
|
|
21
|
+
<ul aria-label={t('Utility keys')}>
|
|
20
22
|
{lists[1].map(key => <li><code>{key}</code></li>)}
|
|
21
23
|
</ul>
|
|
22
24
|
</details>}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '@master/internal-site/styles/system/table.css'
|
|
3
3
|
import DocumentKeyList from '@master/internal-site/components/DocumentKeyList.astro'
|
|
4
|
+
import { siteOf } from '../../astro/site-context'
|
|
5
|
+
const { t } = siteOf(Astro)
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
6
8
|
rows: { namespace: string, consumers: string[] }[]
|
|
@@ -11,8 +13,8 @@ const { rows } = Astro.props
|
|
|
11
13
|
---
|
|
12
14
|
|
|
13
15
|
{/* The table's narrow overflow region must be focusable for native keyboard scrolling. */}
|
|
14
|
-
<div class="ui-table" data-variant="lookup" role="region" aria-label=
|
|
15
|
-
<thead><tr><th scope="col">Namespace</th><th scope="col">Consumers</th></tr></thead>
|
|
16
|
+
<div class="ui-table" data-variant="lookup" role="region" aria-label={t('Namespace consumers')} tabindex="0"><table>
|
|
17
|
+
<thead><tr><th scope="col">{t('Namespace')}</th><th scope="col">{t('Consumers')}</th></tr></thead>
|
|
16
18
|
<tbody>{rows.map(({ namespace, consumers }) => <tr>
|
|
17
19
|
<th scope="row"><code>{namespace}-*</code></th>
|
|
18
20
|
<td><DocumentKeyList keys={consumers} previewCount={6} /></td>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { siteOf } from '../../astro/site-context'
|
|
3
|
+
const { t } = siteOf(Astro)
|
|
2
4
|
import RenderNode from '@master/internal-site/components/RenderNode.astro'
|
|
3
5
|
import DocumentOptionEntry from '@master/internal-site/components/DocumentOptionEntry.astro'
|
|
4
6
|
import DocumentOptionList from '@master/internal-site/components/DocumentOptionList.astro'
|
|
@@ -16,7 +18,7 @@ const { label, options } = Astro.props
|
|
|
16
18
|
|
|
17
19
|
<DocumentOptionList label={label}>
|
|
18
20
|
{options.map(option => <DocumentOptionEntry name={option.name}>
|
|
19
|
-
{option.defaultValue !== undefined && <small>Default <code>{option.defaultValue}</code></small>}
|
|
21
|
+
{option.defaultValue !== undefined && <small>{t('Default')} <code>{option.defaultValue}</code></small>}
|
|
20
22
|
<p><RenderNode node={option.description} /></p>
|
|
21
23
|
</DocumentOptionEntry>)}
|
|
22
24
|
</DocumentOptionList>
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
} = $props()
|
|
27
27
|
|
|
28
28
|
const { locale, t } = getSite()
|
|
29
|
-
const tw = locale === 'tw'
|
|
30
29
|
let alphabetical = $state(false)
|
|
31
30
|
const entries = $derived(sections.flatMap(section => section.groups.flatMap(group => group.entries)))
|
|
32
31
|
const letters = $derived(Map.groupBy([...entries].sort((a, b) => a.title.localeCompare(b.title)), entry => /^[a-z]/i.test(entry.title) ? entry.title[0].toUpperCase() : '#'))
|
|
@@ -73,7 +72,7 @@
|
|
|
73
72
|
|
|
74
73
|
<div class="documentation-index">
|
|
75
74
|
<!-- A contents list, not a second catalog: each section by its number, name and size; the sibling index closes it. -->
|
|
76
|
-
<nav class="documentation-index-directory" aria-label={
|
|
75
|
+
<nav class="documentation-index-directory" aria-label={t('{name} sections').replace('{name}', t(name === 'guide' ? 'Guide' : 'Reference'))}>
|
|
77
76
|
{#each sections as section, index (section.id)}
|
|
78
77
|
<a href={`#${section.id}`} onclick={() => alphabetical = false}>
|
|
79
78
|
<small>{ordinal(index)}</small>
|
|
@@ -89,9 +88,9 @@
|
|
|
89
88
|
</nav>
|
|
90
89
|
|
|
91
90
|
<div class="documentation-index-toolbar">
|
|
92
|
-
<p>{
|
|
93
|
-
<div class="ui-segmented" data-variant="underline" role="group" aria-label={
|
|
94
|
-
<button type="button" aria-pressed={!alphabetical} aria-controls={panelId('category')} onclick={() => alphabetical = false}>{
|
|
91
|
+
<p>{t(name === 'guide' ? 'All guides' : 'All entries')} <span class="documentation-index-count">{entries.length}</span></p>
|
|
92
|
+
<div class="ui-segmented" data-variant="underline" role="group" aria-label={t('Index view')}>
|
|
93
|
+
<button type="button" aria-pressed={!alphabetical} aria-controls={panelId('category')} onclick={() => alphabetical = false}>{t('By category')}</button>
|
|
95
94
|
<button type="button" aria-pressed={alphabetical} aria-controls={panelId('alphabetical')} onclick={() => alphabetical = true}>A–Z</button>
|
|
96
95
|
</div>
|
|
97
96
|
</div>
|
|
@@ -104,7 +103,7 @@
|
|
|
104
103
|
<h2 id={section.id}>{section.title} <span class="documentation-index-count">{count(section)}</span></h2>
|
|
105
104
|
<p>{section.description}</p>
|
|
106
105
|
{#if section.groups.length > 1}
|
|
107
|
-
<nav class="documentation-index-groups" aria-label={
|
|
106
|
+
<nav class="documentation-index-groups" aria-label={t('{name} categories').replace('{name}', section.title)}>
|
|
108
107
|
{#each section.groups as group, index (groupId(section, index))}
|
|
109
108
|
<a href={`#${groupId(section, index)}`}>{group.title}</a>
|
|
110
109
|
{/each}
|
|
@@ -128,7 +127,7 @@
|
|
|
128
127
|
</div>
|
|
129
128
|
|
|
130
129
|
<div id={panelId('alphabetical')} hidden={!alphabetical}>
|
|
131
|
-
<nav class="documentation-index-letters" aria-label={
|
|
130
|
+
<nav class="documentation-index-letters" aria-label={t('Jump to a letter')}>
|
|
132
131
|
{#each [...letters.keys()] as letter (letter)}
|
|
133
132
|
<a href={`#${letterId(letter)}`}>{letter}</a>
|
|
134
133
|
{/each}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
import '@master/internal-site/styles/system/disclosure.css'
|
|
3
|
+
import { siteOf } from '../../astro/site-context'
|
|
4
|
+
const { t } = siteOf(Astro)
|
|
3
5
|
/** A closed native disclosure keeps the complete output in the server HTML. */
|
|
4
6
|
---
|
|
5
7
|
|
|
6
8
|
<details class="ui-disclosure" data-generated="">
|
|
7
|
-
<summary>Generated CSS</summary>
|
|
9
|
+
<summary>{t('Generated CSS')}</summary>
|
|
8
10
|
<slot />
|
|
9
11
|
</details>
|
|
@@ -58,12 +58,13 @@
|
|
|
58
58
|
const isHash = $derived(props.href?.startsWith('#'))
|
|
59
59
|
const external = $derived(props.href?.startsWith('http'))
|
|
60
60
|
const href: string = $derived.by(() => {
|
|
61
|
-
if (!props.href
|
|
61
|
+
if (!props.href) return ''
|
|
62
62
|
return localizePathname(props.href, {
|
|
63
63
|
defaultLocale: i18n.defaultLocale,
|
|
64
64
|
locale,
|
|
65
65
|
locales: i18n.locales,
|
|
66
|
-
localizablePathnameRoots: i18n.localizablePathnameRoots
|
|
66
|
+
localizablePathnameRoots: i18n.localizablePathnameRoots,
|
|
67
|
+
localizableOrigins: i18n.localizableOrigins
|
|
67
68
|
}) || ''
|
|
68
69
|
})
|
|
69
70
|
const canonicalHref = $derived(canonicalizeDefaultLocalePathname(href, i18n.defaultLocale))
|
|
@@ -8,8 +8,7 @@ import SearchButton from './SearchButton.svelte'
|
|
|
8
8
|
import Link from './Link.svelte'
|
|
9
9
|
import config from 'virtual:master-site/config'
|
|
10
10
|
|
|
11
|
-
const { t:
|
|
12
|
-
const tw = locale === 'tw'
|
|
11
|
+
const { t: $ } = siteOf(Astro)
|
|
13
12
|
const guide = Boolean(config.sections.guide)
|
|
14
13
|
---
|
|
15
14
|
|
|
@@ -17,8 +16,8 @@ const guide = Boolean(config.sections.guide)
|
|
|
17
16
|
<div class="ui-pagehead">
|
|
18
17
|
<span class="ui-pagehead-kicker">404</span>
|
|
19
18
|
<h1>{$('This page does not exist')}</h1>
|
|
20
|
-
<p>{guide ? (
|
|
19
|
+
<p>{guide ? $('Try searching the documentation or return to the Guide.') : $('Try searching the site or return to the homepage.')}</p>
|
|
21
20
|
</div>
|
|
22
21
|
<SearchButton client:load class="ui-search-trigger" />
|
|
23
|
-
<Link href={guide ? "/guide" : "/"} class="ui-button" data-variant="quiet">{guide ? (
|
|
22
|
+
<Link href={guide ? "/guide" : "/"} class="ui-button" data-variant="quiet">{guide ? $('Browse the Guide') : $('Back to home')}<span aria-hidden="true">→</span></Link>
|
|
24
23
|
</main>
|
|
@@ -13,7 +13,6 @@ interface Props {
|
|
|
13
13
|
/** The previous and next documents in the section's order, as a `ui-pager`. */
|
|
14
14
|
const { pageCategories, metadata, locale, dictionaries } = Astro.props
|
|
15
15
|
const $ = await createTranslation(locale, dictionaries)
|
|
16
|
-
const tw = locale === 'tw'
|
|
17
16
|
const unit = metadata?.pathname?.split('/')?.[1]
|
|
18
17
|
const pages = (pageCategories?.flatMap((category: any) => category.pages) || [])
|
|
19
18
|
.filter(({ pathname }: any) => pathname.split('/').length <= 3 && unit)
|
|
@@ -26,13 +25,13 @@ const navs = [
|
|
|
26
25
|
].filter(Boolean).map(({ direction, definedMetadata }: any) => ({
|
|
27
26
|
direction,
|
|
28
27
|
href: definedMetadata.pathname,
|
|
29
|
-
label: direction === 'prev' ? (
|
|
28
|
+
label: direction === 'prev' ? $('Previous') : $('Next'),
|
|
30
29
|
title: $(definedMetadata.title.absolute || definedMetadata.title)
|
|
31
30
|
}))
|
|
32
31
|
---
|
|
33
32
|
|
|
34
33
|
{navs.length > 0 && (
|
|
35
|
-
<nav class="ui-pager" aria-label={
|
|
34
|
+
<nav class="ui-pager" aria-label={$('Previous and next documents')}>
|
|
36
35
|
{navs.map(({ direction, href, label, title }) => <Link {href} rel={direction} aria-label={`${label}: ${title}`}>
|
|
37
36
|
<small>{label}</small>
|
|
38
37
|
<b>{title}</b>
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
|
|
39
39
|
/** The page's single search dialog; search triggers in other islands open it by event (`contexts/search`). */
|
|
40
40
|
const { t, locale } = getSite()
|
|
41
|
-
const tw = locale === 'tw'
|
|
42
41
|
let opened = $state(false)
|
|
43
42
|
let query = $state('')
|
|
44
43
|
let shortcutModifier = $state('⌘')
|
|
@@ -130,10 +129,8 @@
|
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
? { utility: 'Utility', rule: '規則', tokens: 'Tokens', directive: '指令', tool: '工具', package: 'API' }
|
|
136
|
-
: { utility: 'Utility', rule: 'Rule', tokens: 'Tokens', directive: 'Directive', tool: 'Tool', package: 'API' }
|
|
132
|
+
const kinds: Record<string, string> = { utility: 'Utility', rule: 'Rule', tokens: 'Tokens', directive: 'Directive', tool: 'Tool', package: 'API' }
|
|
133
|
+
|
|
137
134
|
</script>
|
|
138
135
|
|
|
139
136
|
<svelte:window onkeydown={shortcut} />
|
|
@@ -147,33 +144,33 @@
|
|
|
147
144
|
{/snippet}
|
|
148
145
|
|
|
149
146
|
{#if opened}
|
|
150
|
-
<dialog bind:this={dialog} {@attach portal} {@attach closeOnBackdropClick} class="ui-dialog" data-variant="search" aria-label={
|
|
147
|
+
<dialog bind:this={dialog} {@attach portal} {@attach closeOnBackdropClick} class="ui-dialog" data-variant="search" aria-label={t('Search documentation')} oncancel={close}>
|
|
151
148
|
<div class="ui-dialog-search">
|
|
152
149
|
<IconSearch size={18} stroke={1.5} aria-hidden="true" />
|
|
153
|
-
<input bind:this={input} type="search" role="combobox" aria-autocomplete="list" aria-expanded={Boolean(query)} value={query} autocomplete="off" spellcheck={false} placeholder={
|
|
150
|
+
<input bind:this={input} type="search" role="combobox" aria-autocomplete="list" aria-expanded={Boolean(query)} value={query} autocomplete="off" spellcheck={false} placeholder={t('Search documentation…')} aria-label={t('Search documentation')} aria-controls="documentation-search-results" aria-activedescendant={results[selected] ? `documentation-result-${selected}` : undefined}
|
|
154
151
|
oninput={event => selectQuery(event.currentTarget.value)}
|
|
155
152
|
{onkeydown} />
|
|
156
|
-
{#if query}<button type="button" class="ui-button" data-variant="quiet" data-shape="icon" data-size="sm" onclick={() => selectQuery('')} aria-label={
|
|
157
|
-
<button type="button" class="ui-button ui-dialog-close" data-variant="quiet" data-size="sm" onclick={close} aria-label={
|
|
153
|
+
{#if query}<button type="button" class="ui-button" data-variant="quiet" data-shape="icon" data-size="sm" onclick={() => selectQuery('')} aria-label={t('Clear search')}><IconX size={16} stroke={1.5} aria-hidden="true" /></button>{/if}
|
|
154
|
+
<button type="button" class="ui-button ui-dialog-close" data-variant="quiet" data-size="sm" onclick={close} aria-label={t('Close search')}><span class="ui-kbd">esc</span><span>{t('Cancel')}</span></button>
|
|
158
155
|
</div>
|
|
159
156
|
<div class="ui-dialog-body" bind:this={scroll}>
|
|
160
|
-
<p class="ui-dialog-status" role="status">{loading ? (
|
|
161
|
-
{#if error}<div class="ui-dialog-note"><p>{
|
|
157
|
+
<p class="ui-dialog-status" role="status">{loading ? (t('Loading documentation…')) : error ? (t('Search could not load.')) : query ? t(results.length === 40 ? 'Top {count} results' : '{count} results').replace('{count}', String(results.length)) : (t('Find a name, alias or CSS property'))}</p>
|
|
158
|
+
{#if error}<div class="ui-dialog-note"><p>{t('Check your connection and try again.')}</p><button type="button" class="ui-button" data-size="sm" onclick={() => { retry += 1; input?.focus() }}>{t('Try again')}</button></div>{/if}
|
|
162
159
|
{#if !query && !error && !loading}<div class="ui-dialog-note">
|
|
163
|
-
<p>{
|
|
160
|
+
<p>{t('Try searching')}</p>
|
|
164
161
|
{#each ['padding', 'padding-inline-start:', 'fg-red:hover@sm'] as value (value)}<button type="button" class="ui-button" data-size="sm" onclick={() => selectQuery(value)}><code>{value}</code><IconCornerDownLeft size={14} stroke={1.5} aria-hidden="true" /></button>{/each}
|
|
165
162
|
</div>{/if}
|
|
166
|
-
{#if query && !results.length && !error && !loading}<div class="ui-dialog-note" data-align="center"><IconSearch size={24} stroke={1.25} aria-hidden="true" /><b>{
|
|
167
|
-
<ol id="documentation-search-results" class="ui-results" role="listbox" aria-label={
|
|
163
|
+
{#if query && !results.length && !error && !loading}<div class="ui-dialog-note" data-align="center"><IconSearch size={24} stroke={1.25} aria-hidden="true" /><b>{t('No matching documents')}</b><p>{t('Try a property name, a token name or a different keyword.')}</p></div>{/if}
|
|
164
|
+
<ol id="documentation-search-results" class="ui-results" role="listbox" aria-label={t('Search results')} aria-busy={loading}>
|
|
168
165
|
{#if !error}{#each results as result, index (result.page.url)}<li role="presentation">
|
|
169
166
|
<Link id={`documentation-result-${index}`} role="option" tabindex={-1} aria-selected={selected === index} href={result.href} onclick={close} onpointermove={() => { selected = index }}>
|
|
170
|
-
<small>{t(result.page.category ?? '')} · {kinds[result.page.kind ?? ''] ?? (result.page.url.includes('/guide') ? 'Guide' : 'Page')} · {
|
|
167
|
+
<small>{t(result.page.category ?? '')} · {t(kinds[result.page.kind ?? ''] ?? (result.page.url.includes('/guide') ? 'Guide' : 'Page'))} · {t(result.reason)}</small>
|
|
171
168
|
<b>{result.page.title}</b>
|
|
172
169
|
<span>{@render excerpt(result.excerpt, query)}</span>
|
|
173
170
|
</Link>
|
|
174
171
|
</li>{/each}{/if}
|
|
175
172
|
</ol>
|
|
176
173
|
</div>
|
|
177
|
-
<div class="ui-dialog-foot" aria-hidden="true"><span><span class="ui-kbd"><IconArrowUp size={12} /></span><span class="ui-kbd"><IconArrowDown size={12} /></span>{
|
|
174
|
+
<div class="ui-dialog-foot" aria-hidden="true"><span><span class="ui-kbd"><IconArrowUp size={12} /></span><span class="ui-kbd"><IconArrowDown size={12} /></span>{t('Navigate')}</span><span><span class="ui-kbd"><IconCornerDownLeft size={12} /></span>{t('Open')}</span><span class="ui-search-shortcut">{t('Search anytime')}<kbd class="ui-kbd">{shortcutModifier}</kbd><kbd class="ui-kbd">K</kbd></span></div>
|
|
178
175
|
</dialog>
|
|
179
176
|
{/if}
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
4
4
|
import 'dayjs/locale/zh-tw'
|
|
5
5
|
import 'dayjs/locale/en'
|
|
6
|
+
import 'dayjs/locale/ja'
|
|
7
|
+
import 'dayjs/locale/ko'
|
|
8
|
+
import 'dayjs/locale/zh-cn'
|
|
9
|
+
import { languageOf, formatDate } from '../common/locales'
|
|
6
10
|
|
|
7
11
|
dayjs.extend(relativeTime)
|
|
8
12
|
|
|
9
|
-
const locales: Record<string, string> = {
|
|
10
|
-
tw: 'zh-tw',
|
|
11
|
-
en: 'en'
|
|
12
|
-
}
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<script lang="ts">
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
const { locale } = getSite()
|
|
22
22
|
let hydrated = $state(false)
|
|
23
23
|
const formattedDate = $derived(hydrated
|
|
24
|
-
? dayjs(timestamp).locale(
|
|
25
|
-
:
|
|
24
|
+
? dayjs(timestamp).locale(languageOf(locale).toLowerCase()).fromNow()
|
|
25
|
+
: formatDate(timestamp, locale, { weekday: 'short' }))
|
|
26
26
|
|
|
27
27
|
onMount(() => {
|
|
28
28
|
hydrated = true
|
|
@@ -13,6 +13,6 @@ export function searchLabels(translate: (text: string) => string, locale: string
|
|
|
13
13
|
searchPlaceholder,
|
|
14
14
|
/** The placeholder without its shortcut, which triggers show as a key. */
|
|
15
15
|
searchText: searchPlaceholder.replace(/\s*⌘\s*K\s*…?$/u, ''),
|
|
16
|
-
searchLabel:
|
|
16
|
+
searchLabel: translate('Search documentation'),
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
|
|
3
3
|
import type { PageParams } from '../types/Props'
|
|
4
|
-
import {
|
|
4
|
+
import { resolveDocument, resolveNavigation, type ContentModule } from './create-page'
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
7
|
params: PageParams
|
|
@@ -18,18 +18,23 @@ interface Props {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const { params, metadata, dictionaries, categories, content, Layout, noTOC = false, subtitle, ...options } = Astro.props
|
|
21
|
-
const
|
|
21
|
+
const document = await resolveDocument(params.locale, metadata, content)
|
|
22
|
+
const contentModule = document.content
|
|
23
|
+
const pageMetadata = document.metadata
|
|
24
|
+
const pageCategories = resolveNavigation(params.locale, categories)
|
|
25
|
+
const locals = Astro.locals as Record<string, any>
|
|
26
|
+
locals.masterDocument = contentModule?.frontmatter ?? {}
|
|
22
27
|
if (contentModule && !contentModule.default) throw new Error('Invalid content module')
|
|
23
28
|
const Content = contentModule?.default
|
|
24
29
|
const toc = contentModule?.toc
|
|
25
30
|
---
|
|
26
31
|
|
|
27
32
|
{Layout
|
|
28
|
-
? <Layout {...options} params={params} pageCategories
|
|
29
|
-
{Content && <Content pageCategories
|
|
33
|
+
? <Layout {...options} params={params} {pageCategories} dictionaries={dictionaries} metadata={pageMetadata} toc={noTOC ? null : toc} subtitle={subtitle}>
|
|
34
|
+
{Content && <Content {pageCategories} />}
|
|
30
35
|
<slot />
|
|
31
36
|
</Layout>
|
|
32
37
|
: <Fragment>
|
|
33
|
-
{Content && <Content pageCategories
|
|
38
|
+
{Content && <Content {pageCategories} />}
|
|
34
39
|
<slot />
|
|
35
40
|
</Fragment>}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { AstroComponentFactory } from 'astro/runtime/server/index.js'
|
|
2
2
|
import type { Props } from '../types/Props'
|
|
3
3
|
import generate from '../utils/generate-metadata'
|
|
4
|
+
import config from 'virtual:master-site/config'
|
|
5
|
+
import metadataByPathname from 'virtual:master-site/localized-metadata'
|
|
6
|
+
import { localizedMetadata } from '../utils/localized-metadata'
|
|
4
7
|
|
|
5
8
|
export interface ContentModule {
|
|
6
9
|
default: AstroComponentFactory
|
|
7
10
|
toc?: any
|
|
11
|
+
frontmatter?: Record<string, any>
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
/** Translated MDX lives beside its source as `content.<locale>.mdx`. */
|
|
@@ -20,12 +24,35 @@ export function createPageMetadata(metadata: any, dictionaries: Record<string, (
|
|
|
20
24
|
* Astro collects a page's stylesheets from its module graph when the page module loads, before a pending dynamic import finishes in dev.
|
|
21
25
|
*/
|
|
22
26
|
export async function resolveContent(locale: string, metadata: any, content: ContentModule) {
|
|
23
|
-
const
|
|
24
|
-
|
|
27
|
+
const propagated = await loadLocalizedContent(locale, metadata)
|
|
28
|
+
if (propagated) return await propagated.getMod()
|
|
29
|
+
if (locale !== config.i18n.defaultLocale) throw new Error(`Missing localized content: ${metadata.pathname ?? metadata.sourcePath} (${locale})`)
|
|
30
|
+
return content
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
/** The configured inventory owns content roots; metadata carries a canonical pathname. */
|
|
34
|
+
export async function loadLocalizedContent(locale: string, metadata: any) {
|
|
35
|
+
const documents = translatedContent[metadata.pathname]
|
|
36
|
+
if (!documents) return
|
|
37
|
+
const load = documents[locale]
|
|
38
|
+
if (!load) throw new Error(`Missing localized content: ${metadata.pathname} (${locale})`)
|
|
39
|
+
return await load()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveMetadata(locale: string, metadata: any) {
|
|
43
|
+
const variants = metadataByPathname[metadata.pathname]
|
|
44
|
+
if (!variants) return { ...metadata, locale }
|
|
45
|
+
const frontmatter = variants[locale]
|
|
46
|
+
if (!frontmatter && locale !== config.i18n.defaultLocale) throw new Error(`Missing localized metadata: ${metadata.pathname} (${locale})`)
|
|
47
|
+
return localizedMetadata(metadata, frontmatter ?? {}, locale)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function resolveDocument(locale: string, metadata: any, content?: ContentModule) {
|
|
51
|
+
const module = content ? await resolveContent(locale, metadata, content) : await (await loadLocalizedContent(locale, metadata))?.getMod()
|
|
52
|
+
return { content: module, metadata: resolveMetadata(locale, metadata) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Localized navigation reads lightweight frontmatter, without importing every MDX page. */
|
|
56
|
+
export function resolveNavigation(locale: string, categories: any[] = []) {
|
|
57
|
+
return categories.map(category => ({ ...category, pages: category.pages.filter((page: any) => !metadataByPathname[page.pathname] || metadataByPathname[page.pathname][locale]).map((page: any) => resolveMetadata(locale, page)) }))
|
|
31
58
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import '@master/internal-site/styles/docs-shell/prose.css'
|
|
3
3
|
import '@master/internal-site/styles/blog-materials.css'
|
|
4
4
|
import '@master/internal-site/styles/blog-article.css'
|
|
5
|
-
import translations from 'virtual:master-site/translations'
|
|
6
5
|
import Footer from '@master/internal-site/components/Footer.svelte'
|
|
7
6
|
import PageContentBootstrap from '@master/internal-site/components/PageContentBootstrap.astro'
|
|
8
7
|
import { siteOf } from '@master/internal-site/astro/site-context'
|
|
@@ -10,10 +9,6 @@ import BlogArticleMasthead from '@master/internal-site/blog/article/BlogArticleM
|
|
|
10
9
|
import BlogArticleContents from '@master/internal-site/blog/article/BlogArticleContents.svelte'
|
|
11
10
|
import BlogArticleEnd from '@master/internal-site/blog/article/BlogArticleEnd.astro'
|
|
12
11
|
import { neighborsOf } from '@master/internal-site/blog/article/neighbors'
|
|
13
|
-
import { contentLanguageOf } from '@master/internal-site/blog/article/language'
|
|
14
|
-
|
|
15
|
-
/** Translated MDX lives beside its source as `content.<locale>.mdx` (see docs-shell/factories/create-page). */
|
|
16
|
-
const translatedContent = new Set(Object.keys(translations))
|
|
17
12
|
|
|
18
13
|
/**
|
|
19
14
|
* A blog post, drawn with the homepage's drafting guides: the header, the reading column (the documentation's measure
|
|
@@ -22,11 +17,8 @@ const translatedContent = new Set(Object.keys(translations))
|
|
|
22
17
|
* left margin.
|
|
23
18
|
*/
|
|
24
19
|
const { metadata, toc, pageCategories: posts = [], footerProps = {} } = Astro.props
|
|
25
|
-
const { t:
|
|
26
|
-
const { defaultLocale, hreflangOfLocale } = i18n
|
|
20
|
+
const { t: $ } = siteOf(Astro)
|
|
27
21
|
const { newer, older } = neighborsOf(metadata.sourcePath, posts)
|
|
28
|
-
// A post not yet translated is shown in its own language; the body says so, for screen readers and Han type rules.
|
|
29
|
-
const proseLanguage = contentLanguageOf(metadata.sourcePath, translatedContent, { locale, defaultLocale, hreflangOfLocale })
|
|
30
22
|
const contents = (toc ?? []).filter((item: any) => item.level === 2 || item.level === 3).map(({ id, title, level }: any) => ({ id, title, level }))
|
|
31
23
|
const hasSections = contents.some((item: any) => item.level === 2)
|
|
32
24
|
// As on the blog index, a category every post shares says nothing about the next one.
|
|
@@ -39,9 +31,9 @@ const showCategory = posts.length > 1
|
|
|
39
31
|
{/* The post's own header, so the title, date and authors belong to the article. */}
|
|
40
32
|
<BlogArticleMasthead metadata={metadata} />
|
|
41
33
|
{/* In the page's outer left margin, beside the text it lists. */}
|
|
42
|
-
{hasSections ? <BlogArticleContents client:idle={{ timeout: 500 }} items={contents} label={$('On this page')}
|
|
34
|
+
{hasSections ? <BlogArticleContents client:idle={{ timeout: 500 }} items={contents} label={$('On this page')} /> : null}
|
|
43
35
|
<div class="blog-article-body blog-rails blog-rails-rule">
|
|
44
|
-
<div class="blog-article-prose ui-prose"
|
|
36
|
+
<div class="blog-article-prose ui-prose">
|
|
45
37
|
<slot />
|
|
46
38
|
</div>
|
|
47
39
|
</div>
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import Slugger from 'github-slugger'
|
|
2
1
|
import { visit } from 'unist-util-visit'
|
|
3
|
-
import
|
|
2
|
+
import { createHeadingResolver } from '../utils/resolve-heading.js'
|
|
4
3
|
import { parse } from 'acorn'
|
|
5
4
|
|
|
6
5
|
export default function remarkSlugAndTOC() {
|
|
7
6
|
return (tree) => {
|
|
8
|
-
const
|
|
7
|
+
const resolveHeading = createHeadingResolver()
|
|
9
8
|
const toc = []
|
|
10
9
|
|
|
11
10
|
visit(tree, 'heading', (node) => {
|
|
12
11
|
const depth = node.depth
|
|
13
|
-
if (depth
|
|
12
|
+
if (depth < 2 || depth > 6) return
|
|
14
13
|
|
|
15
14
|
// Extract raw text from heading children
|
|
16
15
|
const title = node.children
|
|
@@ -19,8 +18,7 @@ export default function remarkSlugAndTOC() {
|
|
|
19
18
|
.join(' ')
|
|
20
19
|
.trim()
|
|
21
20
|
|
|
22
|
-
const
|
|
23
|
-
const resolved = resolveHeading(title, id)
|
|
21
|
+
const resolved = resolveHeading(title, depth)
|
|
24
22
|
// Explicit IDs survive title edits and translations; they are not visible prose.
|
|
25
23
|
const lastText = node.children.findLast((child) => child.type === 'text')
|
|
26
24
|
if (lastText) lastText.value = lastText.value.replace(/\s+\{#[\w-]+\}$/, '')
|
|
@@ -29,7 +27,7 @@ export default function remarkSlugAndTOC() {
|
|
|
29
27
|
node.data.id = resolved.id
|
|
30
28
|
node.data.hProperties.id = resolved.id
|
|
31
29
|
|
|
32
|
-
toc.push({
|
|
30
|
+
if (depth === 2 || depth === 3) toc.push({
|
|
33
31
|
id: resolved.id,
|
|
34
32
|
title: resolved.title,
|
|
35
33
|
level: depth
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/** Code blocks render as static markup; these delegated handlers switch tabs and copy code. */
|
|
2
|
+
import { readSiteContext } from '../site'
|
|
3
|
+
|
|
4
|
+
const t = (text: string) => readSiteContext().translations[text] ?? text
|
|
2
5
|
|
|
3
6
|
const tabChange = 'site-code-tabs:change'
|
|
4
7
|
const resetTimers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>()
|
|
@@ -24,7 +27,7 @@ function selectTab(root: HTMLElement, name: string) {
|
|
|
24
27
|
const copy = root.querySelector<HTMLButtonElement>('[data-code-copy]')
|
|
25
28
|
if (copy) {
|
|
26
29
|
copy.dataset.codeCopy = name
|
|
27
|
-
copy.ariaLabel = copy.title =
|
|
30
|
+
copy.ariaLabel = copy.title = t('Copy {name}').replace('{name}', t(name))
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
@@ -55,7 +58,7 @@ function showCopyStatus(button: HTMLButtonElement, status: 'idle' | 'pending' |
|
|
|
55
58
|
const element = button.closest('.ui-copy')?.querySelector<HTMLElement>('.ui-copy-status')
|
|
56
59
|
const result = status === 'copied' ? 'success' : status === 'error' ? 'error' : undefined
|
|
57
60
|
if (element) {
|
|
58
|
-
element.textContent = status === 'copied' ?
|
|
61
|
+
element.textContent = status === 'copied' ? t('{name} copied').replace('{name}', t(name || 'Code')) : status === 'error' ? t('Clipboard unavailable. Select the code to copy it.') : ''
|
|
59
62
|
if (result) element.dataset.result = result
|
|
60
63
|
else delete element.dataset.result
|
|
61
64
|
}
|
package/src/docs-shell/site.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface SiteI18n {
|
|
|
15
15
|
defaultLocale: string
|
|
16
16
|
locales: readonly string[]
|
|
17
17
|
localizablePathnameRoots: string[]
|
|
18
|
+
localizableOrigins?: Record<string, readonly string[]>
|
|
18
19
|
hreflangOfLocale: Record<string, string>
|
|
19
20
|
nameOfLocale: Record<string, string>
|
|
20
21
|
}
|
|
@@ -47,6 +48,7 @@ export function createSite(value: SiteContextValue): Site {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
let clientSite: Site | undefined
|
|
51
|
+
let clientContext: string | undefined
|
|
50
52
|
|
|
51
53
|
/** Read the context Document.astro serializes for client islands. */
|
|
52
54
|
export function readSiteContext(): SiteContextValue {
|
|
@@ -63,5 +65,11 @@ export function getSite(): Site {
|
|
|
63
65
|
const value = getContext<SiteContextValue | undefined>(siteContextKey)
|
|
64
66
|
if (value) return createSite(value)
|
|
65
67
|
if (typeof document === 'undefined') throw new Error('getSite() must run inside a component rendered by the site')
|
|
66
|
-
|
|
68
|
+
const serialized = document.getElementById(siteContextElementId)?.textContent
|
|
69
|
+
if (!serialized) throw new Error(`Missing #${siteContextElementId}`)
|
|
70
|
+
if (serialized !== clientContext) {
|
|
71
|
+
clientContext = serialized
|
|
72
|
+
clientSite = createSite(JSON.parse(serialized))
|
|
73
|
+
}
|
|
74
|
+
return clientSite!
|
|
67
75
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const proseFields = new Set(['title', 'description', 'caption', 'summary', 'label', 'note', 'lead', 'sourceLabel', 'resultLabel', 'alt'])
|
|
2
|
+
|
|
3
|
+
/** Overlay article-owned prose without copying or changing executable examples. */
|
|
4
|
+
export function demoCopy(frontmatter, locale, key, original) {
|
|
5
|
+
if (locale === 'en') return original
|
|
6
|
+
const copy = frontmatter.demos?.[key]
|
|
7
|
+
const walk = (value, translated, field, location) => {
|
|
8
|
+
if (typeof value === 'string') {
|
|
9
|
+
if (!proseFields.has(field) || !value.trim()) return value
|
|
10
|
+
if (typeof translated !== 'string' || !translated.trim()) throw new Error(`Missing localized demo copy: ${key}.${location} (${locale})`)
|
|
11
|
+
return translated
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(value)) return value.map((item, index) => walk(item, translated?.[index], field, `${location}[${index}]`))
|
|
14
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([name, item]) => [name, walk(item, translated?.[name], name, location ? `${location}.${name}` : name)]))
|
|
15
|
+
return value
|
|
16
|
+
}
|
|
17
|
+
return walk(original, copy, '', '')
|
|
18
|
+
}
|
|
@@ -2,6 +2,7 @@ import { defu } from 'defu'
|
|
|
2
2
|
import type { Metadata } from '../types/Metadata'
|
|
3
3
|
import type { Props } from '../types/Props'
|
|
4
4
|
import { createTranslation } from './i18n'
|
|
5
|
+
import { resolveMetadata } from '../factories/create-page'
|
|
5
6
|
|
|
6
7
|
export default async function generate(
|
|
7
8
|
metadata: Metadata | Record<string, any>,
|
|
@@ -9,6 +10,7 @@ export default async function generate(
|
|
|
9
10
|
dictionaries: Record<string, () => Promise<any>>
|
|
10
11
|
): Promise<Metadata> {
|
|
11
12
|
const { locale } = params
|
|
13
|
+
metadata = resolveMetadata(locale, metadata)
|
|
12
14
|
const $ = await createTranslation(locale, dictionaries)
|
|
13
15
|
const description = $(metadata.description)
|
|
14
16
|
const category = $(metadata.category)
|
|
@@ -6,6 +6,7 @@ export interface LocalizePathnameOptions {
|
|
|
6
6
|
locales: readonly string[]
|
|
7
7
|
localePrefixMode?: LocalePrefixMode
|
|
8
8
|
localizablePathnameRoots?: readonly string[]
|
|
9
|
+
localizableOrigins?: Record<string, readonly string[]>
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
const nonPagePathnameRoots = new Set([
|
|
@@ -24,8 +25,18 @@ export function localizePathname(href: string, {
|
|
|
24
25
|
locale,
|
|
25
26
|
locales,
|
|
26
27
|
localePrefixMode = 'canonical',
|
|
27
|
-
localizablePathnameRoots
|
|
28
|
-
|
|
28
|
+
localizablePathnameRoots,
|
|
29
|
+
localizableOrigins
|
|
30
|
+
}: LocalizePathnameOptions): string {
|
|
31
|
+
if (/^https?:\/\//i.test(href)) {
|
|
32
|
+
let url: URL
|
|
33
|
+
try { url = new URL(href) } catch { return href }
|
|
34
|
+
const roots = localizableOrigins?.[url.origin]
|
|
35
|
+
if (!roots) return href
|
|
36
|
+
return url.origin + localizePathname(url.pathname + url.search + url.hash, {
|
|
37
|
+
defaultLocale, locale, locales, localePrefixMode, localizablePathnameRoots: roots
|
|
38
|
+
})
|
|
39
|
+
}
|
|
29
40
|
if (!href || !href.startsWith('/') || href.startsWith('//')) return href
|
|
30
41
|
|
|
31
42
|
const parsedHref = parseLocalHref(href)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import sharedTranslations from '../common/translations'
|
|
2
|
+
|
|
1
3
|
export async function createTranslation(locale: string, dictionaries: Record<string, () => Promise<any>>): Promise<(text: any) => string> {
|
|
2
4
|
const translations = await importTranslations(locale, dictionaries)
|
|
3
5
|
return (text: any) => (translations)[text || ''] || text
|
|
@@ -8,5 +10,6 @@ export const importTranslations = async (locale: string, dictionaries: Record<st
|
|
|
8
10
|
throw new Error('locale is required')
|
|
9
11
|
}
|
|
10
12
|
const load = dictionaries[locale as keyof typeof dictionaries]
|
|
11
|
-
|
|
13
|
+
if (!load && !sharedTranslations[locale]) throw new Error(`Unsupported locale: ${locale}`)
|
|
14
|
+
return { ...sharedTranslations[locale], ...(load ? await load() : {}) }
|
|
12
15
|
}
|