@astrojs/starlight 0.0.8 → 0.0.10

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/404.astro CHANGED
@@ -30,8 +30,8 @@ const { lang = 'en', dir = 'ltr', locale } = config.defaultLocale || {};
30
30
  </head>
31
31
  <body>
32
32
  <ThemeProvider />
33
- <PageFrame>
34
- <Header slot="header" locale={locale} />
33
+ <PageFrame {locale}>
34
+ <Header slot="header" {locale} />
35
35
  <main>
36
36
  <MarkdownContent>
37
37
  <h1 id="starlight__overview">404</h1>
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.0.10
4
+
5
+ ### Patch Changes
6
+
7
+ - [#78](https://github.com/withastro/starlight/pull/78) [`d3ee6fc`](https://github.com/withastro/starlight/commit/d3ee6fc643de7a320a6bb83432cdcfbb0a4e4289) Thanks [@delucis](https://github.com/delucis)! - Add support for customising and translating Starlight’s UI.
8
+
9
+ Users can provide translations in JSON files in `src/content/i18n/` which is a data collection. For example, a `src/content/i18n/de.json` might translate the search UI:
10
+
11
+ ```json
12
+ {
13
+ "search.label": "Suchen",
14
+ "search.shortcutLabel": "(Drücke / zum Suchen)"
15
+ }
16
+ ```
17
+
18
+ This change also allows Starlight to provide built-in support for more languages than just English and adds German & Spanish support.
19
+
20
+ - [#76](https://github.com/withastro/starlight/pull/76) [`5e82073`](https://github.com/withastro/starlight/commit/5e8207350dba0fce92fa101d311db627e2157654) Thanks [@lloydjatkinson](https://github.com/lloydjatkinson)! - Scale down code block font size to match Figma design
21
+
22
+ - [#78](https://github.com/withastro/starlight/pull/78) [`d3ee6fc`](https://github.com/withastro/starlight/commit/d3ee6fc643de7a320a6bb83432cdcfbb0a4e4289) Thanks [@delucis](https://github.com/delucis)! - Require a minimum Astro version of 2.5.0
23
+
24
+ ## 0.0.9
25
+
26
+ ### Patch Changes
27
+
28
+ - [#72](https://github.com/withastro/starlight/pull/72) [`3dc1d0c`](https://github.com/withastro/starlight/commit/3dc1d0c342c6db4e30b016035fa446101b1805a2) Thanks [@delucis](https://github.com/delucis)! - Fix vertical alignment of social icons in site header
29
+
30
+ - [#63](https://github.com/withastro/starlight/pull/63) [`823e351`](https://github.com/withastro/starlight/commit/823e351e1c1fc68ca3c20ab35c9a7d9b13760a70) Thanks [@liruifengv](https://github.com/liruifengv)! - Make sidebar groups to collapsible
31
+
32
+ - [#72](https://github.com/withastro/starlight/pull/72) [`3dc1d0c`](https://github.com/withastro/starlight/commit/3dc1d0c342c6db4e30b016035fa446101b1805a2) Thanks [@delucis](https://github.com/delucis)! - Fix image aspect ratio in Markdown content
33
+
3
34
  ## 0.0.8
4
35
 
5
36
  ### Patch Changes
@@ -1,12 +1,15 @@
1
1
  ---
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import config from 'virtual:starlight/user-config';
4
+ import { useTranslations } from '../utils/translations';
4
5
 
5
6
  interface Props {
6
7
  data: CollectionEntry<'docs'>['data'];
7
8
  id: CollectionEntry<'docs'>['id'];
9
+ locale: string | undefined;
8
10
  }
9
11
 
12
+ const t = useTranslations(Astro.props.locale);
10
13
  const { editUrl } = Astro.props.data;
11
14
 
12
15
  let { baseUrl } = config.editLink;
@@ -20,4 +23,4 @@ const url =
20
23
  : undefined;
21
24
  ---
22
25
 
23
- {editUrl !== false && url && <a href={url}>Edit this page</a>}
26
+ {editUrl !== false && url && <a href={url}>{t('page.editLink')}</a>}
@@ -1,5 +1,12 @@
1
1
  ---
2
+ import { useTranslations } from '../utils/translations';
2
3
  import Icon from './Icon.astro';
4
+
5
+ interface Props {
6
+ locale: string | undefined;
7
+ }
8
+
9
+ const t = useTranslations(Astro.props.locale);
3
10
  ---
4
11
 
5
12
  <p>
@@ -7,7 +14,7 @@ import Icon from './Icon.astro';
7
14
  name={'warning'}
8
15
  size="1.5em"
9
16
  color="var(--sl-color-orange-high)"
10
- /><span>This content is not available in your language yet.</span>
17
+ /><span>{t('i18n.untranslatedContent')}</span>
11
18
  </p>
12
19
 
13
20
  <style>
@@ -14,10 +14,10 @@ const { locale } = Astro.props
14
14
 
15
15
  <div class="header">
16
16
  <SiteTitle {locale} />
17
- <Search />
17
+ <Search {locale} />
18
18
  <div class="hidden md:flex right-group">
19
19
  <SocialIcons />
20
- <ThemeSelect />
20
+ <ThemeSelect {locale} />
21
21
  <LanguageSelect {locale}/>
22
22
  </div>
23
23
  </div>
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import config from 'virtual:starlight/user-config';
3
3
  import { localizedUrl } from '../utils/localizedUrl';
4
+ import { useTranslations } from '../utils/translations';
4
5
  import Select from './Select.astro';
5
6
 
6
7
  interface Props {
@@ -13,6 +14,8 @@ interface Props {
13
14
  function localizedPathname(locale: string | undefined): string {
14
15
  return localizedUrl(Astro.url, locale).pathname;
15
16
  }
17
+
18
+ const t = useTranslations(Astro.props.locale);
16
19
  ---
17
20
 
18
21
  {
@@ -20,7 +23,7 @@ function localizedPathname(locale: string | undefined): string {
20
23
  <starlight-lang-select>
21
24
  <Select
22
25
  icon="translate"
23
- label="Select language"
26
+ label={t('languageSelect.accessibleLabel')}
24
27
  value={localizedPathname(Astro.props.locale)}
25
28
  options={Object.entries(config.locales).map(([code, locale]) => ({
26
29
  value: localizedPathname(code),
@@ -3,13 +3,16 @@ import type { CollectionEntry } from 'astro:content';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import project from 'virtual:starlight/project-context';
5
5
  import { getFileCommitDate } from '../utils/git';
6
+ import { useTranslations } from '../utils/translations';
6
7
 
7
8
  interface Props {
8
9
  id: CollectionEntry<'docs'>['id'];
9
10
  lang: string;
11
+ locale: string | undefined;
10
12
  }
11
13
 
12
- const { id, lang } = Astro.props;
14
+ const { id, lang, locale } = Astro.props;
15
+ const t = useTranslations(locale);
13
16
 
14
17
  const currentFilePath = fileURLToPath(
15
18
  new URL('src/content/docs/' + id, project.root)
@@ -24,7 +27,7 @@ try {
24
27
  {
25
28
  date && (
26
29
  <p>
27
- Last updated:{' '}
30
+ {t('page.lastUpdated')}
28
31
  <time datetime={date.toISOString()}>
29
32
  {date.toLocaleDateString(lang, { dateStyle: 'medium' })}
30
33
  </time>
@@ -44,6 +44,7 @@
44
44
  .content :global(:is(img, picture, video, canvas, svg, iframe)) {
45
45
  display: block;
46
46
  max-width: 100%;
47
+ height: auto;
47
48
  }
48
49
 
49
50
  .content :global(h1) {
@@ -1,11 +1,18 @@
1
1
  ---
2
2
  import Icon from './Icon.astro';
3
+ import { useTranslations } from '../utils/translations';
4
+
5
+ interface Props {
6
+ locale: string | undefined;
7
+ }
8
+
9
+ const t = useTranslations(Astro.props.locale);
3
10
  ---
4
11
 
5
12
  <starlight-menu-button>
6
13
  <button
7
14
  aria-expanded="false"
8
- aria-label="Menu"
15
+ aria-label={t('menuButton.accessibleLabel')}
9
16
  aria-controls="starlight__sidebar"
10
17
  class="md:hidden"
11
18
  >
@@ -1,15 +1,18 @@
1
1
  ---
2
2
  import type { Link } from '../utils/navigation';
3
+ import { useTranslations } from '../utils/translations';
3
4
  import Icon from './Icon.astro';
4
5
 
5
6
  interface Props {
6
7
  prev: Link | undefined;
7
8
  next: Link | undefined;
8
9
  dir: 'ltr' | 'rtl';
10
+ locale: string | undefined;
9
11
  }
10
12
 
11
- const { prev, next, dir } = Astro.props;
13
+ const { prev, next, dir, locale } = Astro.props;
12
14
  const isRtl = dir === 'rtl';
15
+ const t = useTranslations(locale);
13
16
  ---
14
17
 
15
18
  <div class="pagination-links" dir={dir}>
@@ -18,7 +21,7 @@ const isRtl = dir === 'rtl';
18
21
  <a href={prev.href} rel="prev">
19
22
  <Icon name={isRtl ? 'right-arrow' : 'left-arrow'} size="1.5rem" />
20
23
  <span>
21
- Previous
24
+ {t('page.previousLink')}
22
25
  <br />
23
26
  <span class="link-title">{prev.label}</span>
24
27
  </span>
@@ -30,7 +33,7 @@ const isRtl = dir === 'rtl';
30
33
  <a href={next.href} rel="next">
31
34
  <Icon name={isRtl ? 'left-arrow' : 'right-arrow'} size="1.5rem" />
32
35
  <span>
33
- Next
36
+ {t('page.nextLink')}
34
37
  <br />
35
38
  <span class="link-title">{next.label}</span>
36
39
  </span>
@@ -9,20 +9,21 @@ import TableOfContents from './TableOfContents.astro';
9
9
  interface Props {
10
10
  entry: StarlightDocsEntry;
11
11
  headings: MarkdownHeading[];
12
+ locale: string | undefined;
12
13
  }
13
14
 
14
- const { entry, headings } = Astro.props;
15
+ const { entry, headings, locale } = Astro.props;
15
16
  ---
16
17
 
17
18
  <RightSidebarPanel>
18
- <TableOfContents headings={headings} />
19
+ <TableOfContents {headings} {locale} />
19
20
  </RightSidebarPanel>
20
21
  <RightSidebarPanel>
21
22
  {
22
23
  config.editLink.baseUrl && (
23
24
  <>
24
25
  <h2>Contribute</h2>
25
- <EditLink data={entry.data} id={entry.id} />
26
+ <EditLink data={entry.data} id={entry.id} {locale} />
26
27
  </>
27
28
  )
28
29
  }
@@ -1,22 +1,37 @@
1
1
  ---
2
2
  import '@pagefind/default-ui/css/ui.css';
3
+ import { useTranslations } from '../utils/translations';
3
4
  import Icon from './Icon.astro';
5
+
6
+ interface Props {
7
+ locale: string | undefined;
8
+ }
9
+
10
+ const t = useTranslations(Astro.props.locale);
4
11
  ---
5
12
 
6
13
  <site-search>
7
14
  <button data-open-modal disabled>
8
- <Icon name="magnifier" label="Search" />
9
- <span class="hidden md:block" aria-hidden="true">Search</span>
15
+ {
16
+ /* The span is `aria-hidden` because it is not shown on small screens. Instead, the icon label is used for accessibility purposes. */
17
+ }
18
+ <Icon name="magnifier" label={t('search.label')} />
19
+ <span class="hidden md:block" aria-hidden="true">{t('search.label')}</span>
10
20
  <Icon
11
21
  name="forward-slash"
12
22
  class="hidden md:block"
13
- label="(Press / to search)"
23
+ label={t('search.shortcutLabel')}
14
24
  />
15
25
  </button>
16
26
 
17
- <dialog style="padding:0" aria-label="Search the documentation">
27
+ <dialog style="padding:0" aria-label={t('search.label')}>
18
28
  <div class="dialog-frame">
19
- <button data-close-modal class="flex md:hidden">Cancel</button>
29
+ {
30
+ /* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
31
+ }
32
+ <button data-close-modal class="flex md:hidden">
33
+ {t('search.cancelLabel')}
34
+ </button>
20
35
  {
21
36
  import.meta.env.DEV ? (
22
37
  <div style="margin: auto; text-align: center;">
@@ -8,13 +8,15 @@ interface Props {
8
8
  sidebar: ReturnType<typeof getSidebar>;
9
9
  locale: string | undefined;
10
10
  }
11
+
12
+ const { sidebar, locale } = Astro.props;
11
13
  ---
12
14
 
13
15
  <div class="sidebar flex">
14
- <SidebarSublist sublist={Astro.props.sidebar} />
16
+ <SidebarSublist sublist={sidebar} />
15
17
  <div class="mobile-preferences flex md:hidden">
16
- <ThemeSelect />
17
- <LanguageSelect locale={Astro.props.locale} />
18
+ <ThemeSelect {locale} />
19
+ <LanguageSelect {locale} />
18
20
  </div>
19
21
  </div>
20
22
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import type { SidebarEntry } from '../utils/navigation';
3
+ import Icon from './Icon.astro';
3
4
 
4
5
  interface Props {
5
6
  sublist: SidebarEntry[];
@@ -15,10 +16,13 @@ interface Props {
15
16
  {entry.label}
16
17
  </a>
17
18
  ) : (
18
- <>
19
- <h2>{entry.label}</h2>
19
+ <details open>
20
+ <summary>
21
+ <h2>{entry.label}</h2>
22
+ <Icon name="right-caret" class="caret" size="1.25rem" />
23
+ </summary>
20
24
  <Astro.self sublist={entry.entries} />
21
- </>
25
+ </details>
22
26
  )}
23
27
  </li>
24
28
  ))
@@ -36,13 +40,35 @@ interface Props {
36
40
  font-size: var(--sl-text-lg);
37
41
  font-weight: 600;
38
42
  color: var(--sl-color-white);
39
- padding-inline: var(--sl-sidebar-item-padding-inline);
40
43
  }
41
44
 
42
45
  .sidebar-group + .sidebar-group {
43
46
  margin-top: 0.75rem;
44
47
  }
45
48
 
49
+ summary {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ padding-inline: var(--sl-sidebar-item-padding-inline);
54
+ cursor: pointer;
55
+ user-select: none;
56
+ }
57
+ summary::marker,
58
+ summary::-webkit-details-marker {
59
+ display: none;
60
+ }
61
+
62
+ .caret {
63
+ transition: transform 0.2s ease-in-out;
64
+ }
65
+ :global([dir='rtl']) .caret {
66
+ transform: rotateZ(180deg);
67
+ }
68
+ [open] .caret {
69
+ transform: rotateZ(90deg);
70
+ }
71
+
46
72
  a {
47
73
  display: block;
48
74
  border-radius: 0.25rem;
@@ -1,4 +1,14 @@
1
- <a href="#starlight__overview">Skip to content</a>
1
+ ---
2
+ import { useTranslations } from '../utils/translations';
3
+
4
+ interface Props {
5
+ locale: string | undefined;
6
+ }
7
+
8
+ const t = useTranslations(Astro.props.locale);
9
+ ---
10
+
11
+ <a href="#starlight__overview">{t('skipLink.label')}</a>
2
12
 
3
13
  <style>
4
14
  a {
@@ -32,6 +32,7 @@ const links = Object.entries(config.social || {}).filter(([, url]) =>
32
32
 
33
33
  <style>
34
34
  a {
35
+ display: flex;
35
36
  color: var(--sl-color-text-accent);
36
37
  }
37
38
  a:hover {
@@ -1,15 +1,21 @@
1
1
  ---
2
2
  import type { MarkdownHeading } from 'astro';
3
3
  import config from 'virtual:starlight/user-config';
4
- import { generateToC } from './generateToC';
5
- import TableOfContentsList from './TableOfContentsList.astro';
4
+ import { useTranslations } from '../../utils/translations';
6
5
  import Icon from '../Icon.astro';
6
+ import TableOfContentsList from './TableOfContentsList.astro';
7
+ import { generateToC } from './generateToC';
7
8
 
8
9
  interface Props {
9
10
  headings: MarkdownHeading[];
11
+ locale: string | undefined;
10
12
  }
11
13
 
12
- const toc = generateToC(Astro.props.headings, config.tableOfContents);
14
+ const t = useTranslations(Astro.props.locale);
15
+ const toc = generateToC(Astro.props.headings, {
16
+ ...config.tableOfContents,
17
+ title: t('tableOfContents.overview'),
18
+ });
13
19
  ---
14
20
 
15
21
  <mobile-starlight-toc
@@ -20,7 +26,7 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
20
26
  <details id="starlight__mobile-toc">
21
27
  <summary id="starlight__on-this-page--mobile" class="flex">
22
28
  <div class="toggle flex">
23
- On this page
29
+ {t('tableOfContents.onThisPage')}
24
30
  <Icon name={'right-caret'} class="caret" size="1rem" />
25
31
  </div>
26
32
  <span class="display-current">{toc[0]?.text}</span>
@@ -1,8 +1,8 @@
1
1
  ---
2
- import type { generateToC } from './generateToC';
2
+ import type { TocItem } from './generateToC';
3
3
 
4
4
  interface Props {
5
- toc: ReturnType<typeof generateToC>;
5
+ toc: TocItem[];
6
6
  depth?: number;
7
7
  isMobile?: boolean;
8
8
  }
@@ -1,14 +1,21 @@
1
1
  ---
2
2
  import type { MarkdownHeading } from 'astro';
3
3
  import config from 'virtual:starlight/user-config';
4
- import { generateToC } from './TableOfContents/generateToC';
4
+ import { useTranslations } from '../utils/translations';
5
5
  import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
6
+ import { generateToC } from './TableOfContents/generateToC';
6
7
 
7
8
  interface Props {
8
9
  headings: MarkdownHeading[];
10
+ locale: string | undefined;
9
11
  }
10
12
 
11
- const toc = generateToC(Astro.props.headings, config.tableOfContents);
13
+ const { locale, headings } = Astro.props;
14
+ const t = useTranslations(locale);
15
+ const toc = generateToC(headings, {
16
+ ...config.tableOfContents,
17
+ title: t('tableOfContents.overview'),
18
+ });
12
19
  ---
13
20
 
14
21
  <starlight-toc
@@ -16,7 +23,9 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
16
23
  data-max-h={config.tableOfContents.maxHeadingLevel}
17
24
  >
18
25
  <nav aria-labelledby="starlight__on-this-page">
19
- <h2 id="starlight__on-this-page">On this page</h2>
26
+ <h2 id="starlight__on-this-page">
27
+ {t('tableOfContents.onThisPage')}
28
+ </h2>
20
29
  <TableOfContentsList toc={toc} />
21
30
  </nav>
22
31
  </starlight-toc>
@@ -1,18 +1,28 @@
1
1
  ---
2
+ import { useTranslations } from '../utils/translations';
2
3
  import Select from './Select.astro';
4
+
5
+ interface Props {
6
+ locale: string | undefined;
7
+ }
8
+
9
+ const t = useTranslations(Astro.props.locale);
3
10
  ---
4
11
 
5
12
  <starlight-theme-select>
13
+ {
14
+ /* TODO: Can we give this select a width that works well for each language’s strings? */
15
+ }
6
16
  <Select
7
17
  icon="laptop"
8
- label="Select theme"
18
+ label={t('themeSelect.accessibleLabel')}
9
19
  value="auto"
10
20
  options={[
11
- { label: 'Dark', selected: false, value: 'dark' },
12
- { label: 'Light', selected: false, value: 'light' },
13
- { label: 'Auto', selected: true, value: 'auto' },
21
+ { label: t('themeSelect.dark'), selected: false, value: 'dark' },
22
+ { label: t('themeSelect.light'), selected: false, value: 'light' },
23
+ { label: t('themeSelect.auto'), selected: true, value: 'auto' },
14
24
  ]}
15
- width="5.5em"
25
+ width="6.25em"
16
26
  />
17
27
  </starlight-theme-select>
18
28
 
package/index.astro CHANGED
@@ -64,13 +64,13 @@ const prevNextLinks = getPrevNextLinks(sidebar);
64
64
  </head>
65
65
  <body>
66
66
  <ThemeProvider />
67
- <SkipLink />
68
- <PageFrame>
69
- <Header slot="header" locale={locale} />
70
- <Sidebar slot="sidebar" sidebar={sidebar} locale={locale} />
71
- <MobileTableOfContents headings={headings} />
67
+ <SkipLink {locale} />
68
+ <PageFrame {locale}>
69
+ <Header slot="header" {locale} />
70
+ <Sidebar slot="sidebar" {sidebar} {locale} />
71
+ <MobileTableOfContents {headings} {locale} />
72
72
  <TwoColumnContent>
73
- <RightSidebar slot="right-sidebar" entry={entry} headings={headings} />
73
+ <RightSidebar slot="right-sidebar" {entry} {headings} {locale} />
74
74
  <main data-pagefind-body lang={entryMeta.lang} dir={entryMeta.dir}>
75
75
  <ContentPanel>
76
76
  <h1
@@ -79,13 +79,13 @@ const prevNextLinks = getPrevNextLinks(sidebar);
79
79
  >
80
80
  {entry.data.title}
81
81
  </h1>
82
- {isFallback && <FallbackContentNotice />}
82
+ {isFallback && <FallbackContentNotice {locale} />}
83
83
  </ContentPanel>
84
84
  <ContentPanel>
85
85
  <MarkdownContent><Content /></MarkdownContent>
86
86
  <footer>
87
- <LastUpdated id={entry.id} lang={lang} />
88
- <PrevNextLinks {...prevNextLinks} dir={dir} />
87
+ <LastUpdated id={entry.id} {lang} {locale} />
88
+ <PrevNextLinks {...prevNextLinks} {dir} {locale} />
89
89
  </footer>
90
90
  </ContentPanel>
91
91
  </main>
@@ -1,5 +1,13 @@
1
1
  ---
2
2
  import MobileMenuToggle from '../components/MobileMenuToggle.astro';
3
+ import { useTranslations } from '../utils/translations';
4
+
5
+ interface Props {
6
+ locale: string | undefined;
7
+ }
8
+
9
+ const { locale } = Astro.props;
10
+ const t = useTranslations(locale);
3
11
 
4
12
  const hasSidebar = Astro.slots.has('sidebar');
5
13
  ---
@@ -8,8 +16,8 @@ const hasSidebar = Astro.slots.has('sidebar');
8
16
  <header class="header"><slot name="header" /></header>
9
17
  {
10
18
  hasSidebar && (
11
- <nav class="sidebar" aria-label="Main">
12
- <MobileMenuToggle />
19
+ <nav class="sidebar" aria-label={t('sidebarNav.accessibleLabel')}>
20
+ <MobileMenuToggle {locale} />
13
21
  <div id="starlight__sidebar" class="sidebar-pane">
14
22
  <div class="sidebar-content">
15
23
  <slot name="sidebar" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -27,11 +27,11 @@
27
27
  "./404.astro": "./404.astro"
28
28
  },
29
29
  "peerDependencies": {
30
- "astro": "^2.4.3"
30
+ "astro": "^2.5.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^18.15.11",
34
- "astro": "^2.4.3"
34
+ "astro": "^2.5.0"
35
35
  },
36
36
  "dependencies": {
37
37
  "@astrojs/mdx": "^0.19.1",
package/schema.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'astro/zod';
2
2
  import { HeadConfigSchema } from './schemas/head';
3
+ export { i18nSchema } from './schemas/i18n';
3
4
 
4
5
  export function docsSchema() {
5
6
  return z.object({
@@ -0,0 +1,91 @@
1
+ import { z } from 'astro/zod';
2
+
3
+ export function i18nSchema() {
4
+ return z
5
+ .object({
6
+ 'skipLink.label': z
7
+ .string()
8
+ .describe(
9
+ 'Text displayed in the accessible “Skip link” when a keyboard user first tabs into a page.'
10
+ ),
11
+
12
+ 'search.label': z.string().describe('Text displayed in the search bar.'),
13
+
14
+ 'search.shortcutLabel': z
15
+ .string()
16
+ .describe(
17
+ 'Accessible label for the shortcut key to open the search modal.'
18
+ ),
19
+
20
+ 'search.cancelLabel': z
21
+ .string()
22
+ .describe('Text for the “Cancel” button that closes the search modal.'),
23
+
24
+ 'themeSelect.accessibleLabel': z
25
+ .string()
26
+ .describe('Accessible label for the theme selection dropdown.'),
27
+
28
+ 'themeSelect.dark': z.string().describe('Name of the dark color theme.'),
29
+
30
+ 'themeSelect.light': z
31
+ .string()
32
+ .describe('Name of the light color theme.'),
33
+
34
+ 'themeSelect.auto': z
35
+ .string()
36
+ .describe(
37
+ 'Name of the automatic color theme that syncs with system preferences.'
38
+ ),
39
+
40
+ 'languageSelect.accessibleLabel': z
41
+ .string()
42
+ .describe('Accessible label for the language selection dropdown.'),
43
+
44
+ 'menuButton.accessibleLabel': z
45
+ .string()
46
+ .describe('Accessible label for he mobile menu button.'),
47
+
48
+ 'sidebarNav.accessibleLabel': z
49
+ .string()
50
+ .describe(
51
+ 'Accessible label for the main sidebar `<nav>` element to distinguish it fom other `<nav>` landmarks on the page.'
52
+ ),
53
+
54
+ 'tableOfContents.onThisPage': z
55
+ .string()
56
+ .describe('Title for the table of contents component.'),
57
+
58
+ 'tableOfContents.overview': z
59
+ .string()
60
+ .describe(
61
+ 'Label used for the first link in the table of contents, linking to the page title.'
62
+ ),
63
+
64
+ 'i18n.untranslatedContent': z
65
+ .string()
66
+ .describe(
67
+ 'Notice informing users they are on a page that is not yet translated to their language.'
68
+ ),
69
+
70
+ 'page.editLink': z.string().describe('Text for the link to edit a page.'),
71
+
72
+ 'page.lastUpdated': z
73
+ .string()
74
+ .describe(
75
+ 'Text displayed in front of the last updated date in the page footer.'
76
+ ),
77
+
78
+ 'page.previousLink': z
79
+ .string()
80
+ .describe(
81
+ 'Label shown on the “previous page” pagination arrow in the page footer.'
82
+ ),
83
+
84
+ 'page.nextLink': z
85
+ .string()
86
+ .describe(
87
+ 'Label shown on the “next page” pagination arrow in the page footer.'
88
+ ),
89
+ })
90
+ .partial();
91
+ }
package/style/props.css CHANGED
@@ -93,7 +93,7 @@
93
93
 
94
94
  --sl-text-body: var(--sl-text-base);
95
95
  --sl-text-body-sm: var(--sl-text-xs);
96
- --sl-text-code: var(--sl-text-base);
96
+ --sl-text-code: var(--sl-text-sm);
97
97
  --sl-text-code-sm: var(--sl-text-xs);
98
98
  --sl-text-h1: var(--sl-text-4xl);
99
99
  --sl-text-h2: var(--sl-text-3xl);
@@ -0,0 +1,20 @@
1
+ {
2
+ "skipLink.label": "Zum Inhalt springen",
3
+ "search.label": "Suchen",
4
+ "search.shortcutLabel": "(Drücke / zum Suchen)",
5
+ "search.cancelLabel": "Abbrechen",
6
+ "themeSelect.accessibleLabel": "Farbschema wählen",
7
+ "themeSelect.dark": "Dunkel",
8
+ "themeSelect.light": "Hell",
9
+ "themeSelect.auto": "System",
10
+ "languageSelect.accessibleLabel": "Sprache wählen",
11
+ "menuButton.accessibleLabel": "Menü",
12
+ "sidebarNav.accessibleLabel": "Hauptnavigation",
13
+ "tableOfContents.onThisPage": "Auf dieser Seite",
14
+ "tableOfContents.overview": "Überblick",
15
+ "i18n.untranslatedContent": "Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.",
16
+ "page.editLink": "Seite bearbeiten",
17
+ "page.lastUpdated": "Zuletzt bearbeitet:",
18
+ "page.previousLink": "Vorherige Seite",
19
+ "page.nextLink": "Nächste Seite"
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "skipLink.label": "Skip to content",
3
+ "search.label": "Search",
4
+ "search.shortcutLabel": "(Press / to Search)",
5
+ "search.cancelLabel": "Cancel",
6
+ "themeSelect.accessibleLabel": "Select theme",
7
+ "themeSelect.dark": "Dark",
8
+ "themeSelect.light": "Light",
9
+ "themeSelect.auto": "Auto",
10
+ "languageSelect.accessibleLabel": "Select language",
11
+ "menuButton.accessibleLabel": "Menu",
12
+ "sidebarNav.accessibleLabel": "Main",
13
+ "tableOfContents.onThisPage": "On this page",
14
+ "tableOfContents.overview": "Overview",
15
+ "i18n.untranslatedContent": "This content is not available in your language yet.",
16
+ "page.editLink": "Edit page",
17
+ "page.lastUpdated": "Last updated:",
18
+ "page.previousLink": "Next",
19
+ "page.nextLink": "Previous"
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "skipLink.label": "Saltearse al contenido",
3
+ "search.label": "Buscar",
4
+ "search.shortcutLabel": "(Presiona / para buscar)",
5
+ "search.cancelLabel": "Interrumpir",
6
+ "themeSelect.accessibleLabel": "Seleccionar tema",
7
+ "themeSelect.dark": "Oscuro",
8
+ "themeSelect.light": "Claro",
9
+ "themeSelect.auto": "Automático",
10
+ "languageSelect.accessibleLabel": "Seleccionar idioma",
11
+ "menuButton.accessibleLabel": "Menú",
12
+ "sidebarNav.accessibleLabel": "Primario",
13
+ "tableOfContents.onThisPage": "En esta página",
14
+ "tableOfContents.overview": "Sinopsis",
15
+ "i18n.untranslatedContent": "Esta página aún no está disponible en tu idioma.",
16
+ "page.editLink": "Edita esta página",
17
+ "page.lastUpdated": "Última actualización:",
18
+ "page.previousLink": "Página anterior",
19
+ "page.nextLink": "Siguiente página"
20
+ }
@@ -0,0 +1,10 @@
1
+ import { i18nSchema } from '../schemas/i18n';
2
+ import en from './en.json';
3
+ import es from './es.json';
4
+ import de from './de.json';
5
+
6
+ const parse = i18nSchema().required().strict().parse;
7
+
8
+ export default Object.fromEntries(
9
+ Object.entries({ en, es, de }).map(([key, dict]) => [key, parse(dict)])
10
+ );
package/utils/slugs.ts CHANGED
@@ -32,11 +32,13 @@ export function slugToLocaleData(slug: string): LocaleData {
32
32
  * Get the BCP-47 language tag for the given locale.
33
33
  * @param locale Locale string or `undefined` for the root locale.
34
34
  */
35
- function localeToLang(locale: string | undefined): string {
35
+ export function localeToLang(locale: string | undefined): string {
36
36
  const lang = locale
37
37
  ? config.locales?.[locale]?.lang
38
38
  : config.locales?.root?.lang;
39
- return lang || 'en';
39
+ const defaultLang =
40
+ config.defaultLocale?.lang || config.defaultLocale?.locale;
41
+ return lang || defaultLang || 'en';
40
42
  }
41
43
 
42
44
  /**
@@ -0,0 +1,58 @@
1
+ import { CollectionEntry, getCollection } from 'astro:content';
2
+ import config from 'virtual:starlight/user-config';
3
+ import builtinTranslations from '../translations';
4
+ import { localeToLang } from './slugs';
5
+
6
+ /** User-configured default locale. */
7
+ const defaultLocale = config.defaultLocale?.locale || 'root';
8
+
9
+ /** All translation data from the i18n collection, keyed by `id`, which matches locale. */
10
+ let userTranslations: Record<string, CollectionEntry<'i18n'>['data']> = {};
11
+ try {
12
+ // Load the user’s i18n collection and ignore the error if it doesn’t exist.
13
+ userTranslations = Object.fromEntries(
14
+ (await getCollection('i18n')).map(({ id, data }) => [id, data] as const)
15
+ );
16
+ } catch {}
17
+
18
+ /** Default map of UI strings based on Starlight and user-configured defaults. */
19
+ const defaults = buildDictionary(
20
+ builtinTranslations.en!,
21
+ userTranslations.en,
22
+ builtinTranslations[defaultLocale],
23
+ userTranslations[defaultLocale]
24
+ );
25
+
26
+ /**
27
+ * Generate a utility function that returns UI strings for the given `locale`.
28
+ * @param {string | undefined} [locale]
29
+ * @example
30
+ * const t = useTranslations('en');
31
+ * const label = t('search.label'); // => 'Search'
32
+ */
33
+ export function useTranslations(locale: string | undefined) {
34
+ // TODO: Use better mapping, e.g. so that `en-GB` matches `en`.
35
+ const lang = localeToLang(locale);
36
+ const dictionary = buildDictionary(
37
+ defaults,
38
+ builtinTranslations[lang],
39
+ userTranslations[lang]
40
+ );
41
+ return (key: keyof typeof dictionary) => dictionary[key];
42
+ }
43
+
44
+ /** Build a dictionary by layering preferred translation sources. */
45
+ function buildDictionary(
46
+ base: (typeof builtinTranslations)[string],
47
+ ...dictionaries: (CollectionEntry<'i18n'>['data'] | undefined)[]
48
+ ) {
49
+ const dictionary = { ...base };
50
+ // Iterate over alternate dictionaries to avoid overwriting preceding values with `undefined`.
51
+ for (const dict of dictionaries) {
52
+ for (const key in dict) {
53
+ const value = dict[key as keyof typeof dict];
54
+ if (value) dictionary[key as keyof typeof dict] = value;
55
+ }
56
+ }
57
+ return dictionary;
58
+ }
@@ -273,7 +273,12 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
273
273
  /** Flag indicating if this site has multiple locales set up. */
274
274
  isMultilingual: false,
275
275
  /** Full locale object for this site’s default language. */
276
- defaultLocale: undefined,
276
+ defaultLocale: {
277
+ label: 'English',
278
+ lang: 'en',
279
+ dir: 'ltr',
280
+ ...locales?.root,
281
+ },
277
282
  locales: undefined,
278
283
  } as const;
279
284
  }