Skip to content

Commit

Permalink
Add support for Astro.currentLocale (#1841)
Browse files Browse the repository at this point in the history
Co-authored-by: Chris Swithinbank <357379+delucis@users.noreply.github.com>
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
  • Loading branch information
3 people committed Jun 5, 2024
1 parent dd64836 commit ee0cd38
Show file tree
Hide file tree
Showing 15 changed files with 619 additions and 13 deletions.
10 changes: 10 additions & 0 deletions .changeset/early-pots-perform.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions docs/src/content/docs/guides/i18n.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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>
```
4 changes: 3 additions & 1 deletion packages/starlight/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/starlight/__tests__/basics/config-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ test('parses valid config successfully', () => {
},
"head": [],
"isMultilingual": false,
"isUsingBuiltInDefaultLocale": true,
"lastUpdated": false,
"locales": undefined,
"pagefind": true,
Expand Down
250 changes: 248 additions & 2 deletions packages/starlight/__tests__/basics/i18n.test.ts
Original file line number Diff line number Diff line change
@@ -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' };
Expand All @@ -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'];
}
Original file line number Diff line number Diff line change
@@ -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/"
`);
});
});
Loading

0 comments on commit ee0cd38

Please sign in to comment.