From ee5bd74920df82fc6867e06d7d41c223c5c743c7 Mon Sep 17 00:00:00 2001 From: Mathias Oterhals Myklebust Date: Tue, 15 Oct 2024 11:26:00 +0200 Subject: [PATCH] feat(i18): translate full path in language middleware --- src/middlewares/languageMiddleware.ts | 207 +++++++++++++++++++------ studio/lib/queries/slugTranslations.ts | 44 ++++++ 2 files changed, 207 insertions(+), 44 deletions(-) diff --git a/src/middlewares/languageMiddleware.ts b/src/middlewares/languageMiddleware.ts index 3d8703b3c..962688ade 100644 --- a/src/middlewares/languageMiddleware.ts +++ b/src/middlewares/languageMiddleware.ts @@ -1,6 +1,7 @@ import Negotiator from "negotiator"; import { headers } from "next/headers"; import { NextRequest, NextResponse } from "next/server"; +import { SanityClient } from "next-sanity"; import { client } from "studio/lib/client"; import { SlugTranslations } from "studio/lib/interfaces/slugTranslations"; @@ -10,38 +11,106 @@ import { LANGUAGES_QUERY, } from "studio/lib/queries/siteSettings"; import { + SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_BY_TYPE_QUERY, SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY, + SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_BY_TYPE_QUERY, SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_QUERY, + SLUG_TRANSLATIONS_FROM_LANGUAGE_BY_TYPE_QUERY, SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY, + SLUG_TRANSLATIONS_TO_LANGUAGE_BY_TYPE_QUERY, SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY, } from "studio/lib/queries/slugTranslations"; +import { sharedClient } from "studioShared/lib/client"; -async function translateSlug( +async function translateDocumentSlug( + queryClient: SanityClient, slug: string, targetLanguageId: string, sourceLanguageId?: string, -): Promise { - if (slug.length === 0) { - return slug; - } - const queryParams = { - slug, - language: sourceLanguageId ?? targetLanguageId, - }; - // query document-based slug translations - let slugTranslations = await client.fetch( + docType?: string, +) { + return queryClient.fetch( sourceLanguageId !== undefined - ? SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY - : SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY, - queryParams, + ? docType !== undefined + ? SLUG_TRANSLATIONS_FROM_LANGUAGE_BY_TYPE_QUERY + : SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY + : docType !== undefined + ? SLUG_TRANSLATIONS_TO_LANGUAGE_BY_TYPE_QUERY + : SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY, + { + slug, + language: sourceLanguageId ?? targetLanguageId, + ...(docType !== undefined + ? { + type: docType, + } + : {}), + }, + { + perspective: "published", + }, ); - if (slugTranslations === null) { - // try field-based slug translations instead - slugTranslations = await client.fetch( - sourceLanguageId !== undefined - ? SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY +} + +async function translateFieldSlug( + queryClient: SanityClient, + slug: string, + targetLanguageId: string, + sourceLanguageId?: string, + docType?: string, +) { + return queryClient.fetch( + sourceLanguageId !== undefined + ? docType !== undefined + ? SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_BY_TYPE_QUERY + : SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY + : docType !== undefined + ? SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_BY_TYPE_QUERY : SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_QUERY, - queryParams, + { + slug, + language: sourceLanguageId ?? targetLanguageId, + ...(docType !== undefined + ? { + type: docType, + } + : {}), + }, + { + perspective: "published", + }, + ); +} + +async function translateSlug( + slug: string, + targetLanguageId: string, + sourceLanguageId?: string, + docType?: string, + translationType?: "document" | "field", + project: "studio" | "shared" = "studio", +): Promise { + const queryClient = project === "studio" ? client : sharedClient; + let slugTranslations; + if (translationType === "document" || translationType === undefined) { + slugTranslations = await translateDocumentSlug( + queryClient, + slug, + targetLanguageId, + sourceLanguageId, + docType, + ); + } + if ( + translationType === "field" || + (translationType === undefined && slugTranslations === null) + ) { + slugTranslations = await translateFieldSlug( + queryClient, + slug, + targetLanguageId, + sourceLanguageId, + docType, ); } return slugTranslations?._translations.find( @@ -50,6 +119,56 @@ async function translateSlug( )?.slug; } +async function translateCustomerCasePath( + path: string[], + targetLanguageId: string, + sourceLanguageId?: string, +): Promise { + const pageSlugTranslation = await translateSlug( + path[0], + targetLanguageId, + sourceLanguageId, + "customerCasesPage", + ); + if (pageSlugTranslation === undefined) { + return undefined; + } + return translateSlug( + path[1], + targetLanguageId, + sourceLanguageId, + "customerCase", + "field", + "shared", + ).then((slug) => + slug !== undefined ? [pageSlugTranslation, slug] : undefined, + ); +} + +async function translatePath( + path: string[], + targetLanguageId: string, + sourceLanguageId?: string, +): Promise { + if (path.length === 0) { + return path; + } + if (path.length === 1) { + return translateSlug(path[0], targetLanguageId, sourceLanguageId).then( + (slug) => (slug !== undefined ? [slug] : undefined), + ); + } + const pathTranslation = await translateCustomerCasePath( + path, + targetLanguageId, + sourceLanguageId, + ); + if (pathTranslation !== undefined) { + return pathTranslation; + } + return undefined; +} + function negotiateClientLanguage( availableLanguages: string[], ): string | undefined { @@ -100,14 +219,12 @@ export async function languageMiddleware( console.error("No languages available, language middleware aborted."); return; } - const pathname = request.nextUrl.pathname; - const language = availableLanguages.find( - ({ id }) => pathname.startsWith(`/${id}/`) || pathname === `/${id}`, - ); + const path = request.nextUrl.pathname.replace(/^\//, "").split("/"); + const language = availableLanguages.find(({ id }) => path[0] === id); if (language === undefined) { - return redirectMissingLanguage(pathname, availableLanguages, request.url); + return redirectMissingLanguage(path, availableLanguages, request.url); } - return redirectWithLanguage(pathname, language, request.url); + return redirectWithLanguage(path, language, request.url); } /** @@ -117,23 +234,26 @@ export async function languageMiddleware( * - If translated, user is redirected to `/[language]/[translatedSlug]` * - Otherwise, user is redirected to the slug with default language (`/[slug]`) * - * @param pathname - * @param language - * @param baseUrl + * @param {string[]} path - The current URL path segments. + * @param {LanguageObject} language - Language object from URL path + * @param {string} baseUrl - The base URL of the site. */ async function redirectWithLanguage( - pathname: string, + path: string[], language: LanguageObject, baseUrl: string, ) { - const slug = pathname.split("/").slice(2).join("/"); - const translatedSlug = await translateSlug(slug, language.id); - if (translatedSlug === undefined) { - return NextResponse.redirect(new URL(`/${slug}`, baseUrl)); + const pathWithoutLanguage = path.slice(1); + const translatedPath = await translatePath(pathWithoutLanguage, language.id); + if (translatedPath === undefined) { + return NextResponse.redirect( + new URL(`/${pathWithoutLanguage.join("/")}`, baseUrl), + ); } - if (translatedSlug !== slug) { - const translatedPathname = `/${language.id}/${translatedSlug}`; - return NextResponse.redirect(new URL(translatedPathname, baseUrl)); + if (translatedPath.join("/") !== pathWithoutLanguage.join("/")) { + return NextResponse.redirect( + new URL(`/${language.id}/${translatedPath.join("/")}`, baseUrl), + ); } // all good, no modifications needed return; @@ -149,12 +269,12 @@ async function redirectWithLanguage( * - If a translated version exists, redirect the user to the corresponding path: `/[negotiatedLanguage]/[translatedSlug]`. * - If no translation is found, keep the user on the default language page at `/[slug]`. * - * @param {string} pathname - The current URL path. + * @param {string[]} path - The current URL path segments. * @param {string[]} availableLanguages - A list of languages supported by the site. * @param {string} baseUrl - The base URL of the site. */ async function redirectMissingLanguage( - pathname: string, + path: string[], availableLanguages: LanguageObject[], baseUrl: string, ) { @@ -176,20 +296,19 @@ async function redirectMissingLanguage( if (preferredLanguage === defaultLanguageId) { // Same as default, simply rewrite internally to include language code return NextResponse.rewrite( - new URL(`/${preferredLanguage}${pathname}`, baseUrl), + new URL(`/${preferredLanguage}/${path.join("/")}`, baseUrl), ); } // Attempt to translate to the preferred language - const slug = pathname.replace(/^\//, ""); - const translatedSlug = await translateSlug( - slug, + const translatedSlug = await translatePath( + path, preferredLanguage, defaultLanguageId, ); if (translatedSlug === undefined) { // Translation not available, rewrite to default language return NextResponse.rewrite( - new URL(`/${defaultLanguageId}${pathname}`, baseUrl), + new URL(`/${defaultLanguageId}/${path.join("/")}`, baseUrl), ); } // Redirect with language code and translated slug diff --git a/studio/lib/queries/slugTranslations.ts b/studio/lib/queries/slugTranslations.ts index 2e29fe621..3257cef56 100644 --- a/studio/lib/queries/slugTranslations.ts +++ b/studio/lib/queries/slugTranslations.ts @@ -11,6 +11,15 @@ export const SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY = groq` } `; +export const SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_BY_TYPE_QUERY = groq` + *[${translatedFieldFragment("slug")} == $slug && _type == $type][0] { + "_translations": slug[] { + "language": _key, + "slug": value + } + } +`; + export const SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY = groq` *[slug.current == $slug && language == $language][0]{ "_translations": *[ @@ -23,6 +32,18 @@ export const SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY = groq` } `; +export const SLUG_TRANSLATIONS_FROM_LANGUAGE_BY_TYPE_QUERY = groq` + *[slug.current == $slug && language == $language && _type == $type][0]{ + "_translations": *[ + _type == "translation.metadata" + && references(^._id) + ].translations[].value->{ + language, + "slug": slug.current + } + } +`; + export const SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_QUERY = groq` *[defined(slug[value == $slug][0])][0] { "_translations": (slug[] { @@ -32,6 +53,15 @@ export const SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_QUERY = groq` } `; +export const SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_BY_TYPE_QUERY = groq` + *[defined(slug[value == $slug][0]) && _type == $type][0] { + "_translations": (slug[] { + "language": _key, + "slug": value + })[language == $language] + } +`; + export const SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY = groq` *[slug.current == $slug][0]{ "_translations": ( @@ -45,3 +75,17 @@ export const SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY = groq` )[language == $language] } `; + +export const SLUG_TRANSLATIONS_TO_LANGUAGE_BY_TYPE_QUERY = groq` + *[slug.current == $slug && _type == $type][0]{ + "_translations": ( + *[ + _type == "translation.metadata" + && references(^._id) + ].translations[].value->{ + language, + "slug": slug.current + } + )[language == $language] + } +`;