Skip to content

Commit

Permalink
feat(i18n): language switcher + translated legal links
Browse files Browse the repository at this point in the history
  • Loading branch information
mathiazom committed Sep 26, 2024
1 parent 78ab9a4 commit 6aa3f8e
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 35 deletions.
35 changes: 11 additions & 24 deletions src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import SkipToMain from "src/components/skipToMain/SkipToMain";
import { getDraftModeInfo } from "src/utils/draftmode";
import { BrandAssets } from "studio/lib/interfaces/brandAssets";
import { CompanyInfo } from "studio/lib/interfaces/companyDetails";
import { LegalDocument } from "studio/lib/interfaces/legalDocuments";
import { Navigation } from "studio/lib/interfaces/navigation";
import { SocialMediaProfiles } from "studio/lib/interfaces/socialMedia";
import { BRAND_ASSETS_QUERY } from "studio/lib/queries/brandAssets";
import { COMPANY_INFO_QUERY } from "studio/lib/queries/companyDetails";
import { LEGAL_DOCUMENTS_BY_LANG_QUERY } from "studio/lib/queries/legalDocuments";
import { NAV_QUERY } from "studio/lib/queries/navigation";
import { SOMEPROFILES_QUERY } from "studio/lib/queries/socialMediaProfiles";
import { loadStudioQuery } from "studio/lib/store";
Expand All @@ -27,27 +25,17 @@ export default async function Layout({
}>) {
const { perspective, isDraftMode } = getDraftModeInfo();

const [
initialNav,
initialCompanyInfo,
initialSoMe,
initialLegal,
initialBrandAssets,
] = await Promise.all([
loadStudioQuery<Navigation>(NAV_QUERY, {}, { perspective }),
loadStudioQuery<CompanyInfo>(COMPANY_INFO_QUERY, {}, { perspective }),
loadStudioQuery<SocialMediaProfiles | null>(
SOMEPROFILES_QUERY,
{},
{ perspective },
),
loadStudioQuery<LegalDocument[]>(
LEGAL_DOCUMENTS_BY_LANG_QUERY,
{ language: "en" }, //TODO: replace this with selected language for the page
{ perspective },
),
loadStudioQuery<BrandAssets>(BRAND_ASSETS_QUERY, {}, { perspective }),
]);
const [initialNav, initialCompanyInfo, initialSoMe, initialBrandAssets] =
await Promise.all([
loadStudioQuery<Navigation>(NAV_QUERY, {}, { perspective }),
loadStudioQuery<CompanyInfo>(COMPANY_INFO_QUERY, {}, { perspective }),
loadStudioQuery<SocialMediaProfiles | null>(
SOMEPROFILES_QUERY,
{},
{ perspective },
),
loadStudioQuery<BrandAssets>(BRAND_ASSETS_QUERY, {}, { perspective }),
]);

const hasNavData = hasValidData(initialNav.data);
const hasCompanyInfoData = hasValidData(initialCompanyInfo.data);
Expand Down Expand Up @@ -90,7 +78,6 @@ export default async function Layout({
) : (
<Footer
navigationData={initialNav.data}
legalData={initialLegal.data}
companyInfo={initialCompanyInfo.data}
brandAssets={initialBrandAssets.data}
soMeData={initialSoMe.data}
Expand Down
22 changes: 22 additions & 0 deletions src/components/languageSwitcher/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Link from "next/link";

import Text from "src/components/text/Text";
import useLanguage from "src/utils/hooks/useLanguage";

import styles from "./languageSwitcher.module.css";

export default function LanguageSwitcher() {
const { slugTranslations } = useLanguage();
return (
<div className={styles.wrapper}>
{slugTranslations?.map(
(slugTranslation) =>
slugTranslation?.language && (
<Link key={slugTranslation.language.id} href={slugTranslation.slug}>
<Text type={"small"}>{slugTranslation.language.icon}</Text>
</Link>
),
)}
</div>
);
}
4 changes: 4 additions & 0 deletions src/components/languageSwitcher/languageSwitcher.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.wrapper {
display: flex;
gap: 1rem;
}
24 changes: 20 additions & 4 deletions src/components/navigation/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";

import CustomLink from "src/components/link/CustomLink";
import SoMeLink from "src/components/link/SoMeLink";
import Text from "src/components/text/Text";
import { useConvertSanityImageToNextImage } from "src/utils/hooks/useConvertImage";
import useLanguage from "src/utils/hooks/useLanguage";
import { fetchWithToken } from "studio/lib/fetchWithToken";
import { BrandAssets } from "studio/lib/interfaces/brandAssets";
import { CompanyInfo } from "studio/lib/interfaces/companyDetails";
import { LegalDocument } from "studio/lib/interfaces/legalDocuments";
Expand All @@ -14,6 +16,7 @@ import {
SocialMediaLink,
SocialMediaProfiles,
} from "studio/lib/interfaces/socialMedia";
import { LEGAL_DOCUMENTS_BY_LANG_QUERY } from "studio/lib/queries/legalDocuments";

import styles from "./footer.module.css";

Expand All @@ -22,22 +25,34 @@ export interface IFooter {
companyInfo: CompanyInfo;
brandAssets: BrandAssets;
soMeData: SocialMediaProfiles | null;
legalData: LegalDocument[];
}

const Footer = ({
navigationData,
companyInfo,
brandAssets,
soMeData,
legalData,
}: IFooter) => {
const renderedLogo = useConvertSanityImageToNextImage(
brandAssets?.secondaryLogo,
);

const currentYear = new Date().getFullYear();

const { defaultLanguage, language } = useLanguage();

const [legalData, setLegalData] = useState<LegalDocument[] | null>(null);

useEffect(() => {
const languageId = language?.id ?? defaultLanguage?.id;
if (languageId === undefined) {
return;
}
fetchWithToken<LegalDocument[] | null>(LEGAL_DOCUMENTS_BY_LANG_QUERY, {
language: languageId,
}).then((data) => setLegalData(data));
}, [language, defaultLanguage]);

return (
<footer className={styles.footer}>
<div className={styles.logo}>{renderedLogo}</div>
Expand All @@ -52,14 +67,15 @@ const Footer = ({
</Text>
</li>
{legalData?.map((legal) => {
const link = {
const link: ILink = {
_key: legal._id,
_type: legal._type,
linkTitle: legal.basicTitle,
linkType: LinkType.Internal,
internalLink: {
_ref: legal.slug.current,
},
language: language?.id,
};
return (
<li key={legal._id}>
Expand Down
2 changes: 0 additions & 2 deletions src/components/navigation/footer/FooterPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export default function FooterPreview({
const newCompanyInfo = useInitialData(COMPANY_INFO_QUERY, initialCompanyInfo);
const newBrandAssets = useInitialData(BRAND_ASSETS_QUERY, initialBrandAssets);
const newSoMedata = useInitialData(SOMEPROFILES_QUERY, initialSoMe);
// TODO: add legal preview
return (
newNav &&
newCompanyInfo &&
Expand All @@ -46,7 +45,6 @@ export default function FooterPreview({
companyInfo={newCompanyInfo}
brandAssets={newBrandAssets}
soMeData={newSoMedata}
legalData={[]}
/>
)
);
Expand Down
4 changes: 4 additions & 0 deletions src/components/navigation/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { FocusOn } from "react-focus-on";

import LanguageSwitcher from "src/components/languageSwitcher/LanguageSwitcher";
import CustomLink from "src/components/link/CustomLink";
import LinkButton from "src/components/linkButton/LinkButton";
import { getHref } from "src/utils/get";
Expand Down Expand Up @@ -85,6 +86,9 @@ export const Header = ({ data, assets }: IHeader) => {
)}
{renderPageLinks(links, false, pathname)}
{renderPageCTAs(ctas, false)}
<div className={styles.languageSwitcher}>
<LanguageSwitcher />
</div>
<button
aria-haspopup="true"
aria-controls={sidebarID}
Expand Down
6 changes: 6 additions & 0 deletions src/components/navigation/header/header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@
justify-content: flex-end;
}

.languageSwitcher {
@media (max-width: 1024px) {
display: none;
}
}

.mobileMenu {
flex: 1;

Expand Down
2 changes: 1 addition & 1 deletion src/utils/get.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const getHref = (link: ILink): string => {
case LinkType.Internal:
if (link.internalLink?._ref) {
try {
return `/${link.internalLink._ref}${link.anchor ? `#${link.anchor}` : ""}`;
return `${link.language ? `/${link.language}` : ""}/${link.internalLink._ref}${link.anchor ? `#${link.anchor}` : ""}`;
} catch (error) {
console.error("Error fetching page:", error);
return hash;
Expand Down
94 changes: 94 additions & 0 deletions src/utils/hooks/useLanguage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";

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/languages";
import { SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY } from "studio/lib/queries/slugTranslations";

function useSlugTranslations(
currentLanguage: LanguageObject | undefined,
slug: string,
availableLanguages: LanguageObject[] | null,
) {
const [slugTranslationsData, setSlugTranslationsData] =
useState<SlugTranslations | null>(null);

useEffect(() => {
if (currentLanguage === undefined) {
return;
}
fetchWithToken<SlugTranslations | null>(
SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY,
{
slug,
language: currentLanguage?.id,
},
).then((data) => setSlugTranslationsData(data));
}, [currentLanguage, slug]);

return (
slug === ""
? availableLanguages?.map((lang) => ({
slug: "",
language: lang.id,
}))
: slugTranslationsData?._translations
)
?.filter(
(translation) =>
translation && translation.language !== currentLanguage?.id,
)
?.map(
(translation) =>
translation && {
slug: `/${translation.language}/${translation.slug}`,
language: availableLanguages?.find(
(lang) => lang.id === translation.language,
),
},
);
}

export default function useLanguage() {
const pathname = usePathname();

const [availableLanguages, setAvailableLanguages] = useState<
LanguageObject[] | null
>(null);

const defaultLanguage = availableLanguages?.find(
(language) => language.default,
);

const language = availableLanguages?.find(
({ id }) => pathname.startsWith(`/${id}/`) || pathname === `/${id}`,
);

const currentLanguage = language ?? defaultLanguage;

const slug = pathname
.split("/")
.slice(language !== undefined ? 2 : 1)
.join("/");

const slugTranslations = useSlugTranslations(
currentLanguage,
slug,
availableLanguages,
);

useEffect(() => {
fetchWithToken<LanguageObject[] | null>(LANGUAGES_QUERY).then((data) =>
setAvailableLanguages(data),
);
}, []);

return {
language,
defaultLanguage,
availableLanguages,
slugTranslations,
};
}
1 change: 1 addition & 0 deletions studio/lib/interfaces/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ILink {
anchor?: string;
newTab?: boolean;
ariaLabel?: string;
language?: string;
}

export enum LinkType {
Expand Down
10 changes: 6 additions & 4 deletions studio/lib/interfaces/slugTranslations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export interface SlugTranslation {
language: string;
slug: string;
}

export interface SlugTranslations {
_translations: ({
language: string;
slug: string;
} | null)[];
_translations: (SlugTranslation | null)[];
}

0 comments on commit 6aa3f8e

Please sign in to comment.