Skip to content

Commit

Permalink
feat: Set default locale without locale prefix in pathname (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
karolkarolka authored Dec 12, 2024
1 parent 0e773c5 commit a91e041
Show file tree
Hide file tree
Showing 9 changed files with 797 additions and 663 deletions.
34 changes: 34 additions & 0 deletions apps/storefront/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,40 @@ const isSentryAvailable =

/** @type {import('next').NextConfig} */
const nextConfig = withNextIntl({
redirects: async () => {
return [
{
source: "/en",
destination: "/",
permanent: true,
},
{
source: "/en/:path*",
destination: "/:path*",
permanent: true,
},
{
source: "/us/en",
destination: "/",
permanent: true,
},
{
source: "/us/en/:path*",
destination: "/:path*",
permanent: true,
},
{
source: "/us",
destination: "/",
permanent: true,
},
{
source: "/us/:path*",
destination: "/:path*",
permanent: true,
},
];
},
// TODO: add redirects to footer CMS pages (instead of /pages/slug => /slug)

env: {
Expand Down
3 changes: 3 additions & 0 deletions apps/storefront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"test:watch": "vitest --watch"
},
"dependencies": {
"@formatjs/intl-localematcher": "0.5.9",
"@hookform/resolvers": "3.6.0",
"@sentry/integrations": "^7.114.0",
"@sentry/nextjs": "^8.12.0",
Expand All @@ -28,6 +29,7 @@
"js-cookie": "3.0.5",
"lodash": "4.17.21",
"lucide-react": "^0.368.0",
"negotiator": "1.0.0",
"next": "15.0.3",
"next-auth": "5.0.0-beta.17",
"next-intl": "3.23.5",
Expand Down Expand Up @@ -57,6 +59,7 @@
"@tailwindcss/typography": "0.5.13",
"@types/js-cookie": "3.0.6",
"@types/lodash": "4.17.7",
"@types/negotiator": "0.6.3",
"@types/node": "20.14.12",
"@types/react": "19.0.0",
"@types/react-dom": "19.0.0",
Expand Down
8 changes: 6 additions & 2 deletions apps/storefront/src/components/locale-switch/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as z from "zod";

import { COOKIE_KEY } from "@/config";
import { localePrefixes } from "@/i18n/routing";
import type { Locale } from "@/regions/types";
import { DEFAULT_LOCALE, type Locale } from "@/regions/types";

const NEXT_LOCALE = "NEXT_LOCALE";

Expand All @@ -21,5 +21,9 @@ export const handleLocaleFormSubmit = async (formData: FormData) => {
cookieStore.delete(COOKIE_KEY.checkoutId);
revalidatePath("/");

redirect(localePrefixes[locale as Locale]);
redirect(
locale === DEFAULT_LOCALE
? "/"
: localePrefixes[locale as Exclude<Locale, typeof DEFAULT_LOCALE>],
);
};
16 changes: 11 additions & 5 deletions apps/storefront/src/i18n/routing.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { createNavigation } from "next-intl/navigation";
import { defineRouting } from "next-intl/routing";

import { type Locale, SUPPORTED_LOCALES } from "@/regions/types";
import {
DEFAULT_LOCALE,
type Locale,
SUPPORTED_LOCALES,
} from "@/regions/types";

export const localePrefixes: Record<Locale, string> = {
export const localePrefixes: Record<
Exclude<Locale, typeof DEFAULT_LOCALE>,
string
> = {
"en-GB": "/gb",
"en-US": "/us",
};

export const routing = defineRouting({
locales: SUPPORTED_LOCALES,
defaultLocale: "en-GB",
defaultLocale: DEFAULT_LOCALE,
localePrefix: {
mode: "always",
mode: "as-needed",
prefixes: localePrefixes,
},
});
Expand Down
4 changes: 0 additions & 4 deletions apps/storefront/src/lib/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import type { UrlObject } from "url";

import { loggingService } from "@nimara/infrastructure/logging/service";

import { type DeepObjectKeys } from "./types";

// UrlObject accepted by next `<Link />` component.
export type UrlOpts = Omit<UrlObject, "query"> & {
query?: Record<string, string | number>;
Expand Down Expand Up @@ -141,8 +139,6 @@ export const paths = {
},
};

export type Paths = DeepObjectKeys<typeof paths>;

export const QUERY_PARAMS = {
orderPlace: "orderPlaced",
errorCode: "errorCode",
Expand Down
6 changes: 0 additions & 6 deletions apps/storefront/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,3 @@ import { type Region } from "@/regions/types";
export type Maybe<T> = T | null | undefined;

export type WithRegion = { region: Region };

export type DeepObjectKeys<T> = T extends object
? {
[K in keyof T]: `${Exclude<K, symbol>}${"" | `.${DeepObjectKeys<T[K]>}`}`;
}[keyof T]
: never;
60 changes: 44 additions & 16 deletions apps/storefront/src/middlewares/i18nMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,66 @@
import { type NextFetchEvent, type NextRequest } from "next/server";
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
import type { NextFetchEvent, NextRequest } from "next/server";
import createIntlMiddleware from "next-intl/middleware";

import { COOKIE_KEY } from "@/config";
import { routing } from "@/i18n/routing";
import { LOCALE_CHANNEL_MAP } from "@/regions/config";
import { type Locale } from "@/regions/types";
import { localePrefixes, routing } from "@/i18n/routing";
import {
DEFAULT_LOCALE,
type Locale,
SUPPORTED_LOCALES,
} from "@/regions/types";

import type { CustomMiddleware } from "./chain";

const NEXT_LOCALE = "NEXT_LOCALE";

function getLocale(request: NextRequest) {
const languages = new Negotiator({
headers: {
"accept-language": request.headers.get("accept-language") ?? undefined,
},
}).languages();

return match(languages, SUPPORTED_LOCALES, DEFAULT_LOCALE);
}

export function i18nMiddleware(middleware: CustomMiddleware) {
return async (request: NextRequest, event: NextFetchEvent) => {
const nextLocaleCookie = request.cookies.get(NEXT_LOCALE)?.value ?? "en-GB";
const channelId = request.nextUrl.pathname.split("/")[1];
const locale =
Object.keys(LOCALE_CHANNEL_MAP).find(
(key) => LOCALE_CHANNEL_MAP[key as Locale] === channelId,
) ?? nextLocaleCookie;
const prefferedLocale = getLocale(request);
const nextLocaleCookie = request.cookies.get(NEXT_LOCALE)?.value;
const pathname = request.nextUrl.pathname;
const localePrefix = Object.values(localePrefixes).find(
(localePrefix) =>
pathname.startsWith(localePrefix) || pathname === localePrefix,
);
const isLocalePrefixedPathname = !!localePrefix;

if (locale !== nextLocaleCookie) {
request.cookies.delete(COOKIE_KEY.checkoutId);
}
let locale = prefferedLocale;

const handleI18nRouting = createIntlMiddleware(routing);

const response = handleI18nRouting(request);

if (locale !== nextLocaleCookie) {
response.cookies.delete(COOKIE_KEY.checkoutId);
// INFO: All routes have locale prefixes except for default locale/domain - "/".
// If the user types only domain name it should be navigated to preffered region of the store,
// otherwise navigate to the requested locale prefixed pathname
if (isLocalePrefixedPathname) {
locale =
Object.keys(localePrefixes).find(
(key) =>
localePrefixes[key as Exclude<Locale, typeof DEFAULT_LOCALE>] ===
localePrefix,
) ?? DEFAULT_LOCALE;
}

// INFO: Store the locale in the cookie to know if the locale has changed between requests
response.cookies.set(NEXT_LOCALE, locale);

if (locale !== nextLocaleCookie) {
request.cookies.delete(COOKIE_KEY.checkoutId);
response.cookies.delete(COOKIE_KEY.checkoutId);
}

return middleware(request, event, response);
};
}
1 change: 1 addition & 0 deletions apps/storefront/src/regions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type LanguageId = (typeof SUPPORTED_LANGUAGES)[number];
*/
export const SUPPORTED_LOCALES = ["en-GB", "en-US"] as const;
export type Locale = (typeof SUPPORTED_LOCALES)[number];
export const DEFAULT_LOCALE = "en-US" as const;

/**
* Defines available markets in the App.
Expand Down
Loading

0 comments on commit a91e041

Please sign in to comment.