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

Add support for Astro.currentLocale #1841

Merged
merged 20 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ const site = VERCEL_PREVIEW_SITE || 'https://starlight.astro.build/';
export default defineConfig({
site,
trailingSlash: 'always',
// TODO(HiDeoo) Remove
i18n: {
defaultLocale: 'en',
locales: [
'en',
'de',
'es',
'ja',
'fr',
'it',
'id',
{ codes: ['zh-CN'], path: 'zh-cn' },
{ codes: ['pt-BR'], path: 'pt-br' },
{ codes: ['pt-PT'], path: 'pt-pt' },
'ko',
'tr',
'ru',
'hi',
'da',
'uk',
],
routing: { prefixDefaultLocale: false },
},
integrations: [
starlight({
title: 'Starlight',
Expand Down Expand Up @@ -66,7 +89,8 @@ export default defineConfig({
},
],
customCss: process.env.NO_GRADIENTS ? [] : ['./src/assets/landing.css'],
locales,
// TODO(HiDeoo) Uncomment
// locales,
sidebar: [
{
label: 'Start Here',
Expand Down
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
269 changes: 267 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,265 @@ 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 `fallback` option is used', () => {
expect(() =>
processI18nConfig(
config,
getAstroI18nTestConfig({
defaultLocale: 'en',
locales: ['en', 'fr'],
fallback: { fr: 'en' },
})
)
).toThrowErrorMatchingInlineSnapshot(`
"[AstroUserError]:
Starlight is not compatible with the \`fallback\` option in the Astro i18n configuration.
Hint:
Starlight uses its own fallback strategy showing readers content for a missing page in the default language.
See more at https://starlight.astro.build/guides/i18n/#fallback-content"
`);
});

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 i18n configuration.
Hint:
Remove one of the i18n configurations.
See more at https://starlight.astro.build/guides/i18n/"
`);
});
});
Loading
Loading