summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCabbage2024-04-30 23:03:57 +0800
committerGitHub2024-04-30 17:03:57 +0200
commitca0678ca556d739bda9648edc1b79c764fdea851 (patch)
tree3874cbdd84ca9c0202078aa184e2e495728f986a
parentbcadd25da1bc7d51d659f6dc9914253477dedcf4 (diff)
downloadIT.starlight-ca0678ca556d739bda9648edc1b79c764fdea851.tar.gz
IT.starlight-ca0678ca556d739bda9648edc1b79c764fdea851.tar.bz2
IT.starlight-ca0678ca556d739bda9648edc1b79c764fdea851.zip
feat: Support object title for multiple language (#1620)
Co-authored-by: liruifengv <liruifeng1024@gmail.com> Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r--.changeset/neat-flowers-move.md18
-rw-r--r--docs/src/content/docs/guides/i18n.mdx28
-rw-r--r--docs/src/content/docs/reference/configuration.mdx14
-rw-r--r--docs/src/content/docs/reference/overrides.md8
-rw-r--r--packages/starlight/__tests__/basics/config-errors.test.ts14
-rw-r--r--packages/starlight/__tests__/basics/config.test.ts2
-rw-r--r--packages/starlight/__tests__/basics/routing.test.ts2
-rw-r--r--packages/starlight/__tests__/basics/schema.test.ts35
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts2
-rw-r--r--packages/starlight/__tests__/i18n-root-locale/config.test.ts2
-rw-r--r--packages/starlight/__tests__/i18n-root-locale/routing.test.ts2
-rw-r--r--packages/starlight/__tests__/i18n/config.test.ts2
-rw-r--r--packages/starlight/__tests__/i18n/routing.test.ts2
-rw-r--r--packages/starlight/__tests__/plugins/config.test.ts2
-rw-r--r--packages/starlight/components/Head.astro6
-rw-r--r--packages/starlight/components/SiteTitle.astro3
-rw-r--r--packages/starlight/schemas/site-title.ts22
-rw-r--r--packages/starlight/utils/route-data.ts15
-rw-r--r--packages/starlight/utils/user-config.ts33
19 files changed, 180 insertions, 32 deletions
diff --git a/.changeset/neat-flowers-move.md b/.changeset/neat-flowers-move.md
new file mode 100644
index 00000000..3119314d
--- /dev/null
+++ b/.changeset/neat-flowers-move.md
@@ -0,0 +1,18 @@
+---
+'@astrojs/starlight': minor
+---
+
+Adds support for translating the site title
+
+⚠️ **Potentially breaking change:** The shape of the `title` field on Starlight’s internal config object has changed. This used to be a string, but is now an object.
+
+If you are relying on `config.title` (for example in a custom `<SiteTitle>` or `<Head>` component), you will need to update your code. We recommend using the new [`siteTitle` prop](https://starlight.astro.build/reference/overrides/#sitetitle) available to component overrides:
+
+```astro
+---
+import type { Props } from '@astrojs/starlight/props';
+
+// The site title for this page’s language:
+const { siteTitle } = Astro.props;
+---
+```
diff --git a/docs/src/content/docs/guides/i18n.mdx b/docs/src/content/docs/guides/i18n.mdx
index 85d2f006..2c7564ae 100644
--- a/docs/src/content/docs/guides/i18n.mdx
+++ b/docs/src/content/docs/guides/i18n.mdx
@@ -143,6 +143,34 @@ Starlight expects you to create equivalent pages in all your languages. For exam
If a translation is not yet available for a language, Starlight will show readers the content for that page in the default language (set via `defaultLocale`). For example, if you have not yet created a French version of your About page and your default language is English, visitors to `/fr/about` will see the English content from `/en/about` with a notice that this page has not yet been translated. This helps you add content in your default language and then progressively translate it when your translators have time.
+## Translate the site title
+
+By default, Astro will use the same site title for all languages.
+If you need to customize the title for each locale, you can pass an object to [`title`](/reference/configuration/#title-required) in Starlight’s options:
+
+```diff lang="js"
+// astro.config.mjs
+import { defineConfig } from 'astro/config';
+import starlight from '@astrojs/starlight';
+
+export default defineConfig({
+ integrations: [
+ starlight({
+- title: 'My Docs',
++ title: {
++ en: 'My Docs',
++ 'zh-CN': '我的文档',
++ },
+ defaultLocale: 'en',
+ locales: {
+ en: { label: 'English' },
+ 'zh-cn': { label: '简体中文', lang: 'zh-CN' },
+ },
+ }),
+ ],
+});
+```
+
## Translate Starlight's UI
import LanguagesList from '~/components/languages-list.astro';
diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx
index 380fb9bf..a940a844 100644
--- a/docs/src/content/docs/reference/configuration.mdx
+++ b/docs/src/content/docs/reference/configuration.mdx
@@ -25,10 +25,22 @@ You can pass the following options to the `starlight` integration.
### `title` (required)
-**type:** `string`
+**type:** `string | Record<string, string>`
Set the title for your website. Will be used in metadata and in the browser tab title.
+The value 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 tags (e.g. `en`, `ar`, or `zh-CN`):
+
+```ts
+starlight({
+ title: {
+ en: 'My delightful docs site',
+ de: 'Meine bezaubernde Dokumentationsseite',
+ },
+});
+```
+
### `description`
**type:** `string`
diff --git a/docs/src/content/docs/reference/overrides.md b/docs/src/content/docs/reference/overrides.md
index 3d9e4a86..b758b1ea 100644
--- a/docs/src/content/docs/reference/overrides.md
+++ b/docs/src/content/docs/reference/overrides.md
@@ -50,6 +50,12 @@ BCP-47 language tag for this page’s locale, e.g. `en`, `zh-CN`, or `pt-BR`.
The base path at which a language is served. `undefined` for root locale slugs.
+#### `siteTitle`
+
+**Type:** `string`
+
+The site title for this page’s locale.
+
#### `slug`
**Type:** `string`
@@ -218,7 +224,7 @@ These components render Starlight’s top navigation bar.
**Default component:** [`Header.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Header.astro)
Header component displayed at the top of every page.
-The default implementation displays [`<SiteTitle />`](#sitetitle), [`<Search />`](#search), [`<SocialIcons />`](#socialicons), [`<ThemeSelect />`](#themeselect), and [`<LanguageSelect />`](#languageselect).
+The default implementation displays [`<SiteTitle />`](#sitetitle-1), [`<Search />`](#search), [`<SocialIcons />`](#socialicons), [`<ThemeSelect />`](#themeselect), and [`<LanguageSelect />`](#languageselect).
#### `SiteTitle`
diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts
index 36bbb03b..2b9eab93 100644
--- a/packages/starlight/__tests__/basics/config-errors.test.ts
+++ b/packages/starlight/__tests__/basics/config-errors.test.ts
@@ -66,7 +66,9 @@ test('parses valid config successfully', () => {
"maxHeadingLevel": 3,
"minHeadingLevel": 2,
},
- "title": "",
+ "title": {
+ "en": "",
+ },
"titleDelimiter": "|",
}
`);
@@ -80,12 +82,13 @@ test('errors if title is missing', () => {
"[AstroUserError]:
Invalid config passed to starlight integration
Hint:
- **title**: Required"
- `
+ **title**: Did not match union.
+ > Required"
+ `
);
});
-test('errors if title value is not a string', () => {
+test('errors if title value is not a string or an Object', () => {
expect(() =>
parseStarlightConfigWithFriendlyErrors({ title: 5 } as any)
).toThrowErrorMatchingInlineSnapshot(
@@ -93,7 +96,8 @@ test('errors if title value is not a string', () => {
"[AstroUserError]:
Invalid config passed to starlight integration
Hint:
- **title**: Expected type \`"string"\`, received \`"number"\`"
+ **title**: Did not match union.
+ > Expected type \`"string" | "object"\`, received \`"number"\`"
`
);
});
diff --git a/packages/starlight/__tests__/basics/config.test.ts b/packages/starlight/__tests__/basics/config.test.ts
index 43293c82..1d5852ac 100644
--- a/packages/starlight/__tests__/basics/config.test.ts
+++ b/packages/starlight/__tests__/basics/config.test.ts
@@ -2,7 +2,7 @@ import config from 'virtual:starlight/user-config';
import { expect, test } from 'vitest';
test('test suite is using correct env', () => {
- expect(config.title).toBe('Basics');
+ expect(config.title).toMatchObject({ en: 'Basics' });
});
test('isMultilingual is false when no locales configured ', () => {
diff --git a/packages/starlight/__tests__/basics/routing.test.ts b/packages/starlight/__tests__/basics/routing.test.ts
index a33dd441..84c3ae58 100644
--- a/packages/starlight/__tests__/basics/routing.test.ts
+++ b/packages/starlight/__tests__/basics/routing.test.ts
@@ -14,7 +14,7 @@ vi.mock('astro:content', async () =>
);
test('test suite is using correct env', () => {
- expect(config.title).toBe('Basics');
+ expect(config.title).toMatchObject({ en: 'Basics' });
});
test('route slugs are normalized', () => {
diff --git a/packages/starlight/__tests__/basics/schema.test.ts b/packages/starlight/__tests__/basics/schema.test.ts
index 85526db9..c3bd3530 100644
--- a/packages/starlight/__tests__/basics/schema.test.ts
+++ b/packages/starlight/__tests__/basics/schema.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'vitest';
import { FaviconSchema } from '../../schemas/favicon';
+import { TitleTransformConfigSchema } from '../../schemas/site-title';
describe('FaviconSchema', () => {
test('returns the proper href and type attributes', () => {
@@ -15,3 +16,37 @@ describe('FaviconSchema', () => {
expect(() => FaviconSchema().parse('/favicon.pdf')).toThrow();
});
});
+
+describe('TitleTransformConfigSchema', () => {
+ test('title can be a string', () => {
+ const title = 'My Site';
+ const defaultLang = 'en';
+
+ const siteTitle = TitleTransformConfigSchema(defaultLang).parse(title);
+
+ expect(siteTitle).toEqual({
+ en: title,
+ });
+ });
+
+ test('title can be an object', () => {
+ const title = {
+ en: 'My Site',
+ es: 'Mi Sitio',
+ };
+ const defaultLang = 'en';
+
+ const siteTitle = TitleTransformConfigSchema(defaultLang).parse(title);
+
+ expect(siteTitle).toEqual(title);
+ });
+
+ test('throws on missing default language key', () => {
+ const title = {
+ es: 'Mi Sitio',
+ };
+ const defaultLang = 'en';
+
+ expect(() => TitleTransformConfigSchema(defaultLang).parse(title)).toThrow();
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts
index a7b7262e..934827d1 100644
--- a/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts
@@ -2,7 +2,7 @@ import config from 'virtual:starlight/user-config';
import { expect, test } from 'vitest';
test('test suite is using correct env', () => {
- expect(config.title).toBe('i18n with a non-root single locale');
+ expect(config.title).toMatchObject({ fr: 'i18n with a non-root single locale' });
});
test('config.isMultilingual is false with a single locale', () => {
diff --git a/packages/starlight/__tests__/i18n-root-locale/config.test.ts b/packages/starlight/__tests__/i18n-root-locale/config.test.ts
index 25e5ac32..fca94f3b 100644
--- a/packages/starlight/__tests__/i18n-root-locale/config.test.ts
+++ b/packages/starlight/__tests__/i18n-root-locale/config.test.ts
@@ -2,7 +2,7 @@ import config from 'virtual:starlight/user-config';
import { expect, test } from 'vitest';
test('test suite is using correct env', () => {
- expect(config.title).toBe('i18n with root locale');
+ expect(config.title).toMatchObject({ fr: 'i18n with root locale' });
});
test('config.isMultilingual is true with multiple locales', () => {
diff --git a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
index 879a444e..dd5b4505 100644
--- a/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
+++ b/packages/starlight/__tests__/i18n-root-locale/routing.test.ts
@@ -22,7 +22,7 @@ vi.mock('astro:content', async () =>
);
test('test suite is using correct env', () => {
- expect(config.title).toBe('i18n with root locale');
+ expect(config.title).toMatchObject({ fr: 'i18n with root locale' });
});
test('routes includes fallback entries for untranslated pages', () => {
diff --git a/packages/starlight/__tests__/i18n/config.test.ts b/packages/starlight/__tests__/i18n/config.test.ts
index 0e8e9c8d..193595bf 100644
--- a/packages/starlight/__tests__/i18n/config.test.ts
+++ b/packages/starlight/__tests__/i18n/config.test.ts
@@ -2,7 +2,7 @@ import config from 'virtual:starlight/user-config';
import { expect, test } from 'vitest';
test('test suite is using correct env', () => {
- expect(config.title).toBe('i18n with no root locale');
+ expect(config.title).toMatchObject({ 'en-US': 'i18n with no root locale' });
});
test('config.isMultilingual is true with multiple locales', () => {
diff --git a/packages/starlight/__tests__/i18n/routing.test.ts b/packages/starlight/__tests__/i18n/routing.test.ts
index 18d8ed89..b854794b 100644
--- a/packages/starlight/__tests__/i18n/routing.test.ts
+++ b/packages/starlight/__tests__/i18n/routing.test.ts
@@ -19,7 +19,7 @@ vi.mock('astro:content', async () =>
);
test('test suite is using correct env', () => {
- expect(config.title).toBe('i18n with no root locale');
+ expect(config.title).toMatchObject({ 'en-US': 'i18n with no root locale' });
});
test('routes includes fallback entries for untranslated pages', () => {
diff --git a/packages/starlight/__tests__/plugins/config.test.ts b/packages/starlight/__tests__/plugins/config.test.ts
index 0b866b1d..948c0726 100644
--- a/packages/starlight/__tests__/plugins/config.test.ts
+++ b/packages/starlight/__tests__/plugins/config.test.ts
@@ -5,7 +5,7 @@ import { runPlugins } from '../../utils/plugins';
import { createTestPluginContext } from '../test-plugin-utils';
test('reads and updates a configuration option', () => {
- expect(config.title).toBe('Plugins - Custom');
+ expect(config.title).toMatchObject({ en: 'Plugins - Custom' });
});
test('overwrites a configuration option', () => {
diff --git a/packages/starlight/components/Head.astro b/packages/starlight/components/Head.astro
index ec2431fc..6aac6ec9 100644
--- a/packages/starlight/components/Head.astro
+++ b/packages/starlight/components/Head.astro
@@ -8,7 +8,7 @@ import { createHead } from '../utils/head';
import { localizedUrl } from '../utils/localizedUrl';
import type { Props } from '../props';
-const { entry, lang } = Astro.props;
+const { entry, lang, siteTitle } = Astro.props;
const { data } = entry;
const canonical = Astro.site ? new URL(Astro.url.pathname, Astro.site) : undefined;
@@ -20,7 +20,7 @@ const headDefaults: z.input<ReturnType<typeof HeadConfigSchema>> = [
tag: 'meta',
attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1' },
},
- { tag: 'title', content: `${data.title} ${config.titleDelimiter} ${config.title}` },
+ { tag: 'title', content: `${data.title} ${config.titleDelimiter} ${siteTitle}` },
{ tag: 'link', attrs: { rel: 'canonical', href: canonical?.href } },
{ tag: 'meta', attrs: { name: 'generator', content: Astro.generator } },
{
@@ -42,7 +42,7 @@ const headDefaults: z.input<ReturnType<typeof HeadConfigSchema>> = [
{ tag: 'meta', attrs: { property: 'og:url', content: canonical?.href } },
{ tag: 'meta', attrs: { property: 'og:locale', content: lang } },
{ tag: 'meta', attrs: { property: 'og:description', content: description } },
- { tag: 'meta', attrs: { property: 'og:site_name', content: config.title } },
+ { tag: 'meta', attrs: { property: 'og:site_name', content: siteTitle } },
// Twitter Tags
{
tag: 'meta',
diff --git a/packages/starlight/components/SiteTitle.astro b/packages/starlight/components/SiteTitle.astro
index 6f6fc9ed..1f09a7df 100644
--- a/packages/starlight/components/SiteTitle.astro
+++ b/packages/starlight/components/SiteTitle.astro
@@ -4,6 +4,7 @@ import config from 'virtual:starlight/user-config';
import type { Props } from '../props';
import { formatPath } from '../utils/format-path';
+const { siteTitle } = Astro.props;
const href = formatPath(Astro.props.locale || '/');
---
@@ -32,7 +33,7 @@ const href = formatPath(Astro.props.locale || '/');
)
}
<span class:list={{ 'sr-only': config.logo?.replacesTitle }}>
- {config.title}
+ {siteTitle}
</span>
</a>
diff --git a/packages/starlight/schemas/site-title.ts b/packages/starlight/schemas/site-title.ts
new file mode 100644
index 00000000..ad5e62b6
--- /dev/null
+++ b/packages/starlight/schemas/site-title.ts
@@ -0,0 +1,22 @@
+import { z } from 'astro/zod';
+
+export const TitleConfigSchema = () =>
+ z
+ .union([z.string(), z.record(z.string())])
+ .describe('Title for your website. Will be used in metadata and as browser tab title.');
+
+// transform the title for runtime use
+export const TitleTransformConfigSchema = (defaultLang: string) =>
+ TitleConfigSchema().transform((title, ctx) => {
+ if (typeof title === 'string') {
+ return { [defaultLang]: title };
+ }
+ if (!title[defaultLang] && title[defaultLang] !== '') {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `Title must have a key for the default language "${defaultLang}"`,
+ });
+ return z.NEVER;
+ }
+ return title;
+ });
diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts
index 1ad5c9ca..f9c6717d 100644
--- a/packages/starlight/utils/route-data.ts
+++ b/packages/starlight/utils/route-data.ts
@@ -15,6 +15,8 @@ export interface PageProps extends Route {
}
export interface StarlightRouteData extends Route {
+ /** Title of the site. */
+ siteTitle: string;
/** Array of Markdown headings extracted from the current page. */
headings: MarkdownHeading[];
/** Site navigation sidebar entries for this page. */
@@ -40,10 +42,12 @@ export function generateRouteData({
props: PageProps;
url: URL;
}): StarlightRouteData {
- const { entry, locale } = props;
+ const { entry, locale, lang } = props;
const sidebar = getSidebar(url.pathname, locale);
+ const siteTitle = getSiteTitle(lang);
return {
...props,
+ siteTitle,
sidebar,
hasSidebar: entry.data.template !== 'splash',
pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
@@ -105,3 +109,12 @@ function getEditUrl({ entry, id, isFallback }: PageProps): URL | undefined {
}
return url ? new URL(url) : undefined;
}
+
+/** Get the site title for a given language. **/
+function getSiteTitle(lang: string): string {
+ const defaultLang = config.defaultLocale.lang as string;
+ if (lang && config.title[lang]) {
+ return config.title[lang] as string;
+ }
+ return config.title[defaultLang] as string;
+}
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index 8e566346..31f00077 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -8,6 +8,7 @@ import { LogoConfigSchema } from '../schemas/logo';
import { SidebarItemSchema } from '../schemas/sidebar';
import { SocialLinksSchema } from '../schemas/social';
import { TableOfContentsSchema } from '../schemas/tableOfContents';
+import { TitleConfigSchema, TitleTransformConfigSchema } from '../schemas/site-title';
const LocaleSchema = z.object({
/** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"简体中文"`. */
@@ -33,9 +34,7 @@ const LocaleSchema = z.object({
const UserConfigSchema = z.object({
/** Title for your website. Will be used in metadata and as browser tab title. */
- title: z
- .string()
- .describe('Title for your website. Will be used in metadata and as browser tab title.'),
+ title: TitleConfigSchema(),
/** Description metadata for your website. Can be used in page metadata. */
description: z
@@ -211,7 +210,7 @@ const UserConfigSchema = z.object({
});
export const StarlightConfigSchema = UserConfigSchema.strict().transform(
- ({ locales, defaultLocale, ...config }, ctx) => {
+ ({ title, locales, defaultLocale, ...config }, ctx) => {
const configuredLocales = Object.keys(locales ?? {});
// This is a multilingual site (more than one locale configured) or a monolingual site with
@@ -236,8 +235,13 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
return z.NEVER;
}
+ // Transform the title
+ const TitleSchema = TitleTransformConfigSchema(defaultLocaleConfig.lang as string);
+ const parsedTitle = TitleSchema.parse(title);
+
return {
...config,
+ title: parsedTitle,
/** Flag indicating if this site has multiple locales set up. */
isMultilingual: configuredLocales.length > 1,
/** Full locale object for this site’s default language. */
@@ -248,18 +252,23 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
// This is a monolingual site with no locales configured or only a root locale, so things are
// pretty simple.
+ /** Full locale object for this site’s default language. */
+ const defaultLocaleConfig = {
+ label: 'English',
+ lang: 'en',
+ dir: 'ltr',
+ locale: undefined,
+ ...locales?.root,
+ };
+ /** Transform the title */
+ const TitleSchema = TitleTransformConfigSchema(defaultLocaleConfig.lang);
+ const parsedTitle = TitleSchema.parse(title);
return {
...config,
+ title: parsedTitle,
/** Flag indicating if this site has multiple locales set up. */
isMultilingual: false,
- /** Full locale object for this site’s default language. */
- defaultLocale: {
- label: 'English',
- lang: 'en',
- dir: 'ltr',
- locale: undefined,
- ...locales?.root,
- },
+ defaultLocale: defaultLocaleConfig,
locales: undefined,
} as const;
}