Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3 - language switcher and translated legal links #719

Merged
merged 4 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ const hasValidData = (data: unknown) => data && Object.keys(data).length > 0;

export default async function Layout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: {
lang: string;
};
}>) {
const { perspective, isDraftMode } = getDraftModeInfo();

Expand All @@ -47,7 +51,7 @@ export default async function Layout({
),
loadStudioQuery<LegalDocument[]>(
LEGAL_DOCUMENTS_BY_LANG_QUERY,
{ language: "en" }, //TODO: replace this with selected language for the page
{ language: params.lang },
{ perspective },
),
loadStudioQuery<BrandAssets>(BRAND_ASSETS_QUERY, {}, { perspective }),
Expand Down Expand Up @@ -90,6 +94,8 @@ export default async function Layout({
initialCompanyInfo={initialCompanyInfo}
initialBrandAssets={initialBrandAssets}
initialSoMe={initialSoMe}
initialLegal={initialLegal}
language={params.lang}
/>
) : (
<Footer
Expand Down
6 changes: 4 additions & 2 deletions src/app/(main)/[lang]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ 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 { LANGUAGES_QUERY } from "studio/lib/queries/languages";
import { PAGE_QUERY, SEO_PAGE_QUERY } from "studio/lib/queries/pages";
import { LANDING_PAGE_REF_QUERY } from "studio/lib/queries/siteSettings";
import {
LANDING_PAGE_REF_QUERY,
LANGUAGES_QUERY,
} from "studio/lib/queries/siteSettings";
import { loadStudioQuery } from "studio/lib/store";

export async function generateMetadata(): Promise<Metadata> {
Expand Down
52 changes: 52 additions & 0 deletions src/components/languageSwitcher/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Link from "next/link";
import { Fragment } from "react";

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, language, defaultLanguage } = useLanguage();

const currentLanguage = language ?? defaultLanguage;

// make sure the current language is the first item in the languages list
const sortedTranslations = slugTranslations?.toSorted((a, b) =>
a?.language?.id === currentLanguage?.id
? -1
: b?.language?.id === currentLanguage?.id
? 1
: 0,
);

return (
<ul className={styles.list}>
{sortedTranslations?.map((slugTranslation, index) => {
if (slugTranslation?.language === undefined) {
return null;
}
const linkText = (
<Text type={"small"}>
{slugTranslation.language.id.toUpperCase()}
</Text>
);
return (
<Fragment key={slugTranslation.language.id}>
<li>
{currentLanguage === undefined ||
slugTranslation.language.id !== currentLanguage.id ? (
<Link href={slugTranslation.slug}>{linkText}</Link>
) : (
linkText
)}
</li>
{index < sortedTranslations.length - 1 && (
<span className={styles.divider}></span>
)}
</Fragment>
);
})}
</ul>
);
}
11 changes: 11 additions & 0 deletions src/components/languageSwitcher/languageSwitcher.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.list {
display: flex;
gap: 1rem;
list-style: none;
padding: 0;
}

.divider {
border-left: 1px solid var(--primary-black);
flex-grow: 1;
}
3 changes: 2 additions & 1 deletion src/components/navigation/footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,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: legal.language,
};
return (
<li key={legal._id}>
Expand Down
21 changes: 17 additions & 4 deletions src/components/navigation/footer/FooterPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { QueryResponseInitial, useQuery } from "@sanity/react-loader";

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 { COMPANY_INFO_QUERY } from "studio/lib/queries/admin";
import {
COMPANY_INFO_QUERY,
LEGAL_DOCUMENTS_BY_LANG_QUERY,
} from "studio/lib/queries/admin";
import {
BRAND_ASSETS_QUERY,
NAV_QUERY,
Expand All @@ -27,28 +31,37 @@ export default function FooterPreview({
initialCompanyInfo,
initialBrandAssets,
initialSoMe,
initialLegal,
language,
}: {
initialNav: QueryResponseInitial<Navigation>;
initialCompanyInfo: QueryResponseInitial<CompanyInfo>;
initialBrandAssets: QueryResponseInitial<BrandAssets>;
initialSoMe: QueryResponseInitial<SocialMediaProfiles | null>;
initialLegal: QueryResponseInitial<LegalDocument[] | null>;
language: string;
}) {
const newNav = useInitialData(NAV_QUERY, initialNav);
const newCompanyInfo = useInitialData(COMPANY_INFO_QUERY, initialCompanyInfo);
const newBrandAssets = useInitialData(BRAND_ASSETS_QUERY, initialBrandAssets);
const newSoMedata = useInitialData(SOME_PROFILES_QUERY, initialSoMe);
// TODO: add legal preview
const { data: newLegal } = useQuery(
LEGAL_DOCUMENTS_BY_LANG_QUERY,
{ language },
{ initial: initialLegal },
);
return (
newNav &&
newCompanyInfo &&
newBrandAssets &&
newSoMedata && (
newSoMedata &&
newLegal && (
<Footer
navigationData={newNav}
companyInfo={newCompanyInfo}
brandAssets={newBrandAssets}
soMeData={newSoMedata}
legalData={[]}
legalData={newLegal}
/>
)
);
Expand Down
5 changes: 5 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}>
mathiazom marked this conversation as resolved.
Show resolved Hide resolved
<LanguageSwitcher />
</div>
<button
aria-haspopup="true"
aria-controls={sidebarID}
Expand All @@ -102,6 +106,7 @@ export const Header = ({ data, assets }: IHeader) => {
>
{renderPageLinks(sidebarLinks, true, pathname)}
{renderPageCTAs(sidebarCtas, true)}
<LanguageSwitcher />
</div>
)}
</nav>
Expand Down
8 changes: 8 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,14 @@
justify-content: flex-end;
}

.languageSwitcher {
display: none;

@media (min-width: 1024px) {
display: flex;
}
}

.mobileMenu {
flex: 1;

Expand Down
2 changes: 1 addition & 1 deletion src/middlewares/languageMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { LanguageObject } from "studio/lib/interfaces/supportedLanguages";
import {
DEFAULT_LANGUAGE_QUERY,
LANGUAGES_QUERY,
} from "studio/lib/queries/languages";
} from "studio/lib/queries/siteSettings";
import {
SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY,
SLUG_TRANSLATIONS_TO_LANGUAGE_QUERY,
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
107 changes: 107 additions & 0 deletions src/utils/hooks/useLanguage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { usePathname } from "next/navigation";
mathiazom marked this conversation as resolved.
Show resolved Hide resolved
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/siteSettings";
import { SLUG_TRANSLATIONS_FROM_LANGUAGE_QUERY } from "studio/lib/queries/slugTranslations";

/**
* Client hook providing access to the available Sanity translations for the given slug
*
* Includes the trivial translation for the current language
*
* @param currentLanguage the language of the given slug
* @param slug slug to translate
* @param availableLanguages languages with possible slug translation
*/
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(setSlugTranslationsData);
}, [currentLanguage, slug]);

const slugTranslations =
slug === ""
? availableLanguages?.map((lang) => ({
slug: "",
language: lang.id,
}))
: slugTranslationsData?._translations;

// include full language object, not just id, in slug translation
return slugTranslations?.map(
(translation) =>
translation && {
slug: `/${translation.language}/${translation.slug}`,
language: availableLanguages?.find(
(lang) => lang.id === translation.language,
),
},
);
}

/**
* Client hook providing access to:
* - the currently selected language from URL
* - all enabled languages in Sanity
* - default language selected in Sanity
* - available translations of the current page slug
*/
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/legalDocuments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface LegalDocument {
slug: Slug;
basicTitle: string;
richText: PortableTextBlock[];
language: string;
_translations: Translations[];
}

Expand Down
1 change: 1 addition & 0 deletions studio/lib/interfaces/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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)[];
}
5 changes: 0 additions & 5 deletions studio/lib/queries/languages.ts

This file was deleted.

Loading