diff options
author | Chris Swithinbank | 2023-11-29 20:25:09 +0100 |
---|---|---|
committer | GitHub | 2023-11-29 20:25:09 +0100 |
commit | e5a863a98b2e5335e122ca440dcb84e9426939b4 (patch) | |
tree | c69eea637435d2849f875c788cdd2406686551f8 | |
parent | 64d25c125c80375ff7b2cbf749f3c35d49b8bc98 (diff) | |
download | IT.starlight-e5a863a98b2e5335e122ca440dcb84e9426939b4.tar.gz IT.starlight-e5a863a98b2e5335e122ca440dcb84e9426939b4.tar.bz2 IT.starlight-e5a863a98b2e5335e122ca440dcb84e9426939b4.zip |
Expose localized UI strings in route data (#1135)
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(), }; } |