summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiDeoo2024-04-09 12:44:27 +0200
committerGitHub2024-04-09 12:44:27 +0200
commitc5cd181186b42422f3e47052bf8182cb490bda6b (patch)
tree3a5defc6740498ab3a58080cf5cb92abe558577e
parentca031c0b73a73a5351f93e2c1e71903db18b2f60 (diff)
downloadIT.starlight-c5cd181186b42422f3e47052bf8182cb490bda6b.tar.gz
IT.starlight-c5cd181186b42422f3e47052bf8182cb490bda6b.tar.bz2
IT.starlight-c5cd181186b42422f3e47052bf8182cb490bda6b.zip
Fix translation issue for sites with a single non-root language different from English (#1709)
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r--.changeset/khaki-hairs-end.md5
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts17
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/localizedUrl.test.ts26
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts19
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/routing.test.ts35
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/slugs.test.ts37
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/src/content/i18n/fr.json3
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/translations-fs.test.ts43
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/translations-with-user-config.test.ts17
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/translations.test.ts24
-rw-r--r--packages/starlight/__tests__/i18n-non-root-single-locale/vitest.config.ts9
-rw-r--r--packages/starlight/utils/user-config.ts19
12 files changed, 247 insertions, 7 deletions
diff --git a/.changeset/khaki-hairs-end.md b/.changeset/khaki-hairs-end.md
new file mode 100644
index 00000000..377b40ec
--- /dev/null
+++ b/.changeset/khaki-hairs-end.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/starlight': patch
+---
+
+Fixes a UI strings translation issue for sites configured with a single non-root language different from English.
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts
new file mode 100644
index 00000000..a7b7262e
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts
@@ -0,0 +1,17 @@
+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');
+});
+
+test('config.isMultilingual is false with a single locale', () => {
+ expect(config.isMultilingual).toBe(false);
+ expect(config.locales).keys('fr');
+});
+
+test('config.defaultLocale is populated from default locale', () => {
+ expect(config.defaultLocale.lang).toBe('fr');
+ expect(config.defaultLocale.dir).toBe('ltr');
+ expect(config.defaultLocale.locale).toBe('fr');
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/localizedUrl.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/localizedUrl.test.ts
new file mode 100644
index 00000000..49ef69aa
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/localizedUrl.test.ts
@@ -0,0 +1,26 @@
+import { describe, expect, test } from 'vitest';
+import { localizedUrl } from '../../utils/localizedUrl';
+
+describe('with `build.output: "directory"`', () => {
+ test('it has no effect in a monolingual project with a non-root single locale', () => {
+ const url = new URL('https://example.com/fr/guide/');
+ expect(localizedUrl(url, 'fr').href).toBe(url.href);
+ });
+
+ test('has no effect on index route in a monolingual project with a non-root single locale', () => {
+ const url = new URL('https://example.com/fr/');
+ expect(localizedUrl(url, 'fr').href).toBe(url.href);
+ });
+});
+
+describe('with `build.output: "file"`', () => {
+ test('it has no effect in a monolingual project with a non-root single locale', () => {
+ const url = new URL('https://example.com/fr/guide.html');
+ expect(localizedUrl(url, 'fr').href).toBe(url.href);
+ });
+
+ test('has no effect on index route in a monolingual project with a non-root single locale', () => {
+ const url = new URL('https://example.com/fr.html');
+ expect(localizedUrl(url, 'fr').href).toBe(url.href);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts
new file mode 100644
index 00000000..dbf5d354
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts
@@ -0,0 +1,19 @@
+import { expect, test, vi } from 'vitest';
+import { generateRouteData } from '../../utils/route-data';
+import { routes } from '../../utils/routing';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [['fr/index.mdx', { title: 'Accueil' }]],
+ })
+);
+
+test('includes localized labels (fr)', () => {
+ const route = routes[0]!;
+ const data = generateRouteData({
+ props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] },
+ url: new URL('https://example.com'),
+ });
+ expect(data.labels).toBeDefined();
+ expect(data.labels['skipLink.label']).toBe('Aller au contenu');
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/routing.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/routing.test.ts
new file mode 100644
index 00000000..2caef2f5
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/routing.test.ts
@@ -0,0 +1,35 @@
+import { expect, test, vi } from 'vitest';
+import { routes } from '../../utils/routing';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ docs: [
+ ['fr/index.mdx', { title: 'Accueil' }],
+ ['en/index.mdx', { title: 'Home page' }],
+ ],
+ })
+);
+
+test('route slugs are normalized', () => {
+ const indexRoute = routes.find((route) => route.id.startsWith('fr/index.md'));
+ expect(indexRoute?.slug).toBe('fr');
+});
+
+test('routes for the configured locale have locale data added', () => {
+ for (const route of routes) {
+ if (route.id.startsWith('fr')) {
+ expect(route.lang).toBe('fr');
+ expect(route.dir).toBe('ltr');
+ expect(route.locale).toBe('fr');
+ } else {
+ expect(route.lang).toBe('fr');
+ expect(route.dir).toBe('ltr');
+ expect(route.locale).toBeUndefined();
+ }
+ }
+});
+
+test('does not mark any route as fallback routes', () => {
+ const fallbacks = routes.filter((route) => route.isFallback);
+ expect(fallbacks.length).toBe(0);
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/slugs.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/slugs.test.ts
new file mode 100644
index 00000000..1904ee11
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/slugs.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, test } from 'vitest';
+import { localeToLang, localizedId, localizedSlug, slugToLocaleData } from '../../utils/slugs';
+
+describe('slugToLocaleData', () => {
+ test('returns default "fr" locale', () => {
+ expect(slugToLocaleData('fr/test').locale).toBe('fr');
+ expect(slugToLocaleData('fr/dir/test').locale).toBe('fr');
+ });
+ test('returns default locale "fr" lang', () => {
+ expect(slugToLocaleData('fr/test').lang).toBe('fr');
+ expect(slugToLocaleData('fr/dir/test').lang).toBe('fr');
+ });
+ test('returns default locale "ltr" dir', () => {
+ expect(slugToLocaleData('fr/test').dir).toBe('ltr');
+ expect(slugToLocaleData('fr/dir/test').dir).toBe('ltr');
+ });
+});
+
+describe('localeToLang', () => {
+ test('returns lang for default locale', () => {
+ expect(localeToLang('fr')).toBe('fr');
+ });
+});
+
+describe('localizedId', () => {
+ test('returns unchanged for default locale', () => {
+ expect(localizedId('fr/test.md', 'fr')).toBe('fr/test.md');
+ });
+});
+
+describe('localizedSlug', () => {
+ test('returns unchanged for default locale', () => {
+ expect(localizedSlug('fr', 'fr')).toBe('fr');
+ expect(localizedSlug('fr/test', 'fr')).toBe('fr/test');
+ expect(localizedSlug('fr/dir/test', 'fr')).toBe('fr/dir/test');
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/src/content/i18n/fr.json b/packages/starlight/__tests__/i18n-non-root-single-locale/src/content/i18n/fr.json
new file mode 100644
index 00000000..2b27eb1e
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/src/content/i18n/fr.json
@@ -0,0 +1,3 @@
+{
+ "page.editLink": "Changer cette page"
+}
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/translations-fs.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/translations-fs.test.ts
new file mode 100644
index 00000000..1248df1e
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/translations-fs.test.ts
@@ -0,0 +1,43 @@
+import { describe, expect, test } from 'vitest';
+import { createTranslationSystemFromFs } from '../../utils/translations-fs';
+
+describe('createTranslationSystemFromFs', () => {
+ test('creates a translation system that returns default strings', () => {
+ const useTranslations = createTranslationSystemFromFs(
+ {
+ locales: { fr: { label: 'Français', dir: 'ltr' } },
+ defaultLocale: { label: 'Français', locale: 'fr', dir: 'ltr' },
+ },
+ // Using non-existent `_src/` to ignore custom files in this test fixture.
+ { srcDir: new URL('./_src/', import.meta.url) }
+ );
+ const t = useTranslations('fr');
+ expect(t('page.editLink')).toMatchInlineSnapshot('"Modifier cette page"');
+ });
+
+ test('creates a translation system that uses custom strings', () => {
+ const useTranslations = createTranslationSystemFromFs(
+ {
+ locales: { fr: { label: 'Français', dir: 'ltr' } },
+ defaultLocale: { label: 'Français', locale: 'fr', dir: 'ltr' },
+ },
+ // Using `src/` to load custom files in this test fixture.
+ { srcDir: new URL('./src/', import.meta.url) }
+ );
+ const t = useTranslations('fr');
+ expect(t('page.editLink')).toMatchInlineSnapshot('"Changer cette page"');
+ });
+
+ test('returns translation for unknown language', () => {
+ const useTranslations = createTranslationSystemFromFs(
+ {
+ locales: { fr: { label: 'Français', dir: 'ltr', lang: 'fr' } },
+ defaultLocale: { label: 'Français', locale: 'fr', dir: 'ltr' },
+ },
+ // Using `src/` to load custom files in this test fixture.
+ { srcDir: new URL('./src/', import.meta.url) }
+ );
+ const t = useTranslations('ar');
+ expect(t('page.editLink')).toMatchInlineSnapshot('"Changer cette page"');
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/translations-with-user-config.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/translations-with-user-config.test.ts
new file mode 100644
index 00000000..bc76cc28
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/translations-with-user-config.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, test, vi } from 'vitest';
+import translations from '../../translations';
+import { useTranslations } from '../../utils/translations';
+
+vi.mock('astro:content', async () =>
+ (await import('../test-utils')).mockedAstroContent({
+ i18n: [['fr', { 'page.editLink': 'Modifier cette doc!' }]],
+ })
+);
+
+describe('useTranslations()', () => {
+ test('uses user-defined translations', () => {
+ const t = useTranslations('fr');
+ expect(t('page.editLink')).toBe('Modifier cette doc!');
+ expect(t('page.editLink')).not.toBe(translations.fr?.['page.editLink']);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/translations.test.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/translations.test.ts
new file mode 100644
index 00000000..3287cdf2
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/translations.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, test } from 'vitest';
+import translations from '../../translations';
+import { useTranslations } from '../../utils/translations';
+
+describe('built-in translations', () => {
+ test('includes French', () => {
+ expect(translations).toHaveProperty('fr');
+ });
+});
+
+describe('useTranslations()', () => {
+ test('works when no i18n collection is available', () => {
+ const t = useTranslations('fr');
+ expect(t).toBeTypeOf('function');
+ expect(t('page.editLink')).toBe(translations.fr?.['page.editLink']);
+ });
+
+ test('returns default locale for unknown language', () => {
+ const locale = 'xx';
+ expect(translations).not.toHaveProperty(locale);
+ const t = useTranslations(locale);
+ expect(t('page.editLink')).toBe(translations.fr?.['page.editLink']);
+ });
+});
diff --git a/packages/starlight/__tests__/i18n-non-root-single-locale/vitest.config.ts b/packages/starlight/__tests__/i18n-non-root-single-locale/vitest.config.ts
new file mode 100644
index 00000000..be081a7c
--- /dev/null
+++ b/packages/starlight/__tests__/i18n-non-root-single-locale/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineVitestConfig } from '../test-config';
+
+export default defineVitestConfig({
+ title: 'i18n with a non-root single locale',
+ defaultLocale: 'fr',
+ locales: {
+ fr: { label: 'Français', lang: 'fr' },
+ },
+});
diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts
index f77f4e90..8e566346 100644
--- a/packages/starlight/utils/user-config.ts
+++ b/packages/starlight/utils/user-config.ts
@@ -212,16 +212,20 @@ const UserConfigSchema = z.object({
export const StarlightConfigSchema = UserConfigSchema.strict().transform(
({ locales, defaultLocale, ...config }, ctx) => {
- if (locales !== undefined && Object.keys(locales).length > 1) {
- // This is a multilingual site (more than one locale configured).
+ const configuredLocales = Object.keys(locales ?? {});
+
+ // This is a multilingual site (more than one locale configured) or a monolingual site with
+ // only one locale configured (not a root locale).
+ // Monolingual sites with only one non-root locale needs their configuration to be defined in
+ // `config.locales` so that slugs can be correctly generated by taking into consideration the
+ // base path at which a language is served which is the key of the `config.locales` object.
+ if (locales !== undefined && configuredLocales.length >= 1) {
// Make sure we can find the default locale and if not, help the user set it.
// We treat the root locale as the default if present and no explicit default is set.
const defaultLocaleConfig = locales[defaultLocale || 'root'];
if (!defaultLocaleConfig) {
- const availableLocales = Object.keys(locales)
- .map((l) => `"${l}"`)
- .join(', ');
+ const availableLocales = configuredLocales.map((l) => `"${l}"`).join(', ');
ctx.addIssue({
code: 'custom',
message:
@@ -235,14 +239,15 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
return {
...config,
/** Flag indicating if this site has multiple locales set up. */
- isMultilingual: true,
+ isMultilingual: configuredLocales.length > 1,
/** Full locale object for this site’s default language. */
defaultLocale: { ...defaultLocaleConfig, locale: defaultLocale },
locales,
} as const;
}
- // This is a monolingual site, so things are pretty simple.
+ // This is a monolingual site with no locales configured or only a root locale, so things are
+ // pretty simple.
return {
...config,
/** Flag indicating if this site has multiple locales set up. */