summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiDeoo2024-06-05 19:23:37 +0200
committerGitHub2024-06-05 19:23:37 +0200
commitee0cd38a1fae31717fe820e779baeabe693cd67a (patch)
tree51f33ed1a351d1e2e099f152d889093daba390b1
parentdd64836af45f33df4a99ab864eabb91fc9b8e204 (diff)
downloadIT.starlight-ee0cd38a1fae31717fe820e779baeabe693cd67a.tar.gz
IT.starlight-ee0cd38a1fae31717fe820e779baeabe693cd67a.tar.bz2
IT.starlight-ee0cd38a1fae31717fe820e779baeabe693cd67a.zip
Add support for `Astro.currentLocale` (#1841)
Co-authored-by: Chris Swithinbank <357379+delucis@users.noreply.github.com> Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r--.changeset/early-pots-perform.md10
-rw-r--r--docs/src/content/docs/guides/i18n.mdx16
-rw-r--r--packages/starlight/404.astro4
-rw-r--r--packages/starlight/__tests__/basics/config-errors.test.ts1
-rw-r--r--packages/starlight/__tests__/basics/i18n.test.ts250
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/i18n.test.ts39
-rw-r--r--packages/starlight/__tests__/i18n-root-locale/i18n.test.ts51
-rw-r--r--packages/starlight/__tests__/i18n/i18n.test.ts57
-rw-r--r--packages/starlight/index.ts15
-rw-r--r--packages/starlight/integrations/shared/localeToLang.ts3
-rw-r--r--packages/starlight/utils/createTranslationSystem.ts3
-rw-r--r--packages/starlight/utils/i18n.ts166
-rw-r--r--packages/starlight/utils/routing.ts3
-rw-r--r--packages/starlight/utils/slugs.ts3
-rw-r--r--packages/starlight/utils/user-config.ts11
15 files changed, 619 insertions, 13 deletions
diff --git a/.changeset/early-pots-perform.md b/.changeset/early-pots-perform.md
new file mode 100644
index 00000000..14c33315
--- /dev/null
+++ b/.changeset/early-pots-perform.md
@@ -0,0 +1,10 @@
+---
+"@astrojs/starlight": minor
+---
+
+Adds support for `Astro.currentLocale` and Astro’s i18n routing.
+
+⚠️ **Potentially breaking change:** Starlight now configures Astro’s `i18n` option for you based on its `locales` config.
+
+If you are currently using Astro’s `i18n` option as well as Starlight’s `locales` option, you will need to remove one of these.
+In general we recommend using Starlight’s `locales`, but if you have a more advanced configuration you may choose to keep Astro’s `i18n` config instead.
diff --git a/docs/src/content/docs/guides/i18n.mdx b/docs/src/content/docs/guides/i18n.mdx
index 6f1513f8..a7455d41 100644
--- a/docs/src/content/docs/guides/i18n.mdx
+++ b/docs/src/content/docs/guides/i18n.mdx
@@ -67,6 +67,8 @@ Starlight provides built-in support for multilingual sites, including routing, f
</Steps>
+For more advanced i18n scenarios, Starlight also supports configuring internationalization using the [Astro’s `i18n` config](https://docs.astro.build/en/guides/internationalization/#configure-i18n-routing) option.
+
### Use a root locale
You can use a “root” locale to serve a language without any i18n prefix in its path. For example, if English is your root locale, an English page path would look like `/about` instead of `/en/about`.
@@ -272,3 +274,17 @@ export const collections = {
```
Learn more about content collection schemas in [“Defining a collection schema”](https://docs.astro.build/en/guides/content-collections/#defining-a-collection-schema) in the Astro docs.
+
+## Accessing the current locale
+
+You can use [`Astro.currentLocale`](https://docs.astro.build/en/reference/api-reference/#astrocurrentlocale) to read the current locale in `.astro` components.
+
+The following example reads the current locale and uses it to generate a link to an about page in the current language:
+
+```astro
+---
+// src/components/AboutLink.astro
+---
+
+<a href={`/${Astro.currentLocale}/about`}>About</a>
+```
diff --git a/packages/starlight/404.astro b/packages/starlight/404.astro
index ed7dc6f6..15cabd6a 100644
--- a/packages/starlight/404.astro
+++ b/packages/starlight/404.astro
@@ -6,10 +6,12 @@ import Page from './components/Page.astro';
import { generateRouteData } from './utils/route-data';
import type { StarlightDocsEntry } from './utils/routing';
import { useTranslations } from './utils/translations';
+import { BuiltInDefaultLocale } from './utils/i18n';
export const prerender = true;
-const { lang = 'en', dir = 'ltr' } = config.defaultLocale || {};
+const { lang = BuiltInDefaultLocale.lang, dir = BuiltInDefaultLocale.dir } =
+ config.defaultLocale || {};
let locale = config.defaultLocale?.locale;
if (locale === 'root') locale = undefined;
diff --git a/packages/starlight/__tests__/basics/config-errors.test.ts b/packages/starlight/__tests__/basics/config-errors.test.ts
index c76e0675..551c5600 100644
--- a/packages/starlight/__tests__/basics/config-errors.test.ts
+++ b/packages/starlight/__tests__/basics/config-errors.test.ts
@@ -59,6 +59,7 @@ test('parses valid config successfully', () => {
},
"head": [],
"isMultilingual": false,
+ "isUsingBuiltInDefaultLocale": true,
"lastUpdated": false,
"locales": undefined,
"pagefind": true,
diff --git a/packages/starlight/__tests__/basics/i18n.test.ts b/packages/starlight/__tests__/basics/i18n.test.ts
index 1ab6262f..f609e528 100644
--- a/packages/starlight/__tests__/basics/i18n.test.ts
+++ b/packages/starlight/__tests__/basics/i18n.test.ts
@@ -1,5 +1,8 @@
-import { describe, expect, test } from 'vitest';
-import { pickLang } from '../../utils/i18n';
+import { assert, describe, expect, test } from 'vitest';
+import config from 'virtual:starlight/user-config';
+import { processI18nConfig, pickLang } from '../../utils/i18n';
+import type { AstroConfig } from 'astro';
+import type { AstroUserConfig } from 'astro/config';
describe('pickLang', () => {
const dictionary = { en: 'Hello', fr: 'Bonjour' };
@@ -13,3 +16,246 @@ describe('pickLang', () => {
expect(pickLang(dictionary, 'ar' as any)).toBeUndefined();
});
});
+
+describe('processI18nConfig', () => {
+ test('returns the Astro i18n config for an unconfigured monolingual site using the built-in default locale', () => {
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(config, undefined);
+
+ expect(astroI18nConfig.defaultLocale).toBe('en');
+ expect(astroI18nConfig.locales).toEqual(['en']);
+ assert(typeof astroI18nConfig.routing !== 'string');
+ expect(astroI18nConfig.routing?.prefixDefaultLocale).toBe(false);
+
+ // The Starlight configuration should not be modified.
+ expect(config).toStrictEqual(starlightConfig);
+ });
+
+ describe('with a provided Astro i18n config', () => {
+ test('throws an error when an Astro i18n `manual` routing option is used', () => {
+ expect(() =>
+ processI18nConfig(
+ config,
+ getAstroI18nTestConfig({
+ defaultLocale: 'en',
+ locales: ['en', 'fr'],
+ routing: 'manual',
+ })
+ )
+ ).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Starlight is not compatible with the \`manual\` routing option in the Astro i18n configuration.
+ Hint:
+ "
+ `);
+ });
+
+ test('throws an error when an Astro i18n config contains an invalid locale', () => {
+ expect(() =>
+ processI18nConfig(
+ config,
+ getAstroI18nTestConfig({
+ defaultLocale: 'en',
+ locales: ['en', 'foo'],
+ })
+ )
+ ).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Failed to get locale informations for the 'foo' locale.
+ Hint:
+ Make sure to provide a valid BCP-47 tags (e.g. en, ar, or zh-CN)."
+ `);
+ });
+
+ test.each([
+ {
+ i18nConfig: { defaultLocale: 'en', locales: ['en'] },
+ expected: {
+ defaultLocale: { label: 'English', lang: 'en', dir: 'ltr', locale: undefined },
+ },
+ },
+ {
+ i18nConfig: { defaultLocale: 'fr', locales: [{ codes: ['fr'], path: 'fr' }] },
+ expected: {
+ defaultLocale: { label: 'Français', lang: 'fr', dir: 'ltr', locale: undefined },
+ },
+ },
+ {
+ i18nConfig: {
+ defaultLocale: 'fa',
+ locales: ['fa'],
+ routing: { prefixDefaultLocale: false },
+ },
+ expected: {
+ defaultLocale: { label: 'فارسی', lang: 'fa', dir: 'rtl', locale: undefined },
+ },
+ },
+ ])(
+ 'updates the Starlight i18n config for a monolingual site with a single root locale',
+ ({ i18nConfig, expected }) => {
+ const astroI18nTestConfig = getAstroI18nTestConfig(i18nConfig);
+
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(config, astroI18nTestConfig);
+
+ expect(starlightConfig.isMultilingual).toBe(false);
+ expect(starlightConfig.locales).not.toBeDefined();
+ expect(starlightConfig.defaultLocale).toStrictEqual(expected.defaultLocale);
+
+ // The Astro i18n configuration should not be modified.
+ expect(astroI18nConfig).toStrictEqual(astroI18nConfig);
+ }
+ );
+
+ test.each([
+ {
+ i18nConfig: {
+ defaultLocale: 'en',
+ locales: ['en'],
+ routing: { prefixDefaultLocale: true },
+ },
+ expected: {
+ defaultLocale: { label: 'English', lang: 'en', dir: 'ltr', locale: 'en' },
+ locales: { en: { label: 'English', lang: 'en', dir: 'ltr' } },
+ },
+ },
+ {
+ i18nConfig: {
+ defaultLocale: 'french',
+ locales: [{ codes: ['fr'], path: 'french' }],
+ routing: { prefixDefaultLocale: true },
+ },
+ expected: {
+ defaultLocale: { label: 'Français', lang: 'fr', dir: 'ltr', locale: 'fr' },
+ locales: { french: { label: 'Français', lang: 'fr', dir: 'ltr' } },
+ },
+ },
+ {
+ i18nConfig: {
+ defaultLocale: 'farsi',
+ locales: [{ codes: ['fa'], path: 'farsi' }],
+ routing: { prefixDefaultLocale: true },
+ },
+ expected: {
+ defaultLocale: { label: 'فارسی', lang: 'fa', dir: 'rtl', locale: 'fa' },
+ locales: { farsi: { label: 'فارسی', lang: 'fa', dir: 'rtl' } },
+ },
+ },
+ ])(
+ 'updates the Starlight i18n config for a monolingual site with a single non-root locale',
+ ({ i18nConfig, expected }) => {
+ const astroI18nTestConfig = getAstroI18nTestConfig(i18nConfig);
+
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(config, astroI18nTestConfig);
+
+ expect(starlightConfig.isMultilingual).toBe(false);
+ expect(starlightConfig.locales).toStrictEqual(expected.locales);
+ expect(starlightConfig.defaultLocale).toStrictEqual(expected.defaultLocale);
+
+ // The Astro i18n configuration should not be modified.
+ expect(astroI18nConfig).toStrictEqual(astroI18nConfig);
+ }
+ );
+
+ test.each([
+ {
+ i18nConfig: {
+ defaultLocale: 'en',
+ locales: ['en', { codes: ['fr'], path: 'french' }],
+ },
+ expected: {
+ defaultLocale: { label: 'English', lang: 'en', dir: 'ltr', locale: 'en' },
+ locales: {
+ root: { label: 'English', lang: 'en', dir: 'ltr' },
+ french: { label: 'Français', lang: 'fr', dir: 'ltr' },
+ },
+ },
+ },
+ {
+ i18nConfig: {
+ defaultLocale: 'farsi',
+ // This configuration is a bit confusing as `prefixDefaultLocale` is `false` but the
+ // default locale is defined with a custom path.
+ // In this case, the default locale is considered to be a root locale and the custom path
+ // is ignored.
+ locales: [{ codes: ['fa'], path: 'farsi' }, 'de'],
+ routing: { prefixDefaultLocale: false },
+ },
+ expected: {
+ defaultLocale: { label: 'فارسی', lang: 'fa', dir: 'rtl', locale: 'fa' },
+ locales: {
+ root: { label: 'فارسی', lang: 'fa', dir: 'rtl' },
+ de: { label: 'Deutsch', lang: 'de', dir: 'ltr' },
+ },
+ },
+ },
+ ])(
+ 'updates the Starlight i18n config for a multilingual site with a root locale',
+ ({ i18nConfig, expected }) => {
+ const astroI18nTestConfig = getAstroI18nTestConfig(i18nConfig);
+
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(config, astroI18nTestConfig);
+
+ expect(starlightConfig.isMultilingual).toBe(true);
+ expect(starlightConfig.locales).toEqual(expected.locales);
+ expect(starlightConfig.defaultLocale).toEqual(expected.defaultLocale);
+
+ // The Astro i18n configuration should not be modified.
+ expect(astroI18nConfig).toEqual(astroI18nConfig);
+ }
+ );
+
+ test.each([
+ {
+ i18nConfig: {
+ defaultLocale: 'en',
+ locales: ['en', { codes: ['fr'], path: 'french' }],
+ routing: { prefixDefaultLocale: true },
+ },
+ expected: {
+ defaultLocale: { label: 'English', lang: 'en', dir: 'ltr', locale: 'en' },
+ locales: {
+ en: { label: 'English', lang: 'en', dir: 'ltr' },
+ french: { label: 'Français', lang: 'fr', dir: 'ltr' },
+ },
+ },
+ },
+ {
+ i18nConfig: {
+ defaultLocale: 'farsi',
+ locales: [{ codes: ['fa'], path: 'farsi' }, 'de'],
+ routing: { prefixDefaultLocale: true },
+ },
+ expected: {
+ defaultLocale: { label: 'فارسی', lang: 'fa', dir: 'rtl', locale: 'fa' },
+ locales: {
+ farsi: { label: 'فارسی', lang: 'fa', dir: 'rtl' },
+ de: { label: 'Deutsch', lang: 'de', dir: 'ltr' },
+ },
+ },
+ },
+ ])(
+ 'updates the Starlight i18n config for a multilingual site with no root locale',
+ ({ i18nConfig, expected }) => {
+ const astroI18nTestConfig = getAstroI18nTestConfig(i18nConfig);
+
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(config, astroI18nTestConfig);
+
+ expect(starlightConfig.isMultilingual).toBe(true);
+ expect(starlightConfig.locales).toEqual(expected.locales);
+ expect(starlightConfig.defaultLocale).toEqual(expected.defaultLocale);
+
+ // The Astro i18n configuration should not be modified.
+ expect(astroI18nConfig).toEqual(astroI18nConfig);
+ }
+ );
+ });
+});
+
+function getAstroI18nTestConfig(i18nConfig: AstroUserConfig['i18n']): AstroConfig['i18n'] {
+ return {
+ ...i18nConfig,
+ routing:
+ typeof i18nConfig?.routing !== 'string'
+ ? { prefixDefaultLocale: false, ...i18nConfig?.routing }
+ : i18nConfig.routing,
+ } as AstroConfig['i18n'];
+}
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/i18n.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/i18n.test.ts
new file mode 100644
index 00000000..b42d2a29
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/i18n.test.ts
@@ -0,0 +1,39 @@
+import { assert, describe, expect, test } from 'vitest';
+import type { AstroConfig } from 'astro';
+import config from 'virtual:starlight/user-config';
+import { processI18nConfig } from '../../utils/i18n';
+
+describe('processI18nConfig', () => {
+ test('returns the Astro i18n config for a monolingual site with a non-root single locale', () => {
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(config, undefined);
+
+ expect(astroI18nConfig.defaultLocale).toBe('fr-CA');
+ expect(astroI18nConfig.locales).toMatchInlineSnapshot(`
+ [
+ {
+ "codes": [
+ "fr-CA",
+ ],
+ "path": "fr",
+ },
+ ]
+ `);
+ assert(typeof astroI18nConfig.routing !== 'string');
+ expect(astroI18nConfig.routing?.prefixDefaultLocale).toBe(true);
+
+ // The Starlight configuration should not be modified.
+ expect(config).toStrictEqual(starlightConfig);
+ });
+
+ test('throws an error when an Astro i18n config is also provided', () => {
+ expect(() =>
+ processI18nConfig(config, { defaultLocale: 'en', locales: ['en'] } as AstroConfig['i18n'])
+ ).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Cannot provide both an Astro \`i18n\` configuration and a Starlight \`locales\` configuration.
+ Hint:
+ Remove one of the two configurations.
+ See more at https://starlight.astro.build/guides/i18n/"
+ `);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-root-locale/i18n.test.ts b/packages/starlight/__tests__/i18n-root-locale/i18n.test.ts
new file mode 100644
index 00000000..049fad8f
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-root-locale/i18n.test.ts
@@ -0,0 +1,51 @@
+import { assert, describe, expect, test } from 'vitest';
+import type { AstroConfig } from 'astro';
+import config from 'virtual:starlight/user-config';
+import { processI18nConfig } from '../../utils/i18n';
+
+describe('processI18nConfig', () => {
+ test('returns Astro i18n config for a multilingual site with a root locale', () => {
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(config, undefined);
+
+ expect(astroI18nConfig.defaultLocale).toBe('fr');
+ expect(astroI18nConfig.locales).toMatchInlineSnapshot(`
+ [
+ {
+ "codes": [
+ "fr",
+ ],
+ "path": "fr",
+ },
+ {
+ "codes": [
+ "en-US",
+ ],
+ "path": "en",
+ },
+ {
+ "codes": [
+ "ar",
+ ],
+ "path": "ar",
+ },
+ ]
+ `);
+ assert(typeof astroI18nConfig.routing !== 'string');
+ expect(astroI18nConfig.routing?.prefixDefaultLocale).toBe(false);
+
+ // The Starlight configuration should not be modified.
+ expect(config).toStrictEqual(starlightConfig);
+ });
+
+ test('throws an error when an Astro i18n config is also provided', () => {
+ expect(() =>
+ processI18nConfig(config, { defaultLocale: 'en', locales: ['en'] } as AstroConfig['i18n'])
+ ).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Cannot provide both an Astro \`i18n\` configuration and a Starlight \`locales\` configuration.
+ Hint:
+ Remove one of the two configurations.
+ See more at https://starlight.astro.build/guides/i18n/"
+ `);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n/i18n.test.ts b/packages/starlight/__tests__/i18n/i18n.test.ts
new file mode 100644
index 00000000..383e57ca
--- /dev/null
+++ b/packages/starlight/__tests__/i18n/i18n.test.ts
@@ -0,0 +1,57 @@
+import { assert, describe, expect, test } from 'vitest';
+import type { AstroConfig } from 'astro';
+import config from 'virtual:starlight/user-config';
+import { processI18nConfig } from '../../utils/i18n';
+
+describe('processI18nConfig', () => {
+ test('returns the Astro i18n config for a multilingual site with no root locale', () => {
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(config, undefined);
+
+ expect(astroI18nConfig.defaultLocale).toBe('en-US');
+ expect(astroI18nConfig.locales).toMatchInlineSnapshot(`
+ [
+ {
+ "codes": [
+ "fr",
+ ],
+ "path": "fr",
+ },
+ {
+ "codes": [
+ "en-US",
+ ],
+ "path": "en",
+ },
+ {
+ "codes": [
+ "ar",
+ ],
+ "path": "ar",
+ },
+ {
+ "codes": [
+ "pt-BR",
+ ],
+ "path": "pt-br",
+ },
+ ]
+ `);
+ assert(typeof astroI18nConfig.routing !== 'string');
+ expect(astroI18nConfig.routing?.prefixDefaultLocale).toBe(true);
+
+ // The Starlight configuration should not be modified.
+ expect(config).toStrictEqual(starlightConfig);
+ });
+
+ test('throws an error when an Astro i18n config is also provided', () => {
+ expect(() =>
+ processI18nConfig(config, { defaultLocale: 'en', locales: ['en'] } as AstroConfig['i18n'])
+ ).toThrowErrorMatchingInlineSnapshot(`
+ "[AstroUserError]:
+ Cannot provide both an Astro \`i18n\` configuration and a Starlight \`locales\` configuration.
+ Hint:
+ Remove one of the two configurations.
+ See more at https://starlight.astro.build/guides/i18n/"
+ `);
+ });
+});
diff --git a/packages/starlight/index.ts b/packages/starlight/index.ts
index ee31491f..a7876ab8 100644
--- a/packages/starlight/index.ts
+++ b/packages/starlight/index.ts
@@ -10,6 +10,7 @@ import { vitePluginStarlightUserConfig } from './integrations/virtual-user-confi
import { rehypeRtlCodeSupport } from './integrations/code-rtl-support';
import { createTranslationSystemFromFs } from './utils/translations-fs';
import { runPlugins, type StarlightUserConfigWithPlugins } from './utils/plugins';
+import { processI18nConfig } from './utils/i18n';
import type { StarlightConfig } from './types';
export default function StarlightIntegration({
@@ -28,18 +29,25 @@ export default function StarlightIntegration({
logger,
updateConfig,
}) => {
- // Run plugins to get the final configuration and any extra Astro integrations to load.
- const { integrations, starlightConfig } = await runPlugins(opts, plugins, {
+ // Run plugins to get the updated configuration and any extra Astro integrations to load.
+ const pluginResult = await runPlugins(opts, plugins, {
command,
config,
isRestart,
logger,
});
+ // Process the Astro and Starlight configurations for i18n and translations.
+ const { astroI18nConfig, starlightConfig } = processI18nConfig(
+ pluginResult.starlightConfig,
+ config.i18n
+ );
+
+ const { integrations } = pluginResult;
userConfig = starlightConfig;
const useTranslations = createTranslationSystemFromFs(starlightConfig, config);
- if (!userConfig.disable404Route) {
+ if (!starlightConfig.disable404Route) {
injectRoute({
pattern: '404',
entrypoint: '@astrojs/starlight/404.astro',
@@ -92,6 +100,7 @@ export default function StarlightIntegration({
experimental: {
globalRoutePriority: true,
},
+ i18n: astroI18nConfig,
});
},
diff --git a/packages/starlight/integrations/shared/localeToLang.ts b/packages/starlight/integrations/shared/localeToLang.ts
index 5d79d017..6c21b814 100644
--- a/packages/starlight/integrations/shared/localeToLang.ts
+++ b/packages/starlight/integrations/shared/localeToLang.ts
@@ -1,4 +1,5 @@
import type { StarlightConfig } from '../../types';
+import { BuiltInDefaultLocale } from '../../utils/i18n';
/**
* Get the BCP-47 language tag for the given locale.
@@ -7,5 +8,5 @@ import type { StarlightConfig } from '../../types';
export function localeToLang(config: StarlightConfig, locale: string | undefined): string {
const lang = locale ? config.locales?.[locale]?.lang : config.locales?.root?.lang;
const defaultLang = config.defaultLocale?.lang || config.defaultLocale?.locale;
- return lang || defaultLang || 'en';
+ return lang || defaultLang || BuiltInDefaultLocale.lang;
}
diff --git a/packages/starlight/utils/createTranslationSystem.ts b/packages/starlight/utils/createTranslationSystem.ts
index de1d3c82..c65c7f6b 100644
--- a/packages/starlight/utils/createTranslationSystem.ts
+++ b/packages/starlight/utils/createTranslationSystem.ts
@@ -1,5 +1,6 @@
import type { i18nSchemaOutput } from '../schemas/i18n';
import builtinTranslations from '../translations/index';
+import { BuiltInDefaultLocale } from './i18n';
import type { StarlightConfig } from './user-config';
export function createTranslationSystem<T extends i18nSchemaOutput>(
@@ -64,7 +65,7 @@ function localeToLang(
): string {
const lang = locale ? locales?.[locale]?.lang : locales?.root?.lang;
const defaultLang = defaultLocale?.lang || defaultLocale?.locale;
- return lang || defaultLang || 'en';
+ return lang || defaultLang || BuiltInDefaultLocale.lang;
}
type BuiltInStrings = (typeof builtinTranslations)['en'];
diff --git a/packages/starlight/utils/i18n.ts b/packages/starlight/utils/i18n.ts
index e00dfc66..cb7c052a 100644
--- a/packages/starlight/utils/i18n.ts
+++ b/packages/starlight/utils/i18n.ts
@@ -1,3 +1,166 @@
+import type { AstroConfig } from 'astro';
+import { AstroError } from 'astro/errors';
+import type { StarlightConfig } from './user-config';
+
+/** Informations about the built-in default locale used as a fallback when no locales are defined. */
+export const BuiltInDefaultLocale = { ...getLocaleInfo('en'), lang: 'en' };
+
+/**
+ * Processes the Astro and Starlight i18n configurations to generate/update them accordingly:
+ *
+ * - If no Astro and Starlight i18n configurations are provided, the built-in default locale is
+ * used in Starlight and the generated Astro i18n configuration will match it.
+ * - If only a Starlight i18n configuration is provided, an equivalent Astro i18n configuration is
+ * generated.
+ * - If only an Astro i18n configuration is provided, an equivalent Starlight i18n configuration is
+ * used.
+ * - If both an Astro and Starlight i18n configurations are provided, an error is thrown.
+ */
+export function processI18nConfig(
+ starlightConfig: StarlightConfig,
+ astroI18nConfig: AstroConfig['i18n']
+) {
+ // We don't know what to do if both an Astro and Starlight i18n configuration are provided.
+ if (astroI18nConfig && !starlightConfig.isUsingBuiltInDefaultLocale) {
+ throw new AstroError(
+ 'Cannot provide both an Astro `i18n` configuration and a Starlight `locales` configuration.',
+ 'Remove one of the two configurations.\nSee more at https://starlight.astro.build/guides/i18n/'
+ );
+ } else if (astroI18nConfig) {
+ // If a Starlight compatible Astro i18n configuration is provided, we generate the matching
+ // Starlight configuration.
+ return {
+ astroI18nConfig,
+ starlightConfig: {
+ ...starlightConfig,
+ ...getStarlightI18nConfig(astroI18nConfig),
+ } as StarlightConfig,
+ };
+ }
+ // Otherwise, we generate the Astro i18n configuration based on the Starlight configuration.
+ return { astroI18nConfig: getAstroI18nConfig(starlightConfig), starlightConfig: starlightConfig };
+}
+
+/** Generate an Astro i18n configuration based on a Starlight configuration. */
+function getAstroI18nConfig(config: StarlightConfig): NonNullable<AstroConfig['i18n']> {
+ return {
+ defaultLocale:
+ config.defaultLocale.lang ?? config.defaultLocale.locale ?? BuiltInDefaultLocale.lang,
+ locales: config.locales
+ ? Object.entries(config.locales).map(([locale, localeConfig]) => {
+ return {
+ codes: [localeConfig?.lang ?? locale],
+ path: locale === 'root' ? localeConfig?.lang ?? BuiltInDefaultLocale.lang : locale,
+ };
+ })
+ : [BuiltInDefaultLocale.lang],
+ routing: {
+ prefixDefaultLocale:
+ // Sites with multiple languages without a root locale.
+ (config.isMultilingual && config.locales?.root === undefined) ||
+ // Sites with a single non-root language different from the built-in default locale.
+ (!config.isMultilingual && config.locales !== undefined),
+ redirectToDefaultLocale: false,
+ },
+ };
+}
+
+/** Generate a Starlight i18n configuration based on an Astro configuration. */
+function getStarlightI18nConfig(
+ astroI18nConfig: NonNullable<AstroConfig['i18n']>
+): Pick<StarlightConfig, 'isMultilingual' | 'locales' | 'defaultLocale'> {
+ if (astroI18nConfig.routing === 'manual') {
+ throw new AstroError(
+ 'Starlight is not compatible with the `manual` routing option in the Astro i18n configuration.'
+ );
+ }
+
+ const prefixDefaultLocale = astroI18nConfig.routing.prefixDefaultLocale;
+ const isMultilingual = astroI18nConfig.locales.length > 1;
+ const isMonolingualWithRootLocale = !isMultilingual && !prefixDefaultLocale;
+
+ const locales = isMonolingualWithRootLocale
+ ? undefined
+ : Object.fromEntries(
+ astroI18nConfig.locales.map((locale) => [
+ isDefaultAstroLocale(astroI18nConfig, locale) && !prefixDefaultLocale
+ ? 'root'
+ : isAstroLocaleExtendedConfig(locale)
+ ? locale.path
+ : locale,
+ inferStarlightLocaleFromAstroLocale(locale),
+ ])
+ );
+
+ const defaultAstroLocale = astroI18nConfig.locales.find((locale) =>
+ isDefaultAstroLocale(astroI18nConfig, locale)
+ );
+
+ // This should never happen as Astro validation should prevent this case.
+ if (!defaultAstroLocale) {
+ throw new AstroError(
+ 'Astro default locale not found.',
+ 'This should never happen. Please open a new issue: https://github.com/withastro/starlight/issues/new?template=---01-bug-report.yml'
+ );
+ }
+
+ return {
+ isMultilingual,
+ locales,
+ defaultLocale: {
+ ...inferStarlightLocaleFromAstroLocale(defaultAstroLocale),
+ locale: isMonolingualWithRootLocale
+ ? undefined
+ : isAstroLocaleExtendedConfig(defaultAstroLocale)
+ ? defaultAstroLocale.codes[0]
+ : defaultAstroLocale,
+ },
+ };
+}
+
+/** Infer Starlight locale informations based on a locale from an Astro i18n configuration. */
+function inferStarlightLocaleFromAstroLocale(astroLocale: AstroLocale) {
+ const lang = isAstroLocaleExtendedConfig(astroLocale) ? astroLocale.codes[0] : astroLocale;
+ return { ...getLocaleInfo(lang), lang };
+}
+
+/** Check if the passed locale is the default locale in an Astro i18n configuration. */
+function isDefaultAstroLocale(
+ astroI18nConfig: NonNullable<AstroConfig['i18n']>,
+ locale: AstroLocale
+) {
+ return (
+ (isAstroLocaleExtendedConfig(locale) ? locale.path : locale) === astroI18nConfig.defaultLocale
+ );
+}
+
+/**
+ * Check if the passed Astro locale is using the object variant.
+ * @see AstroLocaleExtendedConfig
+ */
+function isAstroLocaleExtendedConfig(locale: AstroLocale): locale is AstroLocaleExtendedConfig {
+ return typeof locale !== 'string';
+}
+
+/** Returns the locale informations such as a label and a direction based on a BCP-47 tag. */
+function getLocaleInfo(lang: string) {
+ try {
+ const locale = new Intl.Locale(lang);
+ const label = new Intl.DisplayNames(locale, { type: 'language' }).of(lang);
+ if (!label || lang === label) throw new Error('Label not found.');
+ return {
+ label: label[0]?.toLocaleUpperCase(locale) + label.slice(1),
+ // @ts-expect-error - `textInfo` is not part of the `Intl.Locale` type but is available in Node.js 18.0.0+.
+ dir: locale.textInfo.direction as 'ltr' | 'rtl',
+ };
+ } catch (error) {
+ throw new AstroError(
+ `Failed to get locale informations for the '${lang}' locale.`,
+ 'Make sure to provide a valid BCP-47 tags (e.g. en, ar, or zh-CN).'
+ );
+ }
+}
+
/**
* Get the string for the passed language from a dictionary object.
*
@@ -14,3 +177,6 @@ export function pickLang<T extends Record<string, string>>(
): string | undefined {
return dictionary[lang];
}
+
+type AstroLocale = NonNullable<AstroConfig['i18n']>['locales'][number];
+type AstroLocaleExtendedConfig = Exclude<AstroLocale, string>;
diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts
index 8ca555b4..a0b15fd3 100644
--- a/packages/starlight/utils/routing.ts
+++ b/packages/starlight/utils/routing.ts
@@ -9,6 +9,7 @@ import {
slugToParam,
} from './slugs';
import { validateLogoImports } from './validateLogoImports';
+import { BuiltInDefaultLocale } from './i18n';
// Validate any user-provided logos imported correctly.
// We do this here so all pages trigger it and at the top level so it runs just once.
@@ -86,7 +87,7 @@ function getRoutes(): Route[] {
slug,
id,
isFallback: true,
- lang: localeConfig.lang || 'en',
+ lang: localeConfig.lang || BuiltInDefaultLocale.lang,
locale,
dir: localeConfig.dir,
entryMeta: slugToLocaleData(fallback.slug),
diff --git a/packages/starlight/utils/slugs.ts b/packages/starlight/utils/slugs.ts
index b7e076c9..75f90721 100644
--- a/packages/starlight/utils/slugs.ts
+++ b/packages/starlight/utils/slugs.ts
@@ -1,4 +1,5 @@
import config from 'virtual:starlight/user-config';
+import { BuiltInDefaultLocale } from './i18n';
export interface LocaleData {
/** Writing direction. */
@@ -35,7 +36,7 @@ export function slugToLocaleData(slug: string): LocaleData {
export function localeToLang(locale: string | undefined): string {
const lang = locale ? config.locales?.[locale]?.lang : config.locales?.root?.lang;
const defaultLang = config.defaultLocale?.lang || config.defaultLocale?.locale;
- return lang || defaultLang || 'en';
+ return lang || defaultLang || BuiltInDefaultLocale.lang;
}
/**
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index efd6193e..d80c5d83 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -9,6 +9,7 @@ import { SidebarItemSchema } from '../schemas/sidebar';
import { SocialLinksSchema } from '../schemas/social';
import { TableOfContentsSchema } from '../schemas/tableOfContents';
import { TitleConfigSchema, TitleTransformConfigSchema } from '../schemas/site-title';
+import { BuiltInDefaultLocale } from './i18n';
const LocaleSchema = z.object({
/** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"简体中文"`. */
@@ -244,6 +245,8 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
title: parsedTitle,
/** Flag indicating if this site has multiple locales set up. */
isMultilingual: configuredLocales.length > 1,
+ /** Flag indicating if the Starlight built-in default locale is used. */
+ isUsingBuiltInDefaultLocale: false,
/** Full locale object for this site’s default language. */
defaultLocale: { ...defaultLocaleConfig, locale: defaultLocale },
locales,
@@ -254,9 +257,9 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
// pretty simple.
/** Full locale object for this site’s default language. */
const defaultLocaleConfig = {
- label: 'English',
- lang: 'en',
- dir: 'ltr' as const,
+ label: BuiltInDefaultLocale.label,
+ lang: BuiltInDefaultLocale.lang,
+ dir: BuiltInDefaultLocale.dir,
locale: undefined,
...locales?.root,
};
@@ -268,6 +271,8 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
title: parsedTitle,
/** Flag indicating if this site has multiple locales set up. */
isMultilingual: false,
+ /** Flag indicating if the Starlight built-in default locale is used. */
+ isUsingBuiltInDefaultLocale: locales?.root === undefined,
defaultLocale: defaultLocaleConfig,
locales: undefined,
} as const;