Skip to content
This repository has been archived by the owner on Jan 28, 2025. It is now read-only.

fix(core): allow lowercase locale prefixes #1496

Merged
merged 3 commits into from
Aug 4, 2021
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 @@ -7,7 +7,9 @@ describe("Pages Tests", () => {
[
{ path: "/ssr-page" },
{ path: "/en/ssr-page" },
{ path: "/fr/ssr-page" }
{ path: "/fr/ssr-page" },
{ path: "/en-GB/ssr-page" },
{ path: "/en-gb/ssr-page" }
].forEach(({ path }) => {
it(`serves but does not cache page ${path}`, () => {
cy.ensureRouteNotCached(path);
Expand Down Expand Up @@ -37,7 +39,9 @@ describe("Pages Tests", () => {
[
{ path: "/ssr-page-2", locale: "en" },
{ path: "/en/ssr-page-2", locale: "en" },
{ path: "/fr/ssr-page-2", locale: "fr" }
{ path: "/fr/ssr-page-2", locale: "fr" },
{ path: "/en-GB/ssr-page-2", locale: "en-GB" },
{ path: "/en-gb/ssr-page-2", locale: "en-GB" }
].forEach(({ locale, path }) => {
it(`serves but does not cache page ${path}`, () => {
if (path === "/") {
Expand Down Expand Up @@ -75,10 +79,11 @@ describe("Pages Tests", () => {

describe("SSG pages", () => {
[
{ path: "/ssg-page" },
{ path: "/en/ssg-page" },
{ path: "/fr/ssg-page" }
].forEach(({ path }) => {
{ path: "/ssg-page", locale: "en" },
{ path: "/en/ssg-page", locale: "en" },
{ path: "/fr/ssg-page", locale: "fr" },
{ path: "/en-GB/ssg-page", locale: "en-GB" }
].forEach(({ path, locale }) => {
it(`serves and caches page ${path}`, () => {
cy.visit(path);

Expand All @@ -92,6 +97,8 @@ describe("Pages Tests", () => {

cy.ensureRouteCached(path);
cy.visit(path);

cy.get("[data-cy=locale]").contains(locale);
});

it(`supports preview mode ${path}`, () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e-tests/next-app-with-locales/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ module.exports = {
];
},
i18n: {
locales: ["en", "fr"],
locales: ["en", "en-GB", "fr"],
defaultLocale: "en"
}
};
16 changes: 11 additions & 5 deletions packages/e2e-tests/next-app-with-locales/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import React from "react";
import { NextPageContext } from "next";
import { useRouter } from "next/router";

type IndexPageProps = {
name: string;
};

export default function IndexPage(props: IndexPageProps): JSX.Element {
const {
query: { segments = [] },
locale
} = useRouter();
return (
<React.Fragment>
<div>
{`Hello ${props.name}. This is an SSG page using getStaticProps(). It also has an image.`}
<p>
{`Hello ${props.name}. This is an SSG page using getStaticProps(). It also has an image.`}
</p>
<p data-cy="locale">{locale}</p>
<p data-cy="segments">{segments}</p>
</div>
<img src={"/app-store-badge.png"} alt={"An image"} />
</React.Fragment>
);
}

export async function getStaticProps(
ctx: NextPageContext
): Promise<{ props: IndexPageProps }> {
export function getStaticProps(): { props: IndexPageProps } {
return {
props: { name: "serverless-next.js" }
};
Expand Down
13 changes: 10 additions & 3 deletions packages/e2e-tests/next-app-with-locales/pages/ssg-page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import React from "react";
import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";

type SSGPageProps = {
name: string;
preview: boolean;
};

export default function SSGPage(props: any): JSX.Element {
const {
query: { segments = [] },
locale
} = useRouter();
return (
<React.Fragment>
{`Hello ${props.name}! This is an SSG Page using getStaticProps().`}
<div>
<p data-cy="preview-mode">{String(props.preview)}</p>
</div>
<p data-cy="locale">{locale}</p>
<p data-cy="segments">{segments}</p>
</React.Fragment>
);
}

export async function getStaticProps(
ctx: GetStaticPropsContext
): Promise<{ props: SSGPageProps }> {
export function getStaticProps(ctx: GetStaticPropsContext): {
props: SSGPageProps;
} {
return {
props: {
name: "serverless-next.js",
Expand Down
41 changes: 25 additions & 16 deletions packages/libs/core/src/route/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,17 @@ export function addDefaultLocaleToPath(
? routesManifest.basePath
: "";

// If prefixed with a locale, return that path
// If prefixed with a locale, return that path with normalized locale
const pathLowerCase = path.toLowerCase();
for (const locale of locales) {
if (
path === `${basePath}/${locale}` ||
path.startsWith(`${basePath}/${locale}/`)
pathLowerCase === `${basePath}/${locale}`.toLowerCase() ||
pathLowerCase.startsWith(`${basePath}/${locale}/`.toLowerCase())
) {
return typeof forceLocale === "string"
? path.replace(`${locale}/`, `${forceLocale}/`)
: path;
return path.replace(
new RegExp(`${basePath}/${locale}`, "i"),
`${basePath}/${forceLocale ?? locale}`
);
}
}

Expand All @@ -62,16 +64,17 @@ export function dropLocaleFromPath(
routesManifest: RoutesManifest
): string {
if (routesManifest.i18n) {
const pathLowerCase = path.toLowerCase();
const locales = routesManifest.i18n.locales;

// If prefixed with a locale, return path without
for (const locale of locales) {
const prefix = `/${locale}`;
if (path === prefix) {
const prefixLowerCase = `/${locale.toLowerCase()}`;
if (pathLowerCase === prefixLowerCase) {
return "/";
}
if (path.startsWith(`${prefix}/`)) {
return `${path.slice(prefix.length)}`;
if (pathLowerCase.startsWith(`${prefixLowerCase}/`)) {
return `${pathLowerCase.slice(prefixLowerCase.length)}`;
}
}
}
Expand All @@ -87,9 +90,10 @@ export const getAcceptLanguageLocale = async (
if (routesManifest.i18n) {
const defaultLocaleLowerCase =
routesManifest.i18n.defaultLocale?.toLowerCase();
const locales = new Set(
routesManifest.i18n.locales.map((locale) => locale.toLowerCase())
);
const localeMap: { [key: string]: string } = {};
for (const locale of routesManifest.i18n.locales) {
localeMap[locale.toLowerCase()] = locale;
}

// Accept.language(header, locales) prefers the locales order,
// so we ask for all to find the order preferred by user.
Expand All @@ -99,8 +103,8 @@ export const getAcceptLanguageLocale = async (
if (localeLowerCase === defaultLocaleLowerCase) {
break;
}
if (locales.has(localeLowerCase)) {
return `${routesManifest.basePath}/${language}${
if (localeMap[localeLowerCase]) {
return `${routesManifest.basePath}/${localeMap[localeLowerCase]}${
manifest.trailingSlash ? "/" : ""
}`;
}
Expand All @@ -117,8 +121,13 @@ export function getLocalePrefixFromUri(
}

if (routesManifest.i18n) {
const uriLowerCase = uri.toLowerCase();
for (const locale of routesManifest.i18n.locales) {
if (uri === `/${locale}` || uri.startsWith(`/${locale}/`)) {
const localeLowerCase = locale.toLowerCase();
if (
uriLowerCase === `/${localeLowerCase}` ||
uriLowerCase.startsWith(`/${localeLowerCase}/`)
) {
return `/${locale}`;
}
}
Expand Down
24 changes: 15 additions & 9 deletions packages/libs/core/tests/route/locale.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Headers, Manifest, RoutesManifest } from "../../src";
import { Manifest, RoutesManifest } from "../../src";
import {
addDefaultLocaleToPath,
dropLocaleFromPath,
Expand All @@ -19,19 +19,21 @@ describe("Locale Utils Tests", () => {
redirects: [],
rewrites: [],
i18n: {
locales: ["en", "fr", "nl"],
locales: ["en", "fr", "nl", "en-GB"],
defaultLocale: "en",
localeDetection: true
}
};
});

it.each`
path | forceLocale | expectedPath
${"/a"} | ${null} | ${"/en/a"}
${"/en/a"} | ${null} | ${"/en/a"}
${"/fr/a"} | ${null} | ${"/fr/a"}
${"/nl/a"} | ${"en"} | ${"/en/a"}
path | forceLocale | expectedPath
${"/a"} | ${null} | ${"/en/a"}
${"/en/a"} | ${null} | ${"/en/a"}
${"/fr/a"} | ${null} | ${"/fr/a"}
${"/en-GB/a"} | ${null} | ${"/en-GB/a"}
${"/en-gb/a"} | ${null} | ${"/en-GB/a"}
${"/nl/a"} | ${"en"} | ${"/en/a"}
`(
"changes path $path to $expectedPath",
({ path, forceLocale, expectedPath }) => {
Expand Down Expand Up @@ -157,7 +159,7 @@ describe("Locale Utils Tests", () => {
redirects: [],
rewrites: [],
i18n: {
locales: ["en", "fr"],
locales: ["en", "fr", "en-GB"],
defaultLocale: "en",
localeDetection: true
}
Expand All @@ -168,6 +170,7 @@ describe("Locale Utils Tests", () => {
path | expectedPath
${"/en"} | ${"/"}
${"/en/test"} | ${"/test"}
${"/en-GB/test"} | ${"/test"}
${"/fr/api/foo"} | ${"/api/foo"}
`("changes path $path to $expectedPath", ({ path, expectedPath }) => {
const newPath = dropLocaleFromPath(path, routesManifest);
Expand All @@ -179,6 +182,7 @@ describe("Locale Utils Tests", () => {
path
${"/base/en"} | ${"/base"}
${"/base/en/test"} | ${"/base/test"}
${"/base/en-GB/test"} | ${"/base/test"}
${"/base/fr/api/foo"} | ${"/base/api/foo"}
`("keeps path $path unchanged", ({ path }) => {
const newPath = dropLocaleFromPath(path, routesManifest);
Expand All @@ -201,7 +205,7 @@ describe("Locale Utils Tests", () => {
redirects: [],
rewrites: [],
i18n: {
locales: ["en", "fr", "nl"],
locales: ["en", "en-GB", "fr", "nl"],
defaultLocale: "en",
localeDetection: true
}
Expand All @@ -214,6 +218,8 @@ describe("Locale Utils Tests", () => {
${"nl"} | ${"/nl/"}
${"de, fr"} | ${"/fr/"}
${"fr;q=0.7, nl;q=0.9"} | ${"/nl/"}
${"en-GB"} | ${"/en-GB/"}
${"en-gb"} | ${"/en-GB/"}
`(
"returns $expectedPath for $acceptLang",
async ({ acceptLang, expectedPath }) => {
Expand Down