Security News
Supply Chain Attack Detected in Solana's web3.js Library
A supply chain attack has been detected in versions 1.95.6 and 1.95.7 of the popular @solana/web3.js library.
next-multilingual
Advanced tools
An opinionated end-to-end solution for Next.js applications that requires multiple languages.
next-multilingual
is an opinionated end-to-end solution for Next.js applications that requires multiple languages.
Check out our demo app!
npm install next-multilingual
useMessages
hook that supports ICU MessageFormat and JSX injection out of the box./en-us/contact-us
for U.S. English and /fr-ca/nous-joindre
for Canadian French).next-multilingual
has put a lot of effort into adding TSDoc to all its APIs. Please check directly in your IDE if you are unsure how to use certain APIs provided in our examples.
Also, having an opinion on "best practices" is not an easy task. This is why we documented our design decisions in a special document that can be consulted here. If you feel that some of our APIs don't offer what you would expect, make sure to consult this document before opening an issue.
For those who prefer to jump right into the action, look in the example
directory for an end-to-end implementation of next-multilingual
. For the rest, the section below will provide a complete, step by step, configuration guide.
There are many options to configure in Next.js to achieve our goals. next-multilingual
mostly cares about:
We offer two APIs to simplify this step:
getConfig
(simple config)This function will generate a Next.js config that will meet most use cases. getConfig
takes the following arguments:
applicationId
— The unique application identifier that will be used as a messages key prefix.locales
— The actual desired locales of the multilingual application. The first locale will be the default locale. Only BCP 47 language tags following the language
-country
format are accepted. For more details on why, refer to the design decisions document.options
(optional) — Options part of a Next.js configuration object.getConfig
will return a Next.js configuration object.
To use it, simply add the following code in your application's next.config.js
:
const { getConfig } = require('next-multilingual/config')
const config = getConfig('exampleApp', ['en-US', 'fr-CA'], {
poweredByHeader: false,
})
module.exports = config
Not all configuration options are not supported by getConfig
. If you ever happen to use one, an error message will point you directly to the next section: advanced config.
Config
(advanced config)If you have more advanced needs, you can use the Config
object directly and insert the configuration required by next-multilingual
directly in an existing next.config.js
. The arguments of Config
are almost identical to getConfig
(minus the options
) - check in your IDE (TSDoc) for details. Here is an example of how it can be used:
const { Config, webpackConfigurationHandler } = require('next-multilingual/config')
const config = new Config('exampleApp', ['en-US', 'fr-CA'])
module.exports = {
reactStrictMode: true,
i18n: {
locales: config.getUrlLocalePrefixes(),
defaultLocale: config.getDefaultUrlLocalePrefix(),
localeDetection: false,
},
poweredByHeader: false,
/* This is required since Next.js 11.1.3-canary.69 until we support ESM. */
experimental: {
esmExternals: false,
},
webpack: webpackConfigurationHandler,
}
If you need to customize your own Webpack configuration, we recommend extending our handler like this:
import Webpack from 'webpack'
import { webpackConfigurationHandler, WebpackContext } from 'next-multilingual/config'
export function myWebpackConfigurationHandler(
config: Webpack.Configuration,
context: WebpackContext
): Webpack.Configuration {
const myConfig = webpackConfigurationHandler(config, context)
// Do stuff here.
return myConfig
}
Or directly in next.config.js
:
// Webpack handler wrapping next-multilingual's handler.
function webpack(config, context) {
config = webpackConfigurationHandler(config, context)
// Do stuff here.
return config
}
next-multilingual/config
does 2 things leveraging Next.js' current routing capability:
next-multilingual/config
also handles the special Webpack configuration required for server side rendering of localized
URLs using next-multilingual/link/ssr
for Link
components and next-multilingual/head/ssr
for canonical and alternate links in the Head
component.
For more details on the implementation such as why we are using UTF-8 characters, refer to the design decisions document.
next-multilingual/messages/babel-plugin
To display localized messages with the useMessages()
hook, we need to configure our custom Babel plugin that will automatically inject strings into pages and components. The recommended way to do this is to include a .babelrc
at the base of your application:
{
"presets": ["next/babel"],
"plugins": ["next-multilingual/messages/babel-plugin"]
}
If you do not configure the plugin you will get an error when trying to use useMessages
.
App
(_app.tsx
)We need to create a custom App
by adding _app.tsx
in the pages
directory:
import type { AppProps } from 'next/app'
import { useActualLocale } from 'next-multilingual'
export default function MyApp({ Component, pageProps }: AppProps): JSX.Element {
useActualLocale() // Forces Next.js to use the actual (proper) locale.
return <Component {...pageProps} />
}
This basically does two things, as mentioned in the comments:
/
). If you do not want to use next-multilingual
's locale detection you can use useActualLocale(false)
instead.Document
(_document.tsx
)We also need to create a custom Document
by adding _document.tsx
in the pages
directory:
import { getHtmlLang } from 'next-multilingual'
import Document, { Head, Html, Main, NextScript } from 'next/document'
class MyDocument extends Document {
render(): JSX.Element {
return (
<Html lang={getHtmlLang(this)} translate="no" className="notranslate">
<Head>
<meta name="google" content="notranslate" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
This serves only 1 purpose: display the correct server side locale in the <html>
tag. Since we are using a "fake" default locale, it's important to keep the correct SSR markup, especially when resolving a dynamic locale on /
.
next-multilingual/head
provides a <Head>
component which automatically creates a canonical link and alternate links in the header. This is something that is not provided out of the box by Next.js.
NEXT_PUBLIC_ORIGIN
environment variableAs per Google, alternate links must be fully-qualified, including the transport method (http/https). Because Next.js does not know which URL is used at build time, we need to specify the absolute URL that will be used, in an environment variable. For example, for the development environment, create an .env.development
file at the root of your application with the following variable (adjust based on your setup):
NEXT_PUBLIC_ORIGIN=http://localhost:3000
Regardless of the environment, next-multilingual
will look for a variable called NEXT_PUBLIC_ORIGIN
to generate fully-qualified URLs. If you are using Next.js' basePath
, it will be added automatically to the base URL.
NEXT_PUBLIC_ORIGIN
will only accept fully qualified domains (e.g., http://example.com
), without any paths.
next-multilingual
🎬Now that everything has been configured, we can focus on using next-multilingual
!
⚠️ Note that while we recommend using smart locale detection to dynamically render the homepage, this is completely optional. By using advanced configuration with
localeDetection: true
, you will restore the default Next.js behavior without the need of usinggetServerSideProps
.
The homepage is a bit more complex than other pages, because we need to implement dynamic locale detection (and display) for the following reason:
/
can have a negative impact on SEO and is not the best user experience.next-multilingual
comes with a getPreferredLocale
API that offers smarter auto-detection than the default Next.js implementation.You can find a full implementation in the example, but here is a stripped down version:
import type { GetServerSideProps, NextPage } from 'next'
import { ResolvedLocaleServerSideProps, resolveLocale, useResolvedLocale } from 'next-multilingual'
import { getTitle, useMessages } from 'next-multilingual/messages'
import Layout from '@/layout'
const Home: NextPage<ResolvedLocaleServerSideProps> = ({ resolvedLocale }) => {
// Force Next.js to use a locale that was resolved dynamically on the homepage.
useResolvedLocale(resolvedLocale)
// Load the messages in the correct locale.
const messages = useMessages()
return (
<Layout title={getTitle(messages)}>
<h1>{messages.format('headline')}</h1>
</Layout>
)
}
export default Home
export const getServerSideProps: GetServerSideProps<ResolvedLocaleServerSideProps> = async (
nextPageContext
) => {
return {
props: {
resolvedLocale: resolveLocale(nextPageContext),
},
}
}
In a nutshell, this is what is happening:
next-multilingual
's locale cookie.Every time that you create a tsx
, ts
, jsx
or js
(compilable) file and that you need localized messages, you can simply create a message file in your supported locales that will only be usable by these files. Just like CSS modules, the idea is that you can have message files associated with another file's local scope. This has the benefit of making messages more modular and also avoids sharing messages across different contexts (more details in the design decisions document on why this is bad).
Message files have 2 main use cases:
pages
directory, you can specify a localized URL segment (part of a URL in between /
or at the end of the path) using the slug
key identifier. More details on how to do this below.useMessages
hook. Imagine CSS but for localizable strings.To summarize:
Creating and managing those files is as simple as creating a style sheet, but here are the important details:
.properties
files. Yes, you might wonder why, but there are good reasons documented in the design decision document.UTF-8
. Not doing so will replace non-Latin characters with �
..properties
files, we follow a strict naming convention: <PageFilename>.<locale>.properties
<applicationId>.<context>.<id>
where:
next-multilingual/config
aboutUsPage
or footerComponent
could be good examples of context. Each file can only contain 1 context and context should not be used across many files as this could cause "key collision" (non-unique keys)..
and can only contain between 1 to 50 alphanumeric characters - we recommend using camel case for readability.slug
identifier.title
identifier.getTitle
API provided in next-multilingual/messages
to automatically fallback between the title
and slug
keys.useMessages
hook.Also, make sure to check your console log for warnings about potential issues with your messages. It can be tricky to get used to how it works first, but we tried to make it easy to detect and fix problems. Note that those logs will only show in non-production environments.
As mentioned previously, there is one special key for pages
, where the id
is slug
. Unlike traditional slugs that look like this-is-a-page
, we ask you to write the slug as a normal and human readable sentence, so that it can be translated like any other string. This avoids having special processes for slugs which can be costly and complex to manage in multiple languages.
Basically, the slug
is the human readable "short description" of your page, and represents a segment (part between /
or at the end of the path) of a URL. When used as a URL segment, the following transformation is applied:
-
For example, About Us
will become about-us
.
For the homepage, the URL will always be /
which means that slug
keys will not be used to create localized URL segments.
Don't forget, slugs must be written as a normal short description, which means skipping words to keep it shorter for SEO is discouraged. The main reason for this, is that if you write "a bunch of keywords", a linguist who is not familiar with SEO might have a hard time translating that message. Having SEO specialists in many languages would also be very costly and difficult to scale. In an ideal scenario, market-specific SEO pages should probably be authored and optimized in the native languages, but this is no longer part of the translation process. next-multilingual
's focus is to provide an easy, streamlined solution to localize URLs in many languages.
The slug
key will also be used as a fallback of the title
key when using the getTitle
API provided in next-multilingual/messages
. This API makes it easy to customize titles when a slug feels insufficient.
⚠️ Note that changing a
slug
value means that a URL will change. Since those changes are happening innext.config.js
, like any Next.js config change, the server must be restarted to see the changes in effect. The same applies if you change the folder structure since the underlying configuration relies on this.
If you want to have a directory without any pages, you can still localize it by creating an index.<locale>.properties
file (where locale
are the locales you support). While this option is supported, we don't recommend using it as this will make URL paths longer which goes against SEO best practice.
By default, next-multilingual
will exclude some files like custom error pages, or any API routes under the /api
directory. You can always use slug
keys when using messages for these files, but they will not be used to create localized URLs.
You can always look into the example to see message files in action, but here is a sample that could be used on the homepage:
# Homepage title
exampleApp.homepage.title = Homepage
# Homepage headline
exampleApp.homepage.headline = Welcome to the homepage
Now that we learned how to create the homepage and some of the details around how things work, we can easily create other pages. We create many pages in the example, but here is a sample of what about-us.jsx
could look like:
import { NextPage } from 'next'
import { getTitle, useMessages } from 'next-multilingual/messages'
import Layout from '@/layout'
import styles from './index.module.css'
const AboutUs: NextPage = () => {
const messages = useMessages()
const title = getTitle(messages)
return (
<Layout title={title}>
<h1 className={styles.headline}>{title}</h1>
<p>{messages.format('details')}</p>
</Layout>
)
}
export default AboutUs
And of course you would have this message file about-us.en-US.properties
:
# Page localized URL segment (slug) in (translatable) human readable format.
# This key will be "slugified" (e.g, "About Us" will become "about-us"). All non-alphanumeric characters will be replaced by "-".
exampleApp.aboutUsPage.slug = About Us
# Page details.
exampleApp.aboutUsPage.details = This is just some english boilerplate text.
next-multilingual
comes with its own <Link>
component that allows for client side and server side rendering of localized URL. It's usage is simple, it works exactly like Next.js' <Link>
.
The only important thing to remember is that the href
attribute should always contain the Next.js URL. Meaning, the file structure under the pages
folder should be what is used and not the localized versions.
In other words, the file structure is considered as the "non-localized" URL representation, and <Link>
will take care of replacing the URLs with the localized versions (from the messages files), if they differ from the structure.
The API is available under next-multilingual/link
and you can use it like this:
import Link from 'next-multilingual/link'
import { useMessages } from 'next-multilingual/messages'
export default function Menu(): JSX.Element {
const messages = useMessages()
return (
<nav>
<Link href="/">
<a>{messages.format('home')}</a>
</Link>
<Link href="/about-us">
<a>{messages.format('aboutUs')}</a>
</Link>
<Link href="/contact-us">
<a>{messages.format('contactUs')}</a>
</Link>
</nav>
)
}
Each of these links will be automatically localized when the slug
key is specified in that page's message file. For example, in U.S. English the "Contact Us" URL path will be /en-us/contact-us
while in Canadian French it will be /fr-ca/nous-joindre
.
As the data for this mapping is not immediately available during rendering, next-multilingual/link/ssr
will take care of the server side rendering (SSR). By using next-multilingual/config
's getConfig
, the Webpack configuration will be added automatically. If you are using the advanced Config
method, this explains why the special Webpack configuration is required in the example provided prior.
Not all localized URLs are using the <Link>
component and this is also why Next.js has the router.push
method that can be used by many other use cases. next-multilingual
can support these use cases with the useLocalizedUrl
hook that will return a localized URL, usable by any components. Here is an example on how it can be leveraged:
import { NextPage } from 'next'
import { useMessages } from 'next-multilingual/messages'
import { useLocalizedUrl } from 'next-multilingual/url'
import router from 'next/router'
const Tests: NextPage = () => {
const messages = useMessages()
const localizedUrl = useLocalizedUrl('/about-us')
return <button onClick={() => router.push(localizedUrl)}>{messages.format('clickMe')}</button>
}
export default Tests
There could be cases where you need to use localized URLs on the server side and hooks (useLocalizedUrl
) cannot be used. Imagine using Next.js' API to send transactional emails and wanting to leverage next-multilingual
's localized URLs without having to hardcode them in a configuration. This is where getLocalizedUrl
comes in. getLocalizedUrl
is only usable on the server side which is why it is imported directly from next-multilingual/url/ssr
. Here is an example of how it can be used:
import type { NextApiRequest, NextApiResponse } from 'next'
import { isLocale } from 'next-multilingual'
import { getLocalizedUrl } from 'next-multilingual/url/ssr'
import { getMessages } from 'next-multilingual/messages'
import { sendEmail } from '../send-email/'
/**
* The "/api/send-email" handler.
*/
export default async function handler(
request: NextApiRequest,
response: NextApiResponse
): Promise<void> {
const locale = request.headers['accept-language']
let emailAddress = ''
try {
emailAddress = JSON.parse(request.body).emailAddress
} catch (error) {
response.status(400)
return
}
if (locale === undefined || !isLocale(locale) || !emailAddress.length) {
response.status(400)
return
}
const messages = getMessages(locale)
sendEmail(
emailAddress,
messages.format('welcome', { loginUrl: getLocalizedUrl('/login', locale, true) })
)
response.status(200)
}
Creating components is the same as pages but they live outside the pages
directory. Also, the slug
key (if used) will not have any impact on URLs. We have a few example components that should be self-explanatory but here is an example of a Footer.tsx
component:
import { useMessages } from 'next-multilingual/messages'
export default function Footer(): JSX.Element {
const messages = useMessages()
return <footer>{messages.format('footerMessage')}</footer>
}
And its messages file:
# This is the message in the footer at the bottom of pages
exampleApp.footerComponent.footerMessage = © Footer
Also make sure to look at the language picker component example that is a must in all multilingual applications.
We've been clear that sharing messages is a bad practice from the beginning, so what are we talking about here? In fact, sharing messages by itself is not bad. What can cause problems is when you share messages in different contexts. For example, you might be tempted to create a Button.ts
shared message file containing yesButton
, noButton
keys - but this would be wrong. In many languages simple words such as "yes" and "no" can have different spellings depending on the context, even if it's a button.
When is it good to share messages? For lists of items.
For example, to keep your localization process simple, you want to avoid storing localizable strings in your database (more details on why in the design decision document). In your database you would identify the context using unique identifiers and you would store your messages in shared message files, where your key's identifiers would match the ones from the database.
To illustrate this we created one example using fruits. All you need to do, is create a hook that calls useMessages
like this:
export { useMessages as useFruitsMessages } from 'next-multilingual/messages'
Of course, you will have your messages files in the same directory:
exampleApp.fruits.banana = Banana
exampleApp.fruits.apple = Apple
exampleApp.fruits.strawberry = Strawberry
exampleApp.fruits.grape = Grape
exampleApp.fruits.orange = Orange
exampleApp.fruits.watermelon = Watermelon
exampleApp.fruits.blueberry = Blueberry
exampleApp.fruits.lemon = Lemon
And to use it, simple import this hook from anywhere you might need these values:
import { useFruitsMessages } from '../messages/useFruitsMessages'
export default function FruitList(): JSX.Element {
const fruitsMessages = useFruitsMessages()
return (
<>
{fruitsMessages
.getAll()
.map((message) => message.format())
.join(', ')}
</>
)
}
You can also call individual messages like this:
fruitsMessages.format('banana')
The idea to share those lists of items is that you can have a consistent experience across different components. Imagine a dropdown with a list of fruits in one page, and in another page an auto-complete input. But the important part to remember is that the list must always be used in the same context, not to re-use some of the messages in a different context.
Using placeholders in messages is a critical functionality as not all messages contain static text. next-multilingual
supports the ICU MessageFormat syntax out of the box which means that you can use the following message:
exampleApp.homepage.welcome = Hello {name}!
And inject back the values using:
messages.format('welcome', { name: 'John Doe' })
format
There are a few simple rules to keep in mind when using format
:
values
argument when formatting the message, it will simply output the message as static text.values
argument when formatting the message, you must include the values of all placeholders using the {placeholder}
syntax in your message. Otherwise the message will not be displayed.values
that are not in your message, they will be silently ignored.One of the main benefits of ICU MessageFormat is to use Unicode's tools and standards to enable applications to sound fluent in most languages. A lot of engineers might believe that by having 2 messages, one for singular and one for plural is enough to stay fluent in all languages. In fact, Unicode documented the plural rules of over 200 languages and some languages like Arabic can have up to 6 plural forms.
To ensure that your sentence will stay fluent in all languages, you can use the following message:
exampleApp.homepage.mfPlural = {count, plural, =0 {No candy left.} one {Got # candy left.} other {Got # candies left.}}
And the correct plural form will be picked, using the correct plural categories defined by Unicode:
messages.format('mfPlural', { count })
There is a lot to learn on this topic. Make sure to read the Unicode documentation and try the syntax yourself to get more familiar with this under-hyped i18n capability.
In a rare event where you would need to use both placeholders using the {placeholder}
syntax and also display the {
and }
characters in a message, you will need to replace them by the {
(for {
) and }
(for }
) HTML entities which are recognized by translation tools like this:
exampleApp.debuggingPage.variableInfo = Your variable contains the following values: {{values}}
If you have a message without values (placeholders), escaping {
and }
with HTML entities is not required and will display entities as static text.
It is a very common situation that we need to have inline HTML, inside a single message. One way to do this would be:
# Bad example, do not ever do this!
exampleApp.homepage.createAccount1 = Please
exampleApp.homepage.createAccount2 = create your account
exampleApp.homepage.createAccount3 = today for free.
And then:
<div>
{messages.format('createAccount1')}
<Link href="/sign-up">{messages.format('createAccount2')}</Link>
{messages.format('createAccount3')}
</div>
There are 2 problems with this approach:
This is actually an anti-pattern called concatenation and should always be avoided. This is the correct way to do this, using formatJsx
:
exampleApp.homepage.createAccount = Please <link>create your account</link> today for free.
And then:
<div>{messages.formatJsx('createAccount', { link: <Link href="/sign-up"></Link> })}</div>
formatJsx
formatJsx
support both placeholders and JSX elements as values
which means that you can benefit from the standard format
features (e.g., plurals) while injecting JSX elements.
There are a few simple rules to keep in mind when using format
:
formatJsx
.<link>
XML tag, the JSX element needs to be provided using link: <Link href="/"></Link>
.<i>
many times in a sentence, you will need to create unique tags like <i1>
, <i2>
, etc. and pass their values in argument as JSX elements.Hello <name>{name}</name>
) is not supported..properties
file.<Link href="/contact-us><a id="test"></a></Link>
is valid but <div><span1></span1><span2></span2></div>
is invalid. Instead you must use same-level XML markup in the .properties
file and not as a JSX argument.<
and >
When using formatJsx
you will still need to escape curly brackets if you want to display them as text. Additionally, since we will be using XML in the formatJsx
messages, similar rules will apply to <
and >
which are used to identify tags.
In a rare event where you would need to inject JSX in a message using the <element></element>
(XML) syntax and also display the <
and >
characters in a message, you will need to replace them by the <
(for <
) and >
(for >
) HTML entities which are recognized by translation tools like this:
exampleApp.statsPage.targetAchieved = You achieved your weekly target (<5) and are eligible for a <link>reward</link>.
Anchor links are links that takes you to a particular place in a document rather than the top of it.
One of next-multilingual
's core feature is supporting localized URLs. Our design has been built using normal sentences that are easy to localize and then transformed into SEO-friendly slugs. We can use the same function to slugify anchor links, so that instead of having /fr-ca/nous-joindre#our-team
you can have /fr-ca/nous-joindre#notre-équipe
.
There are two type of anchor links:
If the anchor links are on the same page, and not referred on any other pages, you can simply add them in the .properties
file associate with that page like this:
# Table of content header
exampleApp.longPage.tableOfContent = Table of Content
# This key will be used both as content and "slugified". Make sure when translating that its value is unique.
exampleApp.longPage.p1Header = Paragraph 1
# "Lorem ipsum" text to make the (long) page scroll
exampleApp.longPage.p1 = Lorem ipsum dolor sit amet...
And then the page can use the slugify
function to link to to the unique identifier associated with the element you want to point the URL fragment to:
import { NextPage } from 'next'
import Link from 'next-multilingual/link'
import { slugify, useMessages } from 'next-multilingual/messages'
import { useRouter } from 'next/router'
const LongPage: NextPage = () => {
const messages = useMessages()
const { locale } = useRouter()
return (
<div>
<div>
<h2>{messages.format('tableOfContent')}</h2>
<ul>
<li>
<Link href={`#${slugify(messages.format('p1Header'), locale)}`}>
{messages.format('p1Header')}
</Link>
</li>
</ul>
</div>
<div>
<h2 id={slugify(messages.format('p1Header'), locale)}>{messages.format('p1Header')}</h2>
<p>{messages.format('p1')}</p>
</div>
</div>
)
}
export default LongPage
It's also common to use anchor links across pages, so that when you click a link, your browser will directly show the relevant content on that page. To do this, you need to make your page's message available to other pages by adding this simple export that will act just like "shared messages":
export const useLongPageMessages = useMessages
And then you can use this hook from another page like this:
import { NextPage } from 'next'
import Link from 'next-multilingual/link'
import { slugify, useMessages } from 'next-multilingual/messages'
import { useRouter } from 'next/router'
import { useLongPageMessages } from './long-page'
const AnchorLinks: NextPage = () => {
const messages = useMessages()
const { locale, pathname } = useRouter()
const longPageMessages = useLongPageMessages()
return (
<div>
<div>
<Link
href={`${pathname}/long-page#${slugify(longPageMessages.format('p3Header'), locale)}`}
>
{messages.format('linkAction')}
</Link>
</div>
</div>
)
}
export default AnchorLinks
This pattern also works for components. The benefit of doing this is that if you delete, or refactor the page, the anchor links associated with it will always stay with the page.
You could create a separate shared message component just for the anchor links but this would break the proximity principle.
A full example of anchor links can be found in the example application.
One feature that is missing from Next.js is managing important HTML tags used for SEO. We added the <Head>
component to deal with two very important tags that live in the HTML <head>
:
<link rel=canonical>
): this tells search engines that the source of truth for the page being browsed is this URL. Very important to avoid being penalized for duplicate content, especially since URLs are case insensitive, but Google treats them as case-sensitive.<link rel=alternate>
): this tells search engines that the page being browsed is also available in other languages and facilitates crawling of the site.The API is available under next-multilingual/head
and you can import it like this:
import Head from 'next-multilingual/head'
Just like <Link>
, <Head>
is meant to be a drop-in replacement for Next.js' <Head>
component. In our example, we are using it in the Layout component, like this:
<Head>
<title>{title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
</Head>
All this does is insert the canonical and alternate links so that search engines can better crawl your application. For example, if you are on the /en-us/about-us
page, the following HTML will be added automatically under your HTML <head>
tag:
<link rel="canonical" href="http://localhost:3000/en-us/about-us" />
<link rel="alternate" href="http://localhost:3000/en-us/about-us" hreflang="en-US" />
<link rel="alternate" href="http://localhost:3000/fr-ca/%C3%A0-propos-de-nous" hreflang="fr-CA" />
To fully benefit from the SEO markup, <Head>
must be included on all pages. There are multiple ways to achieve this, but in the example, we created a <Layout>
component that is used on all pages.
Like most sites, you will want to leverage Next.js' custom error pages capability. With useMessages()
, it's just as easy as creating any other pages. For example, for a 404
error, you can create your 404.tsx
:
import { NextPage } from 'next'
import Link from 'next-multilingual/link'
import { getTitle, useMessages } from 'next-multilingual/messages'
import Layout from '@/layout'
const Custom400: NextPage = () => {
const messages = useMessages()
const title = getTitle(messages)
return (
<Layout title={title}>
<h1>{title}</h1>
<Link href="/">
<a>{messages.format('goBack')}</a>
</Link>
</Layout>
)
}
export default Custom400
And of course, your messages, for example 404.en-US.properties
:
# Page title
exampleApp.pageNotFoundError.title = 404 - Page Not Found
# Go back link text
exampleApp.pageNotFoundError.goBack = Go back home
One of Next.js' core features is its builtin API support. It's not uncommon for APIs to return content in different languages. next-multilingual
has an equivalent API just for this use case: getMessages
. Unlike the useMessages
hook, getMessages
can be used in API Routes. Here is an "Hello API" example on how to use it:
import type { NextApiRequest, NextApiResponse } from 'next'
import { getMessages } from 'next-multilingual/messages'
/**
* Example API schema.
*/
type Schema = {
message: string
}
/**
* The "hello API" handler.
*/
export default function handler(request: NextApiRequest, response: NextApiResponse<Schema>): void {
const locale = request.headers['accept-language']
if (locale === undefined || !isLocale(locale)) {
response.status(400)
return
}
const messages = getMessages(locale)
response.status(200).json({ message: messages.format('message') })
}
This is very similar to the API implemented in the example application. We are using the Accept-Language
HTTP header to tell the API in which locale we want its response to be. Unlike the useMessages
hook that has the context of the current locale, we need to tell getMessages
in which locale to return messages.
Message files behave exactly the same as with useMessages
You simply need to create one next to the API Route's file, in our case hello.en-US.properties
:
# API message
exampleApp.helloApi.message = Hello, from API.
You can implement this in any pages, just like any other React-based API call, like this:
const SomePage: NextPage = () => {
const [apiError, setApiError] = useState(null)
const [isApiLoaded, setApiIsLoaded] = useState(false)
const [apiMessage, setApiMessage] = useState('')
useEffect(() => {
setApiIsLoaded(false)
const requestHeaders: HeadersInit = new Headers()
requestHeaders.set('Accept-Language', normalizeLocale(router.locale as string))
fetch('/api/hello', { headers: requestHeaders })
.then((result) => result.json())
.then(
(result) => {
setApiIsLoaded(true)
setApiMessage(result.message)
},
(apiError) => {
setApiIsLoaded(true)
setApiError(apiError)
}
)
}, [router.locale])
function showApiMessage(): JSX.Element {
if (apiError) {
return (
<>
{messages.format('apiError')}
{(apiError as Error).message}
</>
)
} else if (!isApiLoaded) {
return <>{messages.format('apiLoading')}</>
} else {
return <>{apiMessage}</>
}
}
return (
<div>
<h2>{messages.format('apiHeader')}</h2>
<div>{showApiMessage()}</div>
</div>
)
}
The normalizeLocale
is not mandatory but a recommended ISO 3166 convention. Since Next.js uses the locales as URL prefixes, they are lower-cased in the configuration and can be re-normalized as needed.
Dynamic routes are very common and supported out of the box by Next.js. For simplicity, next-multilingual
currently only supports path matching which is also the most common dynamic route use case. To make dynamic routes work with next-multilingual
all that you need to do is to use the href
property as a UrlObject
instead of a string
. Just like any other links, we want to pass the non-localized path used by the Next.js' router (pathname
). For dynamic routes, the router uses the bracket syntax (e.g., [page]
) to identify parameters. For example, if you want to create a <Link>
for for /test/[id]
you will need to do the following:
<Link href={{ pathname: '/test/[id]', query: { id: '123' } }} />
In UrlObject
, parameters are stored in the query
property, just like the Next.js router. In a language picker, we can use the properties coming directly from the router as shown in the example below:
import {
getActualLocale,
getActualLocales,
normalizeLocale,
setCookieLocale,
} from 'next-multilingual'
import Link from 'next-multilingual/link'
import { KeyValueObject } from 'next-multilingual/messages'
import { useRouter } from 'next/router'
// Locales don't need to be localized.
const localeStrings: KeyValueObject = {
'en-US': 'English (United States)',
'fr-CA': 'Français (Canada)',
}
export default function LanguagePicker(): JSX.Element {
const { pathname, locale, locales, defaultLocale, query } = useRouter()
const actualLocale = getActualLocale(locale, defaultLocale, locales)
const actualLocales = getActualLocales(locales, defaultLocale)
return (
<div>
<button>
{localeStrings[normalizeLocale(actualLocale)]}
<i></i>
</button>
<div>
{actualLocales
.filter((locale) => locale !== actualLocale)
.map((locale) => {
return (
<Link key={locale} href={{ pathname, query }} locale={locale}>
<a
onClick={() => {
setCookieLocale(locale)
}}
>
{localeStrings[normalizeLocale(locale)]}
</a>
</Link>
)
})}
</div>
</div>
)
}
Note that while this example is using the <Link>
component, this is also supported by the useLocalizedUrl
hook when other components are used.
There is one last thing that needs to be taken care of, and it's making query parameters available for SSR. By default Next.js' router will return {}
for its query
property. To fix this and get the SEO benefits from SSR markup, we can simply add a getStaticPaths
or a getServerSideProps
on the page with the dynamic route. As soon as we add these, Next.js will make all the data available without any additional work. Here is an example using getStaticPaths
:
export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [
{
params: {
id: '123',
},
},
],
fallback: 'blocking',
}
}
export const getStaticProps: GetStaticProps = async () => {
return { props: {} } // Empty properties, since we are only using this for the static paths to work.
}
If you don't need the build-time optimization of getStaticPaths
, you can also achieve this with a simple getServerSideProps
:
export const getServerSideProps: GetServerSideProps = async () => {
return { props: {} } // Empty properties, since we are only using this to get the query parameters.
}
This allows a seamless experience across localized URLs when using simple parameters such as unique identifiers (e.g., UUIDs or numerical). If your parameter itself needs to be localized, you will have to handle that logic yourself using getServerSideProps
.
We also provided a fully working example for those who want to see it in action.
If you ever need to use the locale
, defaultLocale
, locales
properties from both GetStaticPropsContext
or GetServerSidePropsContext
and are tired of casting types since Next.js allows them to be undefined
, you can use our wrappers like this:
import { MultilingualServerSideProps, MultilingualStaticProps } from 'next-multilingual/messages'
export const getServerSideProps: MultilingualServerSideProps<GetServerSideProps> = async (
context
) => {
// `context.locale`, `context.defaultLocale`, `context.locales` are never `undefined`
}
export const getStaticProps: MultilingualStaticProps<GetStaticProps> = async (context) => {
// `context.locale`, `context.defaultLocale`, `context.locales` are never `undefined`
}
You might also have noticed the getActual*
APIs. Theses API is part of a set of "utility" APIs that helps abstract some of the complexity that we configured in Next.js. These APIs are very useful, since we can no longer rely on the locales provided by Next.js. The main reason for this is that we set the default Next.js locale to mul
(for multilingual) to allow us to do the dynamic detection on the homepage. These APIs are simple and more details are available in your IDE (JSDoc).
Our ideal translation process is one where you send the modified files to your localization vendor (while working in a branch), and get back the translated files, with the correct locale in the filenames. Once you get the files back you basically submit them back in your branch which means localization becomes an integral part of the development process. Basically, the idea is:
We don't have any "export/import" tool to help as at the time of writing this document.
next-multilingual
? 🗳️Why did we put so much effort into these details? Because our hypothesis is that it can have a major impact on:
More details can be found on the implementation and design decision in the individual README files of each API and in the documentation directory.
FAQs
An opinionated end-to-end solution for Next.js applications that requires multiple languages.
We found that next-multilingual demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
A supply chain attack has been detected in versions 1.95.6 and 1.95.7 of the popular @solana/web3.js library.
Research
Security News
A malicious npm package targets Solana developers, rerouting funds in 2% of transactions to a hardcoded address.
Security News
Research
Socket researchers have discovered malicious npm packages targeting crypto developers, stealing credentials and wallet data using spyware delivered through typosquats of popular cryptographic libraries.