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

feat: app router [CFISO-1826] #183

Merged
merged 11 commits into from
Oct 7, 2024
5 changes: 5 additions & 0 deletions src/app/[locale]/[...notFound]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { notFound } from 'next/navigation';

export default function NotFoundCatchAll() {
notFound();
}
12 changes: 8 additions & 4 deletions src/app/[locale]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { headers } from 'next/headers';
import Link from 'next/link';
import { Trans } from 'react-i18next/TransWithoutContext';

import { Container } from '@src/components/shared/container';
import initTranslations from '@src/i18n';
import { defaultLocale } from '@src/i18n/config';

export default async function NotFound() {
const { t } = await initTranslations({ locale: defaultLocale });
const headersList = headers();
const locale = headersList.get('x-next-i18n-router-locale') || defaultLocale;
const { t } = await initTranslations({ locale });

return (
<Container>
<h1 className="h2">{t('notFound.title')}</h1>
<p className="mt-4">
{t('notFound.description')}

<Link className="text-blue500" href="/" />
<Trans i18nKey="notFound.description" t={t}>
<Link className="text-blue500" href="/" />
</Trans>
</p>
</Container>
);
Expand Down
50 changes: 47 additions & 3 deletions src/components/features/language-selector/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
'use client';

import { usePathname, useRouter } from 'next/navigation';
import { SyntheticEvent } from 'react';
import { useTranslation } from 'react-i18next';

import { LanguageSelectorDesktop } from '@src/components/features/language-selector/LanguageSelectorDesktop';
import { LanguageSelectorMobile } from '@src/components/features/language-selector/LanguageSelectorMobile';
import { locales } from '@src/i18n/config';
import i18nConfig, { locales } from '@src/i18n/config';

const localeName = locale => locale.split('-')[0];

Expand All @@ -11,15 +15,55 @@ const displayName = locale =>
type: 'language',
});

const isChangeEvent = (event: SyntheticEvent): event is React.ChangeEvent<HTMLSelectElement> => {
return event.type === 'change';
};

export const LanguageSelector = () => {
const { i18n } = useTranslation();
const currentLocale = i18n.language;
const router = useRouter();
const currentPathname = usePathname();

const handleLocaleChange: React.EventHandler<React.SyntheticEvent> = e => {
let newLocale: string | undefined = undefined;

if (isChangeEvent(e)) {
newLocale = e.target.value;
}

// set cookie for next-i18n-router
const days = 30;
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `NEXT_LOCALE=${newLocale};expires=${date.toUTCString()};path=/`;
Comment on lines +36 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels strange to do this manually...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, so as an explanation: The bug you found with the language switcher was that Next.js middleware doesn't run for cached pages, and the middleware is what automatically sets the language cookie. To get around the bug we can force set the cookie and then call router.refresh() to apply it.


// redirect to the new locale path
if (currentLocale === i18nConfig.defaultLocale) {
router.push('/' + newLocale + currentPathname);
} else {
router.push(currentPathname.replace(`/${currentLocale}`, `/${newLocale}`));
}

router.refresh();
};

return locales && locales.length > 1 ? (
<>
<div className="hidden md:block">
<LanguageSelectorDesktop displayName={displayName} localeName={localeName} />
<LanguageSelectorDesktop
displayName={displayName}
onChange={handleLocaleChange}
localeName={localeName}
/>
</div>

<div className="block md:hidden">
<LanguageSelectorMobile displayName={displayName} localeName={localeName} />
<LanguageSelectorMobile
displayName={displayName}
onChange={handleLocaleChange}
localeName={localeName}
/>
</div>
</>
) : null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LanguageIcon, ChevronDownTrimmedIcon, ChevronUpTrimmedIcon } from '@contentful/f36-icons';
import { LanguageIcon, ChevronDownIcon, ChevronUpIcon } from '@contentful/f36-icons';
import { useCurrentLocale } from 'next-i18n-router/client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
Expand All @@ -23,7 +23,7 @@ const useClickOutside = (ref, setIsOpen) => {
}, [ref, setIsOpen]);
};

export const LanguageSelectorDesktop = ({ localeName, displayName }) => {
export const LanguageSelectorDesktop = ({ localeName, onChange, displayName }) => {
const currentLocale = useCurrentLocale(i18nConfig);
const menuRef = useRef<HTMLUListElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -99,9 +99,9 @@ export const LanguageSelectorDesktop = ({ localeName, displayName }) => {
<LanguageIcon width="18px" height="18px" variant="secondary" className="mr-1 ml-1" />
{localeName(currentLocale)}
{isOpen ? (
<ChevronUpTrimmedIcon variant="secondary" className="pl-1" />
<ChevronUpIcon variant="secondary" className="pl-1" />
) : (
<ChevronDownTrimmedIcon variant="secondary" className="pl-1" />
<ChevronDownIcon variant="secondary" className="pl-1" />
)}
</button>
<FocusLock disabled={!isOpen} returnFocus={true}>
Expand All @@ -128,7 +128,10 @@ export const LanguageSelectorDesktop = ({ localeName, displayName }) => {
: `/${availableLocale}${pathname}`
}
locale={availableLocale}
onClick={() => setIsOpen(false)}
onClick={event => {
onChange(event);
setIsOpen(false);
}}
>
{displayName(availableLocale).of(localeName(availableLocale))}
</Link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { LanguageIcon, CloseIcon } from '@contentful/f36-icons';
import { useCurrentLocale } from 'next-i18n-router/client';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import FocusLock from 'react-focus-lock';
import { useTranslation } from 'react-i18next';
Expand All @@ -11,15 +10,11 @@ import { twMerge } from 'tailwind-merge';
import { Portal } from '@src/components/shared/portal';
import i18nConfig, { locales } from '@src/i18n/config';

export const LanguageSelectorMobile = ({ localeName, displayName }) => {
export const LanguageSelectorMobile = ({ localeName, onChange, displayName }) => {
const currentLocale = useCurrentLocale(i18nConfig);
const pathname = usePathname();
const router = useRouter();
const { t } = useTranslation();
const [showDrawer, setShowDrawer] = useState(false);
// Try to extract and match a locale from a pattern of `/en-US/:slug`
denkristoffer marked this conversation as resolved.
Show resolved Hide resolved
const pathnameHasLocale = locales.includes(pathname.slice(1, 6));
const pathnameWithoutLocale = pathname.slice(6);

useEffect(() => {
const close = e => {
Expand Down Expand Up @@ -75,16 +70,7 @@ export const LanguageSelectorMobile = ({ localeName, displayName }) => {
<select
className="mt-2 block w-full rounded-md border border-gray300 py-2 px-2 text-sm"
defaultValue={currentLocale}
onChange={event => {
const locale = String(event.target.value);
router.push(
pathnameHasLocale
? `/${locale}${pathnameWithoutLocale}`
: `/${locale}${pathname}`,
{},
);
setShowDrawer(!showDrawer);
}}
onChange={onChange}
>
{locales?.map(availableLocale => (
<option key={availableLocale} value={availableLocale}>
Expand Down
Loading