@astrojs/starlight 0.27.1 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/components/DraftContentNotice.astro +1 -3
  3. package/components/EditLink.astro +2 -2
  4. package/components/FallbackContentNotice.astro +1 -3
  5. package/components/Footer.astro +1 -1
  6. package/components/LanguageSelect.astro +1 -3
  7. package/components/LastUpdated.astro +2 -2
  8. package/components/MobileMenuToggle.astro +1 -3
  9. package/components/MobileTableOfContents.astro +2 -2
  10. package/components/PageFrame.astro +2 -2
  11. package/components/Pagination.astro +3 -3
  12. package/components/Search.astro +8 -10
  13. package/components/SidebarRestorePoint.astro +1 -1
  14. package/components/SkipLink.astro +1 -3
  15. package/components/TableOfContents.astro +2 -2
  16. package/components/ThemeSelect.astro +4 -6
  17. package/i18n.d.ts +18 -0
  18. package/index.ts +32 -4
  19. package/integrations/asides.ts +3 -2
  20. package/integrations/expressive-code/translations.ts +1 -1
  21. package/integrations/virtual-user-config.ts +4 -1
  22. package/locals.d.ts +17 -0
  23. package/locals.ts +8 -0
  24. package/package.json +4 -2
  25. package/schemas/badge.ts +20 -10
  26. package/schemas/sidebar.ts +3 -3
  27. package/user-components/rehype-file-tree.ts +1 -1
  28. package/user-components/rehype-steps.ts +1 -1
  29. package/utils/createTranslationSystem.ts +68 -25
  30. package/utils/i18n.ts +20 -0
  31. package/utils/navigation.ts +57 -8
  32. package/utils/plugins.ts +67 -2
  33. package/utils/route-data.ts +5 -4
  34. package/utils/starlight-page.ts +3 -18
  35. package/utils/translations-fs.ts +4 -3
  36. package/utils/translations.ts +10 -3
  37. package/utils/types.ts +15 -0
  38. package/virtual.d.ts +5 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.28.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2334](https://github.com/withastro/starlight/pull/2334) [`79b9ade`](https://github.com/withastro/starlight/commit/79b9ade194cf704dad79267715a6970e0d7a7277) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fixes an issue with Expressive Code UI labels not displaying correctly.
8
+
9
+ ## 0.28.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#1923](https://github.com/withastro/starlight/pull/1923) [`5269aad`](https://github.com/withastro/starlight/commit/5269aad928773ae08b35ba8e19c0f2832d0d2c89) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Overhauls the built-in localization system which is now powered by the [`i18next`](https://www.i18next.com/) library and available to use anywhere in your documentation website.
14
+
15
+ See the [“Using UI translations”](https://starlight.astro.build/guides/i18n/#using-ui-translations) guide to learn more about how to access built-in UI labels or your own custom strings in your project. Plugin authors can also use the new [`injectTranslations()`](https://starlight.astro.build/reference/plugins/#injecttranslations) helper to add or update translation strings.
16
+
17
+ ⚠️ **BREAKING CHANGE:** The `Astro.props.labels` props has been removed from the props passed down to custom component overrides.
18
+
19
+ If you are relying on `Astro.props.labels` (for example to read a built-in UI label), you will need to update your code to use the new [`Astro.locals.t()`](https://starlight.astro.build/guides/i18n/#using-ui-translations) helper instead.
20
+
21
+ ```astro
22
+ ---
23
+ import type { Props } from '@astrojs/starlight/props';
24
+ // The `search.label` UI label for this page’s language:
25
+ const searchLabel = Astro.locals.t('search.label');
26
+ ---
27
+ ```
28
+
29
+ - [#2285](https://github.com/withastro/starlight/pull/2285) [`7286220`](https://github.com/withastro/starlight/commit/728622037602999ed67dedc2757ca5654236feb8) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Adds support for translating sidebar badges.
30
+
31
+ - [#1923](https://github.com/withastro/starlight/pull/1923) [`5269aad`](https://github.com/withastro/starlight/commit/5269aad928773ae08b35ba8e19c0f2832d0d2c89) Thanks [@HiDeoo](https://github.com/HiDeoo)! - ⚠️ **BREAKING CHANGE:** The minimum supported version of Astro is now 4.14.0
32
+
33
+ Please update Astro and Starlight together:
34
+
35
+ ```sh
36
+ npx @astrojs/upgrade
37
+ ```
38
+
39
+ ### Patch Changes
40
+
41
+ - [#2327](https://github.com/withastro/starlight/pull/2327) [`d7a295e`](https://github.com/withastro/starlight/commit/d7a295e5f63171c7eee9fc11333157d8c7e6c803) Thanks [@tritao](https://github.com/tritao)! - Fixes restoration of remark directives for nodes with custom data attached.
42
+
3
43
  ## 0.27.1
4
44
 
5
45
  ### Patch Changes
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  import ContentNotice from './ContentNotice.astro';
3
3
  import type { Props } from '../props';
4
-
5
- const { labels } = Astro.props;
6
4
  ---
7
5
 
8
- <ContentNotice icon="warning" label={labels['page.draft']} />
6
+ <ContentNotice icon="warning" label={Astro.locals.t('page.draft')} />
@@ -2,14 +2,14 @@
2
2
  import Icon from '../user-components/Icon.astro';
3
3
  import type { Props } from '../props';
4
4
 
5
- const { editUrl, labels } = Astro.props;
5
+ const { editUrl } = Astro.props;
6
6
  ---
7
7
 
8
8
  {
9
9
  editUrl && (
10
10
  <a href={editUrl} class="sl-flex">
11
11
  <Icon name="pencil" size="1.2em" />
12
- {labels['page.editLink']}
12
+ {Astro.locals.t('page.editLink')}
13
13
  </a>
14
14
  )
15
15
  }
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  import ContentNotice from './ContentNotice.astro';
3
3
  import type { Props } from '../props';
4
-
5
- const { labels } = Astro.props;
6
4
  ---
7
5
 
8
- <ContentNotice icon="warning" label={labels['i18n.untranslatedContent']} />
6
+ <ContentNotice icon="warning" label={Astro.locals.t('i18n.untranslatedContent')} />
@@ -18,7 +18,7 @@ import { Icon } from '../components';
18
18
  {
19
19
  config.credits && (
20
20
  <a class="kudos sl-flex" href="https://starlight.astro.build">
21
- <Icon name={'starlight'} /> {Astro.props.labels['builtWithStarlight.label']}
21
+ <Icon name={'starlight'} /> {Astro.locals.t('builtWithStarlight.label')}
22
22
  </a>
23
23
  )
24
24
  }
@@ -10,8 +10,6 @@ import type { Props } from '../props';
10
10
  function localizedPathname(locale: string | undefined): string {
11
11
  return localizedUrl(Astro.url, locale).pathname;
12
12
  }
13
-
14
- const { labels } = Astro.props;
15
13
  ---
16
14
 
17
15
  {
@@ -19,7 +17,7 @@ const { labels } = Astro.props;
19
17
  <starlight-lang-select>
20
18
  <Select
21
19
  icon="translate"
22
- label={labels['languageSelect.accessibleLabel']}
20
+ label={Astro.locals.t('languageSelect.accessibleLabel')}
23
21
  value={localizedPathname(Astro.props.locale)}
24
22
  options={Object.entries(config.locales).map(([code, locale]) => ({
25
23
  value: localizedPathname(code),
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  import type { Props } from '../props';
3
3
 
4
- const { labels, lang, lastUpdated } = Astro.props;
4
+ const { lang, lastUpdated } = Astro.props;
5
5
  ---
6
6
 
7
7
  {
8
8
  lastUpdated && (
9
9
  <p>
10
- {labels['page.lastUpdated']}{' '}
10
+ {Astro.locals.t('page.lastUpdated')}{' '}
11
11
  <time datetime={lastUpdated.toISOString()}>
12
12
  {lastUpdated.toLocaleDateString(lang, { dateStyle: 'medium', timeZone: 'UTC' })}
13
13
  </time>
@@ -1,14 +1,12 @@
1
1
  ---
2
2
  import type { Props } from '../props';
3
3
  import Icon from '../user-components/Icon.astro';
4
-
5
- const { labels } = Astro.props;
6
4
  ---
7
5
 
8
6
  <starlight-menu-button>
9
7
  <button
10
8
  aria-expanded="false"
11
- aria-label={labels['menuButton.accessibleLabel']}
9
+ aria-label={Astro.locals.t('menuButton.accessibleLabel')}
12
10
  aria-controls="starlight__sidebar"
13
11
  class="sl-flex md:sl-hidden"
14
12
  >
@@ -3,7 +3,7 @@ import Icon from '../user-components/Icon.astro';
3
3
  import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
4
4
  import type { Props } from '../props';
5
5
 
6
- const { labels, toc } = Astro.props;
6
+ const { toc } = Astro.props;
7
7
  ---
8
8
 
9
9
  {
@@ -13,7 +13,7 @@ const { labels, toc } = Astro.props;
13
13
  <details id="starlight__mobile-toc">
14
14
  <summary id="starlight__on-this-page--mobile" class="sl-flex">
15
15
  <div class="toggle sl-flex">
16
- {labels['tableOfContents.onThisPage']}
16
+ {Astro.locals.t('tableOfContents.onThisPage')}
17
17
  <Icon name={'right-caret'} class="caret" size="1rem" />
18
18
  </div>
19
19
  <span class="display-current" />
@@ -2,14 +2,14 @@
2
2
  import MobileMenuToggle from 'virtual:starlight/components/MobileMenuToggle';
3
3
  import type { Props } from '../props';
4
4
 
5
- const { hasSidebar, labels } = Astro.props;
5
+ const { hasSidebar } = Astro.props;
6
6
  ---
7
7
 
8
8
  <div class="page sl-flex">
9
9
  <header class="header"><slot name="header" /></header>
10
10
  {
11
11
  hasSidebar && (
12
- <nav class="sidebar" aria-label={labels['sidebarNav.accessibleLabel']}>
12
+ <nav class="sidebar" aria-label={Astro.locals.t('sidebarNav.accessibleLabel')}>
13
13
  <MobileMenuToggle {...Astro.props} />
14
14
  <div id="starlight__sidebar" class="sidebar-pane">
15
15
  <div class="sidebar-content sl-flex">
@@ -2,7 +2,7 @@
2
2
  import Icon from '../user-components/Icon.astro';
3
3
  import type { Props } from '../props';
4
4
 
5
- const { dir, labels, pagination } = Astro.props;
5
+ const { dir, pagination } = Astro.props;
6
6
  const { prev, next } = pagination;
7
7
  const isRtl = dir === 'rtl';
8
8
  ---
@@ -13,7 +13,7 @@ const isRtl = dir === 'rtl';
13
13
  <a href={prev.href} rel="prev">
14
14
  <Icon name={isRtl ? 'right-arrow' : 'left-arrow'} size="1.5rem" />
15
15
  <span>
16
- {labels['page.previousLink']}
16
+ {Astro.locals.t('page.previousLink')}
17
17
  <br />
18
18
  <span class="link-title">{prev.label}</span>
19
19
  </span>
@@ -25,7 +25,7 @@ const isRtl = dir === 'rtl';
25
25
  <a href={next.href} rel="next">
26
26
  <Icon name={isRtl ? 'left-arrow' : 'right-arrow'} size="1.5rem" />
27
27
  <span>
28
- {labels['page.nextLink']}
28
+ {Astro.locals.t('page.nextLink')}
29
29
  <br />
30
30
  <span class="link-title">{next.label}</span>
31
31
  </span>
@@ -4,12 +4,10 @@ import Icon from '../user-components/Icon.astro';
4
4
  import project from 'virtual:starlight/project-context';
5
5
  import type { Props } from '../props';
6
6
 
7
- const { labels } = Astro.props;
8
-
9
7
  const pagefindTranslations = {
10
- placeholder: labels['search.label'],
8
+ placeholder: Astro.locals.t('search.label'),
11
9
  ...Object.fromEntries(
12
- Object.entries(labels)
10
+ Object.entries(Astro.locals.t.all())
13
11
  .filter(([key]) => key.startsWith('pagefind.'))
14
12
  .map(([key, value]) => [key.replace('pagefind.', ''), value])
15
13
  ),
@@ -23,28 +21,28 @@ const pagefindTranslations = {
23
21
  <button
24
22
  data-open-modal
25
23
  disabled
26
- aria-label={labels['search.label']}
24
+ aria-label={Astro.locals.t('search.label')}
27
25
  aria-keyshortcuts="Control+K"
28
26
  >
29
27
  <Icon name="magnifier" />
30
- <span class="sl-hidden md:sl-block" aria-hidden="true">{labels['search.label']}</span>
28
+ <span class="sl-hidden md:sl-block" aria-hidden="true">{Astro.locals.t('search.label')}</span>
31
29
  <kbd class="sl-hidden md:sl-flex" style="display: none;">
32
- <kbd>{labels['search.ctrlKey']}</kbd><kbd>K</kbd>
30
+ <kbd>{Astro.locals.t('search.ctrlKey')}</kbd><kbd>K</kbd>
33
31
  </kbd>
34
32
  </button>
35
33
 
36
- <dialog style="padding:0" aria-label={labels['search.label']}>
34
+ <dialog style="padding:0" aria-label={Astro.locals.t('search.label')}>
37
35
  <div class="dialog-frame sl-flex">
38
36
  {
39
37
  /* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
40
38
  }
41
39
  <button data-close-modal class="sl-flex md:sl-hidden">
42
- {labels['search.cancelLabel']}
40
+ {Astro.locals.t('search.cancelLabel')}
43
41
  </button>
44
42
  {
45
43
  import.meta.env.DEV ? (
46
44
  <div style="margin: auto; text-align: center; white-space: pre-line;" dir="ltr">
47
- <p>{labels['search.devWarning']}</p>
45
+ <p>{Astro.locals.t('search.devWarning')}</p>
48
46
  </div>
49
47
  ) : (
50
48
  <div class="search-container">
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  /** Unique symbol for storing a running index in `locals`. */
3
3
  const currentGroupIndexSymbol = Symbol.for('starlight-sidebar-group-index');
4
- const locals = Astro.locals as Record<typeof currentGroupIndexSymbol, number>;
4
+ const locals = Astro.locals as App.Locals & { [currentGroupIndexSymbol]: number };
5
5
 
6
6
  /** The current sidebar group’s index retrieved from `locals` if set, starting at `0`. */
7
7
  const index = locals[currentGroupIndexSymbol] || 0;
@@ -1,11 +1,9 @@
1
1
  ---
2
2
  import { PAGE_TITLE_ID } from '../constants';
3
3
  import type { Props } from '../props';
4
-
5
- const { labels } = Astro.props;
6
4
  ---
7
5
 
8
- <a href={`#${PAGE_TITLE_ID}`}>{labels['skipLink.label']}</a>
6
+ <a href={`#${PAGE_TITLE_ID}`}>{Astro.locals.t('skipLink.label')}</a>
9
7
 
10
8
  <style>
11
9
  a {
@@ -2,14 +2,14 @@
2
2
  import TableOfContentsList from './TableOfContents/TableOfContentsList.astro';
3
3
  import type { Props } from '../props';
4
4
 
5
- const { labels, toc } = Astro.props;
5
+ const { toc } = Astro.props;
6
6
  ---
7
7
 
8
8
  {
9
9
  toc && (
10
10
  <starlight-toc data-min-h={toc.minHeadingLevel} data-max-h={toc.maxHeadingLevel}>
11
11
  <nav aria-labelledby="starlight__on-this-page">
12
- <h2 id="starlight__on-this-page">{labels['tableOfContents.onThisPage']}</h2>
12
+ <h2 id="starlight__on-this-page">{Astro.locals.t('tableOfContents.onThisPage')}</h2>
13
13
  <TableOfContentsList toc={toc.items} />
14
14
  </nav>
15
15
  </starlight-toc>
@@ -1,20 +1,18 @@
1
1
  ---
2
2
  import Select from './Select.astro';
3
3
  import type { Props } from '../props';
4
-
5
- const { labels } = Astro.props;
6
4
  ---
7
5
 
8
6
  <starlight-theme-select>
9
7
  {/* TODO: Can we give this select a width that works well for each language’s strings? */}
10
8
  <Select
11
9
  icon="laptop"
12
- label={labels['themeSelect.accessibleLabel']}
10
+ label={Astro.locals.t('themeSelect.accessibleLabel')}
13
11
  value="auto"
14
12
  options={[
15
- { label: labels['themeSelect.dark'], selected: false, value: 'dark' },
16
- { label: labels['themeSelect.light'], selected: false, value: 'light' },
17
- { label: labels['themeSelect.auto'], selected: true, value: 'auto' },
13
+ { label: Astro.locals.t('themeSelect.dark'), selected: false, value: 'dark' },
14
+ { label: Astro.locals.t('themeSelect.light'), selected: false, value: 'light' },
15
+ { label: Astro.locals.t('themeSelect.auto'), selected: true, value: 'auto' },
18
16
  ]}
19
17
  width="6.25em"
20
18
  />
package/i18n.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /*
2
+ * This file imports the original `i18next` types and extends them to configure the
3
+ * Starlight namespace.
4
+ *
5
+ * Note that the top-level `import` makes this module non-ambient, so can’t be
6
+ * combined with other `.d.ts` files such as `locals.d.ts`.
7
+ */
8
+
9
+ import 'i18next';
10
+
11
+ declare module 'i18next' {
12
+ interface CustomTypeOptions {
13
+ defaultNS: typeof import('./utils/createTranslationSystem').I18nextNamespace;
14
+ resources: {
15
+ starlight: Record<import('./utils/createTranslationSystem').I18nKeys, string>;
16
+ };
17
+ }
18
+ }
package/index.ts CHANGED
@@ -1,3 +1,11 @@
1
+ /**
2
+ * These triple-slash directives defines dependencies to various declaration files that will be
3
+ * loaded when a user imports the Starlight integration in their Astro configuration file. These
4
+ * directives must be first at the top of the file and can only be preceded by this comment.
5
+ */
6
+ /// <reference path="./locals.d.ts" />
7
+ /// <reference path="./i18n.d.ts" />
8
+
1
9
  import mdx from '@astrojs/mdx';
2
10
  import type { AstroIntegration } from 'astro';
3
11
  import { spawn } from 'node:child_process';
@@ -9,7 +17,12 @@ import { starlightSitemap } from './integrations/sitemap';
9
17
  import { vitePluginStarlightUserConfig } from './integrations/virtual-user-config';
10
18
  import { rehypeRtlCodeSupport } from './integrations/code-rtl-support';
11
19
  import { createTranslationSystemFromFs } from './utils/translations-fs';
12
- import { runPlugins, type StarlightUserConfigWithPlugins } from './utils/plugins';
20
+ import {
21
+ injectPluginTranslationsTypes,
22
+ runPlugins,
23
+ type PluginTranslations,
24
+ type StarlightUserConfigWithPlugins,
25
+ } from './utils/plugins';
13
26
  import { processI18nConfig } from './utils/i18n';
14
27
  import type { StarlightConfig } from './types';
15
28
 
@@ -18,10 +31,12 @@ export default function StarlightIntegration({
18
31
  ...opts
19
32
  }: StarlightUserConfigWithPlugins): AstroIntegration {
20
33
  let userConfig: StarlightConfig;
34
+ let pluginTranslations: PluginTranslations = {};
21
35
  return {
22
36
  name: '@astrojs/starlight',
23
37
  hooks: {
24
38
  'astro:config:setup': async ({
39
+ addMiddleware,
25
40
  command,
26
41
  config,
27
42
  injectRoute,
@@ -42,10 +57,17 @@ export default function StarlightIntegration({
42
57
  config.i18n
43
58
  );
44
59
 
45
- const { integrations } = pluginResult;
60
+ const integrations = pluginResult.integrations;
61
+ pluginTranslations = pluginResult.pluginTranslations;
46
62
  userConfig = starlightConfig;
47
63
 
48
- const useTranslations = createTranslationSystemFromFs(starlightConfig, config);
64
+ const useTranslations = createTranslationSystemFromFs(
65
+ starlightConfig,
66
+ config,
67
+ pluginTranslations
68
+ );
69
+
70
+ addMiddleware({ entrypoint: '@astrojs/starlight/locals', order: 'pre' });
49
71
 
50
72
  if (!starlightConfig.disable404Route) {
51
73
  injectRoute({
@@ -91,7 +113,9 @@ export default function StarlightIntegration({
91
113
 
92
114
  updateConfig({
93
115
  vite: {
94
- plugins: [vitePluginStarlightUserConfig(command, starlightConfig, config)],
116
+ plugins: [
117
+ vitePluginStarlightUserConfig(command, starlightConfig, config, pluginTranslations),
118
+ ],
95
119
  },
96
120
  markdown: {
97
121
  remarkPlugins: [
@@ -112,6 +136,10 @@ export default function StarlightIntegration({
112
136
  });
113
137
  },
114
138
 
139
+ 'astro:config:done': ({ injectTypes }) => {
140
+ injectPluginTranslationsTypes(pluginTranslations, injectTypes);
141
+ },
142
+
115
143
  'astro:build:done': ({ dir }) => {
116
144
  if (!userConfig.pagefind) return;
117
145
  const targetDir = fileURLToPath(dir);
@@ -166,7 +166,7 @@ function remarkAsides(options: AsidesOptions): Plugin<[], Root> {
166
166
  // children with the `directiveLabel` property set to true. We want to pass it as the title
167
167
  // prop to <Aside>, so when we find a directive label, we store it for the title prop and
168
168
  // remove the paragraph from the container’s children.
169
- let title = t(`aside.${variant}`);
169
+ let title: string = t(`aside.${variant}`);
170
170
  let titleNode: PhrasingContent[] = [{ type: 'text', value: title }];
171
171
  const firstChild = node.children[0];
172
172
  if (
@@ -227,7 +227,8 @@ export function remarkDirectivesRestoration() {
227
227
  if (
228
228
  index !== undefined &&
229
229
  parent &&
230
- (node.type === 'textDirective' || node.type === 'leafDirective')
230
+ (node.type === 'textDirective' || node.type === 'leafDirective') &&
231
+ node.data === undefined
231
232
  ) {
232
233
  transformUnhandledDirective(node, index, parent);
233
234
  return;
@@ -29,7 +29,7 @@ function addTranslationsForLocale(
29
29
  'expressiveCode.terminalWindowFallbackTitle',
30
30
  ] as const;
31
31
  translationKeys.forEach((key) => {
32
- const translation = t(key);
32
+ const translation = t.exists(key) ? t(key) : undefined;
33
33
  if (!translation) return;
34
34
  const ecId = key.replace(/^expressiveCode\./, '');
35
35
  pluginFramesTexts.overrideTexts(lang, { [ecId]: translation });
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import type { StarlightConfig } from '../utils/user-config';
5
5
  import { getAllNewestCommitDate } from '../utils/git';
6
+ import type { PluginTranslations } from '../utils/plugins';
6
7
 
7
8
  function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
8
9
  return `\0${id}`;
@@ -19,7 +20,8 @@ export function vitePluginStarlightUserConfig(
19
20
  trailingSlash,
20
21
  }: Pick<AstroConfig, 'root' | 'srcDir' | 'trailingSlash'> & {
21
22
  build: Pick<AstroConfig['build'], 'format'>;
22
- }
23
+ },
24
+ pluginTranslations: PluginTranslations
23
25
  ): NonNullable<ViteUserConfig['plugins']>[number] {
24
26
  /**
25
27
  * Resolves module IDs to a usable format:
@@ -80,6 +82,7 @@ export function vitePluginStarlightUserConfig(
80
82
  userCollections = (await import(${resolveId('./content/config.ts', srcDir)})).collections;
81
83
  } catch {}
82
84
  export const collections = userCollections;`,
85
+ 'virtual:starlight/plugin-translations': `export default ${JSON.stringify(pluginTranslations)}`,
83
86
  ...virtualComponentModules,
84
87
  } satisfies Record<string, string>;
85
88
 
package/locals.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * This namespace is reserved for Starlight (only used for i18n at the moment).
3
+ * It can be extended by plugins using module augmentation and interface merging.
4
+ * For an example, see: https://starlight.astro.build/reference/plugins/#injecttranslations
5
+ */
6
+ declare namespace StarlightApp {
7
+ interface I18n {}
8
+ }
9
+
10
+ /**
11
+ * Extending Astro’s `App.Locals` interface registers types for the middleware added by Starlight.
12
+ */
13
+ declare namespace App {
14
+ interface Locals {
15
+ t: import('./utils/createTranslationSystem').I18nT;
16
+ }
17
+ }
package/locals.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineMiddleware } from 'astro:middleware';
2
+ import { useTranslations } from './utils/translations';
3
+
4
+ export const onRequest = defineMiddleware((context, next) => {
5
+ context.locals.t = useTranslations(context.currentLocale);
6
+
7
+ return next();
8
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.27.1",
3
+ "version": "0.28.1",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -21,6 +21,7 @@
21
21
  "type": "module",
22
22
  "exports": {
23
23
  ".": "./index.ts",
24
+ "./locals": "./locals.ts",
24
25
  "./components": "./components.ts",
25
26
  "./components/LanguageSelect.astro": {
26
27
  "types": "./components/LanguageSelect.astro.tsx",
@@ -166,7 +167,7 @@
166
167
  "./style/markdown.css": "./style/markdown.css"
167
168
  },
168
169
  "peerDependencies": {
169
- "astro": "^4.8.6"
170
+ "astro": "^4.14.0"
170
171
  },
171
172
  "devDependencies": {
172
173
  "@astrojs/markdown-remark": "^5.1.0",
@@ -189,6 +190,7 @@
189
190
  "hast-util-select": "^6.0.2",
190
191
  "hast-util-to-string": "^3.0.0",
191
192
  "hastscript": "^9.0.0",
193
+ "i18next": "^23.11.5",
192
194
  "mdast-util-directive": "^3.0.0",
193
195
  "mdast-util-to-markdown": "^2.1.0",
194
196
  "mdast-util-to-string": "^4.0.0",
package/schemas/badge.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  import { z } from 'astro/zod';
2
2
 
3
- const badgeSchema = () =>
4
- z.object({
5
- variant: z.enum(['note', 'danger', 'success', 'caution', 'tip', 'default']).default('default'),
6
- text: z.string(),
7
- class: z.string().optional(),
8
- });
9
-
10
- export const BadgeComponentSchema = badgeSchema()
3
+ const badgeBaseSchema = z.object({
4
+ variant: z.enum(['note', 'danger', 'success', 'caution', 'tip', 'default']).default('default'),
5
+ class: z.string().optional(),
6
+ });
7
+
8
+ const badgeSchema = badgeBaseSchema.extend({
9
+ text: z.string(),
10
+ });
11
+
12
+ const i18nBadgeSchema = badgeBaseSchema.extend({
13
+ text: z.union([z.string(), z.record(z.string())]),
14
+ });
15
+
16
+ export const BadgeComponentSchema = badgeSchema
11
17
  .extend({
12
18
  size: z.enum(['small', 'medium', 'large']).default('small'),
13
19
  })
@@ -17,7 +23,7 @@ export type BadgeComponentProps = z.input<typeof BadgeComponentSchema>;
17
23
 
18
24
  export const BadgeConfigSchema = () =>
19
25
  z
20
- .union([z.string(), badgeSchema()])
26
+ .union([z.string(), badgeSchema])
21
27
  .transform((badge) => {
22
28
  if (typeof badge === 'string') {
23
29
  return { variant: 'default' as const, text: badge };
@@ -26,4 +32,8 @@ export const BadgeConfigSchema = () =>
26
32
  })
27
33
  .optional();
28
34
 
29
- export type Badge = z.output<ReturnType<typeof badgeSchema>>;
35
+ export const I18nBadgeConfigSchema = () => z.union([z.string(), i18nBadgeSchema]).optional();
36
+
37
+ export type Badge = z.output<typeof badgeSchema>;
38
+ export type I18nBadge = z.output<typeof i18nBadgeSchema>;
39
+ export type I18nBadgeConfig = z.output<ReturnType<typeof I18nBadgeConfigSchema>>;
@@ -1,7 +1,7 @@
1
1
  import type { AstroBuiltinAttributes } from 'astro';
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
  import { z } from 'astro/zod';
4
- import { BadgeConfigSchema } from './badge';
4
+ import { I18nBadgeConfigSchema } from './badge';
5
5
  import { stripLeadingAndTrailingSlashes } from '../utils/path';
6
6
 
7
7
  const SidebarBaseSchema = z.object({
@@ -9,8 +9,8 @@ const SidebarBaseSchema = z.object({
9
9
  label: z.string(),
10
10
  /** Translations of the `label` for each supported language. */
11
11
  translations: z.record(z.string()).default({}),
12
- /** Adds a badge to the link item */
13
- badge: BadgeConfigSchema(),
12
+ /** Adds a badge to the item */
13
+ badge: I18nBadgeConfigSchema(),
14
14
  });
15
15
 
16
16
  const SidebarGroupSchema = SidebarBaseSchema.extend({
@@ -240,7 +240,7 @@ function isElementNode(node: ElementContent): node is Element {
240
240
  function throwFileTreeValidationError(message: string): never {
241
241
  throw new AstroError(
242
242
  message,
243
- 'To learn more about the `<FileTree>` component, see https://starlight.astro.build/guides/components/#file-tree'
243
+ 'To learn more about the `<FileTree>` component, see https://starlight.astro.build/components/file-tree/'
244
244
  );
245
245
  }
246
246
 
@@ -67,7 +67,7 @@ export const processSteps = (html: string | undefined) => {
67
67
  class StepsError extends AstroError {
68
68
  constructor(message: string, html?: string) {
69
69
  let hint =
70
- 'To learn more about the `<Steps>` component, see https://starlight.astro.build/guides/components/#steps';
70
+ 'To learn more about the `<Steps>` component, see https://starlight.astro.build/components/steps/';
71
71
  if (html) {
72
72
  hint += '\n\nFull HTML passed to `<Steps>`:\n' + prettyPrintHtml(html);
73
73
  }
@@ -1,27 +1,60 @@
1
+ import i18next, { type ExistsFunction, type TFunction } from 'i18next';
1
2
  import type { i18nSchemaOutput } from '../schemas/i18n';
2
3
  import builtinTranslations from '../translations/index';
3
4
  import { BuiltInDefaultLocale } from './i18n';
4
5
  import type { StarlightConfig } from './user-config';
6
+ import type { UserI18nKeys, UserI18nSchema } from './translations';
7
+
8
+ /**
9
+ * The namespace for i18next resources used by Starlight.
10
+ * All translations handled by Starlight are stored in the same namespace and Starlight always use
11
+ * a new instance of i18next configured for this namespace.
12
+ */
13
+ export const I18nextNamespace = 'starlight' as const;
5
14
 
6
15
  export function createTranslationSystem<T extends i18nSchemaOutput>(
16
+ config: Pick<StarlightConfig, 'defaultLocale' | 'locales'>,
7
17
  userTranslations: Record<string, T>,
8
- config: Pick<StarlightConfig, 'defaultLocale' | 'locales'>
18
+ pluginTranslations: Record<string, T> = {}
9
19
  ) {
10
- /** User-configured default locale. */
11
- const defaultLocale = config.defaultLocale?.locale || 'root';
20
+ const defaultLocale =
21
+ config.defaultLocale.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang;
12
22
 
13
- /** Default map of UI strings based on Starlight and user-configured defaults. */
14
- const defaults = buildDictionary(
15
- builtinTranslations.en!,
16
- userTranslations.en,
17
- builtinTranslations[defaultLocale] || builtinTranslations[stripLangRegion(defaultLocale)],
18
- userTranslations[defaultLocale]
19
- );
23
+ const translations = {
24
+ [defaultLocale]: buildResources(
25
+ builtinTranslations[defaultLocale],
26
+ builtinTranslations[stripLangRegion(defaultLocale)],
27
+ pluginTranslations[defaultLocale],
28
+ userTranslations[defaultLocale]
29
+ ),
30
+ };
31
+
32
+ if (config.locales) {
33
+ for (const locale in config.locales) {
34
+ const lang = localeToLang(locale, config.locales, config.defaultLocale);
35
+
36
+ translations[lang] = buildResources(
37
+ builtinTranslations[lang] || builtinTranslations[stripLangRegion(lang)],
38
+ pluginTranslations[lang],
39
+ userTranslations[lang]
40
+ );
41
+ }
42
+ }
43
+
44
+ const i18n = i18next.createInstance();
45
+ i18n.init({
46
+ resources: translations,
47
+ fallbackLng:
48
+ config.defaultLocale.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang,
49
+ });
20
50
 
21
51
  /**
22
52
  * Generate a utility function that returns UI strings for the given `locale`.
23
53
  *
24
- * Also includes an `all()` method for getting the entire dictionary.
54
+ * Also includes a few utility methods:
55
+ * - `all()` method for getting the entire dictionary.
56
+ * - `exists()` method for checking if a key exists in the dictionary.
57
+ * - `dir()` method for getting the text direction of the locale.
25
58
  *
26
59
  * @param {string | undefined} [locale]
27
60
  * @example
@@ -30,16 +63,19 @@ export function createTranslationSystem<T extends i18nSchemaOutput>(
30
63
  * // => 'Search'
31
64
  * const dictionary = t.all();
32
65
  * // => { 'skipLink.label': 'Skip to content', 'search.label': 'Search', ... }
66
+ * const exists = t.exists('search.label');
67
+ * // => true
68
+ * const dir = t.dir();
69
+ * // => 'ltr'
33
70
  */
34
- return function useTranslations(locale: string | undefined) {
71
+ return (locale: string | undefined) => {
35
72
  const lang = localeToLang(locale, config.locales, config.defaultLocale);
36
- const dictionary = buildDictionary(
37
- defaults,
38
- builtinTranslations[lang] || builtinTranslations[stripLangRegion(lang)],
39
- userTranslations[lang]
40
- );
41
- const t = <K extends keyof typeof dictionary>(key: K) => dictionary[key];
42
- t.all = () => dictionary;
73
+
74
+ const t = i18n.getFixedT(lang, I18nextNamespace) as I18nT;
75
+ t.all = () => i18n.getResourceBundle(lang, I18nextNamespace);
76
+ t.exists = (key, options) => i18n.exists(key, { lng: lang, ns: I18nextNamespace, ...options });
77
+ t.dir = (dirLang = lang) => i18n.dir(dirLang);
78
+
43
79
  return t;
44
80
  };
45
81
  }
@@ -70,12 +106,11 @@ function localeToLang(
70
106
 
71
107
  type BuiltInStrings = (typeof builtinTranslations)['en'];
72
108
 
73
- /** Build a dictionary by layering preferred translation sources. */
74
- function buildDictionary<T extends Record<string, string | undefined>>(
75
- base: BuiltInStrings,
109
+ /** Build an i18next resources dictionary by layering preferred translation sources. */
110
+ function buildResources<T extends Record<string, string | undefined>>(
76
111
  ...dictionaries: (T | BuiltInStrings | undefined)[]
77
- ): BuiltInStrings & T {
78
- const dictionary = { ...base };
112
+ ): { [I18nextNamespace]: BuiltInStrings & T } {
113
+ const dictionary: Partial<BuiltInStrings> = {};
79
114
  // Iterate over alternate dictionaries to avoid overwriting preceding values with `undefined`.
80
115
  for (const dict of dictionaries) {
81
116
  for (const key in dict) {
@@ -83,5 +118,13 @@ function buildDictionary<T extends Record<string, string | undefined>>(
83
118
  if (value) dictionary[key as keyof typeof dictionary] = value;
84
119
  }
85
120
  }
86
- return dictionary as BuiltInStrings & T;
121
+ return { [I18nextNamespace]: dictionary as BuiltInStrings & T };
87
122
  }
123
+
124
+ export type I18nKeys = UserI18nKeys | keyof StarlightApp.I18n;
125
+
126
+ export type I18nT = TFunction<'starlight', undefined> & {
127
+ all: () => UserI18nSchema;
128
+ exists: ExistsFunction;
129
+ dir: (lang?: string) => 'ltr' | 'rtl';
130
+ };
package/utils/i18n.ts CHANGED
@@ -2,6 +2,26 @@ import type { AstroConfig } from 'astro';
2
2
  import { AstroError } from 'astro/errors';
3
3
  import type { StarlightConfig } from './user-config';
4
4
 
5
+ /**
6
+ * A proxy object that throws an error when a user tries to access the deprecated `labels` prop in
7
+ * a component override.
8
+ *
9
+ * @todo Remove in a future release once people have updated — no later than v1.
10
+ */
11
+ export const DeprecatedLabelsPropProxy = new Proxy<Record<string, never>>(
12
+ {},
13
+ {
14
+ get(_, key) {
15
+ const label = String(key);
16
+ throw new AstroError(
17
+ `The \`labels\` prop in component overrides has been removed.`,
18
+ `Replace \`Astro.props.labels["${label}"]\` with \`Astro.locals.t("${label}")\` instead.\n` +
19
+ 'For more information see https://starlight.astro.build/guides/i18n/#using-ui-translations'
20
+ );
21
+ },
22
+ }
23
+ );
24
+
5
25
  /**
6
26
  * A list of well-known right-to-left languages used as a fallback when determining the text
7
27
  * direction of a locale is not supported by the `Intl.Locale` API in the current environment.
@@ -1,6 +1,6 @@
1
1
  import { AstroError } from 'astro/errors';
2
2
  import config from 'virtual:starlight/user-config';
3
- import type { Badge } from '../schemas/badge';
3
+ import type { Badge, I18nBadge, I18nBadgeConfig } from '../schemas/badge';
4
4
  import type { PrevNextLinkConfig } from '../schemas/prevNextLink';
5
5
  import type {
6
6
  AutoSidebarGroup,
@@ -11,7 +11,7 @@ import type {
11
11
  } from '../schemas/sidebar';
12
12
  import { createPathFormatter } from './createPathFormatter';
13
13
  import { formatPath } from './format-path';
14
- import { pickLang } from './i18n';
14
+ import { BuiltInDefaultLocale, pickLang } from './i18n';
15
15
  import { ensureLeadingSlash, ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path';
16
16
  import { getLocaleRoutes, routes, type Route } from './routing';
17
17
  import { localeToLang, slugToPathname } from './slugs';
@@ -79,12 +79,13 @@ function configItemToEntry(
79
79
  } else if ('slug' in item) {
80
80
  return linkFromInternalSidebarLinkItem(item, locale, currentPathname);
81
81
  } else {
82
+ const label = pickLang(item.translations, localeToLang(locale)) || item.label;
82
83
  return {
83
84
  type: 'group',
84
- label: pickLang(item.translations, localeToLang(locale)) || item.label,
85
+ label,
85
86
  entries: item.items.map((i) => configItemToEntry(i, currentPathname, locale, routes)),
86
87
  collapsed: item.collapsed,
87
- badge: item.badge,
88
+ badge: getSidebarBadge(item.badge, locale, label),
88
89
  };
89
90
  }
90
91
  }
@@ -106,12 +107,13 @@ function groupFromAutogenerateConfig(
106
107
  doc.id.startsWith(localeDir + '/')
107
108
  );
108
109
  const tree = treeify(dirDocs, localeDir);
110
+ const label = pickLang(item.translations, localeToLang(locale)) || item.label;
109
111
  return {
110
112
  type: 'group',
111
- label: pickLang(item.translations, localeToLang(locale)) || item.label,
113
+ label,
112
114
  entries: sidebarFromDir(tree, currentPathname, locale, subgroupCollapsed ?? item.collapsed),
113
115
  collapsed: item.collapsed,
114
- badge: item.badge,
116
+ badge: getSidebarBadge(item.badge, locale, label),
115
117
  };
116
118
  }
117
119
 
@@ -131,7 +133,13 @@ function linkFromSidebarLinkItem(
131
133
  if (locale) href = '/' + locale + href;
132
134
  }
133
135
  const label = pickLang(item.translations, localeToLang(locale)) || item.label;
134
- return makeSidebarLink(href, label, currentPathname, item.badge, item.attrs);
136
+ return makeSidebarLink(
137
+ href,
138
+ label,
139
+ currentPathname,
140
+ getSidebarBadge(item.badge, locale, label),
141
+ item.attrs
142
+ );
135
143
  }
136
144
 
137
145
  /** Create a link entry from an automatic internal link item in user config. */
@@ -161,7 +169,13 @@ function linkFromInternalSidebarLinkItem(
161
169
  }
162
170
  const label =
163
171
  pickLang(item.translations, localeToLang(locale)) || item.label || entry.entry.data.title;
164
- return makeSidebarLink(entry.slug, label, currentPathname, item.badge, item.attrs);
172
+ return makeSidebarLink(
173
+ entry.slug,
174
+ label,
175
+ currentPathname,
176
+ getSidebarBadge(item.badge, locale, label),
177
+ item.attrs
178
+ );
165
179
  }
166
180
 
167
181
  /** Process sidebar link options to create a link entry. */
@@ -446,3 +460,38 @@ function stripExtension(path: string) {
446
460
  const periodIndex = path.lastIndexOf('.');
447
461
  return path.slice(0, periodIndex > -1 ? periodIndex : undefined);
448
462
  }
463
+
464
+ /** Get a sidebar badge for a given item. */
465
+ function getSidebarBadge(
466
+ config: I18nBadgeConfig,
467
+ locale: string | undefined,
468
+ itemLabel: string
469
+ ): Badge | undefined {
470
+ if (!config) return;
471
+ if (typeof config === 'string') {
472
+ return { variant: 'default', text: config };
473
+ }
474
+ return { ...config, text: getSidebarBadgeText(config.text, locale, itemLabel) };
475
+ }
476
+
477
+ /** Get the badge text for a sidebar item. */
478
+ function getSidebarBadgeText(
479
+ text: I18nBadge['text'],
480
+ locale: string | undefined,
481
+ itemLabel: string
482
+ ): string {
483
+ if (typeof text === 'string') return text;
484
+ const defaultLang =
485
+ config.defaultLocale?.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang;
486
+ const defaultText = text[defaultLang];
487
+
488
+ if (!defaultText) {
489
+ throw new AstroError(
490
+ `The badge text for "${itemLabel}" must have a key for the default language "${defaultLang}".`,
491
+ 'Update the Starlight config to include a badge text for the default language.\n' +
492
+ 'Learn more about sidebar badges internationalization at https://starlight.astro.build/guides/sidebar/#internationalization-with-badges'
493
+ );
494
+ }
495
+
496
+ return pickLang(text, localeToLang(locale)) || defaultText;
497
+ }
package/utils/plugins.ts CHANGED
@@ -1,8 +1,9 @@
1
- import type { AstroIntegration } from 'astro';
1
+ import type { AstroIntegration, HookParameters } from 'astro';
2
2
  import { z } from 'astro/zod';
3
3
  import { StarlightConfigSchema, type StarlightUserConfig } from '../utils/user-config';
4
4
  import { parseWithFriendlyErrors } from '../utils/error-map';
5
5
  import { AstroError } from 'astro/errors';
6
+ import type { UserI18nSchema } from './translations';
6
7
 
7
8
  /**
8
9
  * Runs Starlight plugins in the order that they are configured after validating the user-provided
@@ -32,6 +33,8 @@ export async function runPlugins(
32
33
 
33
34
  // A list of Astro integrations added by the various plugins.
34
35
  const integrations: AstroIntegration[] = [];
36
+ // A list of translations injected by the various plugins keyed by locale.
37
+ const pluginTranslations: PluginTranslations = {};
35
38
 
36
39
  for (const {
37
40
  name,
@@ -70,6 +73,13 @@ export async function runPlugins(
70
73
  command: context.command,
71
74
  isRestart: context.isRestart,
72
75
  logger: context.logger.fork(name),
76
+ injectTranslations(translations) {
77
+ // Merge the translations injected by the plugin.
78
+ for (const [locale, localeTranslations] of Object.entries(translations)) {
79
+ pluginTranslations[locale] ??= {};
80
+ Object.assign(pluginTranslations[locale]!, localeTranslations);
81
+ }
82
+ },
73
83
  });
74
84
  }
75
85
 
@@ -81,7 +91,34 @@ export async function runPlugins(
81
91
  );
82
92
  }
83
93
 
84
- return { integrations, starlightConfig };
94
+ return { integrations, starlightConfig, pluginTranslations };
95
+ }
96
+
97
+ export function injectPluginTranslationsTypes(
98
+ translations: PluginTranslations,
99
+ injectTypes: HookParameters<'astro:config:done'>['injectTypes']
100
+ ) {
101
+ const allKeys = new Set<string>();
102
+
103
+ for (const localeTranslations of Object.values(translations)) {
104
+ for (const key of Object.keys(localeTranslations)) {
105
+ allKeys.add(key);
106
+ }
107
+ }
108
+
109
+ // If there are no translations to inject, we don't need to generate any types or cleanup
110
+ // previous ones as they will not be referenced anymore.
111
+ if (allKeys.size === 0) return;
112
+
113
+ injectTypes({
114
+ filename: 'i18n-plugins.d.ts',
115
+ content: `declare namespace StarlightApp {
116
+ type PluginUIStringKeys = {
117
+ ${[...allKeys].map((key) => `'${key}': string;`).join('\n\t\t')}
118
+ };
119
+ interface I18n extends PluginUIStringKeys {}
120
+ }`,
121
+ });
85
122
  }
86
123
 
87
124
  // https://github.com/withastro/astro/blob/910eb00fe0b70ca80bd09520ae100e8c78b675b5/packages/astro/src/core/config/schema.ts#L113
@@ -192,6 +229,32 @@ const starlightPluginSchema = baseStarlightPluginSchema.extend({
192
229
  * @see https://docs.astro.build/en/reference/integrations-reference/#astrointegrationlogger
193
230
  */
194
231
  logger: z.any() as z.Schema<StarlightPluginContext['logger']>,
232
+ /**
233
+ * A callback function to add or update translations strings.
234
+ *
235
+ * @see https://starlight.astro.build/guides/i18n/#extend-translation-schema
236
+ *
237
+ * @example
238
+ * {
239
+ * name: 'My Starlight Plugin',
240
+ * hooks: {
241
+ * setup({ injectTranslations }) {
242
+ * injectTranslations({
243
+ * en: {
244
+ * 'myPlugin.doThing': 'Do the thing',
245
+ * },
246
+ * fr: {
247
+ * 'myPlugin.doThing': 'Faire le truc',
248
+ * },
249
+ * });
250
+ * }
251
+ * }
252
+ * }
253
+ */
254
+ injectTranslations: z.function(
255
+ z.tuple([z.record(z.string(), z.record(z.string(), z.string()))]),
256
+ z.void()
257
+ ),
195
258
  }),
196
259
  ]),
197
260
  z.union([z.void(), z.promise(z.void())])
@@ -222,3 +285,5 @@ export type StarlightPluginContext = Pick<
222
285
  Parameters<NonNullable<AstroIntegration['hooks']['astro:config:setup']>>[0],
223
286
  'command' | 'config' | 'isRestart' | 'logger'
224
287
  >;
288
+
289
+ export type PluginTranslations = Record<string, UserI18nSchema & Record<string, string>>;
@@ -7,8 +7,9 @@ import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
7
7
  import { ensureTrailingSlash } from './path';
8
8
  import type { Route } from './routing';
9
9
  import { localizedId } from './slugs';
10
- import { useTranslations } from './translations';
11
10
  import { formatPath } from './format-path';
11
+ import { useTranslations } from './translations';
12
+ import { DeprecatedLabelsPropProxy } from './i18n';
12
13
 
13
14
  export interface PageProps extends Route {
14
15
  headings: MarkdownHeading[];
@@ -33,8 +34,8 @@ export interface StarlightRouteData extends Route {
33
34
  lastUpdated: Date | undefined;
34
35
  /** URL object for the address where this page can be edited if enabled. */
35
36
  editUrl: URL | undefined;
36
- /** Record of UI strings localized for the current page. */
37
- labels: ReturnType<ReturnType<typeof useTranslations>['all']>;
37
+ /** @deprecated Use `Astro.locals.t()` instead. */
38
+ labels: Record<string, never>;
38
39
  }
39
40
 
40
41
  export function generateRouteData({
@@ -57,7 +58,7 @@ export function generateRouteData({
57
58
  toc: getToC(props),
58
59
  lastUpdated: getLastUpdated(props),
59
60
  editUrl: getEditUrl(props),
60
- labels: useTranslations(locale).all(),
61
+ labels: DeprecatedLabelsPropProxy,
61
62
  };
62
63
  }
63
64
 
@@ -13,8 +13,9 @@ import {
13
13
  import type { StarlightDocsEntry } from './routing';
14
14
  import { slugToLocaleData, urlToSlug } from './slugs';
15
15
  import { getPrevNextLinks, getSidebarFromConfig } from './navigation';
16
- import { useTranslations } from './translations';
17
16
  import { docsSchema } from '../schema';
17
+ import type { Prettify, RemoveIndexSignature } from './types';
18
+ import { DeprecatedLabelsPropProxy } from './i18n';
18
19
  import { SidebarItemSchema } from '../schemas/sidebar';
19
20
  import type { StarlightConfig, StarlightUserConfig } from './user-config';
20
21
 
@@ -151,7 +152,7 @@ export async function generateStarlightPageRouteData({
151
152
  entryMeta,
152
153
  hasSidebar: props.hasSidebar ?? entry.data.template !== 'splash',
153
154
  headings,
154
- labels: useTranslations(localeData.locale).all(),
155
+ labels: DeprecatedLabelsPropProxy,
155
156
  lastUpdated,
156
157
  pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
157
158
  sidebar,
@@ -214,19 +215,3 @@ async function getUserDocsSchema(): Promise<
214
215
  const userCollections = (await import('virtual:starlight/collection-config')).collections;
215
216
  return userCollections?.docs.schema ?? docsSchema();
216
217
  }
217
-
218
- // https://stackoverflow.com/a/66252656/1945960
219
- type RemoveIndexSignature<T> = {
220
- [K in keyof T as string extends K
221
- ? never
222
- : number extends K
223
- ? never
224
- : symbol extends K
225
- ? never
226
- : K]: T[K];
227
- };
228
-
229
- // https://www.totaltypescript.com/concepts/the-prettify-helper
230
- type Prettify<T> = {
231
- [K in keyof T]: T[K];
232
- } & {};
@@ -11,9 +11,10 @@ import type { AstroConfig } from 'astro';
11
11
  *
12
12
  * @see [`./translations.ts`](./translations.ts)
13
13
  */
14
- export function createTranslationSystemFromFs(
14
+ export function createTranslationSystemFromFs<T extends i18nSchemaOutput>(
15
15
  opts: Pick<StarlightConfig, 'defaultLocale' | 'locales'>,
16
- { srcDir }: Pick<AstroConfig, 'srcDir'>
16
+ { srcDir }: Pick<AstroConfig, 'srcDir'>,
17
+ pluginTranslations: Record<string, T> = {}
17
18
  ) {
18
19
  /** All translation data from the i18n collection, keyed by `id`, which matches locale. */
19
20
  let userTranslations: Record<string, i18nSchemaOutput> = {};
@@ -40,5 +41,5 @@ export function createTranslationSystemFromFs(
40
41
  }
41
42
  }
42
43
 
43
- return createTranslationSystem(userTranslations, opts);
44
+ return createTranslationSystem(opts, userTranslations, pluginTranslations);
44
45
  }
@@ -1,13 +1,16 @@
1
1
  import { getCollection, type CollectionEntry, type DataCollectionKey } from 'astro:content';
2
2
  import config from 'virtual:starlight/user-config';
3
+ import pluginTranslations from 'virtual:starlight/plugin-translations';
3
4
  import type { i18nSchemaOutput } from '../schemas/i18n';
4
5
  import { createTranslationSystem } from './createTranslationSystem';
6
+ import type { RemoveIndexSignature } from './types';
5
7
 
6
- type UserI18nSchema = 'i18n' extends DataCollectionKey
8
+ export type UserI18nSchema = 'i18n' extends DataCollectionKey
7
9
  ? CollectionEntry<'i18n'> extends { data: infer T }
8
- ? T
10
+ ? i18nSchemaOutput & T
9
11
  : i18nSchemaOutput
10
12
  : i18nSchemaOutput;
13
+ export type UserI18nKeys = keyof RemoveIndexSignature<UserI18nSchema>;
11
14
 
12
15
  /** Get all translation data from the i18n collection, keyed by `id`, which matches locale. */
13
16
  async function loadTranslations() {
@@ -34,4 +37,8 @@ async function loadTranslations() {
34
37
  * const t = useTranslations('en');
35
38
  * const label = t('search.label'); // => 'Search'
36
39
  */
37
- export const useTranslations = createTranslationSystem(await loadTranslations(), config);
40
+ export const useTranslations = createTranslationSystem(
41
+ config,
42
+ await loadTranslations(),
43
+ pluginTranslations
44
+ );
package/utils/types.ts ADDED
@@ -0,0 +1,15 @@
1
+ // https://stackoverflow.com/a/66252656/1945960
2
+ export type RemoveIndexSignature<T> = {
3
+ [K in keyof T as string extends K
4
+ ? never
5
+ : number extends K
6
+ ? never
7
+ : symbol extends K
8
+ ? never
9
+ : K]: T[K];
10
+ };
11
+
12
+ // https://www.totaltypescript.com/concepts/the-prettify-helper
13
+ export type Prettify<T> = {
14
+ [K in keyof T]: T[K];
15
+ } & {};
package/virtual.d.ts CHANGED
@@ -28,6 +28,11 @@ declare module 'virtual:starlight/user-images' {
28
28
  };
29
29
  }
30
30
 
31
+ declare module 'virtual:starlight/plugin-translations' {
32
+ const PluginTranslations: import('./utils/plugins').PluginTranslations;
33
+ export default PluginTranslations;
34
+ }
35
+
31
36
  declare module 'virtual:starlight/collection-config' {
32
37
  export const collections: import('astro:content').ContentConfig['collections'] | undefined;
33
38
  }