Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support object title for multiple language #1620

Merged
merged 43 commits into from Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
aeba370
feat: Support object title for multiple language
emjio Mar 18, 2024
c8c3ae0
fix: standardized spelling
emjio Mar 18, 2024
7d5b3ce
feat: Update the Header's UI title and document title
emjio Mar 18, 2024
cdf2616
feat: Update title config type support both string and object type.
emjio Mar 18, 2024
a1ddc04
fix: fix the worng config lang
emjio Mar 18, 2024
6149c49
test: Update test case by Type
emjio Mar 19, 2024
82cc0d2
fix: fix code format
emjio Mar 19, 2024
9a2613c
feat: add changeset
emjio Mar 19, 2024
3f87af0
doc: add title feat desction in zh-cn doc
emjio Mar 19, 2024
6ee9478
doc: add title feat desription in en doc
emjio Mar 19, 2024
d4494cb
docs: revert zh-cn docs and updata en docs des
emjio Mar 19, 2024
8ce46c5
docs: revert to default zh-cn doc
emjio Mar 19, 2024
7f3f4d0
fix: remove whitespace
emjio Mar 19, 2024
28f0b8b
Update vitest.config.ts
emjio Mar 19, 2024
6873b36
Update neat-flowers-move.md
emjio Mar 19, 2024
d16c012
doc: revert to default doc
emjio Mar 19, 2024
70f4435
docs: revert to default doc
emjio Mar 19, 2024
df3bfc0
Pending changes exported from your codespace
emjio Apr 10, 2024
3d3d000
Refactor title creation in starlight components
liruifengv Apr 10, 2024
8df1a7b
reset code
liruifengv Apr 10, 2024
5f65bf3
Add TitleTransformConfigSchema tests in schema.test.ts
liruifengv Apr 10, 2024
8ca7f92
format
liruifengv Apr 10, 2024
afef4b6
delete unused
liruifengv Apr 10, 2024
f96600d
update from Chris's suggestions
emjio Apr 11, 2024
0362753
test: update test case
emjio Apr 15, 2024
df08c76
fix: format
emjio Apr 15, 2024
7f66298
fix: fix the title test case error while title equals to an empty string
emjio Apr 15, 2024
80dda21
Rename utility files to `site-title` for clarity
delucis Apr 15, 2024
4f866ad
Add detail to changeset, and mark as minor
delucis Apr 15, 2024
93fc52b
Revert test change
delucis Apr 15, 2024
a4c318f
Revert whitespace change
delucis Apr 15, 2024
8cdf317
Inline `getSiteTitle` in route data file
delucis Apr 15, 2024
4dbfd07
doc: update configuration doc
emjio Apr 15, 2024
fc6f856
doc: update i18n.mdx
emjio Apr 15, 2024
bdc7530
doc:update overrides.md
emjio Apr 15, 2024
34e40f4
fix: Fix TitleConfig anchor
emjio Apr 15, 2024
f54fd85
Update Type Name
emjio Apr 15, 2024
39cec9f
Merge branch 'withastro:main' into main
emjio Apr 16, 2024
5a4cb7b
Apply suggestions from code review
emjio Apr 16, 2024
9fc0d7f
Dedicated docs section in i18n guide
delucis Apr 30, 2024
9d633d1
Revise config reference section
delucis Apr 30, 2024
22bcb90
Revise overrides config
delucis Apr 30, 2024
035b38d
Merge branch 'main' into main
delucis Apr 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions .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;
---
```
28 changes: 28 additions & 0 deletions docs/src/content/docs/guides/i18n.mdx
Expand Up @@ -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';
Expand Down
14 changes: 13 additions & 1 deletion docs/src/content/docs/reference/configuration.mdx
Expand Up @@ -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`
Expand Down
8 changes: 7 additions & 1 deletion docs/src/content/docs/reference/overrides.md
Expand Up @@ -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`
Expand Down Expand Up @@ -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`

Expand Down
14 changes: 9 additions & 5 deletions packages/starlight/__tests__/basics/config-errors.test.ts
Expand Up @@ -66,7 +66,9 @@ test('parses valid config successfully', () => {
"maxHeadingLevel": 3,
"minHeadingLevel": 2,
},
"title": "",
"title": {
"en": "",
},
"titleDelimiter": "|",
}
`);
Expand All @@ -80,20 +82,22 @@ 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(
`
"[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"\`"
`
);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/starlight/__tests__/basics/config.test.ts
Expand Up @@ -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 ', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/starlight/__tests__/basics/routing.test.ts
Expand Up @@ -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', () => {
Expand Down
35 changes: 35 additions & 0 deletions 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', () => {
Expand All @@ -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();
});
});
Expand Up @@ -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', () => {
Expand Down
Expand Up @@ -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', () => {
Expand Down
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/starlight/__tests__/i18n/config.test.ts
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/starlight/__tests__/i18n/routing.test.ts
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/starlight/__tests__/plugins/config.test.ts
Expand Up @@ -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', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/starlight/components/Head.astro
Expand Up @@ -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;
Expand All @@ -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 } },
{
Expand All @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion packages/starlight/components/SiteTitle.astro
Expand Up @@ -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 || '/');
---

Expand Down Expand Up @@ -32,7 +33,7 @@ const href = formatPath(Astro.props.locale || '/');
)
}
<span class:list={{ 'sr-only': config.logo?.replacesTitle }}>
{config.title}
{siteTitle}
</span>
</a>

Expand Down
22 changes: 22 additions & 0 deletions 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;
});
15 changes: 14 additions & 1 deletion packages/starlight/utils/route-data.ts
Expand Up @@ -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. */
Expand All @@ -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),
Expand Down Expand Up @@ -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;
}