React i18next tips and tricks
When making your React application accessible in multiple languages, i18next and its React companion library react-i18next offer a very robust set of features and tools. The libraries work together very well to offer specialized React hooks and components that allow internationalization simply and effectively.
In this article, I will discuss some of the advanced features available. This article assumes you have a basic knowledge of how React applications with i18next
work. If not, please first look at the documentation on how to install and use it.
Here are some good-to-know tips and tricks for using react-i18next.
TypeScript support
To add TypeScript support for i18next
in your React app, there are 2 key methods to ensure your translations are safe with types:
Method 1 - You can define types directly in the i18next.d.ts
file
import "i18next";
import ns1 from "locales/en/ns1.json";
import ns2 from "locales/en/ns2.json";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "ns1";
resources: {
ns1: typeof ns1;
ns2: typeof ns2;
};
}
}
Method 2 - Using configuration to define types
// i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import ns1 from "./locales/en/ns1.json";
import ns2 from "./locales/en/ns2.json";
export const defaultNS = "ns1";
export const resources = { ns1, ns2 } as const;
i18n.use(initReactI18next).init({
debug: true,
fallbackLng: "en",
defaultNS,
resources,
});
// i18next.d.ts
import { defaultNS, resources } from "./i18n";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: typeof defaultNS;
resources: typeof resources;
}
}
There are numerous advantages when using TypeScript:
- TypeScript enforces type safety with your translations, which means it checks that everything matches the correct types and helps reduce errors.
- You'll get autocomplete suggestions for valid keys in your translations
- The
useTranslation
hook will only work with namespaces that have been defined properly - If you try to use a translation key that doesn’t exist, TypeScript will show you errors
Namespaces
Splitting large translation files into distinct groups can improve maintainability in your React applications. In i18next, these groups are called Namespaces, which enable you to organize translations by feature, topic, domain, or any other logical part.
Here are some examples of how to use and integrate namespaces with React i18n:
// i18n.js configuration basic setup
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n
.use(initReactI18next)
.init({
resources: {
en: {
common: {
"welcome": "Welcome",
"login": "Log In",
"logout": "Log Out"
},
auth: {
"username": "Username",
"password": "Password",
"forgotPassword": "Forgot Password?"
},
dashboard: {
"overview": "Overview",
"statistics": "Statistics",
"recentActivity": "Recent Activity"
}
},
hr: {
common: {
"welcome": "Dobrodošli",
"login": "Prijava",
"logout": "Odjava"
},
auth: {
"username": "Korisničko ime",
"password": "Lozinka",
"forgotPassword": "Zaboravili ste lozinku?"
},
dashboard: {
"overview": "Pregled",
"statistics": "Statistika",
"recentActivity": "Nedavna aktivnost"
}
}
},
lng: "en",
fallbackLng: "en",
ns: ["common", "auth", "dashboard"],
defaultNS: "common",
interpolation: {
escapeValue: false
}
});
export default i18n;
After defining namespaces, you can use a specific namespace in your React component:
import React from 'react';
import { useTranslation } from 'react-i18next';
function LoginForm() {
const { t } = useTranslation('auth');
return (
<form>
<h2>{t('username')}</h2>
<input type="text" placeholder={t('username')} />
<h2>{t('password')}</h2>
<input type="password" placeholder={t('password')} />
<a href="#">{t('forgotPassword')}</a>
</form>
);
}
export default LoginForm;
You can also use multiple namespaces:
import React from 'react';
import { useTranslation } from 'react-i18next';
function Header() {
const { t } = useTranslation(['common', 'auth']);
return (
<header>
<h1>{t('common:welcome')}</h1>
<nav>
<button>{t('common:login')}</button>
<button>{t('common:logout')}</button>
<a href="/reset">{t('auth:forgotPassword')}</a>
</nav>
</header>
);
}
export default Header;
If you want you can also split namespaces into multiple files and organize them:
// locales/
// ├── en/
// │ ├── common.json
// │ ├── auth.json
// │ ├── dashboard.json
// │ ├── products.json
// │ └── checkout.json
// └── hr/
// ├── common.json
// ├── auth.json
// ├── dashboard.json
// ├── products.json
// └── checkout.json
// Example content for en/products.json
{
"listing": "Product Listing",
"details": "Product Details",
"reviews": "Customer Reviews",
"relatedItems": "Related Items",
"addToCart": "Add to Cart",
"outOfStock": "Out of Stock"
}
// Example content for en/checkout.json
{
"cart": "Shopping Cart",
"summary": "Order Summary",
"shipping": "Shipping Information",
"payment": "Payment Details",
"confirm": "Confirm Order",
"success": "Order Placed Successfully"
}
Fallbacks and defaults
When dealing with translations in i18next
, you can encounter missing translations or keys during runtime.
Fortunately, i18next
provides some cool solutions to handle these cases like in this scenario:
// Your translation file (en.json)
{
"countries": {
"US": "United States",
"UK": "United Kingdom",
"DE": "Germany",
"FR": "France"
}
}
// Your React component
const CountryDisplay = ({ countryCode }) => {
const { t } = useTranslation();
return (
<div>
<p>{t(`countries.${countryCode}`, `Unknown Country (${countryCode})`)</p>
</div>
);
}
This can fail if we receive an unexpected country code:
<CountryDisplay countryCode="US" /> // Shows "United States"
<CountryDisplay countryCode="JP" /> // Shows "Unknown Country (JP)"
Because of situations like this one, it's worth knowing how defaults and fallbacks work.
Using default values:
The most straightforward method is giving a default value directly inside the translation function:
<p>{t("country.code", "Unknown Country")}</p>
This has 2 advantages: it shows the backup text in case the key is not present, and whenever you run key extraction tools, this default text populates your translation files automatically.
Key as fallback:
If you don't specify any default value, i18next will simply return the key itself:
<p>{t("country.code")}</p>
// You'll see "country.code" displayed
Fallback chain:
You can create a priority list of keys, where i18next will try each one until it finds a match:
<p>{t(["country.code", "country.notFound"])}</p>
With a translation file like this:
{
"country": {
"notFound": "Unknown country"
}
}
If country.code
isn't found, it'll use the value from country.notFound
instead.
i18next also provides various levels of fallback mechanisms, not just for individual translation keys but also for full languages and namespaces. This is how these fallback systems work:
Language fallbacks:
i18next.init({
fallbackLng: "en"
});
This feature lets you specify what happens when translations aren't available in the user's preferred language. You can set one fallback language (for example, English).
You can also specify an ordered list of fallback languages, or develop language-specific fallback rules (such as Austrian German falling back to German, and then to English).
Namespace fallbacks:
i18next.init({
defaultNS: 'main',
fallbackNS: 'base'
});
In the example above, i18next tries to find a translation in your default namespace (main
in the above example). If not specified, it falls back to the default namespace (in this case base
).
This is particularly handy when you want to categorize your translations into groups while having a core of common, shared translations in a root namespace.
Dynamic keys
When using i18next
, translation keys are often determined during runtime instead of being present in the code. This is common with API responses, dynamic content, or system-created messages.
Luckily, i18next
offers flexible ways to manage these dynamic translation situations.
// API Response
const tasks = [
{ id: 1, status: 'pending' },
{ id: 2, status: 'in_progress' },
{ id: 3, status: 'completed' },
{ id: 4, status: 'cancelled' }
];
// Translation file
{
"taskStatus": {
"pending": "Waiting to Start",
"in_progress": "Currently in Progress",
"completed": "Task Completed",
"cancelled": "Task Cancelled"
}
}
// React component
const TaskList = () => {
const { t } = useTranslation();
return (
<ul>
{tasks.map(task => (
<li key={task.id}>
{t(`taskStatus.${task.status}`)}
</li>
))}
</ul>
);
};
In i18next
, dynamic translation keys are simple and powerful. You can create keys using template literals and use arrays for fallback options. The system easily handles nested structures without needing any special setup.
Using context
Sometimes, we must translate the same word or phrase differently depending on the context. The i18next
library makes this easy with context-aware translations.
Imagine we're showing different user access levels on an admin dashboard. Our en.json
translation file might include:
{
"access": "Access granted",
"access_admin": "Full administrative access",
"access_moderator": "Moderator access",
"access_user": "Basic user access",
"access_admin_one": "One administrative privilege",
"access_moderator_one": "One moderator privilege",
"access_user_one": "One user privilege",
"access_admin_other": "{{count}} administrative privileges",
"access_moderator_other": "{{count}} moderator privileges",
"access_user_other": "{{count}} user privileges"
}
We have a basic translation for "access," but it needs to change depending on the user's role. We can also adjust it for pluralization when talking about multiple privileges.
Here’s how we apply it:
// Usage without context
<p>{t("access")}</p>
// "Access granted"
// With role context
<p>{t("access", { context: "admin" })}</p>
// "Full administrative access"
<p>{t("access", { context: "moderator" })}</p>
// "Moderator access"
<p>{t("access", { context: "user" })}</p>
// "Basic user access"
// With context and count
<p>{t("access", { context: "admin", count: 3 })}</p>
// "3 administrative privileges"
<p>{t("access", { context: "moderator", count: 2 })}</p>
// "2 moderator privileges"
The example above shows us that using context can provide more precise and suitable translations for different user roles. The word "access" takes on different meanings based on the context and how many there are.
Pluralization
i18next
makes handling plural forms easy across different languages. Each language can have different rules for how plurals work and i18next
handles this automatically.
Here's a simple example with notifications in English and Croatian:
// en.json
{
"notification_zero": "No new notifications",
"notification_one": "You have 1 new notification",
"notification_other": "You have {{count}} new notifications"
}
// hr.json
{
"notification_zero": "Nema novih obavijesti",
"notification_one": "Imate 1 novu obavijest",
"notification_few": "Imate {{count}} nove obavijesti",
"notification_many": "Imate {{count}} novih obavijesti",
"notification_other": "Imate {{count}} novih obavijesti"
}
Some languages have more complex plural rules. Here's an example with likes on a post:
// en.json
{
"like_zero": "No likes yet",
"like_one": "1 person liked this",
"like_other": "{{count}} people liked this"
}
// hr.json
{
"like_zero": "Još nema oznaka sviđanja",
"like_one": "1 osoba je označila da joj se sviđa",
"like_few": "{{count}} osobe su označile da im se sviđa",
"like_many": "{{count}} osoba je označilo da im se sviđa",
"like_other": "{{count}} osoba je označilo da im se sviđa"
}
Using these translations is simple:
const NotificationBadge = ({ count }) => {
const { t } = useTranslation();
return (
<div className="badge">
{t("notification", { count })}
</div>
);
};
// Usage examples in Croatian:
<NotificationBadge count={0} /> // "Nema novih obavijesti"
<NotificationBadge count={1} /> // "Imate 1 novu obavijest"
<NotificationBadge count={2} /> // "Imate 2 nove obavijesti"
<NotificationBadge count={5} /> // "Imate 5 novih obavijesti"
<NotificationBadge count={21} /> // "Imate 21 novu obavijest"
// Like counter example
const LikeCounter = ({ likes }) => {
const { t } = useTranslation();
return (
<button>
{t("like", { count: likes })}
</button>
);
};
This example is interesting because the Croatian language has different plural forms based on the number:
- For 1: uses the singular form
- For numbers ending in 2-4 (except teens): uses the "few" form
- For numbers ending in 5-9, 0, and teens (11-19): uses the "many" form
i18next
handles all these complex plural rules automatically. You just pass the count, and i18next
selects the correct plural form based on the current language's rules.
ICU support
The International Components for Unicode (ICU) is a collection of open-source tools. These tools are helpful in software internationalization (i18n) and localization (l10n).
They are needed to ensure software works well across different languages and regions. To make i18next, a language translation tool, more effective, you can include ICU message formatting with the i18next-icu plugin.
Although i18next
has its formatting style, and using ICU helps keep things consistent. This is especially important if your backend systems already use ICU formatting methods.
Here are some examples to understand how it works.
The first step to using ICU in your project is to import it.
import i18next from 'i18next';
import ICU from 'i18next-icu';
i18next
.use(ICU)
.init({
// your config
});
Simple pluralization:
// i18next native format
{
"items": "{{count}} item",
"items_plural": "{{count}} items"
}
// ICU format
{
"items": "{count, plural, one {# item} other {# items}}"
}
// Usage
t('items', { count: 2 }) // "2 items"
Gender-based messages:
// ICU format
{
"welcome": "{gender, select, male {He is welcome} female {She is welcome} other {They are welcome}}"
}
// Usage
t('welcome', { gender: 'female' }) // "She is welcome"
Complex nested formatting:
// ICU format
{
"notification": "{username} {itemCount, plural,
=0 {has no items}
one {has # item in their cart}
other {has # items in their cart}
}"
}
// Usage
t('notification', { username: 'Alice', itemCount: 3 })
// "Alice has 3 items in their cart"
Date and number formatting:
// ICU format
{
"lastLogin": "Last login: {date, date, medium}",
"price": "Price: {amount, number, currency}"
}
t('lastLogin', { date: new Date() })
// "Last login: Sep 15, 2023"
t('price', { amount: 42.5 })
// "Price: $42.50"
Nested selection with plurals:
// ICU format
{
"inventory": "{gender, select,
male {He has {itemCount, plural,
=0 {no items}
one {# item}
other {# items}
}}
female {She has {itemCount, plural,
=0 {no items}
one {# item}
other {# items}
}}
other {They have {itemCount, plural,
=0 {no items}
one {# item}
other {# items}
}}
}"
}
// Usage
t('inventory', { gender: 'female', itemCount: 1 })
// "She has 1 item"
ICU does more than translate text, it also manages how dates, numbers, and complex strings are formatted.
One of the most important features of ICU is its ability to deal with the complex rules of plurals in different languages. This includes managing simple singular/plural forms and more complex ones found in languages like Arabic or Slavic languages.
Custom formatting functions
i18next's interpolation formatting helps you change the translated text in real-time. It has easy settings that let you set custom rules to automatically adjust your content.
This means you can make text all capital letters or turn numbers into currency like in the example below:
i18next.init({
interpolation: {
format: (value, format, lng) => {
if (format === 'uppercase') return value.toUpperCase();
if (format === 'currency') {
return new Intl.NumberFormat(lng, {
style: 'currency',
currency: 'USD'
}).format(value);
}
return value;
}
}
});
// Usage
{t('price', { val: 42.5, formatParams: { val: 'currency' } })}
For instance, when you use the translation function with formatParams
i18next will make these changes for you. A number like 42.5 can become "$42.50", showing it as money with the right format for your location.
Validation
Imagine you're building a social media platform that began in English. Now you're expanding it with several other languages. One useful method to keep translations accurate is to use automated translation checking.
// Initial English translations (source)
{
"post": {
"create": "Create post",
"delete": "Delete post",
"share": "Share with friends"
},
"comments": {
"reply": "Reply to comment",
"report": "Report inappropriate content"
}
}
// German translations (target)
{
"post": {
"create": "Beitrag erstellen",
"delete": "Beitrag löschen"
// Missing: "share" key
},
"comments": {
"reply": "Auf Kommentar antworten",
"report": "Unangemessenen Inhalt melden",
"edit": "Kommentar bearbeiten" // Outdated: key no longer exists in source
}
}
Using i18n-check
, you can automatically detect these issues:
# You can run this in your CI pipeline
npm run i18n-check
# Example output from i18n-check
Missing translations in de.json:
- post.share
Obsolete translations in de.json:
- comments.edit
This tool helps ensure that all the new languages are correctly integrated as your platform grows.
Conclusion
We've observed how i18next
goes far beyond simple text translation.
This powerful internationalization library provides software engineers with a deep feature set, dynamic content loading, language detection, plural support, and context-aware translations.
With mastery and utilization of these advanced features, you can create truly global applications that deliver culturally-aware experiences to users around the world.
Comments ()