Add i18n in less than 5 minutes — Built for Next.js 10
Lightweight, simple, easy to integrate, extendable, no custom server required and efficient because it will only download the required translations for your current locale.
Supports typed locales via the Template literal and Recursive types. Requires TypeScript >=4.1.0
Note: Currently types is only supported using dot notation. Eg: t("about.title.0.description")
.
First step: downloading this dependency.
# with npm
npm install next-rosetta
# with yarn
yarn add next-rosetta
Update your next.config.js
by adding a i18n
section:
// ./next.config.js
module.exports = {
i18n: {
locales: ["en", "es"],
defaultLocale: "en",
},
};
For more info refer to: https://nextjs.org/docs/advanced-features/i18n-routing
Make a directory named i18n
on the root of your project. If you are using TypeScript you can define the type schema and create every locale based on that interface. Type safety! Excelente!
// ./i18n/index.tsx
export interface MyLocale {
locale: string;
title: string;
subtitle: string;
profile: {
button: string;
};
welcome: string;
}
// ./i18n/en.tsx
import type { MyLocale } from ".";
export const table: MyLocale = {
locale: "English",
title: "Next.js 10 + Rosetta with native i18n integration",
subtitle: "Click below to update your current locale 👇",
profile: {
button: "Press me!",
},
welcome: "Welcome {{name}}! 😃", // with variable replacement
};
// ./i18n/es.tsx
import type { MyLocale } from ".";
export const table: MyLocale = {
locale: "Español",
title: "Next.js 10 + Rosetta con integración nativa de i18n",
subtitle: "Presiona aquí abajo para cambiar tu lenguaje 👇",
profile: {
button: "Presióname!",
},
welcome: "Bienvenido {{name}}! 👋", // with variable replacement
};
Dealing with long texts? You can use endent
or similar libraries.
import endent from "endent";
import type { MyLocale } from ".";
export const table: MyLocale = {
markdown: endent`
# Title
This string will have a correct right indentation.
`,
}
Import I18nProvider
from "next-rosetta"
and wrap your app in it. From pageProps
take table
which is the current locale object and pass it to I18nProvider
.
// ./pages/_app.tsx
import type { AppProps } from "next/app";
import { I18nProvider } from "next-rosetta";
function MyApp({ Component, pageProps }: AppProps) {
return (
<I18nProvider table={pageProps.table}>
<Component {...pageProps} />
</I18nProvider>
);
}
export default MyApp;
To import locales you must call this on the server side code (or on the static render):
const locale = "en";
const { table = {} } = await import(`../i18n/${locale}`);
Here is an example if you are using getStaticProps
:
// ./pages/index.tsx
import type { GetStaticProps } from "next";
import { useI18n, I18nProps } from "next-rosetta";
// Import typing
import type { MyLocale } from "../i18n";
function HomePage() {
const { t } = useI18n<MyLocale>();
return (
<div>
<h3>
{t("title")}
</h3>
<p>
{t("welcome", { name: "John" })}
</p>
<button>
{t("profile.button")}
</button>
</div>
)
}
// You can use I18nProps<T> for type-safety (optional)
export const getStaticProps: GetStaticProps<I18nProps<MyLocale>> = async (context) => {
const locale = context.locale || context.defaultLocale;
const { table = {} } = await import(`../i18n/${locale}`); // Import locale
return { props: { table } }; // Passed to `/pages/_app.tsx`
};
Any component can access the locale translations by using the useI18n
hook.
// ./pages/index.tsx
import Link from "next/link";
import { useRouter } from "next/router";
import { useI18n } from "next-rosetta";
// Import typing
import type { MyLocale } from "../i18n";
function LocaleSelector() {
const { locale, locales, asPath } = useRouter(); // Get current locale and locale list
const { t } = useI18n<MyLocale>();
// ...
}
For more info regarding rosetta
API please refer to: https://github.com/lukeed/rosetta
Here is a more complete example of page inside the /page
directory:
// ./pages/index.tsx
import { useI18n, I18nProps } from "next-rosetta";
import { useRouter } from "next/router";
import Head from "next/head";
import Link from "next/link";
import type { MyLocale } from "../i18n"; // Import typing
export default function Home() {
const { locale, locales, asPath } = useRouter();
const i18n = useI18n<MyLocale>();
const { t } = i18n;
return (
<div>
<Head>
<title>{t("locale")}</title>
</Head>
<main>
<h1>{t("title")}</h1>
<p>{t("subtitle")}</p>
<p>{t("welcome", { name: "John" })}</p>
<ul>
{locales?.map((loc) => (
<li key={loc}>
<Link href={asPath} locale={loc}>
<a className={loc === locale ? "is-active" : ""}>{loc}</a>
</Link>
</li>
))}
</ul>
</main>
</div>
);
}
// Server-side code
import type { GetStaticProps } from "next";
export const getStaticProps: GetStaticProps<I18nProps<MyLocale>> = async (context) => {
const locale = context.locale || context.defaultLocale;
const { table = {} } = await import(`../i18n/${locale}`); // Import locale
return { props: { table } }; // Passed to `/pages/_app.tsx`
};
This is compatible with your current server side logic. Here is an example:
// ./pages/posts/[id].tsx
import type { GetServerSideProps } from "next";
import { useI18n, I18nProps } from "next-rosetta";
// Import typing
import type { MyLocale } from "../i18n";
type Props = { post: any };
export default function PostPage({ post, ...props }: Props) {
const { t } = useI18n<MyLocale>();
// ...
}
export const getServerSideProps: GetServerSideProps<Props & I18nProps> = async (context) => {
const locale = context.locale || context.defaultLocale;
const data = await fetch(`/posts/${context.params["id"]}`).then(res => res.json());
const { table = {} } = await import(`../../i18n/${locale}`);
return { props: { table, post: data } };
};
Yes. Just import is as await import(
../../i18n/${locale}.json
);
If you have this error:
Type 'unknown' is not assignable to type 'ReactNode'.ts
You are probably using a wrong path, you have a typo or you are using arrays as path (t(["foo", "bar"])
won't infer type).
To force a type:
const en = {
title: "Hello",
}
const { t } = useI18n<typeof en>();
// type is 'unknown'
const text = t("foo") // note 'foo' doesn't exist in locale definition.
// React error
<span>{text}<span>
// type is 'string'
const text = t<string>("foo")
// ok
<span>{text}<span>
Create some <Link />
and set the locale
prop to change locale. It is important to note you should set the href
variable to the current asPath
from useRouter
.
The difference between router.route
and router.asPath
is that the first has path value with params (eg: /products/[id]
) and asPath
has the replaced values.
export default function ChangeLocale() {
const { locale, locales, asPath } = useRouter();
const i18n = useI18n<MyLocale>();
return (
<div>
{locales?.map((loc) => {
const isActive = loc === locale;
return (
<Link key={loc} href={asPath} locale={loc}>
<a>{loc}</a>
</Link>
);
})}
</div>
);
}
IDEs won't autocomplete while typing, only after the path is written you can see the types.
This is a limitation of Typescript, we would require a pre-compilation steps of each possible path to allow this.
- Support pluralization.
- Support function definitions with arguments. Only serializable locales are possible right now.