summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2023-05-23 15:18:26 +0200
committerGitHub2023-05-23 15:18:26 +0200
commitd3ee6fc643de7a320a6bb83432cdcfbb0a4e4289 (patch)
tree0de59dc1aa8754007ffa61ac95fcb2bc314ca171
parent5e8207350dba0fce92fa101d311db627e2157654 (diff)
downloadIT.starlight-d3ee6fc643de7a320a6bb83432cdcfbb0a4e4289.tar.gz
IT.starlight-d3ee6fc643de7a320a6bb83432cdcfbb0a4e4289.tar.bz2
IT.starlight-d3ee6fc643de7a320a6bb83432cdcfbb0a4e4289.zip
Implement UI string translation using data collections (#78)
-rw-r--r--.changeset/afraid-zoos-retire.md16
-rw-r--r--.changeset/light-eggs-relax.md5
-rw-r--r--docs/package.json2
-rw-r--r--docs/src/content/config.ts7
-rw-r--r--docs/src/content/docs/guides/i18n.md59
-rw-r--r--examples/basics/package.json2
-rw-r--r--examples/basics/src/content/config.ts7
-rw-r--r--package.json2
-rw-r--r--packages/starlight/404.astro4
-rw-r--r--packages/starlight/components/EditLink.astro5
-rw-r--r--packages/starlight/components/FallbackContentNotice.astro9
-rw-r--r--packages/starlight/components/Header.astro4
-rw-r--r--packages/starlight/components/LanguageSelect.astro5
-rw-r--r--packages/starlight/components/LastUpdated.astro7
-rw-r--r--packages/starlight/components/MobileMenuToggle.astro9
-rw-r--r--packages/starlight/components/PrevNextLinks.astro9
-rw-r--r--packages/starlight/components/RightSidebar.astro7
-rw-r--r--packages/starlight/components/Search.astro25
-rw-r--r--packages/starlight/components/Sidebar.astro8
-rw-r--r--packages/starlight/components/SkipLink.astro12
-rw-r--r--packages/starlight/components/TableOfContents.astro15
-rw-r--r--packages/starlight/components/TableOfContents/MobileTableOfContents.astro14
-rw-r--r--packages/starlight/components/TableOfContents/TableOfContentsList.astro4
-rw-r--r--packages/starlight/components/ThemeSelect.astro20
-rw-r--r--packages/starlight/index.astro18
-rw-r--r--packages/starlight/layout/PageFrame.astro12
-rw-r--r--packages/starlight/package.json4
-rw-r--r--packages/starlight/schema.ts1
-rw-r--r--packages/starlight/schemas/i18n.ts91
-rw-r--r--packages/starlight/translations/de.json20
-rw-r--r--packages/starlight/translations/en.json20
-rw-r--r--packages/starlight/translations/es.json20
-rw-r--r--packages/starlight/translations/index.ts10
-rw-r--r--packages/starlight/utils/slugs.ts6
-rw-r--r--packages/starlight/utils/translations.ts58
-rw-r--r--packages/starlight/utils/user-config.ts7
-rw-r--r--pnpm-lock.yaml69
37 files changed, 513 insertions, 80 deletions
diff --git a/.changeset/afraid-zoos-retire.md b/.changeset/afraid-zoos-retire.md
new file mode 100644
index 00000000..e2a41c27
--- /dev/null
+++ b/.changeset/afraid-zoos-retire.md
@@ -0,0 +1,16 @@
+---
+'@astrojs/starlight': patch
+---
+
+Add support for customising and translating Starlight’s UI.
+
+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:
+
+```json
+{
+ "search.label": "Suchen",
+ "search.shortcutLabel": "(DrĂĽcke / zum Suchen)"
+}
+```
+
+This change also allows Starlight to provide built-in support for more languages than just English and adds German & Spanish support.
diff --git a/.changeset/light-eggs-relax.md b/.changeset/light-eggs-relax.md
new file mode 100644
index 00000000..f10a7525
--- /dev/null
+++ b/.changeset/light-eggs-relax.md
@@ -0,0 +1,5 @@
+---
+"@astrojs/starlight": patch
+---
+
+Require a minimum Astro version of 2.5.0
diff --git a/docs/package.json b/docs/package.json
index 312abf25..a5b8663c 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -15,7 +15,7 @@
},
"dependencies": {
"@astrojs/starlight": "workspace:*",
- "astro": "^2.4.3"
+ "astro": "^2.5.0"
},
"devDependencies": {
"@size-limit/file": "^8.2.4",
diff --git a/docs/src/content/config.ts b/docs/src/content/config.ts
index 4cdd345e..02ea2ac0 100644
--- a/docs/src/content/config.ts
+++ b/docs/src/content/config.ts
@@ -1,8 +1,7 @@
import { defineCollection } from 'astro:content';
-import { docsSchema } from '@astrojs/starlight/schema';
+import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
export const collections = {
- docs: defineCollection({
- schema: docsSchema(),
- }),
+ docs: defineCollection({ schema: docsSchema() }),
+ i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
};
diff --git a/docs/src/content/docs/guides/i18n.md b/docs/src/content/docs/guides/i18n.md
index 13fe9e3f..eb2afc76 100644
--- a/docs/src/content/docs/guides/i18n.md
+++ b/docs/src/content/docs/guides/i18n.md
@@ -92,8 +92,67 @@ When using a `root` locale, place pages for that language directly in `src/conte
- zh/
- index.md
+#### Monolingual sites
+
+If you have a single language site, you can set the root locale to configure its language.
+This allows you to override Starlight’s default language, which is English, but won’t enable other internationalization features like the language picker.
+
## Fallback content
Starlight expects you to create equivalent pages in all your languages. For example, if you have an `en/about.md` file, you should create an `about.md` for each other language you support.
If a translation is not yet available for a language, Starlight will show readers the content for that page in the default language (set via `defaultLocale`). For example, if you have not yet created a French version of an about page and your default language is English, visitors to `/fr/about` will see the English content. This helps you add content in your default language and then progressively translate it when your translators have time.
+
+## Translate Starlight’s UI
+
+Some of Starlight’s default UI requires text labels to work.
+For example, the table of contents on this page has an “On this page” heading in English.
+We aim to ship these labels in as many languages as possible but currently only have support for English, German, and Spanish.
+
+You can provide translations for additional languages you support — or override our default labels — via the `i18n` data collection.
+
+1. Configure the `i18n` data collection in `src/content/config.ts` if it isn’t configured already:
+
+ ```js
+ import { defineCollection } from 'astro:content';
+ import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
+
+ export const collections = {
+ docs: defineCollection({ schema: docsSchema() }),
+ i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
+ };
+ ```
+
+2. Create a JSON file in `src/content/i18n/` for each locale you want to translate Starlight’s UI for.
+ For example, this would add translation files for Arabic and Simplified Chinese:
+
+ - src/
+ - content/
+ - i18n/
+ - ar.json
+ - zh-CN.json
+
+3. Add translations for the keys you want to translate to the JSON files. You can use the English defaults to help you translate:
+
+ ```json
+ {
+ "skipLink.label": "Skip to content",
+ "search.label": "Search",
+ "search.shortcutLabel": "(Press / to Search)",
+ "search.cancelLabel": "Cancel",
+ "themeSelect.accessibleLabel": "Select theme",
+ "themeSelect.dark": "Dark",
+ "themeSelect.light": "Light",
+ "themeSelect.auto": "Auto",
+ "languageSelect.accessibleLabel": "Select language",
+ "menuButton.accessibleLabel": "Menu",
+ "sidebarNav.accessibleLabel": "Main",
+ "tableOfContents.onThisPage": "On this page",
+ "tableOfContents.overview": "Overview",
+ "i18n.untranslatedContent": "This content is not available in your language yet.",
+ "page.editLink": "Edit page",
+ "page.lastUpdated": "Last updated:",
+ "page.previousLink": "Next",
+ "page.nextLink": "Previous"
+ }
+ ```
diff --git a/examples/basics/package.json b/examples/basics/package.json
index c5d7a3b9..4700f196 100644
--- a/examples/basics/package.json
+++ b/examples/basics/package.json
@@ -12,6 +12,6 @@
},
"dependencies": {
"@astrojs/starlight": "^0.0.8",
- "astro": "^2.4.1"
+ "astro": "^2.5.0"
}
}
diff --git a/examples/basics/src/content/config.ts b/examples/basics/src/content/config.ts
index 4cdd345e..02ea2ac0 100644
--- a/examples/basics/src/content/config.ts
+++ b/examples/basics/src/content/config.ts
@@ -1,8 +1,7 @@
import { defineCollection } from 'astro:content';
-import { docsSchema } from '@astrojs/starlight/schema';
+import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
export const collections = {
- docs: defineCollection({
- schema: docsSchema(),
- }),
+ docs: defineCollection({ schema: docsSchema() }),
+ i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
};
diff --git a/package.json b/package.json
index f4cb8e31..ef3d5816 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"devDependencies": {
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.1",
- "astro": "^2.4.3"
+ "astro": "^2.5.0"
},
"packageManager": "pnpm@8.2.0"
}
diff --git a/packages/starlight/404.astro b/packages/starlight/404.astro
index de897a79..00d0cab0 100644
--- a/packages/starlight/404.astro
+++ b/packages/starlight/404.astro
@@ -30,8 +30,8 @@ const { lang = 'en', dir = 'ltr', locale } = config.defaultLocale || {};
</head>
<body>
<ThemeProvider />
- <PageFrame>
- <Header slot="header" locale={locale} />
+ <PageFrame {locale}>
+ <Header slot="header" {locale} />
<main>
<MarkdownContent>
<h1 id="starlight__overview">404</h1>
diff --git a/packages/starlight/components/EditLink.astro b/packages/starlight/components/EditLink.astro
index 833be123..c72c25ac 100644
--- a/packages/starlight/components/EditLink.astro
+++ b/packages/starlight/components/EditLink.astro
@@ -1,12 +1,15 @@
---
import type { CollectionEntry } from 'astro:content';
import config from 'virtual:starlight/user-config';
+import { useTranslations } from '../utils/translations';
interface Props {
data: CollectionEntry<'docs'>['data'];
id: CollectionEntry<'docs'>['id'];
+ locale: string | undefined;
}
+const t = useTranslations(Astro.props.locale);
const { editUrl } = Astro.props.data;
let { baseUrl } = config.editLink;
@@ -20,4 +23,4 @@ const url =
: undefined;
---
-{editUrl !== false && url && <a href={url}>Edit this page</a>}
+{editUrl !== false && url && <a href={url}>{t('page.editLink')}</a>}
diff --git a/packages/starlight/components/FallbackContentNotice.astro b/packages/starlight/components/FallbackContentNotice.astro
index fc05938d..0faf151b 100644
--- a/packages/starlight/components/FallbackContentNotice.astro
+++ b/packages/starlight/components/FallbackContentNotice.astro
@@ -1,5 +1,12 @@
---
+import { useTranslations } from '../utils/translations';
import Icon from './Icon.astro';
+
+interface Props {
+ locale: string | undefined;
+}
+
+const t = useTranslations(Astro.props.locale);
---
<p>
@@ -7,7 +14,7 @@ import Icon from './Icon.astro';
name={'warning'}
size="1.5em"
color="var(--sl-color-orange-high)"
- /><span>This content is not available in your language yet.</span>
+ /><span>{t('i18n.untranslatedContent')}</span>
</p>
<style>
diff --git a/packages/starlight/components/Header.astro b/packages/starlight/components/Header.astro
index 13bf4ba3..1d80e49e 100644
--- a/packages/starlight/components/Header.astro
+++ b/packages/starlight/components/Header.astro
@@ -14,10 +14,10 @@ const { locale } = Astro.props
<div class="header">
<SiteTitle {locale} />
- <Search />
+ <Search {locale} />
<div class="hidden md:flex right-group">
<SocialIcons />
- <ThemeSelect />
+ <ThemeSelect {locale} />
<LanguageSelect {locale}/>
</div>
</div>
diff --git a/packages/starlight/components/LanguageSelect.astro b/packages/starlight/components/LanguageSelect.astro
index ebdb0546..7529a744 100644
--- a/packages/starlight/components/LanguageSelect.astro
+++ b/packages/starlight/components/LanguageSelect.astro
@@ -1,6 +1,7 @@
---
import config from 'virtual:starlight/user-config';
import { localizedUrl } from '../utils/localizedUrl';
+import { useTranslations } from '../utils/translations';
import Select from './Select.astro';
interface Props {
@@ -13,6 +14,8 @@ interface Props {
function localizedPathname(locale: string | undefined): string {
return localizedUrl(Astro.url, locale).pathname;
}
+
+const t = useTranslations(Astro.props.locale);
---
{
@@ -20,7 +23,7 @@ function localizedPathname(locale: string | undefined): string {
<starlight-lang-select>
<Select
icon="translate"
- label="Select language"
+ label={t('languageSelect.accessibleLabel')}
value={localizedPathname(Astro.props.locale)}
options={Object.entries(config.locales).map(([code, locale]) => ({
value: localizedPathname(code),
diff --git a/packages/starlight/components/LastUpdated.astro b/packages/starlight/components/LastUpdated.astro
index 07e30031..349b190e 100644
--- a/packages/starlight/components/LastUpdated.astro
+++ b/packages/starlight/components/LastUpdated.astro
@@ -3,13 +3,16 @@ import type { CollectionEntry } from 'astro:content';
import { fileURLToPath } from 'node:url';
import project from 'virtual:starlight/project-context';
import { getFileCommitDate } from '../utils/git';
+import { useTranslations } from '../utils/translations';
interface Props {
id: CollectionEntry<'docs'>['id'];
lang: string;
+ locale: string | undefined;
}
-const { id, lang } = Astro.props;
+const { id, lang, locale } = Astro.props;
+const t = useTranslations(locale);
const currentFilePath = fileURLToPath(
new URL('src/content/docs/' + id, project.root)
@@ -24,7 +27,7 @@ try {
{
date && (
<p>
- Last updated:{' '}
+ {t('page.lastUpdated')}
<time datetime={date.toISOString()}>
{date.toLocaleDateString(lang, { dateStyle: 'medium' })}
</time>
diff --git a/packages/starlight/components/MobileMenuToggle.astro b/packages/starlight/components/MobileMenuToggle.astro
index b0d77b84..e6092a50 100644
--- a/packages/starlight/components/MobileMenuToggle.astro
+++ b/packages/starlight/components/MobileMenuToggle.astro
@@ -1,11 +1,18 @@
---
import Icon from './Icon.astro';
+import { useTranslations } from '../utils/translations';
+
+interface Props {
+ locale: string | undefined;
+}
+
+const t = useTranslations(Astro.props.locale);
---
<starlight-menu-button>
<button
aria-expanded="false"
- aria-label="Menu"
+ aria-label={t('menuButton.accessibleLabel')}
aria-controls="starlight__sidebar"
class="md:hidden"
>
diff --git a/packages/starlight/components/PrevNextLinks.astro b/packages/starlight/components/PrevNextLinks.astro
index 70dd4856..79b4f8b0 100644
--- a/packages/starlight/components/PrevNextLinks.astro
+++ b/packages/starlight/components/PrevNextLinks.astro
@@ -1,15 +1,18 @@
---
import type { Link } from '../utils/navigation';
+import { useTranslations } from '../utils/translations';
import Icon from './Icon.astro';
interface Props {
prev: Link | undefined;
next: Link | undefined;
dir: 'ltr' | 'rtl';
+ locale: string | undefined;
}
-const { prev, next, dir } = Astro.props;
+const { prev, next, dir, locale } = Astro.props;
const isRtl = dir === 'rtl';
+const t = useTranslations(locale);
---
<div class="pagination-links" dir={dir}>
@@ -18,7 +21,7 @@ const isRtl = dir === 'rtl';
<a href={prev.href} rel="prev">
<Icon name={isRtl ? 'right-arrow' : 'left-arrow'} size="1.5rem" />
<span>
- Previous
+ {t('page.previousLink')}
<br />
<span class="link-title">{prev.label}</span>
</span>
@@ -30,7 +33,7 @@ const isRtl = dir === 'rtl';
<a href={next.href} rel="next">
<Icon name={isRtl ? 'left-arrow' : 'right-arrow'} size="1.5rem" />
<span>
- Next
+ {t('page.nextLink')}
<br />
<span class="link-title">{next.label}</span>
</span>
diff --git a/packages/starlight/components/RightSidebar.astro b/packages/starlight/components/RightSidebar.astro
index 3979e537..d9c73139 100644
--- a/packages/starlight/components/RightSidebar.astro
+++ b/packages/starlight/components/RightSidebar.astro
@@ -9,20 +9,21 @@ import TableOfContents from './TableOfContents.astro';
interface Props {
entry: StarlightDocsEntry;
headings: MarkdownHeading[];
+ locale: string | undefined;
}
-const { entry, headings } = Astro.props;
+const { entry, headings, locale } = Astro.props;
---
<RightSidebarPanel>
- <TableOfContents headings={headings} />
+ <TableOfContents {headings} {locale} />
</RightSidebarPanel>
<RightSidebarPanel>
{
config.editLink.baseUrl && (
<>
<h2>Contribute</h2>
- <EditLink data={entry.data} id={entry.id} />
+ <EditLink data={entry.data} id={entry.id} {locale} />
</>
)
}
diff --git a/packages/starlight/components/Search.astro b/packages/starlight/components/Search.astro
index ec51c448..a9c0ef73 100644
--- a/packages/starlight/components/Search.astro
+++ b/packages/starlight/components/Search.astro
@@ -1,22 +1,37 @@
---
import '@pagefind/default-ui/css/ui.css';
+import { useTranslations } from '../utils/translations';
import Icon from './Icon.astro';
+
+interface Props {
+ locale: string | undefined;
+}
+
+const t = useTranslations(Astro.props.locale);
---
<site-search>
<button data-open-modal disabled>
- <Icon name="magnifier" label="Search" />
- <span class="hidden md:block" aria-hidden="true">Search</span>
+ {
+ /* The span is `aria-hidden` because it is not shown on small screens. Instead, the icon label is used for accessibility purposes. */
+ }
+ <Icon name="magnifier" label={t('search.label')} />
+ <span class="hidden md:block" aria-hidden="true">{t('search.label')}</span>
<Icon
name="forward-slash"
class="hidden md:block"
- label="(Press / to search)"
+ label={t('search.shortcutLabel')}
/>
</button>
- <dialog style="padding:0" aria-label="Search the documentation">
+ <dialog style="padding:0" aria-label={t('search.label')}>
<div class="dialog-frame">
- <button data-close-modal class="flex md:hidden">Cancel</button>
+ {
+ /* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
+ }
+ <button data-close-modal class="flex md:hidden">
+ {t('search.cancelLabel')}
+ </button>
{
import.meta.env.DEV ? (
<div style="margin: auto; text-align: center;">
diff --git a/packages/starlight/components/Sidebar.astro b/packages/starlight/components/Sidebar.astro
index cd514ffc..4bb4b53c 100644
--- a/packages/starlight/components/Sidebar.astro
+++ b/packages/starlight/components/Sidebar.astro
@@ -8,13 +8,15 @@ interface Props {
sidebar: ReturnType<typeof getSidebar>;
locale: string | undefined;
}
+
+const { sidebar, locale } = Astro.props;
---
<div class="sidebar flex">
- <SidebarSublist sublist={Astro.props.sidebar} />
+ <SidebarSublist sublist={sidebar} />
<div class="mobile-preferences flex md:hidden">
- <ThemeSelect />
- <LanguageSelect locale={Astro.props.locale} />
+ <ThemeSelect {locale} />
+ <LanguageSelect {locale} />
</div>
</div>
diff --git a/packages/starlight/components/SkipLink.astro b/packages/starlight/components/SkipLink.astro
index 29120f89..31b38e70 100644
--- a/packages/starlight/components/SkipLink.astro
+++ b/packages/starlight/components/SkipLink.astro
@@ -1,4 +1,14 @@
-<a href="#starlight__overview">Skip to content</a>
+---
+import { useTranslations } from '../utils/translations';
+
+interface Props {
+ locale: string | undefined;
+}
+
+const t = useTranslations(Astro.props.locale);
+---
+
+<a href="#starlight__overview">{t('skipLink.label')}</a>
<style>
a {
diff --git a/packages/starlight/components/TableOfContents.astro b/packages/starlight/components/TableOfContents.astro
index eff17903..f54483ce 100644
--- a/packages/starlight/components/TableOfContents.astro
+++ b/packages/starlight/components/TableOfContents.astro
@@ -1,14 +1,21 @@
---
import type { MarkdownHeading } from 'astro';
import config from 'virtual:starlight/user-config';
-import { generateToC } from './TableOfContents/generateToC';
+import { useTranslations } from '../utils/translations';
import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
+import { generateToC } from './TableOfContents/generateToC';
interface Props {
headings: MarkdownHeading[];
+ locale: string | undefined;
}
-const toc = generateToC(Astro.props.headings, config.tableOfContents);
+const { locale, headings } = Astro.props;
+const t = useTranslations(locale);
+const toc = generateToC(headings, {
+ ...config.tableOfContents,
+ title: t('tableOfContents.overview'),
+});
---
<starlight-toc
@@ -16,7 +23,9 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
data-max-h={config.tableOfContents.maxHeadingLevel}
>
<nav aria-labelledby="starlight__on-this-page">
- <h2 id="starlight__on-this-page">On this page</h2>
+ <h2 id="starlight__on-this-page">
+ {t('tableOfContents.onThisPage')}
+ </h2>
<TableOfContentsList toc={toc} />
</nav>
</starlight-toc>
diff --git a/packages/starlight/components/TableOfContents/MobileTableOfContents.astro b/packages/starlight/components/TableOfContents/MobileTableOfContents.astro
index 14efd35c..1ed6600d 100644
--- a/packages/starlight/components/TableOfContents/MobileTableOfContents.astro
+++ b/packages/starlight/components/TableOfContents/MobileTableOfContents.astro
@@ -1,15 +1,21 @@
---
import type { MarkdownHeading } from 'astro';
import config from 'virtual:starlight/user-config';
-import { generateToC } from './generateToC';
-import TableOfContentsList from './TableOfContentsList.astro';
+import { useTranslations } from '../../utils/translations';
import Icon from '../Icon.astro';
+import TableOfContentsList from './TableOfContentsList.astro';
+import { generateToC } from './generateToC';
interface Props {
headings: MarkdownHeading[];
+ locale: string | undefined;
}
-const toc = generateToC(Astro.props.headings, config.tableOfContents);
+const t = useTranslations(Astro.props.locale);
+const toc = generateToC(Astro.props.headings, {
+ ...config.tableOfContents,
+ title: t('tableOfContents.overview'),
+});
---
<mobile-starlight-toc
@@ -20,7 +26,7 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
<details id="starlight__mobile-toc">
<summary id="starlight__on-this-page--mobile" class="flex">
<div class="toggle flex">
- On this page
+ {t('tableOfContents.onThisPage')}
<Icon name={'right-caret'} class="caret" size="1rem" />
</div>
<span class="display-current">{toc[0]?.text}</span>
diff --git a/packages/starlight/components/TableOfContents/TableOfContentsList.astro b/packages/starlight/components/TableOfContents/TableOfContentsList.astro
index 83adf903..f8972d7d 100644
--- a/packages/starlight/components/TableOfContents/TableOfContentsList.astro
+++ b/packages/starlight/components/TableOfContents/TableOfContentsList.astro
@@ -1,8 +1,8 @@
---
-import type { generateToC } from './generateToC';
+import type { TocItem } from './generateToC';
interface Props {
- toc: ReturnType<typeof generateToC>;
+ toc: TocItem[];
depth?: number;
isMobile?: boolean;
}
diff --git a/packages/starlight/components/ThemeSelect.astro b/packages/starlight/components/ThemeSelect.astro
index 246747bd..aa408512 100644
--- a/packages/starlight/components/ThemeSelect.astro
+++ b/packages/starlight/components/ThemeSelect.astro
@@ -1,18 +1,28 @@
---
+import { useTranslations } from '../utils/translations';
import Select from './Select.astro';
+
+interface Props {
+ locale: string | undefined;
+}
+
+const t = useTranslations(Astro.props.locale);
---
<starlight-theme-select>
+ {
+ /* TODO: Can we give this select a width that works well for each language’s strings? */
+ }
<Select
icon="laptop"
- label="Select theme"
+ label={t('themeSelect.accessibleLabel')}
value="auto"
options={[
- { label: 'Dark', selected: false, value: 'dark' },
- { label: 'Light', selected: false, value: 'light' },
- { label: 'Auto', selected: true, value: 'auto' },
+ { label: t('themeSelect.dark'), selected: false, value: 'dark' },
+ { label: t('themeSelect.light'), selected: false, value: 'light' },
+ { label: t('themeSelect.auto'), selected: true, value: 'auto' },
]}
- width="5.5em"
+ width="6.25em"
/>
</starlight-theme-select>
diff --git a/packages/starlight/index.astro b/packages/starlight/index.astro
index bf73a6f4..cea4d2e4 100644
--- a/packages/starlight/index.astro
+++ b/packages/starlight/index.astro
@@ -64,13 +64,13 @@ const prevNextLinks = getPrevNextLinks(sidebar);
</head>
<body>
<ThemeProvider />
- <SkipLink />
- <PageFrame>
- <Header slot="header" locale={locale} />
- <Sidebar slot="sidebar" sidebar={sidebar} locale={locale} />
- <MobileTableOfContents headings={headings} />
+ <SkipLink {locale} />
+ <PageFrame {locale}>
+ <Header slot="header" {locale} />
+ <Sidebar slot="sidebar" {sidebar} {locale} />
+ <MobileTableOfContents {headings} {locale} />
<TwoColumnContent>
- <RightSidebar slot="right-sidebar" entry={entry} headings={headings} />
+ <RightSidebar slot="right-sidebar" {entry} {headings} {locale} />
<main data-pagefind-body lang={entryMeta.lang} dir={entryMeta.dir}>
<ContentPanel>
<h1
@@ -79,13 +79,13 @@ const prevNextLinks = getPrevNextLinks(sidebar);
>
{entry.data.title}
</h1>
- {isFallback && <FallbackContentNotice />}
+ {isFallback && <FallbackContentNotice {locale} />}
</ContentPanel>
<ContentPanel>
<MarkdownContent><Content /></MarkdownContent>
<footer>
- <LastUpdated id={entry.id} lang={lang} />
- <PrevNextLinks {...prevNextLinks} dir={dir} />
+ <LastUpdated id={entry.id} {lang} {locale} />
+ <PrevNextLinks {...prevNextLinks} {dir} {locale} />
</footer>
</ContentPanel>
</main>
diff --git a/packages/starlight/layout/PageFrame.astro b/packages/starlight/layout/PageFrame.astro
index b250175b..6aa7bc2c 100644
--- a/packages/starlight/layout/PageFrame.astro
+++ b/packages/starlight/layout/PageFrame.astro
@@ -1,5 +1,13 @@
---
import MobileMenuToggle from '../components/MobileMenuToggle.astro';
+import { useTranslations } from '../utils/translations';
+
+interface Props {
+ locale: string | undefined;
+}
+
+const { locale } = Astro.props;
+const t = useTranslations(locale);
const hasSidebar = Astro.slots.has('sidebar');
---
@@ -8,8 +16,8 @@ const hasSidebar = Astro.slots.has('sidebar');
<header class="header"><slot name="header" /></header>
{
hasSidebar && (
- <nav class="sidebar" aria-label="Main">
- <MobileMenuToggle />
+ <nav class="sidebar" aria-label={t('sidebarNav.accessibleLabel')}>
+ <MobileMenuToggle {locale} />
<div id="starlight__sidebar" class="sidebar-pane">
<div class="sidebar-content">
<slot name="sidebar" />
diff --git a/packages/starlight/package.json b/packages/starlight/package.json
index d2e9b294..a76bfa7e 100644
--- a/packages/starlight/package.json
+++ b/packages/starlight/package.json
@@ -28,11 +28,11 @@
"./404.astro": "./404.astro"
},
"peerDependencies": {
- "astro": "^2.4.3"
+ "astro": "^2.5.0"
},
"devDependencies": {
"@types/node": "^18.15.11",
- "astro": "^2.4.3"
+ "astro": "^2.5.0"
},
"dependencies": {
"@astrojs/mdx": "^0.19.1",
diff --git a/packages/starlight/schema.ts b/packages/starlight/schema.ts
index 0da35089..451b0406 100644
--- a/packages/starlight/schema.ts
+++ b/packages/starlight/schema.ts
@@ -1,5 +1,6 @@
import { z } from 'astro/zod';
import { HeadConfigSchema } from './schemas/head';
+export { i18nSchema } from './schemas/i18n';
export function docsSchema() {
return z.object({
diff --git a/packages/starlight/schemas/i18n.ts b/packages/starlight/schemas/i18n.ts
new file mode 100644
index 00000000..60f76a8c
--- /dev/null
+++ b/packages/starlight/schemas/i18n.ts
@@ -0,0 +1,91 @@
+import { z } from 'astro/zod';
+
+export function i18nSchema() {
+ return z
+ .object({
+ 'skipLink.label': z
+ .string()
+ .describe(
+ 'Text displayed in the accessible “Skip link” when a keyboard user first tabs into a page.'
+ ),
+
+ 'search.label': z.string().describe('Text displayed in the search bar.'),
+
+ 'search.shortcutLabel': z
+ .string()
+ .describe(
+ 'Accessible label for the shortcut key to open the search modal.'
+ ),
+
+ 'search.cancelLabel': z
+ .string()
+ .describe('Text for the “Cancel” button that closes the search modal.'),
+
+ 'themeSelect.accessibleLabel': z
+ .string()
+ .describe('Accessible label for the theme selection dropdown.'),
+
+ 'themeSelect.dark': z.string().describe('Name of the dark color theme.'),
+
+ 'themeSelect.light': z
+ .string()
+ .describe('Name of the light color theme.'),
+
+ 'themeSelect.auto': z
+ .string()
+ .describe(
+ 'Name of the automatic color theme that syncs with system preferences.'
+ ),
+
+ 'languageSelect.accessibleLabel': z
+ .string()
+ .describe('Accessible label for the language selection dropdown.'),
+
+ 'menuButton.accessibleLabel': z
+ .string()
+ .describe('Accessible label for he mobile menu button.'),
+
+ 'sidebarNav.accessibleLabel': z
+ .string()
+ .describe(
+ 'Accessible label for the main sidebar `<nav>` element to distinguish it fom other `<nav>` landmarks on the page.'
+ ),
+
+ 'tableOfContents.onThisPage': z
+ .string()
+ .describe('Title for the table of contents component.'),
+
+ 'tableOfContents.overview': z
+ .string()
+ .describe(
+ 'Label used for the first link in the table of contents, linking to the page title.'
+ ),
+
+ 'i18n.untranslatedContent': z
+ .string()
+ .describe(
+ 'Notice informing users they are on a page that is not yet translated to their language.'
+ ),
+
+ 'page.editLink': z.string().describe('Text for the link to edit a page.'),
+
+ 'page.lastUpdated': z
+ .string()
+ .describe(
+ 'Text displayed in front of the last updated date in the page footer.'
+ ),
+
+ 'page.previousLink': z
+ .string()
+ .describe(
+ 'Label shown on the “previous page” pagination arrow in the page footer.'
+ ),
+
+ 'page.nextLink': z
+ .string()
+ .describe(
+ 'Label shown on the “next page” pagination arrow in the page footer.'
+ ),
+ })
+ .partial();
+}
diff --git a/packages/starlight/translations/de.json b/packages/starlight/translations/de.json
new file mode 100644
index 00000000..ca4c4035
--- /dev/null
+++ b/packages/starlight/translations/de.json
@@ -0,0 +1,20 @@
+{
+ "skipLink.label": "Zum Inhalt springen",
+ "search.label": "Suchen",
+ "search.shortcutLabel": "(DrĂĽcke / zum Suchen)",
+ "search.cancelLabel": "Abbrechen",
+ "themeSelect.accessibleLabel": "Farbschema wählen",
+ "themeSelect.dark": "Dunkel",
+ "themeSelect.light": "Hell",
+ "themeSelect.auto": "System",
+ "languageSelect.accessibleLabel": "Sprache wählen",
+ "menuButton.accessibleLabel": "MenĂĽ",
+ "sidebarNav.accessibleLabel": "Hauptnavigation",
+ "tableOfContents.onThisPage": "Auf dieser Seite",
+ "tableOfContents.overview": "Ăśberblick",
+ "i18n.untranslatedContent": "Dieser Inhalt ist noch nicht in deiner Sprache verfĂĽgbar.",
+ "page.editLink": "Seite bearbeiten",
+ "page.lastUpdated": "Zuletzt bearbeitet:",
+ "page.previousLink": "Vorherige Seite",
+ "page.nextLink": "Nächste Seite"
+}
diff --git a/packages/starlight/translations/en.json b/packages/starlight/translations/en.json
new file mode 100644
index 00000000..b8a74860
--- /dev/null
+++ b/packages/starlight/translations/en.json
@@ -0,0 +1,20 @@
+{
+ "skipLink.label": "Skip to content",
+ "search.label": "Search",
+ "search.shortcutLabel": "(Press / to Search)",
+ "search.cancelLabel": "Cancel",
+ "themeSelect.accessibleLabel": "Select theme",
+ "themeSelect.dark": "Dark",
+ "themeSelect.light": "Light",
+ "themeSelect.auto": "Auto",
+ "languageSelect.accessibleLabel": "Select language",
+ "menuButton.accessibleLabel": "Menu",
+ "sidebarNav.accessibleLabel": "Main",
+ "tableOfContents.onThisPage": "On this page",
+ "tableOfContents.overview": "Overview",
+ "i18n.untranslatedContent": "This content is not available in your language yet.",
+ "page.editLink": "Edit page",
+ "page.lastUpdated": "Last updated:",
+ "page.previousLink": "Next",
+ "page.nextLink": "Previous"
+}
diff --git a/packages/starlight/translations/es.json b/packages/starlight/translations/es.json
new file mode 100644
index 00000000..d92186eb
--- /dev/null
+++ b/packages/starlight/translations/es.json
@@ -0,0 +1,20 @@
+{
+ "skipLink.label": "Saltearse al contenido",
+ "search.label": "Buscar",
+ "search.shortcutLabel": "(Presiona / para buscar)",
+ "search.cancelLabel": "Interrumpir",
+ "themeSelect.accessibleLabel": "Seleccionar tema",
+ "themeSelect.dark": "Oscuro",
+ "themeSelect.light": "Claro",
+ "themeSelect.auto": "Automático",
+ "languageSelect.accessibleLabel": "Seleccionar idioma",
+ "menuButton.accessibleLabel": "MenĂş",
+ "sidebarNav.accessibleLabel": "Primario",
+ "tableOfContents.onThisPage": "En esta página",
+ "tableOfContents.overview": "Sinopsis",
+ "i18n.untranslatedContent": "Esta página aún no está disponible en tu idioma.",
+ "page.editLink": "Edita esta página",
+ "page.lastUpdated": "Ăšltima actualizaciĂłn:",
+ "page.previousLink": "Página anterior",
+ "page.nextLink": "Siguiente página"
+}
diff --git a/packages/starlight/translations/index.ts b/packages/starlight/translations/index.ts
new file mode 100644
index 00000000..ceff3527
--- /dev/null
+++ b/packages/starlight/translations/index.ts
@@ -0,0 +1,10 @@
+import { i18nSchema } from '../schemas/i18n';
+import en from './en.json';
+import es from './es.json';
+import de from './de.json';
+
+const parse = i18nSchema().required().strict().parse;
+
+export default Object.fromEntries(
+ Object.entries({ en, es, de }).map(([key, dict]) => [key, parse(dict)])
+);
diff --git a/packages/starlight/utils/slugs.ts b/packages/starlight/utils/slugs.ts
index 65b98e63..9fce40e6 100644
--- a/packages/starlight/utils/slugs.ts
+++ b/packages/starlight/utils/slugs.ts
@@ -32,11 +32,13 @@ export function slugToLocaleData(slug: string): LocaleData {
* Get the BCP-47 language tag for the given locale.
* @param locale Locale string or `undefined` for the root locale.
*/
-function localeToLang(locale: string | undefined): string {
+export function localeToLang(locale: string | undefined): string {
const lang = locale
? config.locales?.[locale]?.lang
: config.locales?.root?.lang;
- return lang || 'en';
+ const defaultLang =
+ config.defaultLocale?.lang || config.defaultLocale?.locale;
+ return lang || defaultLang || 'en';
}
/**
diff --git a/packages/starlight/utils/translations.ts b/packages/starlight/utils/translations.ts
new file mode 100644
index 00000000..96ef28ab
--- /dev/null
+++ b/packages/starlight/utils/translations.ts
@@ -0,0 +1,58 @@
+import { CollectionEntry, getCollection } from 'astro:content';
+import config from 'virtual:starlight/user-config';
+import builtinTranslations from '../translations';
+import { localeToLang } from './slugs';
+
+/** User-configured default locale. */
+const defaultLocale = config.defaultLocale?.locale || 'root';
+
+/** All translation data from the i18n collection, keyed by `id`, which matches locale. */
+let userTranslations: Record<string, CollectionEntry<'i18n'>['data']> = {};
+try {
+ // Load the user’s i18n collection and ignore the error if it doesn’t exist.
+ userTranslations = Object.fromEntries(
+ (await getCollection('i18n')).map(({ id, data }) => [id, data] as const)
+ );
+} catch {}
+
+/** Default map of UI strings based on Starlight and user-configured defaults. */
+const defaults = buildDictionary(
+ builtinTranslations.en!,
+ userTranslations.en,
+ builtinTranslations[defaultLocale],
+ userTranslations[defaultLocale]
+);
+
+/**
+ * Generate a utility function that returns UI strings for the given `locale`.
+ * @param {string | undefined} [locale]
+ * @example
+ * const t = useTranslations('en');
+ * const label = t('search.label'); // => 'Search'
+ */
+export function useTranslations(locale: string | undefined) {
+ // TODO: Use better mapping, e.g. so that `en-GB` matches `en`.
+ const lang = localeToLang(locale);
+ const dictionary = buildDictionary(
+ defaults,
+ builtinTranslations[lang],
+ userTranslations[lang]
+ );
+ return (key: keyof typeof dictionary) => dictionary[key];
+}
+
+/** Build a dictionary by layering preferred translation sources. */
+function buildDictionary(
+ base: (typeof builtinTranslations)[string],
+ ...dictionaries: (CollectionEntry<'i18n'>['data'] | undefined)[]
+) {
+ const dictionary = { ...base };
+ // Iterate over alternate dictionaries to avoid overwriting preceding values with `undefined`.
+ for (const dict of dictionaries) {
+ for (const key in dict) {
+ const value = dict[key as keyof typeof dict];
+ if (value) dictionary[key as keyof typeof dict] = value;
+ }
+ }
+ return dictionary;
+}
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index 0b38dd67..9aea7325 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -273,7 +273,12 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
/** Flag indicating if this site has multiple locales set up. */
isMultilingual: false,
/** Full locale object for this site’s default language. */
- defaultLocale: undefined,
+ defaultLocale: {
+ label: 'English',
+ lang: 'en',
+ dir: 'ltr',
+ ...locales?.root,
+ },
locales: undefined,
} as const;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4bf4f06a..640dd460 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,8 +11,8 @@ importers:
specifier: ^2.26.1
version: 2.26.1
astro:
- specifier: ^2.4.3
- version: 2.4.3(@types/node@18.15.11)
+ specifier: ^2.5.0
+ version: 2.5.0(@types/node@18.15.11)
docs:
dependencies:
@@ -20,8 +20,8 @@ importers:
specifier: workspace:*
version: link:../packages/starlight
astro:
- specifier: ^2.4.3
- version: 2.4.3(@types/node@18.15.11)
+ specifier: ^2.5.0
+ version: 2.5.0(@types/node@18.15.11)
devDependencies:
'@size-limit/file':
specifier: ^8.2.4
@@ -40,7 +40,7 @@ importers:
dependencies:
'@astrojs/mdx':
specifier: ^0.19.1
- version: 0.19.1(astro@2.4.3)(rollup@3.21.5)
+ version: 0.19.1(astro@2.5.0)(rollup@3.21.5)
'@astrojs/sitemap':
specifier: ^1.3.1
version: 1.3.1
@@ -85,8 +85,8 @@ importers:
specifier: ^18.15.11
version: 18.15.11
astro:
- specifier: ^2.4.3
- version: 2.4.3(@types/node@18.15.11)
+ specifier: ^2.5.0
+ version: 2.5.0(@types/node@18.15.11)
packages:
@@ -118,13 +118,36 @@ packages:
vscode-languageserver-types: 3.17.3
vscode-uri: 3.0.7
- /@astrojs/markdown-remark@2.2.0(astro@2.4.3):
+ /@astrojs/markdown-remark@2.2.0(astro@2.5.0):
resolution: {integrity: sha512-4M1+GzQwDqF0KfX9Ahug43b0avorcK+iTapEaVuNnaCUVS6sZKRkztT3g6hmXiFmGHSL8qYaS9IVEmKtP6hYmw==}
peerDependencies:
astro: ^2.4.0
dependencies:
'@astrojs/prism': 2.1.1
- astro: 2.4.3(@types/node@18.15.11)
+ astro: 2.5.0(@types/node@18.15.11)
+ github-slugger: 1.5.0
+ import-meta-resolve: 2.2.2
+ rehype-raw: 6.1.1
+ rehype-stringify: 9.0.3
+ remark-gfm: 3.0.1
+ remark-parse: 10.0.1
+ remark-rehype: 10.1.0
+ remark-smartypants: 2.0.0
+ shiki: 0.14.2
+ unified: 10.1.2
+ unist-util-visit: 4.1.2
+ vfile: 5.3.7
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
+ /@astrojs/markdown-remark@2.2.1(astro@2.5.0):
+ resolution: {integrity: sha512-VF0HRv4GpC1XEMLnsKf6jth7JSmlt9qpqP0josQgA2eSpCIAC/Et+y94mgdBIZVBYH/yFnMoIxgKVe93xfO2GA==}
+ peerDependencies:
+ astro: ^2.5.0
+ dependencies:
+ '@astrojs/prism': 2.1.2
+ astro: 2.5.0(@types/node@18.15.11)
github-slugger: 1.5.0
import-meta-resolve: 2.2.2
rehype-raw: 6.1.1
@@ -140,11 +163,11 @@ packages:
transitivePeerDependencies:
- supports-color
- /@astrojs/mdx@0.19.1(astro@2.4.3)(rollup@3.21.5):
+ /@astrojs/mdx@0.19.1(astro@2.5.0)(rollup@3.21.5):
resolution: {integrity: sha512-9GNNZbGT+lGvbRkQK/NaEJcnjj1T94/ne0KwPjJgNCBQrJuskX5IW1hKiE5bRSOFvkAOrBGneYKg0GXYArBOQQ==}
engines: {node: '>=16.12.0'}
dependencies:
- '@astrojs/markdown-remark': 2.2.0(astro@2.4.3)
+ '@astrojs/markdown-remark': 2.2.0(astro@2.5.0)
'@astrojs/prism': 2.1.1
'@mdx-js/mdx': 2.3.0
'@mdx-js/rollup': 2.3.0(rollup@3.21.5)
@@ -173,6 +196,13 @@ packages:
engines: {node: '>=16.12.0'}
dependencies:
prismjs: 1.29.0
+ dev: false
+
+ /@astrojs/prism@2.1.2:
+ resolution: {integrity: sha512-3antim1gb34689GHRQFJ88JEo93HuZKQBnmxDT5W/nxiNz1p/iRxnCTEhIbJhqMOTRbbo5h2ldm5qSxx+TMFQA==}
+ engines: {node: '>=16.12.0'}
+ dependencies:
+ prismjs: 1.29.0
/@astrojs/sitemap@1.3.1:
resolution: {integrity: sha512-4ZBug4ml+2Nl5/Uh4VSja8Kij/DU7/RaNMciXCNm1EzQkP/jm+nqMG1liDDcQK5zXPAoLeaat06IbhNlruvQjg==}
@@ -1196,6 +1226,9 @@ packages:
dependencies:
sprintf-js: 1.0.3
+ /argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
/array-buffer-byte-length@1.0.0:
resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==}
dependencies:
@@ -1243,8 +1276,8 @@ packages:
hasBin: true
dev: false
- /astro@2.4.3(@types/node@18.15.11):
- resolution: {integrity: sha512-WU7sMkgFNQs4WZzEmpjOYZthcT8+LSmwIR0GvWzVYlb+dIMfFCQyg99LNHdhg/XZKi08ztaHmRf4ZBjJvZHsgA==}
+ /astro@2.5.0(@types/node@18.15.11):
+ resolution: {integrity: sha512-dZZuK2vEpfinbVALthUW31NOVUFPobgyi0+2PR3FH3diy6X9HBw1PLbS5wRsWOKaEXRbzxBkXc39Rbm0yRzKaA==}
engines: {node: '>=16.12.0', npm: '>=6.14.0'}
hasBin: true
peerDependencies:
@@ -1255,7 +1288,7 @@ packages:
dependencies:
'@astrojs/compiler': 1.4.1
'@astrojs/language-server': 1.0.4
- '@astrojs/markdown-remark': 2.2.0(astro@2.4.3)
+ '@astrojs/markdown-remark': 2.2.1(astro@2.5.0)
'@astrojs/telemetry': 2.1.1
'@astrojs/webapi': 2.1.1
'@babel/core': 7.21.3
@@ -1277,12 +1310,14 @@ packages:
devalue: 4.3.0
diff: 5.1.0
es-module-lexer: 1.2.0
+ esbuild: 0.17.18
estree-walker: 3.0.0
execa: 6.1.0
fast-glob: 3.2.12
github-slugger: 2.0.0
gray-matter: 4.0.3
html-escaper: 3.0.3
+ js-yaml: 4.1.0
kleur: 4.1.5
magic-string: 0.27.0
mime: 3.0.0
@@ -2930,6 +2965,12 @@ packages:
argparse: 1.0.10
esprima: 4.0.1
+ /js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+ dependencies:
+ argparse: 2.0.1
+
/jsesc@2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'}