automatic i18n linking with nextjs 13 app router
October 10, 2023
automatic internationalized links and routing using NextJS's Link component and app router
Yesterday, I posted about implementing i18n routing in a nextjs project, focusing on the app folder and middleware setup. This post follows from that post with an I18NLink component that leverages our i18n set up to automatically prefix your links with the user's current language. This way, you can do something like this:
<I18NLink href="/settings">{t('settings', 'Settings')}</I18NLink>And the link will automatically pick up the lang param to route to either /settings or (for example) /es/settings depending on the lang in the url.
Note: the t function is a stand in for a generic translation function that would take a key like settings and return a string or fallback to the second argument, which is "Settings".
wrapping the Link component
The first thing we need to do is wrap NextJS's default Link component:
import type { ComponentProps } from 'react';
import React from 'react';
import Link from 'next/link';
export type I18NLinkType = Omit<ComponentProps<typeof Link>, 'as'> & {
href: string;
};
function I18NLink({ children, href, ...props }: I18NLinkType) {
return (
<Link href={href} {...props}>
{children}
</Link>
)
}A few things are happening here:
- We are creating a new
I18NLinkTypeby taking theLinktype and omittingasso we can use the component polymorphically. - We are defining
hrefas a string sinceLinkpotentially accepts aURL. - We then create a function that's more or less a pass-through: take the
hrefandchildrenand just pass them toLink.
This isn't doing too much yet. We need to hook it into the useParams hook.
get current lang param
Let's update the component:
import { useParams } from 'next/navigation';
// generic import of your default language
import { defaultLanguage } from 'settings';
function I18NLink({ children, href, ...props }: I18NLinkType) {
const params = useParams();
const currentLanguage = (params?.lang as string) || defaultLanguage;
return (
<Link href={`${currentLanguage}${href}`} {...props}>
{children}
</Link>
)
}Note that params could be null or an empty Record, so we want to coerce lang as a string.
This isn't a bad solution, but we're going to run into a few problems:
- We don't want to prepend the
defaultLanguageif we don't have to since it'll cause an extra redirect in the middleware. - We can't use the Link generically since it'll only work for relative paths. If we pass an absolute or external route, the url will be malformed.
checking incoming href
What we need to do to prevent the above is check the incoming href and make decisions about what href to actually pass to Link.
function getPath(path?: string | null) {
// 1.
if (!path) {
return ['en', ''];
}
// 2.
const pathArray = path.split('/').filter((x) => !!x);
// 3. empty array
if (pathArray.length === 0) {
return ['en', ''];
}
// 4.
const [lang, ...restPath] = pathArray;
// 5.
if (languageArray.includes(lang)) {
return [lang, restPath.join('/')];
}
// 6.
return ['en', pathArray.join('/')];
}
function getPrefixedUrl(href: string, currentLanguage: string) {
// 1. handle absolute links
if (/^((http|https):\/\/)/.test(href)) {
return href;
}
// 2.
const [, tail] = getPath(href);
// 3.
if (currentLanguage === defaultLanguage) {
return `/${tail}`;
}
// 4.
return `/${currentLanguage}/${tail}`;
}Let's start with getPrefixedUrl:
- If the
hrefstarts withhttporhttps, we should follow the absolute route provided. - Otherwise, we want to get the url path without the
langparam if it's provided. - If the
currentLanguageis also the default language, just pass thetailof the path - Otherwise, prepend the current language to the
tailof the path
getPath might seem a little extraneous or verbose, but it's meant to help avoid issues with passing around the root and with overriding the lang provided in the url.
- If the
pathis null, return an array we can use to route the user to the root of the site. - Take the path, and split it at
/. The array is going to be easier to work with. - If the array is empty because
path === '/', return an array we can use to route the user to the root of the site. - We want to deal with just
restPathif (and only if) the first part of the url is actually anlangparam. - We do a check to ensure that
langis include in ourlangaugeArray, which tells us we can safely userestPath - Otherwise we want to return the whole path we were provided
With these helper functions, we can update the component:
function I18NLink({ children, href, ...props }: I18NLinkType) {
const params = useParams();
const currentLanguage = (params?.lang as string) || defaultLanguage;
const prefixedHref = getPrefixedUrl(href, currentLanguage);
return (
<Link href={prefixedHref} {...props}>
{children}
</Link>
)
}bonus: anchor or Link
If you want to escape the client cache with a "hard" route, you might want to be able to use either an anchor or Link component:
export type I18NLinkType = Omit<ComponentProps<typeof Link>, 'as'> & {
isAnchor?: boolean;
href: string;
};
function I18NLink({ children, href, isAnchor, ...props }: I18NLinkType) {
const params = useParams();
const currentLanguage = (params?.lang as string) || defaultLanguage;
const prefixedHref = getPrefixedUrl(href, currentLanguage);
if (isAnchor) {
return (
<a href={prefixedHref} {...props}>
{children}
</a>
);
}
return (
<Link href={prefixedHref} {...props}>
{children}
</Link>
)
}We can just use a boolean and render an anchor tag based on isAnchor if passed.
wrapping up
It would be great to have a more built in way to handle internationalization in NextJS, however it only requires a little bit of boilerplate to get a full fledged i18n setup, including automatic i18n routing with NextJS's default Link component.
Check out the full gist here.