Skip to content

Commit

Permalink
feat(i18n): take account route translations during redirects
Browse files Browse the repository at this point in the history
  • Loading branch information
aralroca committed Oct 5, 2023
1 parent 612aa11 commit be5ecce
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export default {
},
"example.nl": {
defaultLocale: "nl-NL",
protocol: "http", // by default is https
dev: true, // by default is false
},
},
};
Expand Down Expand Up @@ -540,9 +542,31 @@ export default {

Now `t('hello')` returns `"hello"` instead of an empty string `""`.

## Translate the page route
## Translate page pathname

TODO
Many times we want the URL to be different in different languages. For example:

- `/en/about-us``src/pages/about-us.tsx`
- `/es/sobre-nosotros``src/pages/about-us.tsx`

```js filename="src/i18n.js"
export default {
locales: ["en-US", "es"],
defaultLocale: "en-US",
pages: {
"/about-us": {
es: "/sobre-nosotros",
},
"/user/[username]": {
es: "/usuario/[username]",
},
},
};
```

The key of each page item will be the name of the route. It works also with dynamic and catch-all routes.

It will automatically be taken into account in redirects, navigation and the `hrefLang` generation (see [here](#activate-automatic-hreflang) how to active `hrefLang`).

## Transition between locales

Expand Down
71 changes: 71 additions & 0 deletions src/cli/serve/serve-options.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,77 @@ describe("CLI: serve", () => {
);
});

it("should redirect to the correct browser locale changing the subdomain and the page route name", async () => {
globalThis.mockConstants = {
...globalThis.mockConstants,
IS_PRODUCTION: true,
I18N_CONFIG: {
locales: ["en", "es"],
defaultLocale: "es",
domains: {
"en.test.com": {
defaultLocale: "en",
},
"es.test.com": {
defaultLocale: "es",
},
},
pages: {
"/somepage": {
en: "/somepage-en",
},
},
},
};

const req = new Request("https://es.test.com/somepage");

req.headers.set("Accept-Language", "en-US,en;q=0.5");

const response = await testRequest(req);
expect(response.status).toBe(301);
expect(response.headers.get("Location")).toBe(
"https://en.test.com/en/somepage-en",
);
});

it("should redirect to the correct browser locale changing the subdomain, adding trailing slash and translating the route name", async () => {
globalThis.mockConstants = {
...globalThis.mockConstants,
IS_PRODUCTION: true,
CONFIG: {
trailingSlash: true,
},
I18N_CONFIG: {
locales: ["en", "es"],
defaultLocale: "es",
domains: {
"en.test.com": {
defaultLocale: "en",
},
"es.test.com": {
defaultLocale: "es",
},
},
pages: {
"/somepage": {
en: "/somepage-en",
},
},
},
};

const req = new Request("https://es.test.com/somepage");

req.headers.set("Accept-Language", "en-US,en;q=0.5");

const response = await testRequest(req);
expect(response.status).toBe(301);
expect(response.headers.get("Location")).toBe(
"https://en.test.com/en/somepage-en/",
);
});

it("should redirect to the correct browser locale without changing the subdomain in development", async () => {
globalThis.mockConstants = {
...globalThis.mockConstants,
Expand Down
72 changes: 72 additions & 0 deletions src/utils/handle-i18n/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,42 @@ describe("handleI18n util", () => {
expect(response?.headers.get("Location")).toBe("/en/somepage");
});

it("should redirect to the correct browser locale changing the subdomain and the page route name", async () => {
globalThis.mockConstants = {
...globalThis.mockConstants,
IS_PRODUCTION: true,
I18N_CONFIG: {
locales: ["en", "es"],
defaultLocale: "es",
domains: {
"en.test.com": {
defaultLocale: "en",
},
"es.test.com": {
defaultLocale: "es",
},
},
pages: {
"/somepage": {
en: "/somepage-en",
},
},
},
};

const req = extendRequestContext({
originalRequest: new Request("https://es.test.com/somepage"),
});

req.headers.set("Accept-Language", "en-US,en;q=0.5");

const { response } = handleI18n(req);
expect(response?.status).toBe(301);
expect(response?.headers.get("Location")).toBe(
"https://en.test.com/en/somepage-en",
);
});

it("should redirect to the correct default locale of the subdomain", async () => {
globalThis.mockConstants = {
...globalThis.mockConstants,
Expand Down Expand Up @@ -403,6 +439,42 @@ describe("handleI18n util", () => {
);
});

it("should redirect to the correct browser locale changing the subdomain and the page route name", async () => {
globalThis.mockConstants = {
...globalThis.mockConstants,
IS_PRODUCTION: true,
I18N_CONFIG: {
locales: ["en", "es"],
defaultLocale: "es",
domains: {
"en.test.com": {
defaultLocale: "en",
},
"es.test.com": {
defaultLocale: "es",
},
},
pages: {
"/somepage": {
en: "/somepage-en",
},
},
},
};

const req = extendRequestContext({
originalRequest: new Request("https://es.test.com/somepage"),
});

req.headers.set("Accept-Language", "en-US,en;q=0.5");

const { response } = handleI18n(req);
expect(response?.status).toBe(301);
expect(response?.headers.get("Location")).toBe(
"https://en.test.com/en/somepage-en/",
);
});

it("should redirect to the correct browser locale changing the subdomain", async () => {
globalThis.mockConstants = {
...globalThis.mockConstants,
Expand Down
14 changes: 8 additions & 6 deletions src/utils/handle-i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,21 @@ export default function handleI18n(req: RequestContext): {
const [, localeFromUrl] = url.pathname.split("/");
const pathname = url.pathname.replace(/\/$/, "");

const routers = {
pagesRouter: getRouteMatcher(PAGES_DIR, RESERVED_PAGES, locale),
rootRouter: getRouteMatcher(ROOT_DIR, undefined, locale),
};

// Redirect to default locale if there is no locale in the URL
if (localeFromUrl !== locale) {
const { route } = routers.pagesRouter.match(req);
const translatedRoute = pages?.[route?.name]?.[locale] ?? pathname;
const [domain, domainConf] =
Object.entries(domains || {}).find(
([, domainConf]) => domainConf.defaultLocale === locale,
) ?? [];

const finalPathname = `/${locale}${pathname}${url.search}${url.hash}${trailingSlashSymbol}`;
const finalPathname = `/${locale}${translatedRoute}${url.search}${url.hash}${trailingSlashSymbol}`;
const applyDomain = domain && (IS_PRODUCTION || domainConf?.dev);
const location = applyDomain
? `${domainConf?.protocol || "https"}://${domain}${finalPathname}`
Expand Down Expand Up @@ -63,11 +70,6 @@ export default function handleI18n(req: RequestContext): {
t: translateCore(locale),
};

const routers = {
pagesRouter: getRouteMatcher(PAGES_DIR, RESERVED_PAGES, locale),
rootRouter: getRouteMatcher(ROOT_DIR, undefined, locale),
};

if (pages) {
routers.pagesRouter = adaptRouterToPageTranslations(
pages,
Expand Down

0 comments on commit be5ecce

Please sign in to comment.