diff --git a/src/client/app/router.ts b/src/client/app/router.ts index de7fe261..eedcf3b9 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -154,6 +154,10 @@ export function createRouter( } catch (e) {} } + if (siteDataRef.value.localesFallback) { + await loadFallback() + } + if (latestPendingPath === pendingPath) { latestPendingPath = null route.path = inBrowser ? pendingPath : withBase(pendingPath) @@ -168,6 +172,77 @@ export function createRouter( syncRouteQueryAndHash(targetLoc) } } + + async function loadFallback() { + const locales = siteDataRef.value.locales + + for (const [key, value] of Object.entries(locales)) { + if (!value.fallback) continue + if (value.fallback === 'root') { + throw new Error( + `Invalid VitePress Config: A locale (${key}), cannot fall back to (root).` + ) + } + if (key === value.fallback) { + throw new Error( + `Invalid VitePress Config: A locale (${key}), cannot have a fallback to itself.` + ) + } + if (!Object.keys(locales).includes(value.fallback)) { + throw new Error( + `Invalid VitePress Config: A locale (${key}), cannot have a fallback to a non existing locale.` + ) + } + } + + // If the length is less than 2, it means there are no alternative locales to fallback to. + if (!locales || Object.keys(locales).length < 2) { + return + } + + const nonRootLocales = Object.fromEntries( + Object.entries(locales).filter(([name]) => name !== 'root') + ) + + const failedLocaleKey = + Object.keys(nonRootLocales).find( + (lang) => + pendingPath === `/${lang}` || pendingPath.startsWith(`/${lang}/`) + ) || 'root' + + if (failedLocaleKey !== 'root') { + const fallbackLang = + locales[failedLocaleKey].fallback ?? getCustomFallbackLang() + + await loadPage( + pendingPath.replace( + `/${failedLocaleKey}`, + fallbackLang ? `/${fallbackLang}` : '' + ), + scrollPosition, + true + ) + } else { + const fallbackPath = getRootLocaleFallbackPath() + if (!fallbackPath) return + await loadPage(fallbackPath, scrollPosition, true) + } + + function getCustomFallbackLang() { + const customFallbackLang = siteDataRef.value.localesDefaultFallback + if (customFallbackLang && customFallbackLang !== failedLocaleKey) { + return customFallbackLang + } + } + + function getRootLocaleFallbackPath() { + const fallbackLang = locales['root'].fallback + if (!fallbackLang) return + if (pendingPath === '/') return `/${fallbackLang}` + const pathDivider = pendingPath.startsWith('/') ? '' : '/' + return `/${fallbackLang}${pathDivider}${pendingPath}` + } + } } function syncRouteQueryAndHash( diff --git a/src/node/config.ts b/src/node/config.ts index 80342d9e..ec16ecb2 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -335,6 +335,8 @@ export async function resolveSiteData( appearance: userConfig.appearance ?? true, themeConfig: userConfig.themeConfig || {}, locales: userConfig.locales || {}, + localesFallback: userConfig.localesFallback ?? true, + localesDefaultFallback: userConfig.localesDefaultFallback, scrollOffset: userConfig.scrollOffset ?? 134, cleanUrls: !!userConfig.cleanUrls, contentProps: userConfig.contentProps, diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts index e27bbce1..c7fb6de8 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -54,6 +54,16 @@ export interface UserConfig locales?: LocaleConfig + /** + * If a page isn't found in the current language, allow switching to another language as a backup. + */ + localesFallback?: boolean + + /** + * Use a custom locale key to be used as a default fallback for all locales. Default is root. + */ + localesDefaultFallback?: string + router?: { prefetchLinks?: boolean } diff --git a/types/shared.d.ts b/types/shared.d.ts index bc8d28d4..ac5d663b 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -142,6 +142,14 @@ export interface SiteData { | string[] | { selector: string | string[]; padding: number } locales: LocaleConfig + /** + * If a page isn't found in the current language, allow switching to another language as a backup. + */ + localesFallback?: boolean + /** + * Use a custom locale key to be used as a default fallback for all locales. Default is root. + */ + localesDefaultFallback?: string localeIndex?: string contentProps?: Record router: { @@ -179,7 +187,14 @@ export interface LocaleSpecificConfig { export type LocaleConfig = Record< string, - LocaleSpecificConfig & { label: string; link?: string } + LocaleSpecificConfig & { + label: string + link?: string + /** + * If the requested page isn't found in this language, switch to the same page in the specified language as a backup. + */ + fallback?: string + } > export type AdditionalConfig =