@astrojs/starlight 0.11.2 → 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 +23 -0
- package/components/Hero.astro +22 -8
- package/components/MobileMenuFooter.astro +15 -1
- package/components/SidebarSublist.astro +13 -3
- package/components/SiteTitle.astro +2 -8
- package/index.ts +2 -0
- package/package.json +1 -1
- package/schema.ts +3 -55
- package/schemas/hero.ts +70 -0
- package/schemas/i18n.ts +1 -0
- package/schemas/sidebar.ts +2 -2
- package/style/util.css +6 -0
- package/translations/gl.json +22 -0
- package/translations/index.ts +2 -0
- package/utils/createTranslationSystem.ts +79 -0
- package/utils/navigation.ts +4 -0
- package/utils/translations-fs.ts +44 -0
- package/utils/translations.ts +5 -53
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
3
26
|
## 0.11.2
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/components/Hero.astro
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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} />
|
|
@@ -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>
|
|
@@ -36,7 +36,15 @@ interface Props {
|
|
|
36
36
|
open={flattenSidebar(entry.entries).some((i) => i.isCurrent) || !entry.collapsed}
|
|
37
37
|
>
|
|
38
38
|
<summary>
|
|
39
|
-
<
|
|
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
|
|
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={{ '
|
|
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="
|
|
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
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 (
|
|
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:
|
|
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.
|
package/schemas/hero.ts
ADDED
|
@@ -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());
|
package/schemas/sidebar.ts
CHANGED
|
@@ -9,6 +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
14
|
});
|
|
13
15
|
|
|
14
16
|
const SidebarGroupSchema = SidebarBaseSchema.extend({
|
|
@@ -29,8 +31,6 @@ export const SidebarLinkItemHTMLAttributesSchema = () => linkHTMLAttributesSchem
|
|
|
29
31
|
const SidebarLinkItemSchema = SidebarBaseSchema.extend({
|
|
30
32
|
/** The link to this item’s content. Can be a relative link to local files or the full URL of an external page. */
|
|
31
33
|
link: z.string(),
|
|
32
|
-
/** Adds a badge to the link item */
|
|
33
|
-
badge: BadgeConfigSchema(),
|
|
34
34
|
/** HTML attributes to add to the link item. */
|
|
35
35
|
attrs: SidebarLinkItemHTMLAttributesSchema(),
|
|
36
36
|
});
|
package/style/util.css
CHANGED
|
@@ -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
|
+
}
|
package/translations/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/utils/navigation.ts
CHANGED
|
@@ -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
|
|
|
@@ -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
|
+
}
|
package/utils/translations.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
2
|
import config from 'virtual:starlight/user-config';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
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,
|
|
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
|
|
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);
|