diff --git a/internationalization/languageSchemaField.ts b/i18n/languageSchemaField.ts similarity index 100% rename from internationalization/languageSchemaField.ts rename to i18n/languageSchemaField.ts diff --git a/internationalization/supportedLanguages.ts b/i18n/supportedLanguages.ts similarity index 100% rename from internationalization/supportedLanguages.ts rename to i18n/supportedLanguages.ts diff --git a/src/app/(main)/[lang]/[slug]/page.tsx b/src/app/(main)/[lang]/[slug]/page.tsx index 854309b26..b9fc8271f 100644 --- a/src/app/(main)/[lang]/[slug]/page.tsx +++ b/src/app/(main)/[lang]/[slug]/page.tsx @@ -21,14 +21,14 @@ 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 { LOCALE_QUERY } from "studio/lib/queries/locale"; import { BLOG_PAGE_QUERY, + PAGE_BY_SLUG_QUERY, POSTS_QUERY, SEO_SLUG_QUERY, - SLUG_QUERY, } from "studio/lib/queries/pages"; import { COMPENSATIONS_PAGE_QUERY, @@ -70,11 +70,15 @@ async function Page({ params }: Props) { initialLegalDocument, initialLocale, ] = 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( @@ -88,7 +92,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/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 0c49ad5c9..865fe79ee 100644 --- a/src/compensations/CompensationsPreview.tsx +++ b/src/compensations/CompensationsPreview.tsx @@ -25,7 +25,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/components/LanguageSelector.tsx b/studio/components/LanguageSelector.tsx index a23c0de32..973785572 100644 --- a/studio/components/LanguageSelector.tsx +++ b/studio/components/LanguageSelector.tsx @@ -2,10 +2,7 @@ import { Box, Button, Card, Checkbox, Flex, useTheme } from "@sanity/ui"; import React from "react"; import { ArrayOfObjectsInputProps, PatchEvent, set } from "sanity"; -import { - Language, - supportedLanguages, -} from "internationalization/supportedLanguages"; +import { Language, supportedLanguages } from "i18n/supportedLanguages"; const colorMap = { dark: { 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/i18n.ts b/studio/lib/queries/i18n.ts new file mode 100644 index 000000000..fdce669d3 --- /dev/null +++ b/studio/lib/queries/i18n.ts @@ -0,0 +1,28 @@ +import { groq } from "next-sanity"; + +import { translatedFieldFragment } from "studio/lib/queries/utils/i18n"; + +export const LANGUAGE_FIELD_FRAGMENT = groq` + "language": $language +`; +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} +`; diff --git a/studio/lib/queries/pages.ts b/studio/lib/queries/pages.ts index 75a300223..784bd691b 100644 --- a/studio/lib/queries/pages.ts +++ b/studio/lib/queries/pages.ts @@ -1,6 +1,7 @@ import { groq } from "next-sanity"; -// TODO: set all sectons.ref's to slug.current +import { LANGUAGE_FIELD_FRAGMENT, TRANSLATED_LINK_FRAGMENT } from "./i18n"; + const SECTIONS_FRAGMENT = groq` sections[]{ ..., @@ -8,80 +9,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 +92,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..ea7722032 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, + TRANSLATED_LINK_FRAGMENT, + TRANSLATED_SLUG_VALUE_FRAGMENT, +} from "./i18n"; +import { PAGE_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..2e29fe621 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 "./utils/i18n"; + +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..add8e9364 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 "./utils/i18n"; + //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/lib/queries/utils/i18n.ts b/studio/lib/queries/utils/i18n.ts new file mode 100644 index 000000000..70e9ab300 --- /dev/null +++ b/studio/lib/queries/utils/i18n.ts @@ -0,0 +1,5 @@ +import { groq } from "next-sanity"; + +export function translatedFieldFragment(fieldName: string) { + return groq`${fieldName}[_key == $language][0].value`; +} diff --git a/studio/schema.ts b/studio/schema.ts index b662c4053..51c3e0f4f 100644 --- a/studio/schema.ts +++ b/studio/schema.ts @@ -17,9 +17,11 @@ import socialMediaLinks from "./schemas/documents/siteSettings/socialMediaProfil import customerCasesPage from "./schemas/documents/specialPages/customerCasesPage"; import callToActionField from "./schemas/fields/callToActionFields"; import categories from "./schemas/fields/categories"; +import { richText } from "./schemas/fields/text"; import benefitsByLocation from "./schemas/objects/compensations/benefitsByLocation"; import { footerSection } from "./schemas/objects/footerSection"; import { link } from "./schemas/objects/link"; +import seo from "./schemas/objects/seo"; import { socialMedia } from "./schemas/objects/socialMedia"; export const schema: { types: SchemaTypeDefinition[] } = { @@ -45,5 +47,7 @@ export const schema: { types: SchemaTypeDefinition[] } = { brandAssets, languageSettings, locale, + richText, + seo, ], }; diff --git a/studio/schemas/documents/admin/legalDocuments.ts b/studio/schemas/documents/admin/legalDocuments.ts index 6fafb5724..0091b481c 100644 --- a/studio/schemas/documents/admin/legalDocuments.ts +++ b/studio/schemas/documents/admin/legalDocuments.ts @@ -1,7 +1,7 @@ import { defineField } from "sanity"; -import languageSchemaField from "internationalization/languageSchemaField"; -import { getLanguageById } from "internationalization/supportedLanguages"; +import languageSchemaField from "i18n/languageSchemaField"; +import { getLanguageById } from "i18n/supportedLanguages"; import { richText, title } from "studio/schemas/fields/text"; import { titleSlug } from "studio/schemas/schemaTypes/slug"; diff --git a/studio/schemas/documents/compensations.ts b/studio/schemas/documents/compensations.ts index 703567b3b..7aa300b1b 100644 --- a/studio/schemas/documents/compensations.ts +++ b/studio/schemas/documents/compensations.ts @@ -1,12 +1,13 @@ import { defineField, defineType } from "sanity"; -import { title } from "studio/schemas/fields/text"; +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 seo from "studio/schemas/objects/seo"; import { titleSlug } from "studio/schemas/schemaTypes/slug"; +import { firstTranslation } from "studio/utils/i18n"; export const compensationsId = "compensations"; @@ -16,12 +17,16 @@ const compensations = defineType({ title: "Compensations", fields: [ { - ...title, + name: titleID.basic, + type: "internationalizedArrayString", title: "Compensation Page Title", description: "Enter the primary title that will be displayed at the top of the compensation page. This is what users will see when they visit the page.", }, - titleSlug, + { + ...titleSlug, + type: "internationalizedArrayString", + }, defineField({ name: "showSalaryCalculator", title: "Show Salary Calculator", @@ -34,12 +39,25 @@ const compensations = defineType({ bonusesByLocation, benefitsByLocation, salariesByLocation, - seo, + { + name: "seo", + type: "internationalizedArraySeo", + }, ], preview: { select: { title: title.name, }, + prepare({ title }) { + if (!isInternationalizedString(title)) { + throw new TypeError( + `Expected 'title' to be InternationalizedString, was ${typeof title}`, + ); + } + return { + title: firstTranslation(title) ?? undefined, + }; + }, }, }); diff --git a/studio/schemas/documents/languageSettings.ts b/studio/schemas/documents/languageSettings.ts index b3da34c24..ee47c2f5d 100644 --- a/studio/schemas/documents/languageSettings.ts +++ b/studio/schemas/documents/languageSettings.ts @@ -1,6 +1,6 @@ import { defineType } from "sanity"; -import { defaultLanguage } from "internationalization/supportedLanguages"; +import { defaultLanguage } from "i18n/supportedLanguages"; import LanguageSelector from "studio/components/LanguageSelector"; export const languageSettingsID = "languageSettings"; diff --git a/studio/schemas/fields/callToActionFields.ts b/studio/schemas/fields/callToActionFields.ts index 081e491b4..9edf177c7 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,13 @@ const callToActionField = defineField({ title: "linkTitle", }, prepare({ title }) { + if (!isInternationalizedString(title)) { + throw new TypeError( + `Expected 'title' to be InternationalizedString, was ${typeof title}`, + ); + } return { - title: title, + title: firstTranslation(title) ?? undefined, subtitle: "Call to Action", }; }, diff --git a/studio/schemas/fields/text.ts b/studio/schemas/fields/text.ts index aac5ba766..0aca30990 100644 --- a/studio/schemas/fields/text.ts +++ b/studio/schemas/fields/text.ts @@ -2,7 +2,7 @@ import { StringRule, defineField } from "sanity"; import { StringInputWithCharacterCount } from "studio/components/stringInputWithCharacterCount/StringInputWithCharacterCount"; -enum titleID { +export enum titleID { basic = "basicTitle", optional = "optionalTitle", } diff --git a/studio/schemas/objects/compensations/benefitsByLocation.ts b/studio/schemas/objects/compensations/benefitsByLocation.ts index 23d2bb52c..3eefe6778 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 { richText, title } from "studio/schemas/fields/text"; +import { richTextID, titleID } from "studio/schemas/fields/text"; import { location, locationID } from "studio/schemas/objects/locations"; +import { firstTranslation } from "studio/utils/i18n"; import { DocumentWithLocation, @@ -60,18 +62,34 @@ export const benefitsByLocation = defineField({ name: "benefit", type: "object", title: "Benefit", - fields: [benefitType, title, richText], + fields: [ + benefitType, + { + name: titleID.basic, + type: "internationalizedArrayString", + }, + { + name: richTextID, + title: "Body", + type: "internationalizedArrayRichText", + }, + ], preview: { select: { - title: title.name, + title: titleID.basic, type: benefitType.name, }, prepare({ title, type }) { + if (!isInternationalizedString(title)) { + throw new TypeError( + `Expected 'title' to be InternationalizedString, was ${typeof title}`, + ); + } const subtitle = BENEFIT_TYPES.find((o) => o.value === type)?.title ?? "Unknown benefit type"; return { - title, + title: firstTranslation(title) ?? undefined, subtitle, }; }, diff --git a/studio/schemas/objects/link.ts b/studio/schemas/objects/link.ts index b68294b9b..abc752612 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,14 @@ export const link = defineField({ title: "linkTitle", type: "linkType", }, - prepare(selection) { - const { title, type } = selection; + prepare({ title, type }) { + if (!isInternationalizedString(title)) { + throw new TypeError( + `Expected 'title' to be InternationalizedString, was ${typeof title}`, + ); + } return { - title: title[0].value, + title: firstTranslation(title) ?? undefined, subtitle: type ? type.charAt(0).toUpperCase() + type.slice(1) : "", }; }, diff --git a/studio/studioConfig.tsx b/studio/studioConfig.tsx index 20f90e924..864d58438 100644 --- a/studio/studioConfig.tsx +++ b/studio/studioConfig.tsx @@ -6,7 +6,7 @@ import { structureTool } from "sanity/structure"; import { internationalizedArray } from "sanity-plugin-internationalized-array"; import { media } from "sanity-plugin-media"; -import { languageID } from "internationalization/languageSchemaField"; +import { languageID } from "i18n/languageSchemaField"; import StudioIcon from "./components/studioIcon/StudioIcon"; import { deskStructure } from "./deskStructure"; @@ -45,7 +45,7 @@ const config: WorkspaceOptions = { languages: (client) => { return client.fetch(SUPPORTED_LANGUAGES_QUERY); }, - fieldTypes: ["string"], + fieldTypes: ["string", "richText", "seo"], }), presentationTool({ previewUrl: { 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; +} diff --git a/studioShared/deskStructure.ts b/studioShared/deskStructure.ts index 609041027..6f3a2aa4e 100644 --- a/studioShared/deskStructure.ts +++ b/studioShared/deskStructure.ts @@ -1,6 +1,6 @@ import { StructureResolver } from "sanity/structure"; -import { defaultLanguage } from "internationalization/supportedLanguages"; +import { defaultLanguage } from "i18n/supportedLanguages"; import { customerCaseID } from "./schemas/documents/customerCase"; diff --git a/studioShared/schemas/documents/customerCase.ts b/studioShared/schemas/documents/customerCase.ts index 8b9b0806f..9e44cac94 100644 --- a/studioShared/schemas/documents/customerCase.ts +++ b/studioShared/schemas/documents/customerCase.ts @@ -1,6 +1,6 @@ import { defineField, defineType } from "sanity"; -import languageSchemaField from "internationalization/languageSchemaField"; +import languageSchemaField from "i18n/languageSchemaField"; import { richText, title } from "studio/schemas/fields/text"; import { titleSlug } from "studio/schemas/schemaTypes/slug"; diff --git a/studioShared/studioConfig.tsx b/studioShared/studioConfig.tsx index d7e0d06f4..3a0c3a6fc 100644 --- a/studioShared/studioConfig.tsx +++ b/studioShared/studioConfig.tsx @@ -4,11 +4,8 @@ import { WorkspaceOptions } from "sanity"; import { structureTool } from "sanity/structure"; import { media } from "sanity-plugin-media"; -import { languageID } from "internationalization/languageSchemaField"; -import { - defaultLanguage, - supportedLanguages, -} from "internationalization/supportedLanguages"; +import { languageID } from "i18n/languageSchemaField"; +import { defaultLanguage, supportedLanguages } from "i18n/supportedLanguages"; import StudioIcon from "studio/components/studioIcon/StudioIcon"; import { deskStructure } from "./deskStructure";