summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2023-11-29 20:25:09 +0100
committerGitHub2023-11-29 20:25:09 +0100
commite5a863a98b2e5335e122ca440dcb84e9426939b4 (patch)
treec69eea637435d2849f875c788cdd2406686551f8
parent64d25c125c80375ff7b2cbf749f3c35d49b8bc98 (diff)
downloadIT.starlight-e5a863a98b2e5335e122ca440dcb84e9426939b4.tar.gz
IT.starlight-e5a863a98b2e5335e122ca440dcb84e9426939b4.tar.bz2
IT.starlight-e5a863a98b2e5335e122ca440dcb84e9426939b4.zip
Expose localized UI strings in route data (#1135)
-rw-r--r--.changeset/pink-mirrors-cough.md7
-rw-r--r--docs/src/content/docs/reference/overrides.md6
-rw-r--r--packages/starlight/404.astro2
-rw-r--r--packages/starlight/__tests__/basics/route-data.test.ts10
-rw-r--r--packages/starlight/__tests__/i18n/route-data.test.ts32
-rw-r--r--packages/starlight/__tests__/i18n/translations.test.ts8
-rw-r--r--packages/starlight/components/EditLink.astro6
-rw-r--r--packages/starlight/components/FallbackContentNotice.astro5
-rw-r--r--packages/starlight/components/LanguageSelect.astro5
-rw-r--r--packages/starlight/components/LastUpdated.astro6
-rw-r--r--packages/starlight/components/MobileMenuToggle.astro6
-rw-r--r--packages/starlight/components/MobileTableOfContents.astro6
-rw-r--r--packages/starlight/components/PageFrame.astro9
-rw-r--r--packages/starlight/components/Pagination.astro8
-rw-r--r--packages/starlight/components/Search.astro26
-rw-r--r--packages/starlight/components/SkipLink.astro5
-rw-r--r--packages/starlight/components/TableOfContents.astro6
-rw-r--r--packages/starlight/components/ThemeSelect.astro11
-rw-r--r--packages/starlight/utils/createTranslationSystem.ts11
-rw-r--r--packages/starlight/utils/route-data.ts3
20 files changed, 110 insertions, 68 deletions
diff --git a/.changeset/pink-mirrors-cough.md b/.changeset/pink-mirrors-cough.md
new file mode 100644
index 00000000..c43df8ed
--- /dev/null
+++ b/.changeset/pink-mirrors-cough.md
@@ -0,0 +1,7 @@
+---
+'@astrojs/starlight': minor
+---
+
+Exposes localized UI strings in route data
+
+Component overrides can now access a `labels` object in their props which includes all the localized UI strings for the current page. \ No newline at end of file
diff --git a/docs/src/content/docs/reference/overrides.md b/docs/src/content/docs/reference/overrides.md
index 387e485d..9bd6cc9c 100644
--- a/docs/src/content/docs/reference/overrides.md
+++ b/docs/src/content/docs/reference/overrides.md
@@ -135,6 +135,12 @@ JavaScript `Date` object representing when this page was last updated if enabled
`URL` object for the address where this page can be edited if enabled.
+#### `labels`
+
+**Type:** `Record<string, string>`
+
+An object containing UI strings localized for the current page. See the [“Translate Starlight’s UI”](/guides/i18n/#translate-starlights-ui) guide for a list of all the available keys.
+
---
## Components
diff --git a/packages/starlight/404.astro b/packages/starlight/404.astro
index 913c5143..61358234 100644
--- a/packages/starlight/404.astro
+++ b/packages/starlight/404.astro
@@ -26,7 +26,7 @@ const fallbackEntry: StarlightDocsEntry = {
head: [],
hero: { tagline: t('404.text'), actions: [] },
pagefind: false,
- sidebar: { hidden: false },
+ sidebar: { hidden: false, attrs: {} },
},
render: async () => ({
Content: EmptyContent,
diff --git a/packages/starlight/__tests__/basics/route-data.test.ts b/packages/starlight/__tests__/basics/route-data.test.ts
index 13522d02..eacd5334 100644
--- a/packages/starlight/__tests__/basics/route-data.test.ts
+++ b/packages/starlight/__tests__/basics/route-data.test.ts
@@ -85,3 +85,13 @@ test('uses explicit last updated date from frontmatter', () => {
expect(data.lastUpdated).toBeInstanceOf(Date);
expect(data.lastUpdated).toEqual(route.entry.data.lastUpdated);
});
+
+test('includes localized labels', () => {
+ const route = routes[0]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
+ url: new URL('https://example.com'),
+ });
+ expect(data.labels).toBeDefined();
+ expect(data.labels['skipLink.label']).toBe('Skip to content');
+});
diff --git a/packages/starlight/__tests__/i18n/route-data.test.ts b/packages/starlight/__tests__/i18n/route-data.test.ts
new file mode 100644
index 00000000..57beed0c
--- /dev/null
+++ b/packages/starlight/__tests__/i18n/route-data.test.ts
@@ -0,0 +1,32 @@
+import { expect, test, vi } from 'vitest';
+import { generateRouteData } from '../../utils/route-data';
+import { routes } from '../../utils/routing';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [
+ ['fr/index.mdx', { title: 'Accueil' }],
+ ['pt-br/index.mdx', { title: 'Pagina inicial' }],
+ ],
+ })
+);
+
+test('includes localized labels (fr)', () => {
+ const route = routes[0]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
+ url: new URL('https://example.com'),
+ });
+ expect(data.labels).toBeDefined();
+ expect(data.labels['skipLink.label']).toBe('Aller au contenu');
+});
+
+test('includes localized labels (pt-br)', () => {
+ const route = routes[1]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
+ url: new URL('https://example.com'),
+ });
+ expect(data.labels).toBeDefined();
+ expect(data.labels['skipLink.label']).toBe('Pular para o conteúdo');
+});
diff --git a/packages/starlight/__tests__/i18n/translations.test.ts b/packages/starlight/__tests__/i18n/translations.test.ts
index a8442166..1e9fd032 100644
--- a/packages/starlight/__tests__/i18n/translations.test.ts
+++ b/packages/starlight/__tests__/i18n/translations.test.ts
@@ -22,14 +22,6 @@ describe('useTranslations()', () => {
expect(t('page.editLink')).toBe(translations.en?.['page.editLink']);
});
- test('returns a pick method for filtering by key', () => {
- const t = useTranslations('en');
- expect(t.pick('tableOfContents.')).toEqual({
- 'tableOfContents.onThisPage': 'On this page',
- 'tableOfContents.overview': 'Overview',
- });
- });
-
test('uses built-in translations for regional variants', () => {
const t = useTranslations('pt-br');
expect(t('page.nextLink')).toBe(translations.pt?.['page.nextLink']);
diff --git a/packages/starlight/components/EditLink.astro b/packages/starlight/components/EditLink.astro
index f5f5f65b..74be4c8f 100644
--- a/packages/starlight/components/EditLink.astro
+++ b/packages/starlight/components/EditLink.astro
@@ -1,17 +1,15 @@
---
import Icon from '../user-components/Icon.astro';
import type { Props } from '../props';
-import { useTranslations } from '../utils/translations';
-const t = useTranslations(Astro.props.locale);
-const { editUrl } = Astro.props;
+const { editUrl, labels } = Astro.props;
---
{
editUrl && (
<a href={editUrl} class="sl-flex">
<Icon name="pencil" size="1.2em" />
- {t('page.editLink')}
+ {labels['page.editLink']}
</a>
)
}
diff --git a/packages/starlight/components/FallbackContentNotice.astro b/packages/starlight/components/FallbackContentNotice.astro
index 171eeb15..44d553a4 100644
--- a/packages/starlight/components/FallbackContentNotice.astro
+++ b/packages/starlight/components/FallbackContentNotice.astro
@@ -1,14 +1,13 @@
---
import Icon from '../user-components/Icon.astro';
import type { Props } from '../props';
-import { useTranslations } from '../utils/translations';
-const t = useTranslations(Astro.props.locale);
+const { labels } = Astro.props;
---
<p class="sl-flex">
<Icon name={'warning'} size="1.5em" color="var(--sl-color-orange-high)" /><span
- >{t('i18n.untranslatedContent')}</span
+ >{labels['i18n.untranslatedContent']}</span
>
</p>
diff --git a/packages/starlight/components/LanguageSelect.astro b/packages/starlight/components/LanguageSelect.astro
index 077935b3..c85292dc 100644
--- a/packages/starlight/components/LanguageSelect.astro
+++ b/packages/starlight/components/LanguageSelect.astro
@@ -1,7 +1,6 @@
---
import config from 'virtual:starlight/user-config';
import { localizedUrl } from '../utils/localizedUrl';
-import { useTranslations } from '../utils/translations';
import Select from './Select.astro';
import type { Props } from '../props';
@@ -12,7 +11,7 @@ function localizedPathname(locale: string | undefined): string {
return localizedUrl(Astro.url, locale).pathname;
}
-const t = useTranslations(Astro.props.locale);
+const { labels } = Astro.props;
---
{
@@ -20,7 +19,7 @@ const t = useTranslations(Astro.props.locale);
<starlight-lang-select>
<Select
icon="translate"
- label={t('languageSelect.accessibleLabel')}
+ label={labels['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 73a6c2d0..a4e70d51 100644
--- a/packages/starlight/components/LastUpdated.astro
+++ b/packages/starlight/components/LastUpdated.astro
@@ -1,15 +1,13 @@
---
import type { Props } from '../props';
-import { useTranslations } from '../utils/translations';
-const { lang, lastUpdated, locale } = Astro.props;
-const t = useTranslations(locale);
+const { labels, lang, lastUpdated } = Astro.props;
---
{
lastUpdated && (
<p>
- {t('page.lastUpdated')}{' '}
+ {labels['page.lastUpdated']}{' '}
<time datetime={lastUpdated.toISOString()}>
{lastUpdated.toLocaleDateString(lang, { dateStyle: 'medium' })}
</time>
diff --git a/packages/starlight/components/MobileMenuToggle.astro b/packages/starlight/components/MobileMenuToggle.astro
index e6619bbb..b48e1230 100644
--- a/packages/starlight/components/MobileMenuToggle.astro
+++ b/packages/starlight/components/MobileMenuToggle.astro
@@ -1,16 +1,14 @@
---
import type { Props } from '../props';
-import { useTranslations } from '../utils/translations';
-
import Icon from '../user-components/Icon.astro';
-const t = useTranslations(Astro.props.locale);
+const { labels } = Astro.props;
---
<starlight-menu-button>
<button
aria-expanded="false"
- aria-label={t('menuButton.accessibleLabel')}
+ aria-label={labels['menuButton.accessibleLabel']}
aria-controls="starlight__sidebar"
class="sl-flex md:sl-hidden"
>
diff --git a/packages/starlight/components/MobileTableOfContents.astro b/packages/starlight/components/MobileTableOfContents.astro
index 03a4972f..74342e3c 100644
--- a/packages/starlight/components/MobileTableOfContents.astro
+++ b/packages/starlight/components/MobileTableOfContents.astro
@@ -1,11 +1,9 @@
---
-import { useTranslations } from '../utils/translations';
import Icon from '../user-components/Icon.astro';
import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
import type { Props } from '../props';
-const { locale, toc } = Astro.props;
-const t = useTranslations(locale);
+const { labels, toc } = Astro.props;
---
{
@@ -15,7 +13,7 @@ const t = useTranslations(locale);
<details id="starlight__mobile-toc">
<summary id="starlight__on-this-page--mobile" class="sl-flex">
<div class="toggle sl-flex">
- {t('tableOfContents.onThisPage')}
+ {labels['tableOfContents.onThisPage']}
<Icon name={'right-caret'} class="caret" size="1rem" />
</div>
<span class="display-current" />
diff --git a/packages/starlight/components/PageFrame.astro b/packages/starlight/components/PageFrame.astro
index 86e98725..17443043 100644
--- a/packages/starlight/components/PageFrame.astro
+++ b/packages/starlight/components/PageFrame.astro
@@ -1,18 +1,15 @@
---
-import type { Props } from '../props';
-import { useTranslations } from '../utils/translations';
-
import { MobileMenuToggle } from 'virtual:starlight/components';
+import type { Props } from '../props';
-const { hasSidebar, locale } = Astro.props;
-const t = useTranslations(locale);
+const { hasSidebar, labels } = Astro.props;
---
<div class="page sl-flex">
<header class="header"><slot name="header" /></header>
{
hasSidebar && (
- <nav class="sidebar" aria-label={t('sidebarNav.accessibleLabel')}>
+ <nav class="sidebar" aria-label={labels['sidebarNav.accessibleLabel']}>
<MobileMenuToggle {...Astro.props} />
<div id="starlight__sidebar" class="sidebar-pane">
<div class="sidebar-content sl-flex">
diff --git a/packages/starlight/components/Pagination.astro b/packages/starlight/components/Pagination.astro
index 72dc8df3..0c92c31e 100644
--- a/packages/starlight/components/Pagination.astro
+++ b/packages/starlight/components/Pagination.astro
@@ -1,12 +1,10 @@
---
-import { useTranslations } from '../utils/translations';
import Icon from '../user-components/Icon.astro';
import type { Props } from '../props';
-const { dir, locale, pagination } = Astro.props;
+const { dir, labels, pagination } = Astro.props;
const { prev, next } = pagination;
const isRtl = dir === 'rtl';
-const t = useTranslations(locale);
---
<div class="pagination-links" dir={dir}>
@@ -15,7 +13,7 @@ const t = useTranslations(locale);
<a href={prev.href} rel="prev">
<Icon name={isRtl ? 'right-arrow' : 'left-arrow'} size="1.5rem" />
<span>
- {t('page.previousLink')}
+ {labels['page.previousLink']}
<br />
<span class="link-title">{prev.label}</span>
</span>
@@ -27,7 +25,7 @@ const t = useTranslations(locale);
<a href={next.href} rel="next">
<Icon name={isRtl ? 'left-arrow' : 'right-arrow'} size="1.5rem" />
<span>
- {t('page.nextLink')}
+ {labels['page.nextLink']}
<br />
<span class="link-title">{next.label}</span>
</span>
diff --git a/packages/starlight/components/Search.astro b/packages/starlight/components/Search.astro
index 4162f400..2b5f3ac4 100644
--- a/packages/starlight/components/Search.astro
+++ b/packages/starlight/components/Search.astro
@@ -1,14 +1,16 @@
---
import '@pagefind/default-ui/css/ui.css';
-import { useTranslations } from '../utils/translations';
import Icon from '../user-components/Icon.astro';
import type { Props } from '../props';
-const t = useTranslations(Astro.props.locale);
+const { labels } = Astro.props;
+
const pagefindTranslations = {
- placeholder: t('search.label'),
+ placeholder: labels['search.label'],
...Object.fromEntries(
- Object.entries(t.pick('pagefind.')).map(([key, value]) => [key.replace('pagefind.', ''), value])
+ Object.entries(labels)
+ .filter(([key]) => key.startsWith('pagefind.'))
+ .map(([key, value]) => [key.replace('pagefind.', ''), value])
),
};
---
@@ -18,23 +20,27 @@ const pagefindTranslations = {
{
/* 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="sl-hidden md:sl-block" aria-hidden="true">{t('search.label')}</span>
- <Icon name="forward-slash" class="sl-hidden md:sl-block" label={t('search.shortcutLabel')} />
+ <Icon name="magnifier" label={labels['search.label']} />
+ <span class="sl-hidden md:sl-block" aria-hidden="true">{labels['search.label']}</span>
+ <Icon
+ name="forward-slash"
+ class="sl-hidden md:sl-block"
+ label={labels['search.shortcutLabel']}
+ />
</button>
- <dialog style="padding:0" aria-label={t('search.label')}>
+ <dialog style="padding:0" aria-label={labels['search.label']}>
<div class="dialog-frame sl-flex">
{
/* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
}
<button data-close-modal class="sl-flex md:sl-hidden">
- {t('search.cancelLabel')}
+ {labels['search.cancelLabel']}
</button>
{
import.meta.env.DEV ? (
<div style="margin: auto; text-align: center; white-space: pre-line;" dir="ltr">
- <p>{t('search.devWarning')}</p>
+ <p>{labels['search.devWarning']}</p>
</div>
) : (
<div class="search-container">
diff --git a/packages/starlight/components/SkipLink.astro b/packages/starlight/components/SkipLink.astro
index 79de547c..307c1ef9 100644
--- a/packages/starlight/components/SkipLink.astro
+++ b/packages/starlight/components/SkipLink.astro
@@ -1,12 +1,11 @@
---
import { PAGE_TITLE_ID } from '../constants';
-import { useTranslations } from '../utils/translations';
import type { Props } from '../props';
-const t = useTranslations(Astro.props.locale);
+const { labels } = Astro.props;
---
-<a href={`#${PAGE_TITLE_ID}`}>{t('skipLink.label')}</a>
+<a href={`#${PAGE_TITLE_ID}`}>{labels['skipLink.label']}</a>
<style>
a {
diff --git a/packages/starlight/components/TableOfContents.astro b/packages/starlight/components/TableOfContents.astro
index 4b0f1d11..eafd3d86 100644
--- a/packages/starlight/components/TableOfContents.astro
+++ b/packages/starlight/components/TableOfContents.astro
@@ -1,17 +1,15 @@
---
-import { useTranslations } from '../utils/translations';
import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
import type { Props } from '../props';
-const { locale, toc } = Astro.props;
-const t = useTranslations(locale);
+const { labels, toc } = Astro.props;
---
{
toc && (
<starlight-toc data-min-h={toc.minHeadingLevel} data-max-h={toc.maxHeadingLevel}>
<nav aria-labelledby="starlight__on-this-page">
- <h2 id="starlight__on-this-page">{t('tableOfContents.onThisPage')}</h2>
+ <h2 id="starlight__on-this-page">{labels['tableOfContents.onThisPage']}</h2>
<TableOfContentsList toc={toc.items} />
</nav>
</starlight-toc>
diff --git a/packages/starlight/components/ThemeSelect.astro b/packages/starlight/components/ThemeSelect.astro
index 7d8e3fd4..de88627d 100644
--- a/packages/starlight/components/ThemeSelect.astro
+++ b/packages/starlight/components/ThemeSelect.astro
@@ -1,21 +1,20 @@
---
-import { useTranslations } from '../utils/translations';
import Select from './Select.astro';
import type { Props } from '../props';
-const t = useTranslations(Astro.props.locale);
+const { labels } = Astro.props;
---
<starlight-theme-select>
{/* TODO: Can we give this select a width that works well for each language’s strings? */}
<Select
icon="laptop"
- label={t('themeSelect.accessibleLabel')}
+ label={labels['themeSelect.accessibleLabel']}
value="auto"
options={[
- { label: t('themeSelect.dark'), selected: false, value: 'dark' },
- { label: t('themeSelect.light'), selected: false, value: 'light' },
- { label: t('themeSelect.auto'), selected: true, value: 'auto' },
+ { label: labels['themeSelect.dark'], selected: false, value: 'dark' },
+ { label: labels['themeSelect.light'], selected: false, value: 'light' },
+ { label: labels['themeSelect.auto'], selected: true, value: 'auto' },
]}
width="6.25em"
/>
diff --git a/packages/starlight/utils/createTranslationSystem.ts b/packages/starlight/utils/createTranslationSystem.ts
index 188b176c..9f2a5bf3 100644
--- a/packages/starlight/utils/createTranslationSystem.ts
+++ b/packages/starlight/utils/createTranslationSystem.ts
@@ -19,10 +19,16 @@ export function createTranslationSystem(
/**
* Generate a utility function that returns UI strings for the given `locale`.
+ *
+ * Also includes an `all()` method for getting the entire dictionary.
+ *
* @param {string | undefined} [locale]
* @example
* const t = useTranslations('en');
- * const label = t('search.label'); // => 'Search'
+ * const label = t('search.label');
+ * // => 'Search'
+ * const dictionary = t.all();
+ * // => { 'skipLink.label': 'Skip to content', 'search.label': 'Search', ... }
*/
return function useTranslations(locale: string | undefined) {
const lang = localeToLang(locale, config.locales, config.defaultLocale);
@@ -32,8 +38,7 @@ export function createTranslationSystem(
userTranslations[lang]
);
const t = <K extends keyof typeof dictionary>(key: K) => dictionary[key];
- t.pick = (startOfKey: string) =>
- Object.fromEntries(Object.entries(dictionary).filter(([k]) => k.startsWith(startOfKey)));
+ t.all = () => dictionary;
return t;
};
}
diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts
index 966f780e..484fea08 100644
--- a/packages/starlight/utils/route-data.ts
+++ b/packages/starlight/utils/route-data.ts
@@ -29,6 +29,8 @@ export interface StarlightRouteData extends Route {
lastUpdated: Date | undefined;
/** URL object for the address where this page can be edited if enabled. */
editUrl: URL | undefined;
+ /** Record of UI strings localized for the current page. */
+ labels: ReturnType<ReturnType<typeof useTranslations>['all']>;
}
export function generateRouteData({
@@ -48,6 +50,7 @@ export function generateRouteData({
toc: getToC(props),
lastUpdated: getLastUpdated(props),
editUrl: getEditUrl(props),
+ labels: useTranslations(locale).all(),
};
}