Skip to content

Commit ee0cd38

Browse files
HiDeoodelucis
andauthored
Add support for Astro.currentLocale (#1841)
Co-authored-by: Chris Swithinbank <[email protected]> Co-authored-by: Chris Swithinbank <[email protected]>
1 parent dd64836 commit ee0cd38

File tree

15 files changed

+619
-13
lines changed

15 files changed

+619
-13
lines changed

.changeset/early-pots-perform.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@astrojs/starlight": minor
3+
---
4+
5+
Adds support for `Astro.currentLocale` and Astro’s i18n routing.
6+
7+
⚠️ **Potentially breaking change:** Starlight now configures Astro’s `i18n` option for you based on its `locales` config.
8+
9+
If you are currently using Astro’s `i18n` option as well as Starlight’s `locales` option, you will need to remove one of these.
10+
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.

docs/src/content/docs/guides/i18n.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ Starlight provides built-in support for multilingual sites, including routing, f
6767

6868
</Steps>
6969

70+
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.
71+
7072
### Use a root locale
7173

7274
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 = {
272274
```
273275

274276
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.
277+
278+
## Accessing the current locale
279+
280+
You can use [`Astro.currentLocale`](https://docs.astro.build/en/reference/api-reference/#astrocurrentlocale) to read the current locale in `.astro` components.
281+
282+
The following example reads the current locale and uses it to generate a link to an about page in the current language:
283+
284+
```astro
285+
---
286+
// src/components/AboutLink.astro
287+
---
288+
289+
<a href={`/${Astro.currentLocale}/about`}>About</a>
290+
```

packages/starlight/404.astro

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import Page from './components/Page.astro';
66
import { generateRouteData } from './utils/route-data';
77
import type { StarlightDocsEntry } from './utils/routing';
88
import { useTranslations } from './utils/translations';
9+
import { BuiltInDefaultLocale } from './utils/i18n';
910
1011
export const prerender = true;
1112
12-
const { lang = 'en', dir = 'ltr' } = config.defaultLocale || {};
13+
const { lang = BuiltInDefaultLocale.lang, dir = BuiltInDefaultLocale.dir } =
14+
config.defaultLocale || {};
1315
let locale = config.defaultLocale?.locale;
1416
if (locale === 'root') locale = undefined;
1517

packages/starlight/__tests__/basics/config-errors.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ test('parses valid config successfully', () => {
5959
},
6060
"head": [],
6161
"isMultilingual": false,
62+
"isUsingBuiltInDefaultLocale": true,
6263
"lastUpdated": false,
6364
"locales": undefined,
6465
"pagefind": true,

packages/starlight/__tests__/basics/i18n.test.ts

Lines changed: 248 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { describe, expect, test } from 'vitest';
2-
import { pickLang } from '../../utils/i18n';
1+
import { assert, describe, expect, test } from 'vitest';
2+
import config from 'virtual:starlight/user-config';
3+
import { processI18nConfig, pickLang } from '../../utils/i18n';
4+
import type { AstroConfig } from 'astro';
5+
import type { AstroUserConfig } from 'astro/config';
36

47
describe('pickLang', () => {
58
const dictionary = { en: 'Hello', fr: 'Bonjour' };
@@ -13,3 +16,246 @@ describe('pickLang', () => {
1316
expect(pickLang(dictionary, 'ar' as any)).toBeUndefined();
1417
});
1518
});
19+
20+
describe('processI18nConfig', () => {
21+
test('returns the Astro i18n config for an unconfigured monolingual site using the built-in default locale', () => {
22+
const { astroI18nConfig, starlightConfig } = processI18nConfig(config, undefined);
23+
24+
expect(astroI18nConfig.defaultLocale).toBe('en');
25+
expect(astroI18nConfig.locales).toEqual(['en']);
26+
assert(typeof astroI18nConfig.routing !== 'string');
27+
expect(astroI18nConfig.routing?.prefixDefaultLocale).toBe(false);
28+
29+
// The Starlight configuration should not be modified.
30+
expect(config).toStrictEqual(starlightConfig);
31+
});
32+
33+
describe('with a provided Astro i18n config', () => {
34+
test('throws an error when an Astro i18n `manual` routing option is used', () => {
35+
expect(() =>
36+
processI18nConfig(
37+
config,
38+
getAstroI18nTestConfig({
39+
defaultLocale: 'en',
40+
locales: ['en', 'fr'],
41+
routing: 'manual',
42+
})
43+
)
44+
).toThrowErrorMatchingInlineSnapshot(`
45+
"[AstroUserError]:
46+
Starlight is not compatible with the \`manual\` routing option in the Astro i18n configuration.
47+
Hint:
48+
"
49+
`);
50+
});
51+
52+
test('throws an error when an Astro i18n config contains an invalid locale', () => {
53+
expect(() =>
54+
processI18nConfig(
55+
config,
56+
getAstroI18nTestConfig({
57+
defaultLocale: 'en',
58+
locales: ['en', 'foo'],
59+
})
60+
)
61+
).toThrowErrorMatchingInlineSnapshot(`
62+
"[AstroUserError]:
63+
Failed to get locale informations for the 'foo' locale.
64+
Hint:
65+
Make sure to provide a valid BCP-47 tags (e.g. en, ar, or zh-CN)."
66+
`);
67+
});
68+
69+
test.each([
70+
{
71+
i18nConfig: { defaultLocale: 'en', locales: ['en'] },
72+
expected: {
73+
defaultLocale: { label: 'English', lang: 'en', dir: 'ltr', locale: undefined },
74+
},
75+
},
76+
{
77+
i18nConfig: { defaultLocale: 'fr', locales: [{ codes: ['fr'], path: 'fr' }] },
78+
expected: {
79+
defaultLocale: { label: 'Français', lang: 'fr', dir: 'ltr', locale: undefined },
80+
},
81+
},
82+
{
83+
i18nConfig: {
84+
defaultLocale: 'fa',
85+
locales: ['fa'],
86+
routing: { prefixDefaultLocale: false },
87+
},
88+
expected: {
89+
defaultLocale: { label: 'فارسی', lang: 'fa', dir: 'rtl', locale: undefined },
90+
},
91+
},
92+
])(
93+
'updates the Starlight i18n config for a monolingual site with a single root locale',
94+
({ i18nConfig, expected }) => {
95+
const astroI18nTestConfig = getAstroI18nTestConfig(i18nConfig);
96+
97+
const { astroI18nConfig, starlightConfig } = processI18nConfig(config, astroI18nTestConfig);
98+
99+
expect(starlightConfig.isMultilingual).toBe(false);
100+
expect(starlightConfig.locales).not.toBeDefined();
101+
expect(starlightConfig.defaultLocale).toStrictEqual(expected.defaultLocale);
102+
103+
// The Astro i18n configuration should not be modified.
104+
expect(astroI18nConfig).toStrictEqual(astroI18nConfig);
105+
}
106+
);
107+
108+
test.each([
109+
{
110+
i18nConfig: {
111+
defaultLocale: 'en',
112+
locales: ['en'],
113+
routing: { prefixDefaultLocale: true },
114+
},
115+
expected: {
116+
defaultLocale: { label: 'English', lang: 'en', dir: 'ltr', locale: 'en' },
117+
locales: { en: { label: 'English', lang: 'en', dir: 'ltr' } },
118+
},
119+
},
120+
{
121+
i18nConfig: {
122+
defaultLocale: 'french',
123+
locales: [{ codes: ['fr'], path: 'french' }],
124+
routing: { prefixDefaultLocale: true },
125+
},
126+
expected: {
127+
defaultLocale: { label: 'Français', lang: 'fr', dir: 'ltr', locale: 'fr' },
128+
locales: { french: { label: 'Français', lang: 'fr', dir: 'ltr' } },
129+
},
130+
},
131+
{
132+
i18nConfig: {
133+
defaultLocale: 'farsi',
134+
locales: [{ codes: ['fa'], path: 'farsi' }],
135+
routing: { prefixDefaultLocale: true },
136+
},
137+
expected: {
138+
defaultLocale: { label: 'فارسی', lang: 'fa', dir: 'rtl', locale: 'fa' },
139+
locales: { farsi: { label: 'فارسی', lang: 'fa', dir: 'rtl' } },
140+
},
141+
},
142+
])(
143+
'updates the Starlight i18n config for a monolingual site with a single non-root locale',
144+
({ i18nConfig, expected }) => {
145+
const astroI18nTestConfig = getAstroI18nTestConfig(i18nConfig);
146+
147+
const { astroI18nConfig, starlightConfig } = processI18nConfig(config, astroI18nTestConfig);
148+
149+
expect(starlightConfig.isMultilingual).toBe(false);
150+
expect(starlightConfig.locales).toStrictEqual(expected.locales);
151+
expect(starlightConfig.defaultLocale).toStrictEqual(expected.defaultLocale);
152+
153+
// The Astro i18n configuration should not be modified.
154+
expect(astroI18nConfig).toStrictEqual(astroI18nConfig);
155+
}
156+
);
157+
158+
test.each([
159+
{
160+
i18nConfig: {
161+
defaultLocale: 'en',
162+
locales: ['en', { codes: ['fr'], path: 'french' }],
163+
},
164+
expected: {
165+
defaultLocale: { label: 'English', lang: 'en', dir: 'ltr', locale: 'en' },
166+
locales: {
167+
root: { label: 'English', lang: 'en', dir: 'ltr' },
168+
french: { label: 'Français', lang: 'fr', dir: 'ltr' },
169+
},
170+
},
171+
},
172+
{
173+
i18nConfig: {
174+
defaultLocale: 'farsi',
175+
// This configuration is a bit confusing as `prefixDefaultLocale` is `false` but the
176+
// default locale is defined with a custom path.
177+
// In this case, the default locale is considered to be a root locale and the custom path
178+
// is ignored.
179+
locales: [{ codes: ['fa'], path: 'farsi' }, 'de'],
180+
routing: { prefixDefaultLocale: false },
181+
},
182+
expected: {
183+
defaultLocale: { label: 'فارسی', lang: 'fa', dir: 'rtl', locale: 'fa' },
184+
locales: {
185+
root: { label: 'فارسی', lang: 'fa', dir: 'rtl' },
186+
de: { label: 'Deutsch', lang: 'de', dir: 'ltr' },
187+
},
188+
},
189+
},
190+
])(
191+
'updates the Starlight i18n config for a multilingual site with a root locale',
192+
({ i18nConfig, expected }) => {
193+
const astroI18nTestConfig = getAstroI18nTestConfig(i18nConfig);
194+
195+
const { astroI18nConfig, starlightConfig } = processI18nConfig(config, astroI18nTestConfig);
196+
197+
expect(starlightConfig.isMultilingual).toBe(true);
198+
expect(starlightConfig.locales).toEqual(expected.locales);
199+
expect(starlightConfig.defaultLocale).toEqual(expected.defaultLocale);
200+
201+
// The Astro i18n configuration should not be modified.
202+
expect(astroI18nConfig).toEqual(astroI18nConfig);
203+
}
204+
);
205+
206+
test.each([
207+
{
208+
i18nConfig: {
209+
defaultLocale: 'en',
210+
locales: ['en', { codes: ['fr'], path: 'french' }],
211+
routing: { prefixDefaultLocale: true },
212+
},
213+
expected: {
214+
defaultLocale: { label: 'English', lang: 'en', dir: 'ltr', locale: 'en' },
215+
locales: {
216+
en: { label: 'English', lang: 'en', dir: 'ltr' },
217+
french: { label: 'Français', lang: 'fr', dir: 'ltr' },
218+
},
219+
},
220+
},
221+
{
222+
i18nConfig: {
223+
defaultLocale: 'farsi',
224+
locales: [{ codes: ['fa'], path: 'farsi' }, 'de'],
225+
routing: { prefixDefaultLocale: true },
226+
},
227+
expected: {
228+
defaultLocale: { label: 'فارسی', lang: 'fa', dir: 'rtl', locale: 'fa' },
229+
locales: {
230+
farsi: { label: 'فارسی', lang: 'fa', dir: 'rtl' },
231+
de: { label: 'Deutsch', lang: 'de', dir: 'ltr' },
232+
},
233+
},
234+
},
235+
])(
236+
'updates the Starlight i18n config for a multilingual site with no root locale',
237+
({ i18nConfig, expected }) => {
238+
const astroI18nTestConfig = getAstroI18nTestConfig(i18nConfig);
239+
240+
const { astroI18nConfig, starlightConfig } = processI18nConfig(config, astroI18nTestConfig);
241+
242+
expect(starlightConfig.isMultilingual).toBe(true);
243+
expect(starlightConfig.locales).toEqual(expected.locales);
244+
expect(starlightConfig.defaultLocale).toEqual(expected.defaultLocale);
245+
246+
// The Astro i18n configuration should not be modified.
247+
expect(astroI18nConfig).toEqual(astroI18nConfig);
248+
}
249+
);
250+
});
251+
});
252+
253+
function getAstroI18nTestConfig(i18nConfig: AstroUserConfig['i18n']): AstroConfig['i18n'] {
254+
return {
255+
...i18nConfig,
256+
routing:
257+
typeof i18nConfig?.routing !== 'string'
258+
? { prefixDefaultLocale: false, ...i18nConfig?.routing }
259+
: i18nConfig.routing,
260+
} as AstroConfig['i18n'];
261+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { assert, describe, expect, test } from 'vitest';
2+
import type { AstroConfig } from 'astro';
3+
import config from 'virtual:starlight/user-config';
4+
import { processI18nConfig } from '../../utils/i18n';
5+
6+
describe('processI18nConfig', () => {
7+
test('returns the Astro i18n config for a monolingual site with a non-root single locale', () => {
8+
const { astroI18nConfig, starlightConfig } = processI18nConfig(config, undefined);
9+
10+
expect(astroI18nConfig.defaultLocale).toBe('fr-CA');
11+
expect(astroI18nConfig.locales).toMatchInlineSnapshot(`
12+
[
13+
{
14+
"codes": [
15+
"fr-CA",
16+
],
17+
"path": "fr",
18+
},
19+
]
20+
`);
21+
assert(typeof astroI18nConfig.routing !== 'string');
22+
expect(astroI18nConfig.routing?.prefixDefaultLocale).toBe(true);
23+
24+
// The Starlight configuration should not be modified.
25+
expect(config).toStrictEqual(starlightConfig);
26+
});
27+
28+
test('throws an error when an Astro i18n config is also provided', () => {
29+
expect(() =>
30+
processI18nConfig(config, { defaultLocale: 'en', locales: ['en'] } as AstroConfig['i18n'])
31+
).toThrowErrorMatchingInlineSnapshot(`
32+
"[AstroUserError]:
33+
Cannot provide both an Astro \`i18n\` configuration and a Starlight \`locales\` configuration.
34+
Hint:
35+
Remove one of the two configurations.
36+
See more at https://starlight.astro.build/guides/i18n/"
37+
`);
38+
});
39+
});

0 commit comments

Comments
 (0)