From 51a3a40dfd3cdae4013efc4f5dae54f711567e7f Mon Sep 17 00:00:00 2001 From: Mathias Oterhals Myklebust Date: Tue, 8 Oct 2024 13:06:24 +0200 Subject: [PATCH] feat(translations): handle internationalized string in GROQ queries --- src/app/(main)/[lang]/[slug]/page.tsx | 14 ++- src/app/(main)/[lang]/layout.tsx | 6 +- src/app/(main)/[lang]/page.tsx | 6 +- src/app/(main)/legal/[id]/page.tsx | 4 +- src/blog/components/legal/LegalPreview.tsx | 9 +- src/compensations/CompensationsPreview.tsx | 5 +- src/components/link/CustomLink.tsx | 39 +++---- src/components/linkButton/LinkButton.tsx | 5 +- .../navigation/footer/FooterPreview.tsx | 6 +- .../navigation/header/HeaderPreview.tsx | 2 +- src/components/navigation/mockData.ts | 2 + .../sections/article/ArticlePreview.tsx | 2 +- .../callToAction/CallToActionPreview.tsx | 2 +- .../sections/callout/CalloutPreview.tsx | 2 +- src/components/sections/grid/GridPreview.tsx | 2 +- src/components/sections/hero/HeroPreview.tsx | 2 +- .../ImageSectionComponentPreview.tsx | 2 +- .../sections/logoSalad/LogoSaladPreview.tsx | 2 +- .../testimonials/TestimonialsPreview.tsx | 2 +- src/middlewares/languageMiddleware.ts | 23 +++- src/utils/hooks/useLanguage.ts | 19 ++- src/utils/seo.ts | 2 +- studio/lib/interfaces/compensations.ts | 6 +- studio/lib/interfaces/defaultSeo.ts | 13 --- studio/lib/interfaces/global.ts | 23 +++- studio/lib/interfaces/navigation.ts | 9 +- studio/lib/interfaces/pages.ts | 1 + studio/lib/interfaces/seo.ts | 15 +++ studio/lib/queries/admin.ts | 2 +- studio/lib/queries/pages.ts | 108 ++++++++++-------- studio/lib/queries/siteSettings.ts | 49 ++++---- studio/lib/queries/slugTranslations.ts | 30 ++++- studio/lib/queries/specialPages.ts | 18 ++- studio/schemas/documents/compensations.ts | 10 ++ studio/schemas/fields/callToActionFields.ts | 7 +- .../compensations/benefitsByLocation.ts | 7 +- studio/schemas/objects/link.ts | 10 +- studio/utils/i18n.ts | 10 ++ 38 files changed, 316 insertions(+), 160 deletions(-) delete mode 100644 studio/lib/interfaces/defaultSeo.ts create mode 100644 studio/lib/interfaces/seo.ts create mode 100644 studio/utils/i18n.ts diff --git a/src/app/(main)/[lang]/[slug]/page.tsx b/src/app/(main)/[lang]/[slug]/page.tsx index 83611c4d2..5efbc7adb 100644 --- a/src/app/(main)/[lang]/[slug]/page.tsx +++ b/src/app/(main)/[lang]/[slug]/page.tsx @@ -20,13 +20,13 @@ import { BlogPage, PageBuilder, Post } from "studio/lib/interfaces/pages"; import { CustomerCasePage } from "studio/lib/interfaces/specialPages"; import { COMPANY_LOCATIONS_QUERY, - LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY, + LEGAL_DOCUMENT_BY_SLUG_AND_LANG_QUERY, } from "studio/lib/queries/admin"; import { BLOG_PAGE_QUERY, + PAGE_BY_SLUG_QUERY, POSTS_QUERY, SEO_SLUG_QUERY, - SLUG_QUERY, } from "studio/lib/queries/pages"; import { COMPENSATIONS_PAGE_QUERY, @@ -67,11 +67,15 @@ async function Page({ params }: Props) { initialCustomerCases, initialLegalDocument, ] = await Promise.all([ - loadStudioQuery(SLUG_QUERY, { slug }, { perspective }), + loadStudioQuery( + PAGE_BY_SLUG_QUERY, + { slug, language: lang }, + { perspective }, + ), loadStudioQuery(BLOG_PAGE_QUERY, { slug }, { perspective }), loadStudioQuery( COMPENSATIONS_PAGE_QUERY, - { slug }, + { slug, language: lang }, { perspective }, ), loadStudioQuery( @@ -85,7 +89,7 @@ async function Page({ params }: Props) { { perspective }, ), loadStudioQuery( - LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY, + LEGAL_DOCUMENT_BY_SLUG_AND_LANG_QUERY, { slug, language: lang }, { perspective }, ), diff --git a/src/app/(main)/[lang]/layout.tsx b/src/app/(main)/[lang]/layout.tsx index c33decf46..a228b2863 100644 --- a/src/app/(main)/[lang]/layout.tsx +++ b/src/app/(main)/[lang]/layout.tsx @@ -42,7 +42,11 @@ export default async function Layout({ initialLegal, initialBrandAssets, ] = await Promise.all([ - loadStudioQuery(NAV_QUERY, {}, { perspective }), + loadStudioQuery( + NAV_QUERY, + { language: params.lang }, + { perspective }, + ), loadStudioQuery(COMPANY_INFO_QUERY, {}, { perspective }), loadStudioQuery( SOME_PROFILES_QUERY, diff --git a/src/app/(main)/[lang]/page.tsx b/src/app/(main)/[lang]/page.tsx index e63a51439..892e8749c 100644 --- a/src/app/(main)/[lang]/page.tsx +++ b/src/app/(main)/[lang]/page.tsx @@ -10,7 +10,7 @@ import { client } from "studio/lib/client"; import { LinkType } from "studio/lib/interfaces/navigation"; import { PageBuilder } from "studio/lib/interfaces/pages"; import { LanguageObject } from "studio/lib/interfaces/supportedLanguages"; -import { PAGE_QUERY, SEO_PAGE_QUERY } from "studio/lib/queries/pages"; +import { PAGE_QUERY, PAGE_SEO_QUERY } from "studio/lib/queries/pages"; import { LANDING_PAGE_REF_QUERY, LANGUAGES_QUERY, @@ -21,7 +21,7 @@ export async function generateMetadata(): Promise { const { data: landingId } = await loadStudioQuery( LANDING_PAGE_REF_QUERY, ); - const seo = await fetchSeoData(SEO_PAGE_QUERY, { id: landingId }); + const seo = await fetchSeoData(PAGE_SEO_QUERY, { id: landingId }); return generateMetadataFromSeo(seo); } @@ -84,7 +84,7 @@ const Home = async ({ params }: Props) => { const initialLandingPage = await loadStudioQuery( PAGE_QUERY, - { id: landingId }, + { id: landingId, language: params.lang }, { perspective }, ); diff --git a/src/app/(main)/legal/[id]/page.tsx b/src/app/(main)/legal/[id]/page.tsx index 0925758d3..f9c65376f 100644 --- a/src/app/(main)/legal/[id]/page.tsx +++ b/src/app/(main)/legal/[id]/page.tsx @@ -2,7 +2,7 @@ import Legal from "src/blog/components/legal/Legal"; import LegalPreview from "src/blog/components/legal/LegalPreview"; import { getDraftModeInfo } from "src/utils/draftmode"; import { LegalDocument } from "studio/lib/interfaces/legalDocuments"; -import { LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY } from "studio/lib/queries/admin"; +import { LEGAL_DOCUMENT_BY_SLUG_AND_LANG_QUERY } from "studio/lib/queries/admin"; import { loadStudioQuery } from "studio/lib/store"; export const dynamic = "force-dynamic"; @@ -20,7 +20,7 @@ async function Page({ params }: Props) { const { perspective, isDraftMode } = getDraftModeInfo(); const initialDocument = await loadStudioQuery( - LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY, + LEGAL_DOCUMENT_BY_SLUG_AND_LANG_QUERY, { slug: id, language: "en" }, //TODO: replace this with selected language for the page { perspective }, ); diff --git a/src/blog/components/legal/LegalPreview.tsx b/src/blog/components/legal/LegalPreview.tsx index 9df2b4c0e..2670b158e 100644 --- a/src/blog/components/legal/LegalPreview.tsx +++ b/src/blog/components/legal/LegalPreview.tsx @@ -2,7 +2,7 @@ import { QueryResponseInitial, useQuery } from "@sanity/react-loader"; import { LegalDocument } from "studio/lib/interfaces/legalDocuments"; -import { NAV_QUERY } from "studio/lib/queries/siteSettings"; +import { LEGAL_DOCUMENT_BY_SLUG_AND_LANG_QUERY } from "studio/lib/queries/admin"; import Legal from "./Legal"; @@ -12,8 +12,11 @@ export default function LegalPreview({ initialDocument: QueryResponseInitial; }) { const { data: newDoc } = useQuery( - NAV_QUERY, - {}, + LEGAL_DOCUMENT_BY_SLUG_AND_LANG_QUERY, + { + slug: initialDocument.data.slug, + language: initialDocument.data.language, + }, { initial: initialDocument }, ); diff --git a/src/compensations/CompensationsPreview.tsx b/src/compensations/CompensationsPreview.tsx index 872a1534f..5198097de 100644 --- a/src/compensations/CompensationsPreview.tsx +++ b/src/compensations/CompensationsPreview.tsx @@ -21,7 +21,10 @@ const CompensationsPreview = ({ }: CompensationsPreviewProps) => { const { data: compensationsData } = useQuery( COMPENSATIONS_PAGE_QUERY, - { slug: initialCompensations.data.slug.current }, + { + slug: initialCompensations.data.slug, + language: initialCompensations.data.language, + }, { initial: initialCompensations }, ); diff --git a/src/components/link/CustomLink.tsx b/src/components/link/CustomLink.tsx index 0d7334473..4020911a1 100644 --- a/src/components/link/CustomLink.tsx +++ b/src/components/link/CustomLink.tsx @@ -15,10 +15,6 @@ interface ICustomLink { } const CustomLink = ({ type = "link", link, isSelected }: ICustomLink) => { - const linkTitle = link.linkTitle; - // TODO: pick title based on selected language - const linkTitleValue = - (typeof linkTitle === "string" ? linkTitle : linkTitle[0].value) ?? ""; const href = getHref(link); const newTab = link.newTab; const target = newTab ? "_blank" : undefined; @@ -28,29 +24,32 @@ const CustomLink = ({ type = "link", link, isSelected }: ICustomLink) => { ? `${styles.headerLink} ${isSelected ? styles.selected : ""}` : styles.footerLink; - return type === "link" ? ( -
+ return ( + link.linkTitle && + (type === "link" ? ( +
+ + {link.linkTitle} + +
+
+ ) : ( - {linkTitleValue} + {link.linkTitle} -
-
- ) : ( - - {linkTitleValue} - + )) ); }; diff --git a/src/components/linkButton/LinkButton.tsx b/src/components/linkButton/LinkButton.tsx index 09f4a67a7..580da7421 100644 --- a/src/components/linkButton/LinkButton.tsx +++ b/src/components/linkButton/LinkButton.tsx @@ -21,10 +21,7 @@ const typeClassMap: { [key in LinkButtonType]: string } = { const LinkButton = ({ isSmall, type = "primary", link }: IButton) => { const className = `${styles.button} ${isSmall ? styles.small : ""} ${typeClassMap[type]}`; const href = getHref(link); - const linkTitle = link.linkTitle; - // TODO: pick title based on selected language - const linkTitleValue = - typeof linkTitle === "string" ? linkTitle : linkTitle[0].value; + const linkTitleValue = link.linkTitle; return ( href && linkTitleValue && ( diff --git a/src/components/navigation/footer/FooterPreview.tsx b/src/components/navigation/footer/FooterPreview.tsx index 9659ef439..5a2c044da 100644 --- a/src/components/navigation/footer/FooterPreview.tsx +++ b/src/components/navigation/footer/FooterPreview.tsx @@ -41,7 +41,11 @@ export default function FooterPreview({ initialLegal: QueryResponseInitial; language: string; }) { - const newNav = useInitialData(NAV_QUERY, initialNav); + const { data: newNav } = useQuery( + NAV_QUERY, + { language }, + { initial: initialNav }, + ); const newCompanyInfo = useInitialData(COMPANY_INFO_QUERY, initialCompanyInfo); const newBrandAssets = useInitialData(BRAND_ASSETS_QUERY, initialBrandAssets); const newSoMedata = useInitialData(SOME_PROFILES_QUERY, initialSoMe); diff --git a/src/components/navigation/header/HeaderPreview.tsx b/src/components/navigation/header/HeaderPreview.tsx index c2c907798..b4d67e6be 100644 --- a/src/components/navigation/header/HeaderPreview.tsx +++ b/src/components/navigation/header/HeaderPreview.tsx @@ -16,7 +16,7 @@ export default function HeaderPreview({ }) { const { data: newNav } = useQuery( NAV_QUERY, - {}, + { language: initialNav.data.language }, { initial: initialNav }, ); const { data: newBrandAssets } = useQuery( diff --git a/src/components/navigation/mockData.ts b/src/components/navigation/mockData.ts index 0e4e20b3d..b56b25975 100644 --- a/src/components/navigation/mockData.ts +++ b/src/components/navigation/mockData.ts @@ -12,6 +12,7 @@ import { linkID } from "studio/schemas/objects/link"; // Mock Navigation Data export const mockNavigation: Navigation = { _id: "main-navigation", + language: "en", main: [ { _key: "functionality", @@ -63,6 +64,7 @@ export const mockNavigation: Navigation = { sectionTitle: "Social Media", sectionType: SectionType.SocialMedia, socialMediaLinks: { + _type: "reference", _ref: "soMeLinksID", }, }, diff --git a/src/components/sections/article/ArticlePreview.tsx b/src/components/sections/article/ArticlePreview.tsx index 3a7dd12a1..4621a0adf 100644 --- a/src/components/sections/article/ArticlePreview.tsx +++ b/src/components/sections/article/ArticlePreview.tsx @@ -15,7 +15,7 @@ export default function ArticlePreview({ }: PreviewProps) { const { data } = useQuery( PAGE_QUERY, - { id: initialData.data._id }, + { id: initialData.data._id, language: initialData.data.language }, { initial: initialData }, ); diff --git a/src/components/sections/callToAction/CallToActionPreview.tsx b/src/components/sections/callToAction/CallToActionPreview.tsx index e46a9052b..288db6f9e 100644 --- a/src/components/sections/callToAction/CallToActionPreview.tsx +++ b/src/components/sections/callToAction/CallToActionPreview.tsx @@ -15,7 +15,7 @@ export default function CallToActionPreview({ }: PreviewProps) { const { data: newData } = useQuery( PAGE_QUERY, - { id: initialData.data._id }, + { id: initialData.data._id, language: initialData.data.language }, { initial: initialData }, ); diff --git a/src/components/sections/callout/CalloutPreview.tsx b/src/components/sections/callout/CalloutPreview.tsx index 621fb1cf4..a7bf6e3b0 100644 --- a/src/components/sections/callout/CalloutPreview.tsx +++ b/src/components/sections/callout/CalloutPreview.tsx @@ -15,7 +15,7 @@ export default function CalloutPreview({ }: PreviewProps) { const { data: newData } = useQuery( PAGE_QUERY, - { id: initialData.data._id }, + { id: initialData.data._id, language: initialData.data.language }, { initial: initialData }, ); diff --git a/src/components/sections/grid/GridPreview.tsx b/src/components/sections/grid/GridPreview.tsx index 4842eeb8b..b1d9e90ba 100644 --- a/src/components/sections/grid/GridPreview.tsx +++ b/src/components/sections/grid/GridPreview.tsx @@ -15,7 +15,7 @@ export default function GridPreview({ }: PreviewProps) { const { data: newData } = useQuery( PAGE_QUERY, - { id: initialData.data._id }, + { id: initialData.data._id, language: initialData.data.language }, { initial: initialData }, ); diff --git a/src/components/sections/hero/HeroPreview.tsx b/src/components/sections/hero/HeroPreview.tsx index 6cb8b30ea..c5ad51b71 100644 --- a/src/components/sections/hero/HeroPreview.tsx +++ b/src/components/sections/hero/HeroPreview.tsx @@ -20,7 +20,7 @@ export default function HeroPreview({ }: HeroPreviewProps) { const { data: newData } = useQuery( PAGE_QUERY, - { id: initialData.data._id }, + { id: initialData.data._id, language: initialData.data.language }, { initial: initialData }, ); diff --git a/src/components/sections/imageSection/ImageSectionComponentPreview.tsx b/src/components/sections/imageSection/ImageSectionComponentPreview.tsx index 2400cda35..d2aa97c14 100644 --- a/src/components/sections/imageSection/ImageSectionComponentPreview.tsx +++ b/src/components/sections/imageSection/ImageSectionComponentPreview.tsx @@ -14,7 +14,7 @@ export default function ImageSectionComponentPreview({ }: PreviewProps) { const { data: newData } = useQuery( PAGE_QUERY, - { id: initialData.data._id }, + { id: initialData.data._id, language: initialData.data.language }, { initial: initialData }, ); diff --git a/src/components/sections/logoSalad/LogoSaladPreview.tsx b/src/components/sections/logoSalad/LogoSaladPreview.tsx index 4ca84ea0e..55f0b5c70 100644 --- a/src/components/sections/logoSalad/LogoSaladPreview.tsx +++ b/src/components/sections/logoSalad/LogoSaladPreview.tsx @@ -15,7 +15,7 @@ export default function LogoSaladPreview({ }: PreviewProps) { const { data: newData } = useQuery( PAGE_QUERY, - { id: initialData.data._id }, + { id: initialData.data._id, language: initialData.data.language }, { initial: initialData }, ); diff --git a/src/components/sections/testimonials/TestimonialsPreview.tsx b/src/components/sections/testimonials/TestimonialsPreview.tsx index c38c0c107..09731a30c 100644 --- a/src/components/sections/testimonials/TestimonialsPreview.tsx +++ b/src/components/sections/testimonials/TestimonialsPreview.tsx @@ -15,7 +15,7 @@ export default function TestimonialsPreview({ }: PreviewProps) { const { data: newData } = useQuery( PAGE_QUERY, - { id: initialData.data._id }, + { id: initialData.data._id, language: initialData.data.language }, { initial: initialData }, ); diff --git a/src/middlewares/languageMiddleware.ts b/src/middlewares/languageMiddleware.ts index f9d4f7f58..ccbe7d257 100644 --- a/src/middlewares/languageMiddleware.ts +++ b/src/middlewares/languageMiddleware.ts @@ -10,6 +10,8 @@ import { LANGUAGES_QUERY, } from "studio/lib/queries/siteSettings"; import { + SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY, + SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_QUERY, SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY, SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY, } from "studio/lib/queries/slugTranslations"; @@ -22,15 +24,26 @@ async function translateSlug( if (slug.length === 0) { return slug; } - const slugTranslations = await client.fetch( + const queryParams = { + slug, + language: sourceLanguageId ?? targetLanguageId, + }; + // query document-based slug translations + let slugTranslations = await client.fetch( sourceLanguageId !== undefined ? SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY : SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY, - { - slug, - language: sourceLanguageId ?? targetLanguageId, - }, + queryParams, ); + if (slugTranslations === null) { + // try field-based slug translations instead + slugTranslations = await client.fetch( + sourceLanguageId !== undefined + ? SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY + : SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_QUERY, + queryParams, + ); + } return slugTranslations?._translations.find( (translation) => translation !== null && translation.language === targetLanguageId, diff --git a/src/utils/hooks/useLanguage.ts b/src/utils/hooks/useLanguage.ts index ffdeafd38..5c1abf2fc 100644 --- a/src/utils/hooks/useLanguage.ts +++ b/src/utils/hooks/useLanguage.ts @@ -5,7 +5,10 @@ import { fetchWithToken } from "studio/lib/fetchWithToken"; import { SlugTranslations } from "studio/lib/interfaces/slugTranslations"; import { LanguageObject } from "studio/lib/interfaces/supportedLanguages"; import { LANGUAGES_QUERY } from "studio/lib/queries/siteSettings"; -import { SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY } from "studio/lib/queries/slugTranslations"; +import { + SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY, + SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY, +} from "studio/lib/queries/slugTranslations"; /** * Client hook providing access to the available Sanity translations for the given slug @@ -34,7 +37,19 @@ function useSlugTranslations( slug, language: currentLanguage?.id, }, - ).then(setSlugTranslationsData); + ).then(async (translations) => { + if (translations !== null) { + setSlugTranslationsData(translations); + } + const fieldTranslations = await fetchWithToken( + SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY, + { + slug, + language: currentLanguage?.id, + }, + ); + setSlugTranslationsData(fieldTranslations); + }); }, [currentLanguage, slug]); const slugTranslations = diff --git a/src/utils/seo.ts b/src/utils/seo.ts index 2d95a3fd8..e1964977d 100644 --- a/src/utils/seo.ts +++ b/src/utils/seo.ts @@ -6,7 +6,7 @@ import { PortableTextBlock } from "sanity"; import { urlFor } from "studio/lib/image"; import { BrandAssets } from "studio/lib/interfaces/brandAssets"; import { CompanyInfo } from "studio/lib/interfaces/companyDetails"; -import { DefaultSeo } from "studio/lib/interfaces/defaultSeo"; +import { DefaultSeo } from "studio/lib/interfaces/seo"; import { COMPANY_INFO_QUERY } from "studio/lib/queries/admin"; import { BRAND_ASSETS_QUERY, diff --git a/studio/lib/interfaces/compensations.ts b/studio/lib/interfaces/compensations.ts index a94bb1fb0..0b38c4938 100644 --- a/studio/lib/interfaces/compensations.ts +++ b/studio/lib/interfaces/compensations.ts @@ -1,6 +1,6 @@ import { PortableTextBlock, Reference } from "sanity"; -import { Slug } from "./global"; +import { SeoObject } from "./seo"; export interface Benefit { _type: string; @@ -77,12 +77,14 @@ export interface CompensationsPage { _rev: string; _type: string; _updatedAt: string; + language: string; basicTitle: string; page: string; - slug: Slug; + slug: string; pensionPercent?: number; benefitsByLocation: BenefitsByLocation[]; bonusesByLocation: BonusesByLocationPage[]; salariesByLocation: SalariesByLocation[]; showSalaryCalculator: boolean; + seo: SeoObject; } diff --git a/studio/lib/interfaces/defaultSeo.ts b/studio/lib/interfaces/defaultSeo.ts deleted file mode 100644 index 8e184453e..000000000 --- a/studio/lib/interfaces/defaultSeo.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type DefaultSeo = { - _id: string; - _type: "seoFallback"; - _createdAt: string; - _updatedAt: string; - _rev: string; - seo?: { - seoTitle?: string; - seoDescription?: string; - seoKeywords?: string; - seoImageUrl?: string; - }; -}; diff --git a/studio/lib/interfaces/global.ts b/studio/lib/interfaces/global.ts index 6bc8dc31c..eb11d8418 100644 --- a/studio/lib/interfaces/global.ts +++ b/studio/lib/interfaces/global.ts @@ -8,9 +8,26 @@ export interface DocumentWithSlug { _updatedAt: string; } -export type InternationalizedString = InternationalizedStringValue[]; +export function isInternationalizedString( + value: unknown, +): value is InternationalizedString { + return ( + Array.isArray(value) && + value.every( + (item) => + typeof item === "object" && + item !== null && + "_key" in item && + typeof item._key === "string" && + "value" in item && + typeof item.value === "string", + ) + ); +} + +export type InternationalizedString = InternationalizedStringRecord[]; -export interface InternationalizedStringValue { +export interface InternationalizedStringRecord { _key: string; - value?: string; + value: string; } diff --git a/studio/lib/interfaces/navigation.ts b/studio/lib/interfaces/navigation.ts index f1cc5e179..221a5407e 100644 --- a/studio/lib/interfaces/navigation.ts +++ b/studio/lib/interfaces/navigation.ts @@ -1,22 +1,23 @@ -import { InternationalizedString } from "./global"; +import { Reference } from "sanity"; export interface Navigation { _id: string; + language: string; main: ILink[]; sidebar?: ILink[]; footer?: FooterSection[]; } -interface Reference { +interface InternalLinkSlug { _ref: string; } export interface ILink { _key: string; _type: string; - linkTitle: string | InternationalizedString; + linkTitle: string; linkType: LinkType; - internalLink?: Reference; + internalLink?: InternalLinkSlug; url?: string; email?: string; phone?: string; diff --git a/studio/lib/interfaces/pages.ts b/studio/lib/interfaces/pages.ts index b6615dc94..8e37d54b8 100644 --- a/studio/lib/interfaces/pages.ts +++ b/studio/lib/interfaces/pages.ts @@ -99,6 +99,7 @@ export interface PageBuilder { _rev: string; _type: string; _updatedAt: string; + language: string; page: string; sections: Section[]; slug: Slug; diff --git a/studio/lib/interfaces/seo.ts b/studio/lib/interfaces/seo.ts new file mode 100644 index 000000000..59c5c4f12 --- /dev/null +++ b/studio/lib/interfaces/seo.ts @@ -0,0 +1,15 @@ +export interface SeoObject { + seoTitle?: string; + seoDescription?: string; + seoKeywords?: string; + seoImageUrl?: string; +} + +export type DefaultSeo = { + _id: string; + _type: "seoFallback"; + _createdAt: string; + _updatedAt: string; + _rev: string; + seo?: SeoObject; +}; diff --git a/studio/lib/queries/admin.ts b/studio/lib/queries/admin.ts index 5f0f44601..6a6c363b9 100644 --- a/studio/lib/queries/admin.ts +++ b/studio/lib/queries/admin.ts @@ -11,4 +11,4 @@ export const COMPANY_LOCATIONS_QUERY = groq`*[_type == "companyLocation"]`; //Legal Documents export const LEGAL_DOCUMENTS_BY_LANG_QUERY = groq`*[_type == "legalDocument" && language == $language]`; -export const LEGAL_DOCUMENTS_BY_SLUG_AND_LANG_QUERY = groq`*[_type == "legalDocument" && language == $language && slug.current == $slug][0]`; +export const LEGAL_DOCUMENT_BY_SLUG_AND_LANG_QUERY = groq`*[_type == "legalDocument" && language == $language && slug.current == $slug][0]`; diff --git a/studio/lib/queries/pages.ts b/studio/lib/queries/pages.ts index 75a300223..2257ef5ff 100644 --- a/studio/lib/queries/pages.ts +++ b/studio/lib/queries/pages.ts @@ -1,6 +1,38 @@ import { groq } from "next-sanity"; -// TODO: set all sectons.ref's to slug.current +export const LANGUAGE_FIELD_FRAGMENT = groq` + "language": $language +`; + +export function translatedFieldFragment(fieldName: string) { + return groq`${fieldName}[_key == $language][0].value`; +} + +export const TRANSLATED_LINK_TITLE_FRAGMENT = groq` + "linkTitle": ${translatedFieldFragment("linkTitle")} +`; + +export const TRANSLATED_SLUG_VALUE_FRAGMENT = groq` + select( + slug._type == "slug" => slug.current, + ${translatedFieldFragment("slug")} + ) +`; + +export const TRANSLATED_INTERNAL_LINK_FRAGMENT = groq` + ...select(linkType == "internal" => { + "internalLink": internalLink->{ + "_ref": ${TRANSLATED_SLUG_VALUE_FRAGMENT} + } + }) +`; + +export const TRANSLATED_LINK_FRAGMENT = groq` + ${LANGUAGE_FIELD_FRAGMENT}, + ${TRANSLATED_LINK_TITLE_FRAGMENT}, + ${TRANSLATED_INTERNAL_LINK_FRAGMENT} +`; + const SECTIONS_FRAGMENT = groq` sections[]{ ..., @@ -8,80 +40,66 @@ const SECTIONS_FRAGMENT = groq` ..., callToActions[] { ..., - linkType == "internal" => { - ..., - "internalLink": internalLink->{ - "_ref": slug.current - } - } + ${TRANSLATED_LINK_FRAGMENT} } }, _type == "article" => { ..., link { ..., - linkType == "internal" => { - ..., - "internalLink": internalLink->{ - "_ref": slug.current - } - } + ${TRANSLATED_LINK_FRAGMENT} } }, _type == "callout" => { ..., link { ..., - linkType == "internal" => { - ..., - "internalLink": internalLink->{ - "_ref": slug.current - } - } + ${TRANSLATED_LINK_FRAGMENT} } }, - _type == "ctaSection" => { + _type == "ctaSection" => { ..., callToActions[] { ..., - linkType == "internal" => { - ..., - "internalLink": internalLink->{ - "_ref": slug.current - } - } + ${TRANSLATED_LINK_FRAGMENT} } - }, + } } `; +export const PAGE_FRAGMENT = groq` + ..., + ${LANGUAGE_FIELD_FRAGMENT}, + ${SECTIONS_FRAGMENT} +`; + export const PAGE_QUERY = groq` *[_type == "pageBuilder" && _id == $id][0]{ - ..., - ${SECTIONS_FRAGMENT} + ${PAGE_FRAGMENT} } `; -export const SEO_PAGE_QUERY = groq` +export const PAGE_SEO_QUERY = groq` *[_type == "pageBuilder" && _id == $id][0]{ - "title": seo.seoTitle, - "description": seo.seoDescription, - "imageUrl": seo.seoImage.asset->url -}`; + "title": seo.seoTitle, + "description": seo.seoDescription, + "imageUrl": seo.seoImage.asset->url + } +`; -export const SLUG_QUERY = groq` +export const PAGE_BY_SLUG_QUERY = groq` *[_type == "pageBuilder" && slug.current == $slug][0]{ - ..., - ${SECTIONS_FRAGMENT} + ${PAGE_FRAGMENT} } `; export const SEO_SLUG_QUERY = groq` *[defined(seo) && slug.current == $slug][0]{ - "title": seo.seoTitle, - "description": seo.seoDescription, - "imageUrl": seo.seoImage.asset->url -}`; + "title": seo.seoTitle, + "description": seo.seoDescription, + "imageUrl": seo.seoImage.asset->url + } +`; export const BLOG_PAGE_QUERY = groq` *[_type == "blog" && slug.current == $slug][0] @@ -105,10 +123,10 @@ export const POST_SLUG_QUERY = groq` export const SEO_POST_SLUG_QUERY = groq` *[_type == "blogPosts" && slug.current == $id][0]{ - "title": basicTitle, - "description": richText, - "imageUrl": image.asset->url -} + "title": basicTitle, + "description": richText, + "imageUrl": image.asset->url + } `; export const MORE_POST_PREVIEW = groq` diff --git a/studio/lib/queries/siteSettings.ts b/studio/lib/queries/siteSettings.ts index 99cab8822..8bf907aff 100644 --- a/studio/lib/queries/siteSettings.ts +++ b/studio/lib/queries/siteSettings.ts @@ -1,5 +1,12 @@ import { groq } from "next-sanity"; +import { + LANGUAGE_FIELD_FRAGMENT, + PAGE_FRAGMENT, + TRANSLATED_LINK_FRAGMENT, + TRANSLATED_SLUG_VALUE_FRAGMENT, +} from "./pages"; + //Brand Assets export const BRAND_ASSETS_QUERY = groq` *[_type == "brandAssets" && _id == "brandAssets"][0] @@ -8,38 +15,24 @@ export const BRAND_ASSETS_QUERY = groq` //Navigation Manager export const NAV_QUERY = groq` *[_type == "navigationManager"][0]{ + ${LANGUAGE_FIELD_FRAGMENT}, "main": main[] { ..., - linkType == "internal" => { - ..., - "internalLink": internalLink->{ - "_ref": slug.current - } - } + ${TRANSLATED_LINK_FRAGMENT} }, "footer": footer[] { ..., linksAndContent[] { ..., - linkType == "internal" => { - ..., - "internalLink": internalLink->{ - "_ref": slug.current - } - } + ${TRANSLATED_LINK_FRAGMENT} }, socialMediaLinks->{ - "_ref": slug.current + "_ref": ${TRANSLATED_SLUG_VALUE_FRAGMENT} } }, "sidebar": sidebar[] { ..., - linkType == "internal" => { - ..., - "internalLink": internalLink->{ - "_ref": slug.current - } - } + ${TRANSLATED_LINK_FRAGMENT} } } `; @@ -49,7 +42,9 @@ export const LANDING_PAGE_REF_QUERY = groq` `; export const LANDING_PAGE_QUERY = groq` - *[_type == "navigationManager"][0].setLanding -> + *[_type == "navigationManager"][0].setLanding -> { + ${PAGE_FRAGMENT} + } `; //Social Media Profiles @@ -58,15 +53,21 @@ export const SOME_PROFILES_QUERY = groq` `; //Languages -export const LANGUAGES_QUERY = groq`*[_type == "languageSettings" && _id == "languageSettings"][0].languages`; -export const DEFAULT_LANGUAGE_QUERY = groq`*[_type == "languageSettings" && _id == "languageSettings"][0].languages[default][0]`; +export const LANGUAGES_QUERY = groq` + *[_type == "languageSettings" && _id == "languageSettings"][0].languages +`; +export const DEFAULT_LANGUAGE_QUERY = groq` + *[_type == "languageSettings" && _id == "languageSettings"][0].languages[default][0] +`; //Default SEO -export const DEFAULT_SEO_QUERY = groq`*[_type == "seoFallback"]{ +export const DEFAULT_SEO_QUERY = groq` + *[_type == "seoFallback"][0]{ seo { seoTitle, seoDescription, seoKeywords, "seoImageUrl": seoImage.asset->url } - }[0]`; + } +`; diff --git a/studio/lib/queries/slugTranslations.ts b/studio/lib/queries/slugTranslations.ts index b2e636523..8f970a135 100644 --- a/studio/lib/queries/slugTranslations.ts +++ b/studio/lib/queries/slugTranslations.ts @@ -1,14 +1,34 @@ import { groq } from "next-sanity"; +import { translatedFieldFragment } from "./pages"; + +export const SLUG_FIELD_TRANSLATIONS_FROM_LANGUAGE_QUERY = groq` + *[${translatedFieldFragment("slug")} == $slug][0] { + "_translations": slug[] { + "language": _key, + "slug": value + } + } +`; + export const SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY = groq` *[slug.current == $slug && language == $language][0]{ "_translations": *[ _type == "translation.metadata" && references(^._id) - ].translations[].value->{ + ].translations[].value->{ language, - "slug": slug.current, - }, + "slug": slug.current + } + } +`; + +export const SLUG_FIELD_TRANSLATIONS_TO_LANGUAGE_QUERY = groq` + *[defined(slug[value == $slug][0])][0] { + "_translations": (slug[] { + "language": _key, + "slug": value + })[language == $language] } `; @@ -20,8 +40,8 @@ export const SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY = groq` && references(^._id) ].translations[].value->{ language, - "slug": slug.current, + "slug": slug.current } - )[language == $language], + )[language == $language] } `; diff --git a/studio/lib/queries/specialPages.ts b/studio/lib/queries/specialPages.ts index a898c4f97..fc8bd24b7 100644 --- a/studio/lib/queries/specialPages.ts +++ b/studio/lib/queries/specialPages.ts @@ -1,8 +1,24 @@ import { groq } from "next-sanity"; +import { translatedFieldFragment } from "./pages"; + //Compensations export const COMPENSATIONS_PAGE_QUERY = groq` - *[_type == "compensations" && slug.current == $slug][0] + *[_type == "compensations" && ${translatedFieldFragment("slug")} == $slug][0] { + ..., + "language": $language, + "slug": ${translatedFieldFragment("slug")}, + "basicTitle": ${translatedFieldFragment("basicTitle")}, + "benefitsByLocation": benefitsByLocation[] { + ..., + "benefits": benefits[] { + ..., + "basicTitle": ${translatedFieldFragment("basicTitle")}, + "richText": ${translatedFieldFragment("richText")} + } + }, + "seo": ${translatedFieldFragment("seo")} + } `; //Customer Cases diff --git a/studio/schemas/documents/compensations.ts b/studio/schemas/documents/compensations.ts index 32ce0e47d..9f3d75ee3 100644 --- a/studio/schemas/documents/compensations.ts +++ b/studio/schemas/documents/compensations.ts @@ -1,11 +1,13 @@ import { defineField, defineType } from "sanity"; +import { isInternationalizedString } from "studio/lib/interfaces/global"; import { title, titleID } from "studio/schemas/fields/text"; import { benefitsByLocation } from "studio/schemas/objects/compensations/benefitsByLocation"; import { bonusesByLocation } from "studio/schemas/objects/compensations/bonusesByLocation"; import { pensionPercent } from "studio/schemas/objects/compensations/pension"; import { salariesByLocation } from "studio/schemas/objects/compensations/salariesByLocation"; import { titleSlug } from "studio/schemas/schemaTypes/slug"; +import { firstTranslation } from "studio/utils/i18n"; export const compensationsId = "compensations"; @@ -46,6 +48,14 @@ const compensations = defineType({ select: { title: title.name, }, + prepare({ title }) { + const translatedTitle = isInternationalizedString(title) + ? firstTranslation(title) + : null; + return { + title: translatedTitle ?? "Missing title", + }; + }, }, }); diff --git a/studio/schemas/fields/callToActionFields.ts b/studio/schemas/fields/callToActionFields.ts index 081e491b4..045d15858 100644 --- a/studio/schemas/fields/callToActionFields.ts +++ b/studio/schemas/fields/callToActionFields.ts @@ -1,6 +1,8 @@ import { defineField } from "sanity"; +import { isInternationalizedString } from "studio/lib/interfaces/global"; import { link } from "studio/schemas/objects/link"; +import { firstTranslation } from "studio/utils/i18n"; import { clearLinkFields } from "./clearLinkFields"; @@ -16,8 +18,11 @@ const callToActionField = defineField({ title: "linkTitle", }, prepare({ title }) { + const translatedTitle = isInternationalizedString(title) + ? firstTranslation(title) + : null; return { - title: title, + title: translatedTitle ?? "Missing title", subtitle: "Call to Action", }; }, diff --git a/studio/schemas/objects/compensations/benefitsByLocation.ts b/studio/schemas/objects/compensations/benefitsByLocation.ts index 64b1d57e4..c8dcd3b64 100644 --- a/studio/schemas/objects/compensations/benefitsByLocation.ts +++ b/studio/schemas/objects/compensations/benefitsByLocation.ts @@ -1,8 +1,10 @@ import { defineField } from "sanity"; +import { isInternationalizedString } from "studio/lib/interfaces/global"; import { companyLocationNameID } from "studio/schemas/documents/admin/companyLocation"; import { richTextID, titleID } from "studio/schemas/fields/text"; import { location, locationID } from "studio/schemas/objects/locations"; +import { firstTranslation } from "studio/utils/i18n"; import { DocumentWithLocation, @@ -78,11 +80,14 @@ export const benefitsByLocation = defineField({ type: benefitType.name, }, prepare({ title, type }) { + const translatedTitle = isInternationalizedString(title) + ? firstTranslation(title) + : null; const subtitle = BENEFIT_TYPES.find((o) => o.value === type)?.title ?? "Unknown benefit type"; return { - title: title[0]?.value, + title: translatedTitle ?? "Missing title", subtitle, }; }, diff --git a/studio/schemas/objects/link.ts b/studio/schemas/objects/link.ts index b68294b9b..199324d0e 100644 --- a/studio/schemas/objects/link.ts +++ b/studio/schemas/objects/link.ts @@ -3,6 +3,8 @@ import { defineField } from "sanity"; import AnchorSelect from "studio/components/AnchorSelect"; import LinkTypeSelector from "studio/components/LinkTypeSelector"; import NewTabSelector from "studio/components/NewTabSelector"; +import { isInternationalizedString } from "studio/lib/interfaces/global"; +import { firstTranslation } from "studio/utils/i18n"; export const linkID = "link"; @@ -190,10 +192,12 @@ export const link = defineField({ title: "linkTitle", type: "linkType", }, - prepare(selection) { - const { title, type } = selection; + prepare({ title, type }) { + const translatedTitle = isInternationalizedString(title) + ? firstTranslation(title) + : null; return { - title: title[0].value, + title: translatedTitle ?? "Missing title", subtitle: type ? type.charAt(0).toUpperCase() + type.slice(1) : "", }; }, diff --git a/studio/utils/i18n.ts b/studio/utils/i18n.ts new file mode 100644 index 000000000..7b63d5bbf --- /dev/null +++ b/studio/utils/i18n.ts @@ -0,0 +1,10 @@ +import { InternationalizedString } from "studio/lib/interfaces/global"; + +export function firstTranslation( + translatedString: InternationalizedString, +): string | null { + if (translatedString.length === 0) { + return null; + } + return translatedString[0].value; +}