@astrojs/starlight 0.11.1 → 0.12.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#995](https://github.com/withastro/starlight/pull/995) [`5bf4457`](https://github.com/withastro/starlight/commit/5bf44577634935b9fa6d50b040abcd680035075f) Thanks [@kevinzunigacuellar](https://github.com/kevinzunigacuellar)! - Adds support for adding sidebar badges to group headings
8
+
9
+ - [#988](https://github.com/withastro/starlight/pull/988) [`977fe13`](https://github.com/withastro/starlight/commit/977fe135a74661300589898abe98aec73cad9ed3) Thanks [@magicDGS](https://github.com/magicDGS)! - Include social icon links in mobile menu
10
+
11
+ - [#280](https://github.com/withastro/starlight/pull/280) [`72cca2d`](https://github.com/withastro/starlight/commit/72cca2d07644f00595da6ebf7d603adb282f359d) Thanks [@cbontems](https://github.com/cbontems)! - Support light & dark variants of the hero image.
12
+
13
+ ⚠️ **Potentially breaking change:** The `hero.image` schema is now slightly stricter than previously.
14
+
15
+ The `hero.image.html` property can no longer be used alongside the `hero.image.alt` or `hero.image.file` properties.
16
+ Previously, `html` was ignored when used with `file` and `alt` was ignored when used with `html`.
17
+ Now, those combinations will throw errors.
18
+ If you encounter errors, remove the `image.hero` property that is not in use.
19
+
20
+ ### Patch Changes
21
+
22
+ - [#1004](https://github.com/withastro/starlight/pull/1004) [`7f92213`](https://github.com/withastro/starlight/commit/7f92213a0b93de5a844816841a6bc9cdd371de0c) Thanks [@nunhes](https://github.com/nunhes)! - Add Galician language support
23
+
24
+ - [#1003](https://github.com/withastro/starlight/pull/1003) [`f1fdb50`](https://github.com/withastro/starlight/commit/f1fdb50daebe79548c7789d3f7dd968b261d2da7) Thanks [@delucis](https://github.com/delucis)! - Internal: refactor translation string loading to make translations available to Starlight integration code
25
+
26
+ ## 0.11.2
27
+
28
+ ### Patch Changes
29
+
30
+ - [#944](https://github.com/withastro/starlight/pull/944) [`7a6446e`](https://github.com/withastro/starlight/commit/7a6446ebc61ba9cc36d1dcfd13db15c1533751ab) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Fix issue with sidebar autogenerated groups configured with a directory containing leading or trailing slash
31
+
32
+ - [#985](https://github.com/withastro/starlight/pull/985) [`92b3b57`](https://github.com/withastro/starlight/commit/92b3b575404d0dc34f720c0ba29d8ed50be98f58) Thanks [@delucis](https://github.com/delucis)! - Fix edit URLs for pages displaying fallback content
33
+
34
+ - [#986](https://github.com/withastro/starlight/pull/986) [`0470734`](https://github.com/withastro/starlight/commit/0470734e0dc323f9945e06bee4338c2f777ba0d6) Thanks [@dreyfus92](https://github.com/dreyfus92)! - Prevent overscrolling on mobile table of contents by setting 'overscroll-behavior: contain'.
35
+
36
+ - [#924](https://github.com/withastro/starlight/pull/924) [`39d6302`](https://github.com/withastro/starlight/commit/39d6302db42ee1105d690bbd3a66053e6b21e15a) Thanks [@kevinzunigacuellar](https://github.com/kevinzunigacuellar)! - Remove extra margin from markdown lists that uses inline code
37
+
38
+ - [#814](https://github.com/withastro/starlight/pull/814) [`1e517d9`](https://github.com/withastro/starlight/commit/1e517d92a4cf146d2dcae58f7c4299d6f25ea73e) Thanks [@julien-deramond](https://github.com/julien-deramond)! - Prevent text from overflowing pagination items
39
+
3
40
  ## 0.11.1
4
41
 
5
42
  ### Patch Changes
@@ -14,20 +14,34 @@ const imageAttrs = {
14
14
  height: 400,
15
15
  alt: image?.alt || '',
16
16
  };
17
+
18
+ let darkImage: ImageMetadata | undefined;
19
+ let lightImage: ImageMetadata | undefined;
20
+ let rawHtml: string | undefined;
21
+ if (image) {
22
+ if ('file' in image) {
23
+ darkImage = image.file;
24
+ } else if ('dark' in image) {
25
+ darkImage = image.dark;
26
+ lightImage = image.light;
27
+ } else {
28
+ rawHtml = image.html;
29
+ }
30
+ }
17
31
  ---
18
32
 
19
33
  <div class="hero">
20
34
  {
21
- image?.file ? (
22
- image.file.format === 'svg' ? (
23
- <img src={image.file.src} {...imageAttrs} />
24
- ) : (
25
- <Image src={image.file} {...imageAttrs} />
26
- )
27
- ) : (
28
- image?.html && <div class="hero-html sl-flex" set:html={image.html} />
35
+ darkImage && (
36
+ <Image
37
+ src={darkImage}
38
+ {...imageAttrs}
39
+ class:list={{ 'light:sl-hidden': Boolean(lightImage) }}
40
+ />
29
41
  )
30
42
  }
43
+ {lightImage && <Image src={lightImage} {...imageAttrs} class="dark:sl-hidden" />}
44
+ {rawHtml && <div class="hero-html sl-flex" set:html={rawHtml} />}
31
45
  <div class="sl-flex stack">
32
46
  <div class="sl-flex copy">
33
47
  <h1 id={PAGE_TITLE_ID} data-page-title set:html={title} />
@@ -7,8 +7,8 @@ import type { Props } from '../props';
7
7
  <style>
8
8
  .content
9
9
  :global(
10
- :not(a, strong, em, del, span, input)
11
- + :not(a, strong, em, del, span, input, :where(.not-content *))
10
+ :not(a, strong, em, del, span, input, code)
11
+ + :not(a, strong, em, del, span, input, code, :where(.not-content *))
12
12
  ) {
13
13
  margin-top: 1.5rem;
14
14
  }
@@ -1,17 +1,31 @@
1
1
  ---
2
- import { LanguageSelect, ThemeSelect } from 'virtual:starlight/components';
2
+ import { LanguageSelect, ThemeSelect, SocialIcons } from 'virtual:starlight/components';
3
3
  import type { Props } from '../props';
4
4
  ---
5
5
 
6
6
  <div class="mobile-preferences sl-flex">
7
+ <div class="sl-flex social-icons">
8
+ <SocialIcons {...Astro.props} />
9
+ </div>
7
10
  <ThemeSelect {...Astro.props} />
8
11
  <LanguageSelect {...Astro.props} />
9
12
  </div>
10
13
 
11
14
  <style>
15
+ .social-icons {
16
+ margin-inline-end: auto;
17
+ gap: 1rem;
18
+ align-items: center;
19
+ padding-block: 1rem;
20
+ }
21
+ .social-icons:empty {
22
+ display: none;
23
+ }
12
24
  .mobile-preferences {
13
25
  justify-content: space-between;
26
+ flex-wrap: wrap;
14
27
  border-top: 1px solid var(--sl-color-gray-6);
28
+ column-gap: 1rem;
15
29
  padding: 0.5rem 0;
16
30
  }
17
31
  </style>
@@ -105,6 +105,7 @@ const t = useTranslations(locale);
105
105
  overflow-y: auto;
106
106
  background-color: var(--sl-color-black);
107
107
  box-shadow: var(--sl-shadow-md);
108
+ overscroll-behavior: contain;
108
109
  }
109
110
  </style>
110
111
 
@@ -9,7 +9,7 @@ const isRtl = dir === 'rtl';
9
9
  const t = useTranslations(locale);
10
10
  ---
11
11
 
12
- <div class="pagination-links sl-flex" dir={dir}>
12
+ <div class="pagination-links" dir={dir}>
13
13
  {
14
14
  prev && (
15
15
  <a href={prev.href} rel="prev">
@@ -38,7 +38,8 @@ const t = useTranslations(locale);
38
38
 
39
39
  <style>
40
40
  .pagination-links {
41
- flex-wrap: wrap;
41
+ display: grid;
42
+ grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
42
43
  gap: 1rem;
43
44
  }
44
45
 
@@ -56,6 +57,7 @@ const t = useTranslations(locale);
56
57
  text-decoration: none;
57
58
  color: var(--sl-color-gray-2);
58
59
  box-shadow: var(--sl-shadow-md);
60
+ overflow-wrap: anywhere;
59
61
  }
60
62
  [rel='next'] {
61
63
  justify-content: end;
@@ -71,4 +73,8 @@ const t = useTranslations(locale);
71
73
  font-size: var(--sl-text-2xl);
72
74
  line-height: var(--sl-line-height-headings);
73
75
  }
76
+
77
+ svg {
78
+ flex-shrink: 0;
79
+ }
74
80
  </style>
@@ -36,7 +36,15 @@ interface Props {
36
36
  open={flattenSidebar(entry.entries).some((i) => i.isCurrent) || !entry.collapsed}
37
37
  >
38
38
  <summary>
39
- <span class="large">{entry.label}</span>
39
+ <div class="group-label">
40
+ <span class="large">{entry.label}</span>
41
+ {entry.badge && (
42
+ <>
43
+ {' '}
44
+ <Badge text={entry.badge.text} variant={entry.badge.variant} />
45
+ </>
46
+ )}
47
+ </div>
40
48
  <Icon name="right-caret" class="caret" size="1.25rem" />
41
49
  </summary>
42
50
  <Astro.self sublist={entry.entries} nested />
@@ -78,7 +86,8 @@ interface Props {
78
86
  display: flex;
79
87
  align-items: center;
80
88
  justify-content: space-between;
81
- padding-inline: var(--sl-sidebar-item-padding-inline);
89
+ padding: 0.2em var(--sl-sidebar-item-padding-inline);
90
+ line-height: 1.4;
82
91
  cursor: pointer;
83
92
  user-select: none;
84
93
  }
@@ -120,7 +129,8 @@ interface Props {
120
129
  background-color: var(--sl-color-text-accent);
121
130
  }
122
131
 
123
- a > *:not(:last-child) {
132
+ a > *:not(:last-child),
133
+ .group-label > *:not(:last-child) {
124
134
  margin-inline-end: 0.25em;
125
135
  }
126
136
 
@@ -12,7 +12,7 @@ const href = pathWithBase(Astro.props.locale || '/');
12
12
  config.logo && logos.dark && (
13
13
  <>
14
14
  <img
15
- class:list={{ 'dark-only': !('src' in config.logo) }}
15
+ class:list={{ 'light:sl-hidden': !('src' in config.logo) }}
16
16
  alt={config.logo.alt}
17
17
  src={logos.dark.src}
18
18
  width={logos.dark.width}
@@ -21,7 +21,7 @@ const href = pathWithBase(Astro.props.locale || '/');
21
21
  {/* Show light alternate if a user configure both light and dark logos. */}
22
22
  {!('src' in config.logo) && (
23
23
  <img
24
- class="light-only"
24
+ class="dark:sl-hidden"
25
25
  alt={config.logo.alt}
26
26
  src={logos.light?.src}
27
27
  width={logos.light?.width}
@@ -56,10 +56,4 @@ const href = pathWithBase(Astro.props.locale || '/');
56
56
  object-fit: contain;
57
57
  object-position: 0 50%;
58
58
  }
59
- :global([data-theme='light']) .dark-only {
60
- display: none;
61
- }
62
- :global([data-theme='dark']) .light-only {
63
- display: none;
64
- }
65
59
  </style>
package/index.ts CHANGED
@@ -9,6 +9,7 @@ import { vitePluginStarlightUserConfig } from './integrations/virtual-user-confi
9
9
  import { errorMap } from './utils/error-map';
10
10
  import { StarlightConfigSchema, type StarlightUserConfig } from './utils/user-config';
11
11
  import { rehypeRtlCodeSupport } from './integrations/code-rtl-support';
12
+ import { createTranslationSystemFromFs } from './utils/translations-fs';
12
13
 
13
14
  export default function StarlightIntegration(opts: StarlightUserConfig): AstroIntegration {
14
15
  const parsedConfig = StarlightConfigSchema.safeParse(opts, { errorMap });
@@ -26,6 +27,7 @@ export default function StarlightIntegration(opts: StarlightUserConfig): AstroIn
26
27
  name: '@astrojs/starlight',
27
28
  hooks: {
28
29
  'astro:config:setup': ({ config, injectRoute, updateConfig }) => {
30
+ const useTranslations = createTranslationSystemFromFs(userConfig, config);
29
31
  injectRoute({
30
32
  pattern: '404',
31
33
  entryPoint: '@astrojs/starlight/404.astro',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
package/schema.ts CHANGED
@@ -3,16 +3,13 @@ import type { SchemaContext } from 'astro:content';
3
3
  import { HeadConfigSchema } from './schemas/head';
4
4
  import { PrevNextLinkConfigSchema } from './schemas/prevNextLink';
5
5
  import { TableOfContentsSchema } from './schemas/tableOfContents';
6
- import { Icons } from './components/Icons';
7
6
  import { BadgeConfigSchema } from './schemas/badge';
7
+ import { HeroSchema } from './schemas/hero';
8
8
  import { SidebarLinkItemHTMLAttributesSchema } from './schemas/sidebar';
9
9
  export { i18nSchema } from './schemas/i18n';
10
10
 
11
- type IconName = keyof typeof Icons;
12
- const iconNames = Object.keys(Icons) as [IconName, ...IconName[]];
13
-
14
11
  export function docsSchema() {
15
- return ({ image }: SchemaContext) =>
12
+ return (context: SchemaContext) =>
16
13
  z.object({
17
14
  /** The title of the current page. Required. */
18
15
  title: z.string(),
@@ -45,56 +42,7 @@ export function docsSchema() {
45
42
  template: z.enum(['doc', 'splash']).default('doc'),
46
43
 
47
44
  /** Display a hero section on this page. */
48
- hero: z
49
- .object({
50
- /**
51
- * The large title text to show. If not provided, will default to the top-level `title`.
52
- * Can include HTML.
53
- */
54
- title: z.string().optional(),
55
- /**
56
- * A short bit of text about your project.
57
- * Will be displayed in a smaller size below the title.
58
- */
59
- tagline: z.string().optional(),
60
- /** The image to use in the hero. You can provide either a relative `file` path or raw `html`. */
61
- image: z
62
- .object({
63
- /** Alt text for screenreaders and other assistive technologies describing your hero image. */
64
- alt: z.string().default(''),
65
- /** Relative path to an image file in your repo, e.g. `../../assets/hero.png`. */
66
- file: image().optional(),
67
- /** Raw HTML string instead of an image file. Useful for inline SVGs or more complex hero content. */
68
- html: z.string().optional(),
69
- })
70
- .optional(),
71
- /** An array of call-to-action links displayed at the bottom of the hero. */
72
- actions: z
73
- .object({
74
- /** Text label displayed in the link. */
75
- text: z.string(),
76
- /** Value for the link’s `href` attribute, e.g. `/page` or `https://mysite.com`. */
77
- link: z.string(),
78
- /** Button style to use. One of `primary`, `secondary`, or `minimal` (the default). */
79
- variant: z.enum(['primary', 'secondary', 'minimal']).default('minimal'),
80
- /**
81
- * An optional icon to display alongside the link text.
82
- * Can be an inline `<svg>` or the name of one of Starlight’s built-in icons.
83
- */
84
- icon: z
85
- .union([z.enum(iconNames), z.string().startsWith('<svg')])
86
- .transform((icon) => {
87
- const parsedIcon = z.enum(iconNames).safeParse(icon);
88
- return parsedIcon.success
89
- ? ({ type: 'icon', name: parsedIcon.data } as const)
90
- : ({ type: 'raw', html: icon } as const);
91
- })
92
- .optional(),
93
- })
94
- .array()
95
- .default([]),
96
- })
97
- .optional(),
45
+ hero: HeroSchema(context).optional(),
98
46
 
99
47
  /**
100
48
  * The last update date of the current page.
@@ -0,0 +1,70 @@
1
+ import { z } from 'astro/zod';
2
+ import type { SchemaContext } from 'astro:content';
3
+ import { Icons } from '../components/Icons';
4
+
5
+ type IconName = keyof typeof Icons;
6
+ const iconNames = Object.keys(Icons) as [IconName, ...IconName[]];
7
+
8
+ export const HeroSchema = ({ image }: SchemaContext) =>
9
+ z.object({
10
+ /**
11
+ * The large title text to show. If not provided, will default to the top-level `title`.
12
+ * Can include HTML.
13
+ */
14
+ title: z.string().optional(),
15
+ /**
16
+ * A short bit of text about your project.
17
+ * Will be displayed in a smaller size below the title.
18
+ */
19
+ tagline: z.string().optional(),
20
+ /** The image to use in the hero. You can provide either a relative `file` path or raw `html`. */
21
+ image: z
22
+ .union([
23
+ z.object({
24
+ /** Alt text for screenreaders and other assistive technologies describing your hero image. */
25
+ alt: z.string().default(''),
26
+ /** Relative path to an image file in your repo, e.g. `../../assets/hero.png`. */
27
+ file: image(),
28
+ }),
29
+ z.object({
30
+ /** Alt text for screenreaders and other assistive technologies describing your hero image. */
31
+ alt: z.string().default(''),
32
+ /** Relative path to an image file in your repo to use in dark mode, e.g. `../../assets/hero-dark.png`. */
33
+ dark: image(),
34
+ /** Relative path to an image file in your repo to use in light mode, e.g. `../../assets/hero-light.png`. */
35
+ light: image(),
36
+ }),
37
+ z
38
+ .object({
39
+ /** Raw HTML string instead of an image file. Useful for inline SVGs or more complex hero content. */
40
+ html: z.string(),
41
+ })
42
+ .transform(({ html }) => ({ html, alt: '' })),
43
+ ])
44
+ .optional(),
45
+ /** An array of call-to-action links displayed at the bottom of the hero. */
46
+ actions: z
47
+ .object({
48
+ /** Text label displayed in the link. */
49
+ text: z.string(),
50
+ /** Value for the link’s `href` attribute, e.g. `/page` or `https://mysite.com`. */
51
+ link: z.string(),
52
+ /** Button style to use. One of `primary`, `secondary`, or `minimal` (the default). */
53
+ variant: z.enum(['primary', 'secondary', 'minimal']).default('minimal'),
54
+ /**
55
+ * An optional icon to display alongside the link text.
56
+ * Can be an inline `<svg>` or the name of one of Starlight’s built-in icons.
57
+ */
58
+ icon: z
59
+ .union([z.enum(iconNames), z.string().startsWith('<svg')])
60
+ .transform((icon) => {
61
+ const parsedIcon = z.enum(iconNames).safeParse(icon);
62
+ return parsedIcon.success
63
+ ? ({ type: 'icon', name: parsedIcon.data } as const)
64
+ : ({ type: 'raw', html: icon } as const);
65
+ })
66
+ .optional(),
67
+ })
68
+ .array()
69
+ .default([]),
70
+ });
package/schemas/i18n.ts CHANGED
@@ -3,6 +3,7 @@ import { z } from 'astro/zod';
3
3
  export function i18nSchema() {
4
4
  return starlightI18nSchema().merge(pagefindI18nSchema());
5
5
  }
6
+ export type i18nSchemaOutput = z.output<ReturnType<typeof i18nSchema>>;
6
7
 
7
8
  export function builtinI18nSchema() {
8
9
  return starlightI18nSchema().required().strict().merge(pagefindI18nSchema());
@@ -2,12 +2,15 @@ import type { AstroBuiltinAttributes } from 'astro';
2
2
  import type { HTMLAttributes } from 'astro/types';
3
3
  import { z } from 'astro/zod';
4
4
  import { BadgeConfigSchema } from './badge';
5
+ import { stripLeadingAndTrailingSlashes } from '../utils/path';
5
6
 
6
7
  const SidebarBaseSchema = z.object({
7
8
  /** The visible label for this item in the sidebar. */
8
9
  label: z.string(),
9
10
  /** Translations of the `label` for each supported language. */
10
11
  translations: z.record(z.string()).default({}),
12
+ /** Adds a badge to the link item */
13
+ badge: BadgeConfigSchema(),
11
14
  });
12
15
 
13
16
  const SidebarGroupSchema = SidebarBaseSchema.extend({
@@ -28,8 +31,6 @@ export const SidebarLinkItemHTMLAttributesSchema = () => linkHTMLAttributesSchem
28
31
  const SidebarLinkItemSchema = SidebarBaseSchema.extend({
29
32
  /** The link to this item’s content. Can be a relative link to local files or the full URL of an external page. */
30
33
  link: z.string(),
31
- /** Adds a badge to the link item */
32
- badge: BadgeConfigSchema(),
33
34
  /** HTML attributes to add to the link item. */
34
35
  attrs: SidebarLinkItemHTMLAttributesSchema(),
35
36
  });
@@ -39,7 +40,7 @@ const AutoSidebarGroupSchema = SidebarGroupSchema.extend({
39
40
  /** Enable autogenerating a sidebar category from a specific docs directory. */
40
41
  autogenerate: z.object({
41
42
  /** The directory to generate sidebar items for. */
42
- directory: z.string(),
43
+ directory: z.string().transform(stripLeadingAndTrailingSlashes),
43
44
  /**
44
45
  * Whether the autogenerated subgroups should be collapsed by default.
45
46
  * Defaults to the `AutoSidebarGroup` `collapsed` value.
package/schemas/social.ts CHANGED
@@ -1,33 +1,35 @@
1
1
  import { z } from 'astro/zod';
2
2
 
3
+ export const socialLinks = [
4
+ 'twitter',
5
+ 'mastodon',
6
+ 'github',
7
+ 'gitlab',
8
+ 'bitbucket',
9
+ 'discord',
10
+ 'gitter',
11
+ 'codeberg',
12
+ 'codePen',
13
+ 'youtube',
14
+ 'threads',
15
+ 'linkedin',
16
+ 'twitch',
17
+ 'microsoftTeams',
18
+ 'instagram',
19
+ 'stackOverflow',
20
+ 'x.com',
21
+ 'telegram',
22
+ 'rss',
23
+ 'facebook',
24
+ 'email',
25
+ 'reddit',
26
+ 'patreon',
27
+ ] as const;
28
+
3
29
  export const SocialLinksSchema = () =>
4
30
  z
5
31
  .record(
6
- z.enum([
7
- 'twitter',
8
- 'mastodon',
9
- 'github',
10
- 'gitlab',
11
- 'bitbucket',
12
- 'discord',
13
- 'gitter',
14
- 'codeberg',
15
- 'codePen',
16
- 'youtube',
17
- 'threads',
18
- 'linkedin',
19
- 'twitch',
20
- 'microsoftTeams',
21
- 'instagram',
22
- 'stackOverflow',
23
- 'x.com',
24
- 'telegram',
25
- 'rss',
26
- 'facebook',
27
- 'email',
28
- 'reddit',
29
- 'patreon',
30
- ]),
32
+ z.enum(socialLinks),
31
33
  // Link to the respective social profile for this site
32
34
  z.string().url()
33
35
  )
package/style/util.css CHANGED
@@ -41,6 +41,12 @@
41
41
  display: block;
42
42
  }
43
43
  }
44
+ [data-theme='light'] .light\:sl-hidden {
45
+ display: none;
46
+ }
47
+ [data-theme='dark'] .dark\:sl-hidden {
48
+ display: none;
49
+ }
44
50
 
45
51
  /*
46
52
  Flip an element around the y-axis when in an RTL context.
@@ -0,0 +1,22 @@
1
+ {
2
+ "skipLink.label": "Ir ao contido",
3
+ "search.label": "Busca",
4
+ "search.shortcutLabel": "(Preme / para Busca)",
5
+ "search.cancelLabel": "Deixar",
6
+ "search.devWarning": "A busca só está dispoñible nas versións de producción. \nTrata de construir e ollear o sitio para probalo localmente.",
7
+ "themeSelect.accessibleLabel": "Seleciona tema",
8
+ "themeSelect.dark": "Escuro",
9
+ "themeSelect.light": "Claro",
10
+ "themeSelect.auto": "Auto",
11
+ "languageSelect.accessibleLabel": "Seleciona linguaxe",
12
+ "menuButton.accessibleLabel": "Menú",
13
+ "sidebarNav.accessibleLabel": "Principal",
14
+ "tableOfContents.onThisPage": "Nesta paxina",
15
+ "tableOfContents.overview": "Sinopse",
16
+ "i18n.untranslatedContent": "Este contido aínda non está dispoñible no teu idioma.",
17
+ "page.editLink": "Editar paxina",
18
+ "page.lastUpdated": "Última actualización:",
19
+ "page.previousLink": "Anterior",
20
+ "page.nextLink": "Seguinte",
21
+ "404.text": "Paxina non atopada. Comproba a URL ou intenta usar a barra de busca."
22
+ }
@@ -7,6 +7,7 @@ import ja from './ja.json';
7
7
  import pt from './pt.json';
8
8
  import fa from './fa.json';
9
9
  import fr from './fr.json';
10
+ import gl from './gl.json';
10
11
  import he from './he.json';
11
12
  import id from './id.json';
12
13
  import it from './it.json';
@@ -34,6 +35,7 @@ export default Object.fromEntries(
34
35
  pt,
35
36
  fa,
36
37
  fr,
38
+ gl,
37
39
  he,
38
40
  id,
39
41
  it,
@@ -0,0 +1,79 @@
1
+ import type { i18nSchemaOutput } from '../schemas/i18n';
2
+ import builtinTranslations from '../translations';
3
+ import type { StarlightConfig } from './user-config';
4
+
5
+ export function createTranslationSystem(
6
+ userTranslations: Record<string, i18nSchemaOutput>,
7
+ config: Pick<StarlightConfig, 'defaultLocale' | 'locales'>
8
+ ) {
9
+ /** User-configured default locale. */
10
+ const defaultLocale = config.defaultLocale?.locale || 'root';
11
+
12
+ /** Default map of UI strings based on Starlight and user-configured defaults. */
13
+ const defaults = buildDictionary(
14
+ builtinTranslations.en!,
15
+ userTranslations.en,
16
+ builtinTranslations[defaultLocale] || builtinTranslations[stripLangRegion(defaultLocale)],
17
+ userTranslations[defaultLocale]
18
+ );
19
+
20
+ /**
21
+ * Generate a utility function that returns UI strings for the given `locale`.
22
+ * @param {string | undefined} [locale]
23
+ * @example
24
+ * const t = useTranslations('en');
25
+ * const label = t('search.label'); // => 'Search'
26
+ */
27
+ return function useTranslations(locale: string | undefined) {
28
+ const lang = localeToLang(locale, config.locales, config.defaultLocale);
29
+ const dictionary = buildDictionary(
30
+ defaults,
31
+ builtinTranslations[lang] || builtinTranslations[stripLangRegion(lang)],
32
+ userTranslations[lang]
33
+ );
34
+ const t = <K extends keyof typeof dictionary>(key: K) => dictionary[key];
35
+ t.pick = (startOfKey: string) =>
36
+ Object.fromEntries(Object.entries(dictionary).filter(([k]) => k.startsWith(startOfKey)));
37
+ return t;
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Strips the region subtag from a BCP-47 lang string.
43
+ * @param {string} [lang]
44
+ * @example
45
+ * const lang = stripLangRegion('en-GB'); // => 'en'
46
+ */
47
+ function stripLangRegion(lang: string) {
48
+ return lang.replace(/-[a-zA-Z]{2}/, '');
49
+ }
50
+
51
+ /**
52
+ * Get the BCP-47 language tag for the given locale.
53
+ * @param locale Locale string or `undefined` for the root locale.
54
+ */
55
+ function localeToLang(
56
+ locale: string | undefined,
57
+ locales: StarlightConfig['locales'],
58
+ defaultLocale: StarlightConfig['defaultLocale']
59
+ ): string {
60
+ const lang = locale ? locales?.[locale]?.lang : locales?.root?.lang;
61
+ const defaultLang = defaultLocale?.lang || defaultLocale?.locale;
62
+ return lang || defaultLang || 'en';
63
+ }
64
+
65
+ /** Build a dictionary by layering preferred translation sources. */
66
+ function buildDictionary(
67
+ base: (typeof builtinTranslations)[string],
68
+ ...dictionaries: (i18nSchemaOutput | undefined)[]
69
+ ) {
70
+ const dictionary = { ...base };
71
+ // Iterate over alternate dictionaries to avoid overwriting preceding values with `undefined`.
72
+ for (const dict of dictionaries) {
73
+ for (const key in dict) {
74
+ const value = dict[key as keyof typeof dict];
75
+ if (value) dictionary[key as keyof typeof dict] = value;
76
+ }
77
+ }
78
+ return dictionary;
79
+ }
@@ -30,6 +30,7 @@ interface Group {
30
30
  label: string;
31
31
  entries: (Link | Group)[];
32
32
  collapsed: boolean;
33
+ badge: Badge | undefined;
33
34
  }
34
35
 
35
36
  export type SidebarEntry = Link | Group;
@@ -75,6 +76,7 @@ function configItemToEntry(
75
76
  label: pickLang(item.translations, localeToLang(locale)) || item.label,
76
77
  entries: item.items.map((i) => configItemToEntry(i, currentPathname, locale, routes)),
77
78
  collapsed: item.collapsed,
79
+ badge: item.badge,
78
80
  };
79
81
  }
80
82
  }
@@ -101,6 +103,7 @@ function groupFromAutogenerateConfig(
101
103
  label: pickLang(item.translations, localeToLang(locale)) || item.label,
102
104
  entries: sidebarFromDir(tree, currentPathname, locale, subgroupCollapsed ?? item.collapsed),
103
105
  collapsed: item.collapsed,
106
+ badge: item.badge,
104
107
  };
105
108
  }
106
109
 
@@ -231,6 +234,7 @@ function groupFromDir(
231
234
  label: dirName,
232
235
  entries,
233
236
  collapsed,
237
+ badge: undefined,
234
238
  };
235
239
  }
236
240
 
@@ -5,9 +5,10 @@ import config from 'virtual:starlight/user-config';
5
5
  import { generateToC, type TocItem } from './generateToC';
6
6
  import { getFileCommitDate } from './git';
7
7
  import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation';
8
+ import { ensureTrailingSlash } from './path';
8
9
  import type { Route } from './routing';
10
+ import { localizedId } from './slugs';
9
11
  import { useTranslations } from './translations';
10
- import { ensureTrailingSlash } from './path';
11
12
 
12
13
  interface PageProps extends Route {
13
14
  headings: MarkdownHeading[];
@@ -79,7 +80,7 @@ function getLastUpdated({ entry, id }: PageProps): Date | undefined {
79
80
  return;
80
81
  }
81
82
 
82
- function getEditUrl({ entry, id }: PageProps): URL | undefined {
83
+ function getEditUrl({ entry, id, isFallback }: PageProps): URL | undefined {
83
84
  const { editUrl } = entry.data;
84
85
  // If frontmatter value is false, editing is disabled for this page.
85
86
  if (editUrl === false) return;
@@ -90,8 +91,9 @@ function getEditUrl({ entry, id }: PageProps): URL | undefined {
90
91
  url = editUrl;
91
92
  } else if (config.editLink.baseUrl) {
92
93
  const srcPath = project.srcDir.replace(project.root, '');
94
+ const filePath = isFallback ? localizedId(id, config.defaultLocale.locale) : id;
93
95
  // If a base URL was added in Starlight config, synthesize the edit URL from it.
94
- url = ensureTrailingSlash(config.editLink.baseUrl) + srcPath + 'content/docs/' + id;
96
+ url = ensureTrailingSlash(config.editLink.baseUrl) + srcPath + 'content/docs/' + filePath;
95
97
  }
96
98
  return url ? new URL(url) : undefined;
97
99
  }
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import type { i18nSchemaOutput } from '../schemas/i18n';
3
+ import { createTranslationSystem } from './createTranslationSystem';
4
+ import type { StarlightConfig } from './user-config';
5
+ import type { AstroConfig } from 'astro';
6
+
7
+ /**
8
+ * Loads and creates a translation system from the file system.
9
+ * Only for use in integration code.
10
+ * In modules loaded by Vite/Astro, import [`useTranslations`](./translations.ts) instead.
11
+ *
12
+ * @see [`./translations.ts`](./translations.ts)
13
+ */
14
+ export function createTranslationSystemFromFs(
15
+ opts: Pick<StarlightConfig, 'defaultLocale' | 'locales'>,
16
+ { srcDir }: Pick<AstroConfig, 'srcDir'>
17
+ ) {
18
+ /** All translation data from the i18n collection, keyed by `id`, which matches locale. */
19
+ let userTranslations: Record<string, i18nSchemaOutput> = {};
20
+ try {
21
+ const i18nDir = new URL('content/i18n/', srcDir);
22
+ // Load the user’s i18n directory
23
+ const files = fs.readdirSync(i18nDir, 'utf-8');
24
+ // Load the user’s i18n collection and ignore the error if it doesn’t exist.
25
+ userTranslations = Object.fromEntries(
26
+ files
27
+ .filter((file) => file.endsWith('.json'))
28
+ .map((file) => {
29
+ const id = file.slice(0, -5);
30
+ const data = JSON.parse(fs.readFileSync(new URL(file, i18nDir), 'utf-8'));
31
+ return [id, data] as const;
32
+ })
33
+ );
34
+ } catch (e: unknown) {
35
+ if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {
36
+ // i18nDir doesn’t exist, so we ignore the error.
37
+ } else {
38
+ // Other errors may be meaningful, e.g. JSON syntax errors, so should be thrown.
39
+ throw e;
40
+ }
41
+ }
42
+
43
+ return createTranslationSystem(userTranslations, opts);
44
+ }
@@ -1,13 +1,10 @@
1
- import { type CollectionEntry, getCollection } from 'astro:content';
1
+ import { getCollection } from 'astro:content';
2
2
  import config from 'virtual:starlight/user-config';
3
- import builtinTranslations from '../translations';
4
- import { localeToLang } from './slugs';
5
-
6
- /** User-configured default locale. */
7
- const defaultLocale = config.defaultLocale?.locale || 'root';
3
+ import type { i18nSchemaOutput } from '../schemas/i18n';
4
+ import { createTranslationSystem } from './createTranslationSystem';
8
5
 
9
6
  /** All translation data from the i18n collection, keyed by `id`, which matches locale. */
10
- let userTranslations: Record<string, CollectionEntry<'i18n'>['data']> = {};
7
+ let userTranslations: Record<string, i18nSchemaOutput> = {};
11
8
  try {
12
9
  // Load the user’s i18n collection and ignore the error if it doesn’t exist.
13
10
  userTranslations = Object.fromEntries(
@@ -15,24 +12,6 @@ try {
15
12
  );
16
13
  } catch {}
17
14
 
18
- /** Default map of UI strings based on Starlight and user-configured defaults. */
19
- const defaults = buildDictionary(
20
- builtinTranslations.en!,
21
- userTranslations.en,
22
- builtinTranslations[defaultLocale] || builtinTranslations[stripLangRegion(defaultLocale)],
23
- userTranslations[defaultLocale]
24
- );
25
-
26
- /**
27
- * Strips the region subtag from a BCP-47 lang string.
28
- * @param {string} [lang]
29
- * @example
30
- * const lang = stripLangRegion('en-GB'); // => 'en'
31
- */
32
- export function stripLangRegion(lang: string) {
33
- return lang.replace(/-[a-zA-Z]{2}/, '');
34
- }
35
-
36
15
  /**
37
16
  * Generate a utility function that returns UI strings for the given `locale`.
38
17
  * @param {string | undefined} [locale]
@@ -40,31 +19,4 @@ export function stripLangRegion(lang: string) {
40
19
  * const t = useTranslations('en');
41
20
  * const label = t('search.label'); // => 'Search'
42
21
  */
43
- export function useTranslations(locale: string | undefined) {
44
- const lang = localeToLang(locale);
45
- const dictionary = buildDictionary(
46
- defaults,
47
- builtinTranslations[lang] || builtinTranslations[stripLangRegion(lang)],
48
- userTranslations[lang]
49
- );
50
- const t = <K extends keyof typeof dictionary>(key: K) => dictionary[key];
51
- t.pick = (startOfKey: string) =>
52
- Object.fromEntries(Object.entries(dictionary).filter(([k]) => k.startsWith(startOfKey)));
53
- return t;
54
- }
55
-
56
- /** Build a dictionary by layering preferred translation sources. */
57
- function buildDictionary(
58
- base: (typeof builtinTranslations)[string],
59
- ...dictionaries: (CollectionEntry<'i18n'>['data'] | undefined)[]
60
- ) {
61
- const dictionary = { ...base };
62
- // Iterate over alternate dictionaries to avoid overwriting preceding values with `undefined`.
63
- for (const dict of dictionaries) {
64
- for (const key in dict) {
65
- const value = dict[key as keyof typeof dict];
66
- if (value) dictionary[key as keyof typeof dict] = value;
67
- }
68
- }
69
- return dictionary;
70
- }
22
+ export const useTranslations = createTranslationSystem(userTranslations, config);
@@ -1,6 +1,5 @@
1
1
  import { z } from 'astro/zod';
2
2
  import { parse as bcpParse, stringify as bcpStringify } from 'bcp-47';
3
- import { BadgeConfigSchema } from '../schemas/badge';
4
3
  import { ComponentConfigSchema } from '../schemas/components';
5
4
  import { FaviconSchema } from '../schemas/favicon';
6
5
  import { HeadConfigSchema } from '../schemas/head';