From fa77e10b78f8bd9ca01d5bad0ff4f5f7e2760541 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:50:43 +0200 Subject: [PATCH 1/3] fix: translations for single non-root language sites different from english --- .changeset/khaki-hairs-end.md | 5 +++ .../config.test.ts | 17 ++++++++ .../localizedUrl.test.ts | 26 +++++++++++ .../route-data.test.ts | 19 ++++++++ .../routing.test.ts | 35 +++++++++++++++ .../i18n-non-root-single-locale/slugs.test.ts | 37 ++++++++++++++++ .../src/content/i18n/fr.json | 3 ++ .../translations-fs.test.ts | 43 +++++++++++++++++++ .../translations-with-user-config.test.ts | 17 ++++++++ .../translations.test.ts | 24 +++++++++++ .../vitest.config.ts | 9 ++++ packages/starlight/utils/user-config.ts | 6 +-- 12 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 .changeset/khaki-hairs-end.md create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/localizedUrl.test.ts create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/route-data.test.ts create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/routing.test.ts create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/slugs.test.ts create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/src/content/i18n/fr.json create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/translations-fs.test.ts create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/translations-with-user-config.test.ts create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/translations.test.ts create mode 100644 packages/starlight/__tests__/i18n-non-root-single-locale/vitest.config.ts diff --git a/.changeset/khaki-hairs-end.md b/.changeset/khaki-hairs-end.md new file mode 100644 index 0000000000..377b40ec38 --- /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 0000000000..a7b7262eb3 --- /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 0000000000..49ef69aa7f --- /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 0000000000..dbf5d35481 --- /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 0000000000..2caef2f599 --- /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 0000000000..1904ee113b --- /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 0000000000..2b27eb1edf --- /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 0000000000..1248df1ebe --- /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 0000000000..bc76cc2860 --- /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 0000000000..3287cdf27a --- /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 0000000000..be081a7c57 --- /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 f77f4e9058..40b504f68b 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -212,8 +212,8 @@ 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 configuredLocalesCount = locales !== undefined ? Object.keys(locales).length : 0; + if (locales !== undefined && configuredLocalesCount >= 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']; @@ -235,7 +235,7 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform( return { ...config, /** Flag indicating if this site has multiple locales set up. */ - isMultilingual: true, + isMultilingual: configuredLocalesCount > 1, /** Full locale object for this site’s default language. */ defaultLocale: { ...defaultLocaleConfig, locale: defaultLocale }, locales, From b53e843cbb1317b469b75e8d69b4ebdab5ac961b Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Mon, 8 Apr 2024 16:16:32 +0200 Subject: [PATCH 2/3] refactor: move fix to monolingual config section --- .../config.test.ts | 2 +- packages/starlight/utils/slugs.ts | 6 ++- packages/starlight/utils/user-config.ts | 41 +++++++++---------- 3 files changed, 26 insertions(+), 23 deletions(-) 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 index a7b7262eb3..1829789f3b 100644 --- a/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts +++ b/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts @@ -7,7 +7,7 @@ test('test suite is using correct env', () => { test('config.isMultilingual is false with a single locale', () => { expect(config.isMultilingual).toBe(false); - expect(config.locales).keys('fr'); + expect(config.locales).toBeUndefined(); }); test('config.defaultLocale is populated from default locale', () => { diff --git a/packages/starlight/utils/slugs.ts b/packages/starlight/utils/slugs.ts index b7e076c907..60aa927a8a 100644 --- a/packages/starlight/utils/slugs.ts +++ b/packages/starlight/utils/slugs.ts @@ -16,7 +16,11 @@ export interface LocaleData { * @param slug A collection entry slug */ function slugToLocale(slug: string): string | undefined { - const locales = Object.keys(config.locales || {}); + const locales = config.locales + ? Object.keys(config.locales) + : config.defaultLocale.locale + ? [config.defaultLocale.locale] + : []; const baseSegment = slug.split('/')[0]; if (baseSegment && locales.includes(baseSegment)) return baseSegment; return undefined; diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts index 40b504f68b..6057dae4f3 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -212,30 +212,29 @@ const UserConfigSchema = z.object({ export const StarlightConfigSchema = UserConfigSchema.strict().transform( ({ locales, defaultLocale, ...config }, ctx) => { - const configuredLocalesCount = locales !== undefined ? Object.keys(locales).length : 0; - if (locales !== undefined && configuredLocalesCount >= 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']; + const localesKeys = Object.keys(locales ?? {}); + const defaultLocaleConfig = locales?.[defaultLocale || 'root']; - if (!defaultLocaleConfig) { - const availableLocales = Object.keys(locales) - .map((l) => `"${l}"`) - .join(', '); - ctx.addIssue({ - code: 'custom', - message: - 'Could not determine the default locale. ' + - 'Please make sure `defaultLocale` in your Starlight config is one of ' + - availableLocales, - }); - return z.NEVER; - } + // Make sure we can find the default locale if needed and if not, help the user set it. + // We treat the root locale as the default if present and no explicit default is set. + if (locales !== undefined && localesKeys.length >= 1 && !defaultLocaleConfig) { + const availableLocales = localesKeys.map((l) => `"${l}"`).join(', '); + ctx.addIssue({ + code: 'custom', + message: + 'Could not determine the default locale. ' + + 'Please make sure `defaultLocale` in your Starlight config is one of ' + + availableLocales, + }); + return z.NEVER; + } + if (locales !== undefined && localesKeys.length > 1) { + // This is a multilingual site (more than one locale configured). return { ...config, /** Flag indicating if this site has multiple locales set up. */ - isMultilingual: configuredLocalesCount > 1, + isMultilingual: true, /** Full locale object for this site’s default language. */ defaultLocale: { ...defaultLocaleConfig, locale: defaultLocale }, locales, @@ -252,8 +251,8 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform( label: 'English', lang: 'en', dir: 'ltr', - locale: undefined, - ...locales?.root, + locale: defaultLocale || undefined, + ...locales?.[defaultLocale || 'root'], }, locales: undefined, } as const; From 79fef4994956fa6c9bf508c474b2162a14656f85 Mon Sep 17 00:00:00 2001 From: HiDeoo <494699+HiDeoo@users.noreply.github.com> Date: Tue, 9 Apr 2024 12:25:33 +0200 Subject: [PATCH 3/3] refactor: revert approach --- .../config.test.ts | 2 +- packages/starlight/utils/slugs.ts | 6 +-- packages/starlight/utils/user-config.ts | 48 +++++++++++-------- 3 files changed, 29 insertions(+), 27 deletions(-) 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 index 1829789f3b..a7b7262eb3 100644 --- a/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts +++ b/packages/starlight/__tests__/i18n-non-root-single-locale/config.test.ts @@ -7,7 +7,7 @@ test('test suite is using correct env', () => { test('config.isMultilingual is false with a single locale', () => { expect(config.isMultilingual).toBe(false); - expect(config.locales).toBeUndefined(); + expect(config.locales).keys('fr'); }); test('config.defaultLocale is populated from default locale', () => { diff --git a/packages/starlight/utils/slugs.ts b/packages/starlight/utils/slugs.ts index 60aa927a8a..b7e076c907 100644 --- a/packages/starlight/utils/slugs.ts +++ b/packages/starlight/utils/slugs.ts @@ -16,11 +16,7 @@ export interface LocaleData { * @param slug A collection entry slug */ function slugToLocale(slug: string): string | undefined { - const locales = config.locales - ? Object.keys(config.locales) - : config.defaultLocale.locale - ? [config.defaultLocale.locale] - : []; + const locales = Object.keys(config.locales || {}); const baseSegment = slug.split('/')[0]; if (baseSegment && locales.includes(baseSegment)) return baseSegment; return undefined; diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts index 6057dae4f3..8e566346b4 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -212,36 +212,42 @@ const UserConfigSchema = z.object({ export const StarlightConfigSchema = UserConfigSchema.strict().transform( ({ locales, defaultLocale, ...config }, ctx) => { - const localesKeys = Object.keys(locales ?? {}); - const defaultLocaleConfig = locales?.[defaultLocale || 'root']; + const configuredLocales = Object.keys(locales ?? {}); - // Make sure we can find the default locale if needed and if not, help the user set it. - // We treat the root locale as the default if present and no explicit default is set. - if (locales !== undefined && localesKeys.length >= 1 && !defaultLocaleConfig) { - const availableLocales = localesKeys.map((l) => `"${l}"`).join(', '); - ctx.addIssue({ - code: 'custom', - message: - 'Could not determine the default locale. ' + - 'Please make sure `defaultLocale` in your Starlight config is one of ' + - availableLocales, - }); - return z.NEVER; - } + // 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 = configuredLocales.map((l) => `"${l}"`).join(', '); + ctx.addIssue({ + code: 'custom', + message: + 'Could not determine the default locale. ' + + 'Please make sure `defaultLocale` in your Starlight config is one of ' + + availableLocales, + }); + return z.NEVER; + } - if (locales !== undefined && localesKeys.length > 1) { - // This is a multilingual site (more than one locale configured). 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. */ @@ -251,8 +257,8 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform( label: 'English', lang: 'en', dir: 'ltr', - locale: defaultLocale || undefined, - ...locales?.[defaultLocale || 'root'], + locale: undefined, + ...locales?.root, }, locales: undefined, } as const;