From 577d0791dad816c376d38d09dfc4bba372a2ab16 Mon Sep 17 00:00:00 2001 From: babakfp Date: Fri, 8 Dec 2023 17:08:14 -0800 Subject: [PATCH 1/5] feat: fall back if translation is missing --- src/client/app/router.ts | 96 +++++++++++++++++++++++++++++++++++++++- types/shared.d.ts | 7 ++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 3a7d8cb5..a9829f59 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -74,7 +74,14 @@ export function createRouter( let latestPendingPath: string | null = null - async function loadPage(href: string, scrollPosition = 0, isRetry = false) { + async function loadPage( + href: string, + scrollPosition = 0, + isRetry = false, + alreadyTriedLoadingRootFallback = false + ) { + let fallbackLoaded = false + if ((await router.onBeforePageLoad?.(href)) === false) return const targetLoc = new URL(href, fakeHost) const pendingPath = (latestPendingPath = targetLoc.pathname) @@ -149,13 +156,98 @@ export function createRouter( } catch (e) {} } - if (latestPendingPath === pendingPath) { + if (!alreadyTriedLoadingRootFallback) { + fallbackLoaded = await loadPageFallback() + } + + if (!fallbackLoaded && latestPendingPath === pendingPath) { latestPendingPath = null route.path = inBrowser ? pendingPath : withBase(pendingPath) route.component = fallbackComponent ? markRaw(fallbackComponent) : null route.data = notFoundPageData } } + + // If failed to find the page, maybe it's not translated yet! if so, please fallback :) + async function loadPageFallback() { + 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}`) + ) + return fallbackLoaded + } else { + await loadPage(pendingPath.replace(`/${failedLang}`, '')) + return fallbackLoaded + } + } else { + const rootRouteFallbackPath = getRootRouteFallbackPath() + + if (rootRouteFallbackPath) { + await loadPage(rootRouteFallbackPath, 0, true, true) + } + return fallbackLoaded + } + + function getFailedLangFallbackLang() { + const failedLangFallbackLang = locales[failedLang!]?.fallback + if (!failedLangFallbackLang) return + if (!langNames.includes(failedLangFallbackLang)) { + console.warn( + `No keys in 'locales' with the name of 'locales.${failedLang}.fallback'! RECIVED '${failedLangFallbackLang}'` + ) + return + } + + return failedLangFallbackLang + } + + function getCustomFallbackLang() { + const customFallbackLang = Object.entries(namedLocales).filter( + ([_, values]) => values.useAsFallback + )?.[0]?.[0] + if (customFallbackLang && customFallbackLang !== failedLang) { + return customFallbackLang + } + } + + function getRootRouteFallbackPath() { + const fallbackLang = locales['root']?.fallback + + if (!fallbackLang) return + if (!langNames.includes(fallbackLang)) { + console.warn( + `No keys in 'locales' with the name of 'locales.root.fallback'! RECIVED '${fallbackLang}'` + ) + return + } + + return pendingPath === '/' + ? `/${fallbackLang}` + : `/${fallbackLang}${ + pendingPath.startsWith('/') ? pendingPath : `/${pendingPath}` + }` + } + } } if (inBrowser) { diff --git a/types/shared.d.ts b/types/shared.d.ts index 6fdf919b..d350f5e9 100644 --- a/types/shared.d.ts +++ b/types/shared.d.ts @@ -156,7 +156,12 @@ export interface LocaleSpecificConfig { export type LocaleConfig = Record< string, - LocaleSpecificConfig & { label: string; link?: string } + LocaleSpecificConfig & { + label: string + link?: string + fallback?: string + useAsFallback?: boolean + } > // Manually declaring all properties as rollup-plugin-dts From 815e41badb947a2eb1126053ae8a2ff2433fed1c Mon Sep 17 00:00:00 2001 From: babakfp Date: Sat, 9 Dec 2023 06:22:50 -0800 Subject: [PATCH 2/5] better warn message --- src/client/app/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/app/router.ts b/src/client/app/router.ts index a9829f59..13a102df 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -213,7 +213,7 @@ export function createRouter( if (!failedLangFallbackLang) return if (!langNames.includes(failedLangFallbackLang)) { console.warn( - `No keys in 'locales' with the name of 'locales.${failedLang}.fallback'! RECIVED '${failedLangFallbackLang}'` + `Invalid value received in "VitePress Config" > "locales.${failedLang}.fallback"! "${failedLangFallbackLang}" is not a valid value.` ) return } @@ -236,7 +236,7 @@ export function createRouter( if (!fallbackLang) return if (!langNames.includes(fallbackLang)) { console.warn( - `No keys in 'locales' with the name of 'locales.root.fallback'! RECIVED '${fallbackLang}'` + `Invalid value received in "VitePress Config" > "locales.root.fallback"! "${fallbackLang}" is not a valid value.` ) return } From 0d6f340ddd3e3687b675fc693faf0f886052cc59 Mon Sep 17 00:00:00 2001 From: babakfp Date: Sat, 9 Dec 2023 06:28:15 -0800 Subject: [PATCH 3/5] better warn message --- src/client/app/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 13a102df..c338afe0 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -213,7 +213,7 @@ export function createRouter( if (!failedLangFallbackLang) return if (!langNames.includes(failedLangFallbackLang)) { console.warn( - `Invalid value received in "VitePress Config" > "locales.${failedLang}.fallback"! "${failedLangFallbackLang}" is not a valid value.` + `Invalid value received in "VitePress Config" > "locales.${failedLang}.fallback". "${failedLangFallbackLang}" is not a valid value.` ) return } @@ -236,7 +236,7 @@ export function createRouter( if (!fallbackLang) return if (!langNames.includes(fallbackLang)) { console.warn( - `Invalid value received in "VitePress Config" > "locales.root.fallback"! "${fallbackLang}" is not a valid value.` + `Invalid value received in "VitePress Config" > "locales.root.fallback". "${fallbackLang}" is not a valid value.` ) return } 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 4/5] 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 } > From 6194ff3019ed18539ff966553266e0d33b91ee44 Mon Sep 17 00:00:00 2001 From: Babak Farkhoopak <44144724+babakfp@users.noreply.github.com> Date: Thu, 25 Apr 2024 07:34:42 +0330 Subject: [PATCH 5/5] Fixed typos --- src/client/app/router.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/app/router.ts b/src/client/app/router.ts index 33c56577..68efb2de 100644 --- a/src/client/app/router.ts +++ b/src/client/app/router.ts @@ -171,17 +171,17 @@ export function createRouter( if (!value.fallback) continue if (value.fallback === 'root') { throw new Error( - `Invalid Vitepress Config: A locale (${key}), cannot fallback to (root).` + `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.` + `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.` + `Invalid VitePress Config: A locale (${key}), cannot have a fallback to a non existing locale.` ) } }