From 19524cd5d4302c86199c5715baf9dbc6ab2a6aa3 Mon Sep 17 00:00:00 2001 From: Babak Farkhoopak <44144724+babakfp@users.noreply.github.com> Date: Thu, 25 Apr 2024 06:47:14 +0330 Subject: [PATCH] New options, fixes and improvements --- src/client/app/router.ts | 129 +++++++++++++++++---------------------- src/node/config.ts | 2 + src/node/siteConfig.ts | 10 +++ types/shared.d.ts | 12 +++- 4 files changed, 79 insertions(+), 74 deletions(-) diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 3b3f6dda..33c56577 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -77,14 +77,7 @@ export function createRouter( let latestPendingPath: string | null = null - async function loadPage( - href: string, - scrollPosition = 0, - isRetry = false, - alreadyTriedLoadingRootFallback = false - ) { - let fallbackLoaded = false - + async function loadPage(href: string, scrollPosition = 0, isRetry = false) { if ((await router.onBeforePageLoad?.(href)) === false) return const targetLoc = new URL(href, fakeHost) const pendingPath = (latestPendingPath = targetLoc.pathname) @@ -159,11 +152,11 @@ export function createRouter( } catch (e) {} } - if (!alreadyTriedLoadingRootFallback) { - fallbackLoaded = await loadPageFallback() + if (siteDataRef.value.localesFallback) { + await loadFallback() } - if (!fallbackLoaded && latestPendingPath === pendingPath) { + if (latestPendingPath === pendingPath) { latestPendingPath = null route.path = inBrowser ? pendingPath : withBase(pendingPath) route.component = fallbackComponent ? markRaw(fallbackComponent) : null @@ -171,84 +164,74 @@ export function createRouter( } } - // If failed to find the page, maybe it's not translated yet! if so, please fallback :) - async function loadPageFallback() { + async function loadFallback() { const locales = siteDataRef.value.locales - if (!locales) return fallbackLoaded - const namedLocales = Object.fromEntries( - Object.entries(siteDataRef.value.locales).filter( - ([name]) => name !== 'root' - ) - ) - if (!Object.entries(namedLocales).length) return fallbackLoaded - const langNames = Object.keys(namedLocales) - - const failedLang = langNames.find( - (lang) => - pendingPath === `/${lang}` || pendingPath.startsWith(`/${lang}/`) - ) - - if (failedLang) { - const fallbackLang = - getFailedLangFallbackLang() ?? getCustomFallbackLang() - - if (fallbackLang) { - await loadPage( - pendingPath.replace(`/${failedLang}`, `/${fallbackLang}`) + 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 fallback to (root).` ) - return fallbackLoaded - } else { - await loadPage(pendingPath.replace(`/${failedLang}`, '')) - return fallbackLoaded } - } else { - const rootRouteFallbackPath = getRootRouteFallbackPath() - - if (rootRouteFallbackPath) { - await loadPage(rootRouteFallbackPath, 0, true, true) + if (key === value.fallback) { + throw new Error( + `Invalid Vitepress Config: A locale (${key}), cannot have a fallback to itself.` + ) } - return fallbackLoaded - } - - function getFailedLangFallbackLang() { - const failedLangFallbackLang = locales[failedLang!]?.fallback - if (!failedLangFallbackLang) return - if (!langNames.includes(failedLangFallbackLang)) { - console.warn( - `Invalid value received in "VitePress Config" > "locales.${failedLang}.fallback". "${failedLangFallbackLang}" is not a valid value.` + 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.` ) - return } + } - return failedLangFallbackLang + // 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 = Object.entries(namedLocales).filter( - ([_, values]) => values.useAsFallback - )?.[0]?.[0] - if (customFallbackLang && customFallbackLang !== failedLang) { + const customFallbackLang = siteDataRef.value.localesDefaultFallback + if (customFallbackLang && customFallbackLang !== failedLocaleKey) { return customFallbackLang } } - function getRootRouteFallbackPath() { - const fallbackLang = locales['root']?.fallback - + function getRootLocaleFallbackPath() { + const fallbackLang = locales['root'].fallback if (!fallbackLang) return - if (!langNames.includes(fallbackLang)) { - console.warn( - `Invalid value received in "VitePress Config" > "locales.root.fallback". "${fallbackLang}" is not a valid value.` - ) - return - } - - return pendingPath === '/' - ? `/${fallbackLang}` - : `/${fallbackLang}${ - pendingPath.startsWith('/') ? pendingPath : `/${pendingPath}` - }` + if (pendingPath === '/') return `/${fallbackLang}` + const pathDivider = pendingPath.startsWith('/') ? '' : '/' + return `/${fallbackLang}${pathDivider}${pendingPath}` } } } diff --git a/src/node/config.ts b/src/node/config.ts index 6d1bbd49..0993c3ab 100644 --- a/src/node/config.ts +++ b/src/node/config.ts @@ -252,6 +252,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 74174280..36a8efac 100644 --- a/src/node/siteConfig.ts +++ b/src/node/siteConfig.ts @@ -69,6 +69,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 d350f5e9..291efca7 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -124,6 +124,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: { @@ -159,8 +167,10 @@ export type LocaleConfig = Record< 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 - useAsFallback?: boolean } >