simple-i18n-next
v0.0.25
Published
A CLI to generate TypeScript code from translation files in JSON format and Markdown files for Next.js projects.
Downloads
14
Maintainers
Readme
simple-i18n-next [BETA]
This is a CLI to generate TypeScript code from translation files in JSON format and Markdown files for Next.js projects.
Note that this CLI is still WIP. It means every update might break your project.
Why?
The existing solutions for internationalization (i18n) in Next.js are too complicated for my taste. There are too many things to set up, and many are not type-safe.
Features
- Type safe translation. That means there will be build-time errors if you use a translation key that does not exist in a specific language.
- Translation keys cover all languages. As a result, an error message will appear if a translation for a specific language is not added.
- In RSC, the translations are generated inline, so no JS code is sent to the client.
- Only necessary translations are sent. When using the generated
useStrings
hook in client components, only the required translation strings are sent, avoiding any unused translations. - You can use markdown or MDX files for each language.
- Pluralization support.
- Nested keys are supported.
- Multiple JSON files are supported.
Install
npm i -g simple-i18n-next
Or you can run the command directly:
npx simple-i18n-next -i ./locales
CLI
Usage
$ simple-i18n-next [input]
Options
--input, -i <type> The path to the locales directory. [Default: ./locales]
--default-language, -l <type> The default language to use. [Default: the first directory in the locales directory]
--output, -o <type> The path to the output directory. [Default: ./locales/.generated]
--silent, -s <type> Do not show any output. [Default: false]
Examples
$ simple-i18n-next -i ./locales
Video Demo
How to use
To get started:
- Create a
locales
directory in your Next.js project. This directory will contain the translation files in JSON format and Markdown files. - For each language you want to support, create a new directory in the
locales
directory. The name of the directory must be one of the valid language codes. For example, if you want to support English, French, and Italian, you will create the following directories:en
,fr
, andit
. - In each language directory, create a
messages.json
file. This file will contain the translations for the language. For example, you can create alocales/en/messages.json
file that contains the following content:
{
"hello": "Hello",
"welcome": "Welcome to {{name}}",
"about": "About",
"contact": "Contact",
"coming_soon": "Coming soon"
}
and a locales/de/messages.json
file that contains the following content:
{
"hello": "Hallo",
"welcome": "Willkommen bei {{name}}",
"about": "Über",
"contact": "Kontakt",
"coming_soon": "Bald kommen"
}
You can also add multiple JSON files in the same directory. For example, you can also have a locales/en/client.json
file that contains the following content:
{
"hello": "Hello",
"welcome": "Welcome to {{name}}",
"about": "About",
"contact": "Contact",
"coming_soon": "Coming soon"
}
If you do, you need to add the corresponding files in the other language directories.
Note that you can have the same keys in different JSON files.
- Inside each language directory, you can also add several markdown files. For example, you can create a
locales/en/about.mdx
file that contains the following content:
# About
This is the about page.
and a locales/de/about.mdx
file that contains the following content:
# Über
Diese Seite ist die Übersicht.
Finally, run the
npx simple-i18n-next
command in your project directory. This command will generate TypeScript code inside thelocales/.generated
directory that you can use in your Next.js project.You might need to add the generated directory to your
tsconfig.json
file:
{
"include": ["locales/.generated/**/*"]
}
- Add the generated directory to your
.gitignore
file:
locales/.generated
- Update the package.json scripts to include the
simple-i18n-next
command:
{
"scripts": {
"generate-locales": "simple-i18n-next -i ./locales -l en",
"dev": "npm run generate-locales && next dev",
"build": "npm run generate-locales && next build",
"start": "next start"
}
}
Note that if you don't specify the default language with the -l
flag, the first directory in the locales
directory will be used as the default language.
How to use the generated code
Camel case convention
Every key in the JSON files is converted to camel case convention. When the keys are from the default messages.json
file, they are not prefixed with the file name. For example, if you have the following locales/en/messages.json
file:
{
"hello": "Hello"
}
it will be converted to the following TypeScript code:
export const hello = (lang: SupportedLanguage) => {
// content
}
When the keys are from other JSON files, the file name is prefixed to the key. For example, if you have the following locales/en/client.json
file:
{
"hello": "Hello"
}
it will be converted to the following TypeScript code:
export const clientHello = (lang: SupportedLanguage) => {
// content
}
In React Server Components (RSC)
You can use the generated code in your page.tsx
by simply importing the generated function which corresponds to the key you want to use in your translation.
import { SupportedLanguage } from '@/locales/.generated/types'
import { hello } from 'locales/.generated/server'
export default function HomePage({ params: { lang } }: { params: { lang: SupportedLanguage } }) {
return <div>{hello(lang)}</div>
}
In Client Components
There are two ways to use the translations in your client components:
- Pass the required translations from the server component to the client component as props.
// app/[lang]/comingsoon/coming-soon.tsx
'use client'
import { StringKeys, SupportedLanguage } from '@/locales/.generated/types'
export function ComingSoon({
strings,
}: {
strings: Pick<Record<StringKeys, string>, 'comingSoon' | 'hello'>
}) {
return (
<div>
<h1>{strings.comingSoon}</h1>
<p>{strings.hello}</p>
</div>
)
}
// app/[lang]/comingsoon/page.tsx
export default function ComingSoonPage({
params: { lang },
}: {
params: { lang: SupportedLanguage }
}) {
return (
<ComingSoon
strings={{
comingSoon: comingSoon(lang),
hello: submitMessage(lang),
}}
/>
)
}
- Or, you can also use the generated
useStrings
hook to get the translations in the client component.
'use client'
import { useStrings } from '@/locales/.generated/client/hooks'
export default function HomePage() {
const [{ hello, comingSoon }] = useStrings(['hello', 'comingSoon']) // the keys are typed!
// You cannot pass an invalid key.
// By default, it will return strings from the default language. Pass the language code as a second argument to get the translations from a specific language. E.g., useStrings(['hello'], 'de') to get the German translations.
return (
<div>
<h1>{comingSoon}</h1>
<p>{hello}</p>
</div>
)
}
The Markdown MDX files
Let's say you have a locales/en/index/section1.mdx
file that contains the following content:
# Section 1
This is the first section.
And a locales/de/index/section1.mdx
file that contains the following content:
# Sektion 1
Dies ist die erste Sektion.
You can use the generated markdown component in your page.tsx
file like this:
// app/[lang]/page.tsx
import { SupportedLanguage } from '@/locales/.generated/types'
import { IndexSection1 } from 'locales/.generated/locales-markdown'
export default function HomePage({ params: { lang } }: { params: { lang: SupportedLanguage } }) {
return (
<div>
<IndexSection1 lang={lang} />
</div>
)
}
Furthermore, since we're using MDX, we can pass props to the markdown and simple-18n-next will automatically generate the prop types. For example, if we have the following locales/en/about.mdx
file:
# Section 1
This is the first section. My name is {props.name}.
The CLI will generate the following prop types:
type AboutProps = {
name: string
}
And when you use the generated component without passing the name
prop, you will get TypeScript error!
// app/[lang]/about.tsx
import { SupportedLanguage } from '@/locales/.generated/types'
import { About } from 'locales/.generated/locales-markdown'
export default function AboutPage({ params: { lang } }: { params: { lang: SupportedLanguage } }) {
return (
<div>
<About lang={lang} /> {/* TypeScript error! name prop is missing */}
</div>
)
}
Please make sure that you have set up your Next.js project to use Markdown and MDX by following the official documentation.
Check out the example demo here and the code in the repository.
Interpolation
In the messages.json
file, you can use interpolation by using the {{variable_name}}
syntax. For example, you can create a locales/en/messages.json
file that contains the following content:
{
"hello": "Hello, {{name}}!"
}
In the page.tsx
file, you can use the interpolated variable like this:
// app/[lang]/page.tsx
import { SupportedLanguage } from '@/locales/.generated/types'
import { hello } from 'locales/.generated/server'
export default function HomePage({ params: { lang } }: { params: { lang: SupportedLanguage } }) {
return <div>{hello(lang, { name: 'Nico' })}</div>
}
The generated function is fully typed so you have to pass the correct variable name to the function as shown above.
In a client component, you can use the interpolated variable like this:
'use client'
import { interpolateTemplate } from '@/locales/.generated/common'
export default function ClientComponent() {
const [strings] = useStrings(['bye', 'home'])
if (!strings) return null
return (
<div className="flex min-h-screen flex-col items-center justify-between p-24">
<h1>{interpolateTemplate(strings.bye, { name: 'John' })}</h1>
<Link href={`/`}>{strings.home}</Link>
</div>
)
}
In the markdown file, you can use the interpolated variable like this:
# About
This is the about page. My name is {props.name}
Then in the page.tsx
file, you can use the markdown component like this:
// app/[lang]/page.tsx
import { SupportedLanguage } from '@/locales/.generated/types'
import { About } from 'locales/.generated/locales-markdown'
export default function HomePage({ params: { lang } }: { params: { lang: SupportedLanguage } }) {
return (
<div>
<About lang={lang} name="Nico" />
</div>
)
}
Plurals
This CLI also generates code for plurals for both ordinal and cardinal numbers. You need to add one of the following suffixes to let the script know that you want to use plurals: _one
, _two
, _few
, _many
, _other
, or _zero
for cardinal numbers, and _ordinal_one
, _ordinal_two
, _ordinal_few
, _ordinal_many
, _ordinal_other
, or _ordinal_zero
for ordinal numbers. You can read more about these plural rules from the CLDR website.
For example, you can create a locales/en/messages.json
file that contains the following content:
and a locales/de/messages.json
file that contains the following content:
Then in the RSC component like page.tsx
, you can use the generated function like this:
import { SupportedLanguage } from '@/locales/.generated/types'
import {
bookWithCount,
movieWithOrdinalCount,
} from "@/locales/.generated/server";
export default function Home({
params: { lang },
}: Readonly<{ params: { lang: SupportedLanguage } }>) {
return (
<main>
<div>
<p>{movieWithOrdinalCount(lang, 1)}</p>
<p>{movieWithOrdinalCount(lang, 2)}</p>
<p>{movieWithOrdinalCount(lang, 3)}</p>
<p>{movieWithOrdinalCount(lang, 4)}</p>
<p>{movieWithOrdinalCount(lang, 5)}</p>
</div>
<div>
<p>{bookWithCount(lang, 1)}</p>
<p>{bookWithCount(lang, 2)}</p>
<p>{bookWithCount(lang, 3)}</p>
<p>{bookWithCount(lang, 4)}</p>
<p>{bookWithCount(lang, 5)}</p>
</div>
</main>
)
}
which will render the following HTML when the language is German (de
):
<main>
<div>
<p>1. Film</p>
<p>2. Film</p>
<p>3. Film</p>
<p>4. Film</p>
<p>5. Film</p>
</div>
<div>
<p>1 Buch</p>
<p>2 Bücher</p>
<p>3 Bücher</p>
<p>4 Bücher</p>
<p>5 Bücher</p>
</div>
</main>
and when the language is English (en
):
<main>
<div>
<p>1st movie</p>
<p>2nd movie</p>
<p>3rd movie</p>
<p>4th movie</p>
<p>5th movie</p>
</div>
<div>
<p>One book</p>
<p>2 books</p>
<p>3 books</p>
<p>4 books</p>
<p>5 books</p>
</div>
</main>
In a client component, you can use the generated function like this:
"use client";
import { useStrings } from "@/locales/.generated/client/hooks";
export default function ClientComponent() {
const lang = useSelectedLanguageFromPathname();
const [, plurals] = useStrings(
[
"bookWithCount",
"movieWithOrdinalCount",
],
lang
);
if (!plurals) return null;
return (
<div>
<div>
<p>{plurals.bookWithCount(1)}</p>
<p>{plurals.bookWithCount(2)}</p>
<p>{plurals.bookWithCount(3)}</p>
<p>{plurals.bookWithCount(4)}</p>
<p>{plurals.bookWithCount(5)}</p>
</div>
<div>
<p>{plurals.movieWithOrdinalCount(1)}</p>
<p>{plurals.movieWithOrdinalCount(2)}</p>
<p>{plurals.movieWithOrdinalCount(3)}</p>
<p>{plurals.movieWithOrdinalCount(4)}</p>
<p>{plurals.movieWithOrdinalCount(5)}</p>
</div>
</div>
)
}
Note that the plural rules for cardinal and ordinal numbers for a given language can be different. For example, in English, the plural rule for cardinal numbers is one
and other
, while the plural rule for ordinal numbers is one
, two
, few
, and other
. On the other hand, in German, the plural rule for cardinal numbers is one
and other
, while the plural rule for ordinal numbers is only other
.
You can find out the plural rules for a given language by executing the following statement in the Node.js REPL or browser console:
// plural rules for ordinal numbers in German
new Intl.PluralRules('de', { type: 'ordinal' }).resolvedOptions().pluralCategories
// plural rules for cardinal numbers in German
new Intl.PluralRules('de').resolvedOptions().pluralCategories
Nested keys
You can use nested keys in your messages.json
file. The keys are converted to camelCase convention. For example, if you have the following locales/en/messages.json
file:
{
"page": {
"title": "Page title",
"section": {
"title": "Section title"
}
}
}
and a locales/de/messages.json
file that contains the following content:
{
"page": {
"title": "Seitentitel",
"section": {
"title": "Sektionentitel"
}
}
}
Then in your React component, you can use the nested key like this:
import { pageTitle, pageSectionTitle } from 'locales/.generated/server'
export default function HomePage({ params: { lang } }: { params: { lang: SupportedLanguage } }) {
return (
<div>
<h1>{pageTitle(lang)}</h1>
<h2>{pageSectionTitle(lang)}</h2>
</div>
)
}
Example
You can checkout the sample Next.js project that uses this CLI in this repository.
API
Generated constants and functions
The generated constants and functions are using camelCase convention. For example, if you have the followng locales/en/messages.json
file:
{
"hello": "Hello world!",
"greeting": "Hello {{name}}!",
"home": "Home",
"world-cup": "World cup"
}
the CLI will generate the following:
export type StringKeys = 'hello' | 'greeting' | 'home' | 'worldCup'
export const worldCup = (lang: SupportedLanguage) => {
// content
}
export const hello = (lang: SupportedLanguage) => {
// content
}
type greetingProps = {
name: string
}
export const greeting = (lang: SupportedLanguage, data: greetingProps) => {
// content
}
export const home = (lang: SupportedLanguage) => {
// content
}
For the plural keys, the CLI will generate functions with the format: <key>WithCount
for cardinal numbers and <key>WithOrdinalCount
for ordinal numbers. For example, if you have the following locales/en/messages.json
file:
{
"apple_one": "An apple",
"apple_other": "{{count}} apples",
"cat_ordinal_one": "1st cat",
"cat_ordinal_two": "2nd cat",
"cat_ordinal_few": "3rd cat",
"cat_ordinal_other": "{{count}}th cat"
}
Then the CLI will generate the following:
export const appleWithCount = (count: number) => {
// content
}
export const catWithOrdinalCount = (count: number) => {
// content
}
useStrings
useStrings
is a custom React hook so you can only use it in a client component.
Parameters:
keys
: An array of string keys to be used in the component. The keys can only be those defined inStringKeys
. If you pass unknown keys, TypeScript will throw an error.lang
: The language code to use for the translations. The value should be one of the supported language codes.
Returns a tuple:
strings
: An array of translated strings, excluding the plural keys.plurals
: An array of functions that can be used to translate the plural keys.
Example:
You have locales/en/messages.json
and locales/de/messages.json
files that contain the translations for the language like this:
{
"hello": "Hello",
"greeting": "Hello {{name}}!",
"apple_one": "An apple",
"apple_other": "{{count}} apples",
"cat_ordinal_one": "1st cat",
"cat_ordinal_two": "2nd cat",
"cat_ordinal_few": "3rd cat",
"cat_ordinal_other": "{{count}}th cat"
}
{
"hello": "Hallo",
"greeting": "Hallo {{name}}!",
"apple_one": "Ein Apfel",
"apple_other": "{{count}} Äpfel",
"cat_ordinal_other": "1. Katze"
}
Then in the client component, you can use the useStrings
hook like this:
import { useStrings } from '@/locales/.generated/client/hooks'
export default function ClientComponent() {
const lang = useSelectedLanguageFromPathname()
const [strings, plurals] = useStrings(
[
'hello',
'greeting',
'appleWithCount',
'catWithOrdinalCount',
],
lang
)
if (!strings) return null
if (!plurals) return null
return (
<div>
<h1>{strings.hello}</h1>
<p>{strings.greeting({name: 'John'})}</p>
<p>{plurals.appleWithCount(1)}</p>
<p>{plurals.appleWithCount(2)}</p>
<p>{plurals.appleWithCount(3)}</p>
<p>{plurals.appleWithCount(4)}</p>
<p>{plurals.appleWithCount(5)}</p>
<p>{plurals.catWithOrdinalCount(1)}</p>
<p>{plurals.catWithOrdinalCount(2)}</p>
<p>{plurals.catWithOrdinalCount(3)}</p>
<p>{plurals.catWithOrdinalCount(4)}</p>
<p>{plurals.catWithOrdinalCount(5)}</p>
</div>
)
supportedLanguages
You can use the supportedLanguages
array to get the supported languages in your application. For example
import { supportedLanguages } from '@/locales/.generated/types'
import { useRouter } from 'next/router'
export default function LanguageSelector() {
const router = useRouter()
return (
<div>
{supportedLanguages.map((lang) => (
<button key={lang} onClick={() => router.push(`/${lang}`)}>
{lang}
</button>
))}
</div>
)
}
defaultLanguage
You can use the defaultLanguage
string to get the default language in your application. For example
import { defaultLanguage } from '@/locales/.generated/types'
import { useRouter } from 'next/router'
export default function LanguageSelector() {
const router = useRouter()
return (
<div>
<button onClick={() => router.push(`/${defaultLanguage}`)}>{defaultLanguage}</button>
</div>
)
}
Development
Run
npx tsx source/cli.tsx -i "./locales"
License
MIT