Skip to content

Commit

Permalink
Implement UI string translation using data collections (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
delucis authored May 23, 2023
1 parent 5e82073 commit d3ee6fc
Show file tree
Hide file tree
Showing 37 changed files with 513 additions and 80 deletions.
16 changes: 16 additions & 0 deletions .changeset/afraid-zoos-retire.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions .changeset/light-eggs-relax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@astrojs/starlight": patch
---

Require a minimum Astro version of 2.5.0
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
},
"dependencies": {
"@astrojs/starlight": "workspace:*",
"astro": "^2.4.3"
"astro": "^2.5.0"
},
"devDependencies": {
"@size-limit/file": "^8.2.4",
Expand Down
7 changes: 3 additions & 4 deletions docs/src/content/config.ts
Original file line number Diff line number Diff line change
@@ -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() }),
};
59 changes: 59 additions & 0 deletions docs/src/content/docs/guides/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
```
2 changes: 1 addition & 1 deletion examples/basics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
},
"dependencies": {
"@astrojs/starlight": "^0.0.8",
"astro": "^2.4.1"
"astro": "^2.5.0"
}
}
7 changes: 3 additions & 4 deletions examples/basics/src/content/config.ts
Original file line number Diff line number Diff line change
@@ -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() }),
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 2 additions & 2 deletions packages/starlight/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
5 changes: 4 additions & 1 deletion packages/starlight/components/EditLink.astro
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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>}
9 changes: 8 additions & 1 deletion packages/starlight/components/FallbackContentNotice.astro
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
---
import { useTranslations } from '../utils/translations';
import Icon from './Icon.astro';
interface Props {
locale: string | undefined;
}
const t = useTranslations(Astro.props.locale);
---

<p>
<Icon
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>
Expand Down
4 changes: 2 additions & 2 deletions packages/starlight/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
5 changes: 4 additions & 1 deletion packages/starlight/components/LanguageSelect.astro
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,14 +14,16 @@ interface Props {
function localizedPathname(locale: string | undefined): string {
return localizedUrl(Astro.url, locale).pathname;
}
const t = useTranslations(Astro.props.locale);
---

{
config.isMultilingual && (
<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),
Expand Down
7 changes: 5 additions & 2 deletions packages/starlight/components/LastUpdated.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -24,7 +27,7 @@ try {
{
date && (
<p>
Last updated:{' '}
{t('page.lastUpdated')}
<time datetime={date.toISOString()}>
{date.toLocaleDateString(lang, { dateStyle: 'medium' })}
</time>
Expand Down
9 changes: 8 additions & 1 deletion packages/starlight/components/MobileMenuToggle.astro
Original file line number Diff line number Diff line change
@@ -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"
>
Expand Down
9 changes: 6 additions & 3 deletions packages/starlight/components/PrevNextLinks.astro
Original file line number Diff line number Diff line change
@@ -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}>
Expand All @@ -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>
Expand All @@ -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>
Expand Down
7 changes: 4 additions & 3 deletions packages/starlight/components/RightSidebar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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} />
</>
)
}
Expand Down
25 changes: 20 additions & 5 deletions packages/starlight/components/Search.astro
Original file line number Diff line number Diff line change
@@ -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;">
Expand Down
Loading

0 comments on commit d3ee6fc

Please sign in to comment.