summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiDeoo2024-09-18 10:05:45 +0200
committerGitHub2024-09-18 10:05:45 +0200
commit728622037602999ed67dedc2757ca5654236feb8 (patch)
tree461050846c87d0491452c1b75124560996c706f3
parentfd8cd2b8cafc52f8242b75547c62b154b10a0edc (diff)
downloadIT.starlight-728622037602999ed67dedc2757ca5654236feb8.tar.gz
IT.starlight-728622037602999ed67dedc2757ca5654236feb8.tar.bz2
IT.starlight-728622037602999ed67dedc2757ca5654236feb8.zip
Add support for translating sidebar badges (#2285)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r--.changeset/slow-flowers-sort.md5
-rw-r--r--docs/src/components/sidebar-preview.astro5
-rw-r--r--docs/src/content/docs/guides/sidebar.mdx46
-rw-r--r--packages/starlight/__tests__/i18n-sidebar-badge-error/i18n-sidebar-badge-error.test.ts20
-rw-r--r--packages/starlight/__tests__/i18n-sidebar-badge-error/vitest.config.ts15
-rw-r--r--packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts30
-rw-r--r--packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts45
-rw-r--r--packages/starlight/__tests__/i18n-sidebar/vitest.config.ts17
-rw-r--r--packages/starlight/schemas/badge.ts30
-rw-r--r--packages/starlight/schemas/sidebar.ts6
-rw-r--r--packages/starlight/utils/navigation.ts65
-rw-r--r--packages/starlight/vitest.config.ts8
12 files changed, 248 insertions, 44 deletions
diff --git a/.changeset/slow-flowers-sort.md b/.changeset/slow-flowers-sort.md
new file mode 100644
index 00000000..c93a151f
--- /dev/null
+++ b/.changeset/slow-flowers-sort.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': minor
+---
+
+Adds support for translating sidebar badges.
diff --git a/docs/src/components/sidebar-preview.astro b/docs/src/components/sidebar-preview.astro
index 810ac1cc..b7761def 100644
--- a/docs/src/components/sidebar-preview.astro
+++ b/docs/src/components/sidebar-preview.astro
@@ -5,13 +5,16 @@ import type {
InternalSidebarLinkItem,
} from '../../../packages/starlight/schemas/sidebar';
import SidebarSublist from '../../../packages/starlight/components/SidebarSublist.astro';
+import type { Badge } from '../../../packages/starlight/schemas/badge';
import type { SidebarEntry } from '../../../packages/starlight/utils/navigation';
interface Props {
config: SidebarConfig;
}
-type SidebarConfig = Exclude<SidebarItem, AutoSidebarGroup | InternalSidebarLinkItem>[];
+type SidebarConfig = (Exclude<SidebarItem, AutoSidebarGroup | InternalSidebarLinkItem> & {
+ badge?: Badge;
+})[];
const { config } = Astro.props;
diff --git a/docs/src/content/docs/guides/sidebar.mdx b/docs/src/content/docs/guides/sidebar.mdx
index 8e368b15..35500284 100644
--- a/docs/src/content/docs/guides/sidebar.mdx
+++ b/docs/src/content/docs/guides/sidebar.mdx
@@ -520,6 +520,52 @@ Browsing the documentation in Brazilian Portuguese will generate the following s
In multilingual sites, the value of `slug` does not include the language portion of the URL.
For example, if you have pages at `en/intro` and `pt-br/intro`, the slug is `intro` when configuring the sidebar.
+### Internationalization with badges
+
+For [badges](#badges), the `text` property can be a string, or for multilingual sites, an object with values for each different locale.
+When using the object form, the keys must be [BCP-47](https://www.w3.org/International/questions/qa-choosing-language-tags) tags (e.g. `en`, `ar`, or `zh-CN`):
+
+```js {11-16}
+starlight({
+ sidebar: [
+ {
+ label: 'Constellations',
+ translations: {
+ 'pt-BR': 'Constelações',
+ },
+ items: [
+ {
+ slug: 'constellations/andromeda',
+ badge: {
+ text: {
+ en: 'New',
+ 'pt-BR': 'Novo',
+ },
+ },
+ },
+ ],
+ },
+ ],
+});
+```
+
+Browsing the documentation in Brazilian Portuguese will generate the following sidebar:
+
+<SidebarPreview
+ config={[
+ {
+ label: 'Constelações',
+ items: [
+ {
+ label: 'Andrômeda',
+ link: '',
+ badge: { text: 'Novo', variant: 'default' },
+ },
+ ],
+ },
+ ]}
+/>
+
## Collapsing groups
Groups of links can be collapsed by default by setting the `collapsed` property to `true`.
diff --git a/packages/starlight/__tests__/i18n-sidebar-badge-error/i18n-sidebar-badge-error.test.ts b/packages/starlight/__tests__/i18n-sidebar-badge-error/i18n-sidebar-badge-error.test.ts
new file mode 100644
index 00000000..fddea454
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-sidebar-badge-error/i18n-sidebar-badge-error.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, test, vi } from 'vitest';
+import { getSidebar } from '../../utils/navigation';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [['getting-started.mdx', { title: 'Getting Started' }]],
+ })
+);
+
+describe('getSidebar', () => {
+ test('throws an error if an i18n badge doesn’t have a key for the default language', () => {
+ expect(() => getSidebar('/', undefined)).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ The badge text for "Getting Started" must have a key for the default language "en-US".
+ Hint:
+ Update the Starlight config to include a badge text for the default language.
+ Learn more about sidebar badges internationalization at https://starlight.astro.build/guides/sidebar/#internationalization-with-badges"
+ `);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-sidebar-badge-error/vitest.config.ts b/packages/starlight/__tests__/i18n-sidebar-badge-error/vitest.config.ts
new file mode 100644
index 00000000..a741e609
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-sidebar-badge-error/vitest.config.ts
@@ -0,0 +1,15 @@
+import { defineVitestConfig } from '../test-config';
+
+export default defineVitestConfig({
+ title: 'i18n sidebar badge error',
+ locales: {
+ fr: { label: 'French' },
+ root: { label: 'English', lang: 'en-US' },
+ },
+ sidebar: [
+ {
+ slug: 'getting-started',
+ badge: { text: { fr: 'Nouveau' } },
+ },
+ ],
+});
diff --git a/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts
index d6496d42..750c4244 100644
--- a/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts
+++ b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar-fallback-slug.test.ts
@@ -37,7 +37,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "New",
+ "variant": "default",
+ },
"href": "/manual-setup",
"isCurrent": false,
"label": "Do it yourself",
@@ -45,7 +48,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Eco-friendly",
+ "variant": "success",
+ },
"href": "/environmental-impact",
"isCurrent": false,
"label": "Eco-friendly docs",
@@ -65,7 +71,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Deprecated",
+ "variant": "default",
+ },
"href": "/guides/authoring-content",
"isCurrent": false,
"label": "Authoring Content in Markdown",
@@ -107,7 +116,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Nouveau",
+ "variant": "default",
+ },
"href": "/fr/manual-setup",
"isCurrent": false,
"label": "Fait maison",
@@ -115,7 +127,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Écologique",
+ "variant": "success",
+ },
"href": "/fr/environmental-impact",
"isCurrent": false,
"label": "Eco-friendly docs",
@@ -135,7 +150,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Deprecated",
+ "variant": "default",
+ },
"href": "/fr/guides/authoring-content",
"isCurrent": false,
"label": "Authoring Content in Markdown",
diff --git a/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts
index 7206579a..52b7ff7b 100644
--- a/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts
+++ b/packages/starlight/__tests__/i18n-sidebar/i18n-sidebar.test.ts
@@ -44,7 +44,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "New",
+ "variant": "default",
+ },
"href": "/manual-setup",
"isCurrent": false,
"label": "Do it yourself",
@@ -52,7 +55,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Eco-friendly",
+ "variant": "success",
+ },
"href": "/environmental-impact",
"isCurrent": false,
"label": "Eco-friendly docs",
@@ -72,7 +78,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Deprecated",
+ "variant": "default",
+ },
"href": "/guides/authoring-content",
"isCurrent": false,
"label": "Authoring Content in Markdown",
@@ -114,7 +123,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Nouveau",
+ "variant": "default",
+ },
"href": "/fr/manual-setup",
"isCurrent": false,
"label": "Fait maison",
@@ -122,7 +134,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Écologique",
+ "variant": "success",
+ },
"href": "/fr/environmental-impact",
"isCurrent": false,
"label": "Documents écologiques",
@@ -142,7 +157,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Deprecated",
+ "variant": "default",
+ },
"href": "/fr/guides/authoring-content",
"isCurrent": false,
"label": "Création de contenu en Markdown",
@@ -184,7 +202,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Nouveau",
+ "variant": "default",
+ },
"href": "/fr/manual-setup",
"isCurrent": false,
"label": "Fait maison",
@@ -192,7 +213,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Écologique",
+ "variant": "success",
+ },
"href": "/fr/environmental-impact",
"isCurrent": false,
"label": "Documents écologiques",
@@ -212,7 +236,10 @@ describe('getSidebar', () => {
},
{
"attrs": {},
- "badge": undefined,
+ "badge": {
+ "text": "Deprecated",
+ "variant": "default",
+ },
"href": "/fr/guides/authoring-content",
"isCurrent": false,
"label": "Création de contenu en Markdown",
diff --git a/packages/starlight/__tests__/i18n-sidebar/vitest.config.ts b/packages/starlight/__tests__/i18n-sidebar/vitest.config.ts
index 59f3bdf1..6cdb4d74 100644
--- a/packages/starlight/__tests__/i18n-sidebar/vitest.config.ts
+++ b/packages/starlight/__tests__/i18n-sidebar/vitest.config.ts
@@ -9,11 +9,22 @@ export default defineVitestConfig({
sidebar: [
{ slug: 'index' },
'getting-started',
- { slug: 'manual-setup', label: 'Do it yourself', translations: { fr: 'Fait maison' } },
- { slug: 'environmental-impact' },
+ {
+ slug: 'manual-setup',
+ label: 'Do it yourself',
+ translations: { fr: 'Fait maison' },
+ badge: { text: { 'en-US': 'New', fr: 'Nouveau' } },
+ },
+ {
+ slug: 'environmental-impact',
+ badge: {
+ variant: 'success',
+ text: { 'en-US': 'Eco-friendly', fr: 'Écologique' },
+ },
+ },
{
label: 'Guides',
- items: [{ slug: 'guides/pages' }, { slug: 'guides/authoring-content' }],
+ items: [{ slug: 'guides/pages' }, { slug: 'guides/authoring-content', badge: 'Deprecated' }],
},
'resources/plugins',
],
diff --git a/packages/starlight/schemas/badge.ts b/packages/starlight/schemas/badge.ts
index a2cac1b1..f3f1f738 100644
--- a/packages/starlight/schemas/badge.ts
+++ b/packages/starlight/schemas/badge.ts
@@ -1,13 +1,19 @@
import { z } from 'astro/zod';
-const badgeSchema = () =>
- z.object({
- variant: z.enum(['note', 'danger', 'success', 'caution', 'tip', 'default']).default('default'),
- text: z.string(),
- class: z.string().optional(),
- });
-
-export const BadgeComponentSchema = badgeSchema()
+const badgeBaseSchema = z.object({
+ variant: z.enum(['note', 'danger', 'success', 'caution', 'tip', 'default']).default('default'),
+ class: z.string().optional(),
+});
+
+const badgeSchema = badgeBaseSchema.extend({
+ text: z.string(),
+});
+
+const i18nBadgeSchema = badgeBaseSchema.extend({
+ text: z.union([z.string(), z.record(z.string())]),
+});
+
+export const BadgeComponentSchema = badgeSchema
.extend({
size: z.enum(['small', 'medium', 'large']).default('small'),
})
@@ -17,7 +23,7 @@ export type BadgeComponentProps = z.input<typeof BadgeComponentSchema>;
export const BadgeConfigSchema = () =>
z
- .union([z.string(), badgeSchema()])
+ .union([z.string(), badgeSchema])
.transform((badge) => {
if (typeof badge === 'string') {
return { variant: 'default' as const, text: badge };
@@ -26,4 +32,8 @@ export const BadgeConfigSchema = () =>
})
.optional();
-export type Badge = z.output<ReturnType<typeof badgeSchema>>;
+export const I18nBadgeConfigSchema = () => z.union([z.string(), i18nBadgeSchema]).optional();
+
+export type Badge = z.output<typeof badgeSchema>;
+export type I18nBadge = z.output<typeof i18nBadgeSchema>;
+export type I18nBadgeConfig = z.output<ReturnType<typeof I18nBadgeConfigSchema>>;
diff --git a/packages/starlight/schemas/sidebar.ts b/packages/starlight/schemas/sidebar.ts
index b71d159e..2c4cc1d5 100644
--- a/packages/starlight/schemas/sidebar.ts
+++ b/packages/starlight/schemas/sidebar.ts
@@ -1,7 +1,7 @@
import type { AstroBuiltinAttributes } from 'astro';
import type { HTMLAttributes } from 'astro/types';
import { z } from 'astro/zod';
-import { BadgeConfigSchema } from './badge';
+import { I18nBadgeConfigSchema } from './badge';
import { stripLeadingAndTrailingSlashes } from '../utils/path';
const SidebarBaseSchema = z.object({
@@ -9,8 +9,8 @@ const SidebarBaseSchema = z.object({
label: z.string(),
/** Translations of the `label` for each supported language. */
translations: z.record(z.string()).default({}),
- /** Adds a badge to the link item */
- badge: BadgeConfigSchema(),
+ /** Adds a badge to the item */
+ badge: I18nBadgeConfigSchema(),
});
const SidebarGroupSchema = SidebarBaseSchema.extend({
diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts
index cd1475d1..526d9f29 100644
--- a/packages/starlight/utils/navigation.ts
+++ b/packages/starlight/utils/navigation.ts
@@ -1,6 +1,6 @@
import { AstroError } from 'astro/errors';
import config from 'virtual:starlight/user-config';
-import type { Badge } from '../schemas/badge';
+import type { Badge, I18nBadge, I18nBadgeConfig } from '../schemas/badge';
import type { PrevNextLinkConfig } from '../schemas/prevNextLink';
import type {
AutoSidebarGroup,
@@ -11,7 +11,7 @@ import type {
} from '../schemas/sidebar';
import { createPathFormatter } from './createPathFormatter';
import { formatPath } from './format-path';
-import { pickLang } from './i18n';
+import { BuiltInDefaultLocale, pickLang } from './i18n';
import { ensureLeadingSlash, ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path';
import { getLocaleRoutes, routes, type Route } from './routing';
import { localeToLang, slugToPathname } from './slugs';
@@ -79,12 +79,13 @@ function configItemToEntry(
} else if ('slug' in item) {
return linkFromInternalSidebarLinkItem(item, locale, currentPathname);
} else {
+ const label = pickLang(item.translations, localeToLang(locale)) || item.label;
return {
type: 'group',
- label: pickLang(item.translations, localeToLang(locale)) || item.label,
+ label,
entries: item.items.map((i) => configItemToEntry(i, currentPathname, locale, routes)),
collapsed: item.collapsed,
- badge: item.badge,
+ badge: getSidebarBadge(item.badge, locale, label),
};
}
}
@@ -106,12 +107,13 @@ function groupFromAutogenerateConfig(
doc.id.startsWith(localeDir + '/')
);
const tree = treeify(dirDocs, localeDir);
+ const label = pickLang(item.translations, localeToLang(locale)) || item.label;
return {
type: 'group',
- label: pickLang(item.translations, localeToLang(locale)) || item.label,
+ label,
entries: sidebarFromDir(tree, currentPathname, locale, subgroupCollapsed ?? item.collapsed),
collapsed: item.collapsed,
- badge: item.badge,
+ badge: getSidebarBadge(item.badge, locale, label),
};
}
@@ -131,7 +133,13 @@ function linkFromSidebarLinkItem(
if (locale) href = '/' + locale + href;
}
const label = pickLang(item.translations, localeToLang(locale)) || item.label;
- return makeSidebarLink(href, label, currentPathname, item.badge, item.attrs);
+ return makeSidebarLink(
+ href,
+ label,
+ currentPathname,
+ getSidebarBadge(item.badge, locale, label),
+ item.attrs
+ );
}
/** Create a link entry from an automatic internal link item in user config. */
@@ -161,7 +169,13 @@ function linkFromInternalSidebarLinkItem(
}
const label =
pickLang(item.translations, localeToLang(locale)) || item.label || entry.entry.data.title;
- return makeSidebarLink(entry.slug, label, currentPathname, item.badge, item.attrs);
+ return makeSidebarLink(
+ entry.slug,
+ label,
+ currentPathname,
+ getSidebarBadge(item.badge, locale, label),
+ item.attrs
+ );
}
/** Process sidebar link options to create a link entry. */
@@ -446,3 +460,38 @@ function stripExtension(path: string) {
const periodIndex = path.lastIndexOf('.');
return path.slice(0, periodIndex > -1 ? periodIndex : undefined);
}
+
+/** Get a sidebar badge for a given item. */
+function getSidebarBadge(
+ config: I18nBadgeConfig,
+ locale: string | undefined,
+ itemLabel: string
+): Badge | undefined {
+ if (!config) return;
+ if (typeof config === 'string') {
+ return { variant: 'default', text: config };
+ }
+ return { ...config, text: getSidebarBadgeText(config.text, locale, itemLabel) };
+}
+
+/** Get the badge text for a sidebar item. */
+function getSidebarBadgeText(
+ text: I18nBadge['text'],
+ locale: string | undefined,
+ itemLabel: string
+): string {
+ if (typeof text === 'string') return text;
+ const defaultLang =
+ config.defaultLocale?.lang || config.defaultLocale?.locale || BuiltInDefaultLocale.lang;
+ const defaultText = text[defaultLang];
+
+ if (!defaultText) {
+ throw new AstroError(
+ `The badge text for "${itemLabel}" must have a key for the default language "${defaultLang}".`,
+ 'Update the Starlight config to include a badge text for the default language.\n' +
+ 'Learn more about sidebar badges internationalization at https://starlight.astro.build/guides/sidebar/#internationalization-with-badges'
+ );
+ }
+
+ return pickLang(text, localeToLang(locale)) || defaultText;
+}
diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts
index 47ca99be..d9443743 100644
--- a/packages/starlight/vitest.config.ts
+++ b/packages/starlight/vitest.config.ts
@@ -21,10 +21,10 @@ export default defineConfig({
],
thresholds: {
autoUpdate: true,
- lines: 89.18,
- functions: 92.7,
- branches: 93.04,
- statements: 89.18,
+ lines: 89.28,
+ functions: 92.78,
+ branches: 92.83,
+ statements: 89.28,
},
},
},