The basics, extracting strings, wiring up t(), adding a language switcher, get you a long way. But once you support Arabic, need to display formatted prices, or want to add a fifth language without bloating your bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together., you need the techniques in this lesson. These are the things that separate a toy i18nWhat is i18n?Short for internationalization (18 letters between i and n) - structuring your code so it can support multiple languages and regions. implementation from one that ships to millions of users.
RTLWhat is rtl?Right-to-Left - a text direction used by Arabic, Hebrew, and Persian scripts that requires layout mirroring in addition to text translation. support
Right-to-left languages like Arabic and Hebrew flip more than just the text. The entire page layout mirrors: navigation moves to the right, sidebars swap sides, and icons that imply direction need to flip. Getting this right takes a small amount of intentional code.
Detecting and applying direction
Keep a list of RTL language codes and set the dir attribute on the document root whenever the language changes. Everything underneath inherits direction from there.
const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];
function isRTL(locale) {
return RTL_LANGUAGES.includes(locale);
}
function App() {
const { i18n } = useTranslation();
const rtl = isRTL(i18n.language);
useEffect(() => {
document.documentElement.dir = rtl ? 'rtl' : 'ltr';
document.documentElement.lang = i18n.language;
}, [i18n.language, rtl]);
return (
<div className={rtl ? 'rtl' : 'ltr'}>
<Content />
</div>
);
}Writing direction-aware CSS
The old approach of writing separate [dir="rtl"] overrides for every rule is painful to maintain. CSS logical properties are the modern solution: they describe layout in terms of start/end rather than left/right, so the browser does the mirroring for you.
/* Old approach: direction-specific overrides */
.sidebar { float: left; }
[dir="rtl"] .sidebar { float: right; }
/* Modern approach: logical properties (recommended) */
.element {
margin-inline-start: 1rem; /* left in LTR, right in RTL */
padding-inline: 1.5rem; /* padding on both sides */
text-align: start; /* left in LTR, right in RTL */
border-inline-end: 1px solid;
}
/* You still need this for icons that imply a direction */
[dir="rtl"] .icon-arrow {
transform: scaleX(-1);
}Formatting with the Intl APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.
JavaScript ships with a powerful built-in formatting API called Intl. You do not need a library to format dates, numbers, or currencies correctly, just pass the user's localeWhat is locale?A code combining language and region (like fr-FR for French in France) that determines how dates, numbers, and currencies are formatted..
const date = new Date('2024-12-20');
// Date formatting varies significantly by locale
new Intl.DateTimeFormat('en-US').format(date); // "12/20/2024"
new Intl.DateTimeFormat('fr-FR').format(date); // "20/12/2024"
new Intl.DateTimeFormat('ja-JP').format(date); // "2024/12/20"
// Full date with weekday
new Intl.DateTimeFormat('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date); // "vendredi 20 décembre 2024"
// Number formatting
new Intl.NumberFormat('en-US').format(1234567.89); // "1,234,567.89"
new Intl.NumberFormat('fr-FR').format(1234567.89); // "1 234 567,89"
new Intl.NumberFormat('de-DE').format(1234567.89); // "1.234.567,89"
// Currency
new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(99.99); // "99,99 €"
// Relative time
new Intl.RelativeTimeFormat('fr').format(-1, 'day'); // "il y a 1 jour"
new Intl.RelativeTimeFormat('en').format(3, 'month'); // "in 3 months"Reusable formatting components
Wrap Intl in small components that read the current locale from i18next automatically. You never have to pass the locale manually at the call site.
import { useTranslation } from 'react-i18next';
function FormattedDate({ date, options = {} }) {
const { i18n } = useTranslation();
const formatted = new Intl.DateTimeFormat(i18n.language, {
year: 'numeric',
month: 'short',
day: 'numeric',
...options
}).format(new Date(date));
return <time dateTime={date}>{formatted}</time>;
}
function Currency({ amount, currency = 'EUR' }) {
const { i18n } = useTranslation();
const formatted = new Intl.NumberFormat(i18n.language, {
style: 'currency',
currency
}).format(amount);
return <span>{formatted}</span>;
}
// Usage
<FormattedDate date={post.createdAt} />
<Currency amount={19.99} currency="USD" />Complex plurals with ICU MessageFormat
English has two plural forms: one and other. Polish has four. Arabic has six. The simple _one / _other key suffix approach breaks down for these languages. ICU MessageFormat handles all cases with a single key using an inline syntax.
{
"message": "{count, plural, =0 {No messages} one {One message} few {{count} messages} many {{count} messages} other {{count} messages}}"
}function MessageCount({ count }) {
const { t } = useTranslation();
return <p>{t('message', { count })}</p>;
}i18next-icu plugin. Install it with npm install i18next-icu and add .use(ICU) to your i18next init chain.Pseudo-localizationWhat is l10n?Short for localization (10 letters between l and n) - the work of translating content and adapting formats for a specific language or region. for UI testing
Before you have real translations, you need a way to test that your UI does not break when strings are longer or contain unusual characters. Pseudo-localization replaces your English strings with decorated versions that simulate expansion and non-ASCII characters.
// locales/pseudo/common.json
{
"welcome": "[Ŵêļçøɱê ţø ɱý âppļîçâţîøɱ!!!]",
"save": "[Êɳřêĝîšţřêř]",
"description": "[Ţĥîš îš â ɭøɳĝêř ţêхţ ţĥâţ šĥøûļð ƒîţ îɳ ţĥê ÜÎ]"
}The square brackets reveal text that overflows its containerWhat is container?A lightweight, portable package that bundles your application code with all its dependencies so it runs identically on any machine.. The accented characters expose font fallback issues. Run your app in pseudo localeWhat is locale?A code combining language and region (like fr-FR for French in France) that determines how dates, numbers, and currencies are formatted. during development and fix layout problems before your translators start working.
Extracting keys automatically
Manually keeping translation JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. files in sync with your source code does not scale. i18next-parser scans your files for t('key') calls and generates or updates your JSON files.
npm install --save-dev i18next-parser// i18next-parser.config.js
module.exports = {
locales: ['en', 'fr', 'es'],
output: 'src/locales/$LOCALE/$NAMESPACE.json',
input: ['src/**/*.{js,jsx,ts,tsx}'],
sort: true,
removeUnusedKeys: true,
keepRemoved: false
};// package.json scripts
{
"scripts": {
"i18n:extract": "i18next 'src/**/*.{js,jsx}' -c i18next-parser.config.js"
}
}Run npm run i18n:extract after adding new UI strings. It adds missing keys (with an empty value as a placeholder) and can optionally remove keys that no longer appear in source.
Translation context for gender and formality
Some languages need different wording based on context that has nothing to do with plurals. Gendered languages require different adjective endings. Some languages have formal and informal registers.
{
"greeting": "Hello",
"greeting_formal": "Good day",
"greeting_informal": "Hey",
"greeting_male": "Hello sir",
"greeting_female": "Hello madam"
}function Greeting({ formality, gender }) {
const { t } = useTranslation();
// Combine multiple context dimensions
const context = `${formality}_${gender}`;
// → resolves to 'greeting_formal_male', etc.
return <p>{t('greeting', { context })}</p>;
}Quick reference
| Technique | When to use | Tool |
|---|---|---|
dir="rtl" on <html> | Supporting Arabic, Hebrew, Persian, Urdu | Native DOM + useEffect |
| CSS logical properties | Any layout that needs RTL support | CSS (margin-inline-start, etc.) |
Intl.DateTimeFormat | Displaying dates per locale | Built into JS |
Intl.NumberFormat | Numbers, currencies, units | Built into JS |
Intl.RelativeTimeFormat | "3 days ago", "in 2 months" | Built into JS |
| ICU MessageFormat | Languages with 3+ plural forms | i18next-icu plugin |
| Pseudo-localization | UI testing before translations exist | Custom JSON locale |
i18next-parser | Keeping JSON keys in sync with code | Dev dependency |
// Complete i18n example with RTL and formatting
// ========== utils/i18n-helpers.js ==========
export const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur'];
export function isRTL(locale) {
return RTL_LANGUAGES.includes(locale);
}
export function useRTL() {
const { i18n } = useTranslation();
return isRTL(i18n.language);
}
export function formatDate(date, locale, options = {}) {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
...options
}).format(new Date(date));
}
export function formatNumber(number, locale, options = {}) {
return new Intl.NumberFormat(locale, options).format(number);
}
export function formatCurrency(amount, locale, currency) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(amount);
}
export function formatRelativeTime(value, unit, locale) {
return new Intl.RelativeTimeFormat(locale).format(value, unit);
}
// ========== components/FormattedDate.jsx ==========
import { useTranslation } from 'react-i18next';
export function FormattedDate({ date, options }) {
const { i18n } = useTranslation();
const formatted = formatDate(date, i18n.language, options);
return <time dateTime={date}>{formatted}</time>;
}
// ========== components/Currency.jsx ==========
export function Currency({ amount, currency = 'EUR' }) {
const { i18n } = useTranslation();
const formatted = formatCurrency(amount, i18n.language, currency);
return <span className="currency">{formatted}</span>;
}
// ========== components/RTLProvider.jsx ==========
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { isRTL } from '../utils/i18n-helpers';
export function RTLProvider({ children }) {
const { i18n } = useTranslation();
const rtl = isRTL(i18n.language);
useEffect(() => {
document.documentElement.dir = rtl ? 'rtl' : 'ltr';
document.documentElement.lang = i18n.language;
}, [i18n.language, rtl]);
return (
<div className={rtl ? 'rtl' : 'ltr'}>
{children}
</div>
);
}
// ========== Usage ==========
function Post({ post }) {
return (
<article>
<h2>{post.title}</h2>
<FormattedDate date={post.createdAt} />
<Currency amount={post.price} currency="USD" />
<p>{post.content}</p>
</article>
);
}
// App with RTL support
function App() {
return (
<RTLProvider>
<Router>
<Routes />
</Router>
</RTLProvider>
);
}
// CSS with logical properties
/* styles.css */
.card {
/* Logical properties adapt to direction */
margin-inline-start: 1rem;
margin-inline-end: 1rem;
padding-inline: 1rem;
border-inline-start: 2px solid blue;
text-align: start;
}
/* RTL specific adjustments */
[dir='rtl'] .icon-arrow {
transform: scaleX(-1);
}
[dir='rtl'] .sidebar {
left: auto;
right: 0;
}