From 152490cd670b57b7bab26ac89b7dfd57a0e48c5d Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Fri, 13 Feb 2026 09:43:45 +0100 Subject: [PATCH] Normalize undefined site-space language to English in variant categorization (fix language selector) (#4002) --- .../SpaceLayout/SpaceLayout.test.ts | 8 ++---- .../SpaceLayout/categorizeVariants.ts | 28 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts index a43c06d6a..a3077c2f1 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts @@ -98,16 +98,15 @@ describe('categorizeVariants', () => { ]); }); - it('keeps one-per-language translations when there are more than 1 language and an undefined language', () => { + it('treats undefined language as English when mixed with translated spaces', () => { const ctx = makeContext(englishA, [englishA, frenchA, undefinedLanguage]); const result = categorizeVariants(ctx); - expect(result.generic.map((s) => s.id)).toEqual(['en-a']); + expect(result.generic.map((s) => s.id)).toEqual(['en-a', 'undefined']); expect(result.translations.map((s) => ({ id: s.id, title: s.title }))).toEqual([ { id: 'en-a', title: 'Docs EN A' }, { id: 'fr-a', title: 'Docs FR A' }, - { id: 'undefined', title: 'Docs in Undefined Language' }, ]); }); @@ -139,11 +138,10 @@ describe('categorizeVariants', () => { const result = categorizeVariants(ctx); - expect(result.generic.map((s) => s.id)).toEqual(['en-a', 'en-b']); + expect(result.generic.map((s) => s.id)).toEqual(['en-a', 'en-b', 'undefined']); expect(result.translations.map((s) => ({ id: s.id, title: s.title }))).toEqual([ { id: 'en-a', title: languages.en.language }, { id: 'fr-a', title: languages.fr.language }, - { id: 'undefined', title: 'Docs in Undefined Language' }, { id: 'unsupported', title: 'Docs in Unsupported Language' }, ]); }); diff --git a/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts b/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts index 81064f59f..419ecbabf 100644 --- a/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts +++ b/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts @@ -6,26 +6,34 @@ import type { GitBookSiteContext } from '@/lib/context'; */ export function categorizeVariants(context: GitBookSiteContext) { const { siteSpace, visibleSiteSpaces: siteSpaces } = context; - const currentLanguage = siteSpace.space.language; + const normalizeLanguage = (language: string | undefined) => + language === undefined ? languages.en.locale : language; + + const currentLanguage = normalizeLanguage(siteSpace.space.language); // Get all languages of the variants. - const variantLanguages = [...new Set(siteSpaces.map((space) => space.space.language))]; + const variantLanguages = [ + ...new Set(siteSpaces.map((space) => normalizeLanguage(space.space.language))), + ]; - // We only show the language picker if there are at least 2 distinct languages, excluding undefined. - const isMultiLanguage = - variantLanguages.filter((language) => language !== undefined).length > 1; + // We show the language picker when there are at least 2 distinct languages. + // Spaces without an explicit language are treated as English, matching runtime defaults. + const isMultiLanguage = variantLanguages.length > 1; - // Generic variants are all spaces that have the same language as the current (can also be undefined). + const toNormalizedLanguage = (space: (typeof siteSpaces)[number]) => + normalizeLanguage(space.space.language); + + // Generic variants are all spaces that have the same language as the current (undefined is normalized to English). const genericVariants = isMultiLanguage ? siteSpaces.filter( - (space) => space === siteSpace || space.space.language === currentLanguage + (space) => space === siteSpace || toNormalizedLanguage(space) === currentLanguage ) : siteSpaces; // Translation variants are all spaces that have a different language than the current. let translationVariants = isMultiLanguage ? siteSpaces.filter( - (space) => space === siteSpace || space.space.language !== currentLanguage + (space) => space === siteSpace || toNormalizedLanguage(space) !== currentLanguage ) : []; @@ -35,13 +43,13 @@ export function categorizeVariants(context: GitBookSiteContext) { translationVariants = variantLanguages // Get the first space of each language. .map((variantLanguage) => - translationVariants.find((space) => space.space.language === variantLanguage) + translationVariants.find((space) => toNormalizedLanguage(space) === variantLanguage) ) // Filter out unmatched languages. .filter((space) => space !== undefined) // Transform the title to include the language name if we have a translation. Otherwise, use the original title. .map((space) => { - const language = languages[space.space.language as keyof typeof languages]; + const language = languages[toNormalizedLanguage(space) as keyof typeof languages]; return { ...space, title: language ? language.language : space.title,