Last active
March 28, 2024 13:19
-
-
Save codeflorist/506f7cd08cd0d733a616d996c56b17b1 to your computer and use it in GitHub Desktop.
Multilingual route management for using Nuxt3 with Storyblok CMS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const useStoryRoutes = () => { | |
type StoryRoute = { | |
storyPath: string // The backend-storyblok-path of the story. | |
uuid: string // The uuid of the story. | |
localeTitles: LocalizedText | |
localePaths: LocalizedText | |
} | |
const pageTypes = 'page,news-article' | |
const { defaultLocale, locales: availableLocales, locale: currentLocale } = useI18n() | |
const i18nSwitchLocalePath = useSwitchLocalePath() | |
const route = useRoute() | |
const storyRoutes = useState<StoryRoute[]>('storyRoutes', () => []) | |
const isDev = typeof process !== 'undefined' && process.env.NODE_ENV === 'development' | |
const isStoryblokEditor = !!route.query?._storyblok | |
const isStoryblokFetchAllowed = process.server || isStoryblokEditor | |
const storyVersion: 'draft' | 'published' = isStoryblokEditor || isDev ? 'draft' : 'published' | |
const doThrowErrors = isDev || process.server | |
const storyUsesTranslatedSlugs = (story: Story): boolean => 'translated_slugs' in story && Array.isArray(story.translated_slugs) | |
/* Makes sure, paths have trailing but no ending slash */ | |
const normalizePath = (path: string): string => `/${path.replace(/\/$/, '').replace(/^\//, '')}` | |
/* Gets the storyblok-backend-path for a story */ | |
const getStoryPathFromStory = (story: Story): string => { | |
if (storyUsesTranslatedSlugs(story)) { | |
return normalizePath(story.default_full_slug) | |
} | |
return normalizePath( | |
story.lang === 'default' | |
? story.full_slug | |
: story.full_slug.split('/').slice(1).join('/') | |
) | |
} | |
/* Gets the translated title for a story */ | |
const getTranslatedTitle = (story: Story, locale: string): string => { | |
// First candidate is always the story.content.title field. | |
if (story.content.title) { | |
return story.content.title | |
} | |
// If the story uses translated slugs, it might also contain a candidate for the translated title. | |
if (storyUsesTranslatedSlugs(story) && locale !== defaultLocale) { | |
const translatedSlug = story.translated_slugs.find( | |
translatedSlug => translatedSlug.lang === locale | |
) | |
if (typeof translatedSlug !== 'undefined' && translatedSlug.name) { | |
return translatedSlug.name | |
} | |
} | |
// By default we just return the story.name (which might be untranslated!) | |
return story.name | |
} | |
/* Generates the translated path for a story */ | |
const generateTranslatedPath = (story: Story, locale: string): string => { | |
const isDefaultLocale = locale === defaultLocale | |
const fullStorySlug = normalizePath(story.full_slug) | |
// Special treatment for home page. | |
const isHomePage = isDefaultLocale | |
? fullStorySlug === '/home' | |
: fullStorySlug === `/${locale}/home` | |
if (isHomePage) { | |
return isDefaultLocale ? '/' : `/${locale}` | |
} | |
// If the space uses the "Translated slugs" app, | |
// we get the complete path already in a finished state. | |
if (storyUsesTranslatedSlugs(story)) { | |
return normalizePath(story.full_slug) | |
} | |
// If the space does NO use the "Translated slugs" app, | |
// we use the custom story.content.slug field to get the slug | |
// and have to build the translated path ourselves. | |
// Get the story-slug (either a language specific one, or the default-storyblok one) | |
const localeStorySlug = story.content.slug || story.slug | |
// Check if page has a parent-page or -folder. | |
// Non default-languages begin with /<locale>/, | |
// so we have to look for 2 slashes here. | |
const hasParent = isDefaultLocale | |
? fullStorySlug.split('/').length > 2 | |
: fullStorySlug.split('/').length > 3 | |
// If there is no parent, we simply return the localeStorySlug, | |
// (prefixed by the locale, if not default locale). | |
if (!hasParent) { | |
return normalizePath(isDefaultLocale ? localeStorySlug : `${locale}/${localeStorySlug}`) | |
} | |
// Otherwise we look for the parent inside storyRoutes.value | |
const parentStoryPath = getStoryPathFromStory(story).split('/').slice(0, -1).join('/') | |
const parentStoryRoute = storyRoutes.value.find( | |
storyRoute => storyRoute.storyPath === parentStoryPath | |
) | |
if (typeof parentStoryRoute !== 'undefined') { | |
return `${parentStoryRoute.localePaths[locale]}/${localeStorySlug}` | |
} | |
// If no parent is present, this means the story lies in a folder | |
// without any root-story. So we cannot translate the folder. | |
return `${fullStorySlug.split('/').slice(0, -1).join('/')}/${localeStorySlug}` | |
} | |
const fetch = async () => { | |
if (isStoryblokFetchAllowed && storyRoutes.value.length === 0) { | |
for (const localeData of availableLocales.value) { | |
const stories: Story[] = await useStoryblokApi().getAll('cdn/stories', { | |
sort_by: 'parent_id:asc,is_startpage:desc', | |
filter_query: { | |
component: { | |
in: pageTypes, | |
}, | |
}, | |
version: storyVersion, | |
language: localeData.code, | |
}) | |
for (const story of stories) { | |
let routeIndex = null | |
const storyPath = getStoryPathFromStory(story) | |
// Check, if story is already present in other language | |
const alreadyPresentIndex = storyRoutes.value.findIndex( | |
(route: StoryRoute) => route.storyPath === storyPath | |
) | |
// If not, we push new StoryRoute object to fill | |
if (alreadyPresentIndex === -1) { | |
routeIndex | |
= storyRoutes.value.push({ | |
storyPath, | |
uuid: story.uuid, | |
localeTitles: {}, | |
localePaths: {}, | |
}) - 1 | |
} | |
else { | |
routeIndex = alreadyPresentIndex | |
} | |
storyRoutes.value[routeIndex].localeTitles[localeData.code] | |
= getTranslatedTitle(story, localeData.code) | |
storyRoutes.value[routeIndex].localePaths[localeData.code] | |
= generateTranslatedPath(story, localeData.code) | |
} | |
} | |
} | |
} | |
/* Gets the translated frontend path for a storyblok-backend-path */ | |
const localePath = (storyPath: string, locale = ''): string | null => { | |
const targetLocale = locale || currentLocale.value | |
const storyRoute = storyRoutes.value.find( | |
storyRoute => normalizePath(storyPath) === storyRoute.storyPath | |
) | |
if (typeof storyRoute !== 'undefined') { | |
return storyRoute.localePaths[targetLocale] | |
} | |
const errMsg = `Error: no translated path found for storyPath "${storyPath}" and locale "${targetLocale}"` | |
console.error(errMsg) | |
if (doThrowErrors) { | |
throw createError({ | |
statusCode: 500, | |
statusMessage: errMsg, | |
}) | |
} | |
return null | |
} | |
/* Gets the translated frontend path for a story uuid */ | |
const localePathByUuid = (uuid: string, locale = ''): string | null => { | |
const targetLocale = locale || currentLocale.value | |
const storyRoute = storyRoutes.value.find( | |
storyRoute => uuid === storyRoute.uuid | |
) | |
if (typeof storyRoute !== 'undefined') { | |
return storyRoute.localePaths[targetLocale] | |
} | |
const errMsg = `Error: no translated path found for story with uuid "${uuid}" and locale "${targetLocale}"` | |
console.error(errMsg) | |
if (doThrowErrors) { | |
throw createError({ | |
statusCode: 500, | |
statusMessage: errMsg, | |
}) | |
} | |
return null | |
} | |
/* Gets the translated Page Title for a story */ | |
const localeTitle = (storyPath: string, locale = ''): string | null => { | |
const targetLocale = locale || currentLocale.value | |
const storyRoute = storyRoutes.value.find( | |
storyRoute => normalizePath(storyPath) === storyRoute.storyPath | |
) | |
if (typeof storyRoute !== 'undefined') { | |
return storyRoute.localeTitles[targetLocale] | |
} | |
const errMsg = `Error: no translated page title found for storyPath "${storyPath}" and locale "${targetLocale}"` | |
console.error(errMsg) | |
if (doThrowErrors) { | |
throw createError({ | |
statusCode: 500, | |
statusMessage: errMsg, | |
}) | |
} | |
return null | |
} | |
/* Gets the translated Page Title for a story */ | |
const localeTitleByUuid = (uuid: string, locale = ''): string | null => { | |
const targetLocale = locale || currentLocale.value | |
const storyRoute = storyRoutes.value.find( | |
storyRoute => uuid === storyRoute.storyPath | |
) | |
if (typeof storyRoute !== 'undefined') { | |
return storyRoute.localeTitles[targetLocale] | |
} | |
const errMsg = `Error: no translated page title found for story with uuid "${uuid}" and locale "${targetLocale}"` | |
console.error(errMsg) | |
if (doThrowErrors) { | |
throw createError({ | |
statusCode: 500, | |
statusMessage: errMsg, | |
}) | |
} | |
return null | |
} | |
/* Gets the storyblok-backend-path for a translated frontend path */ | |
const storyPath = (localePath: string): string | null => { | |
const storyRoute = storyRoutes.value.find(storyRoute => | |
Object.values(storyRoute.localePaths).find( | |
storyLocalePath => storyLocalePath === localePath | |
) | |
) | |
if (typeof storyRoute !== 'undefined') { | |
return storyRoute.storyPath | |
} | |
return null | |
} | |
/* Gets the storyblok-backend-path for a translated frontend path */ | |
const storyPathByUuid = (uuid: string): string | null => { | |
const storyRoute = storyRoutes.value.find(storyRoute => storyRoute.uuid === uuid) | |
if (typeof storyRoute !== 'undefined') { | |
return storyRoute.storyPath | |
} | |
return null | |
} | |
/* Gets the path to the current page in another language */ | |
const switchLocalePath = (locale: string): string => { | |
const storyRoute = storyRoutes.value.find(storyRoute => | |
Object.values(storyRoute.localePaths).find( | |
localePath => localePath === normalizePath(route.path) | |
) | |
) | |
if (typeof storyRoute !== 'undefined') { | |
return storyRoute.localePaths[locale] | |
} | |
// Fall back to i18n-version by default. | |
return i18nSwitchLocalePath(locale) | |
} | |
return { | |
fetch, | |
storyRoutes, | |
localePath, | |
localePathByUuid, | |
localeTitleByUuid, | |
localeTitle, | |
storyPath, | |
switchLocalePath, | |
normalizePath, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment