Let’s dive into frontend internationalization (i18n), the art of making your web app support multiple languages to cater to global users. i18n goes beyond text translation—it handles dates, numbers, currency formats, and even text direction (like right-to-left for Arabic). We’ll break down i18n strategies and tools with detailed code examples, guiding you step-by-step to implement internationalization in React, Vue, and plain JavaScript. Our focus is on practical, technical details to help you master multi-language support!
What is i18n?
i18n, short for “internationalization” (18 letters between “i” and “n”), is about adapting your app for different languages and regions. Its core tasks include:
- Text Translation: Translating UI text (buttons, prompts, menus) into target languages.
- Formatting: Handling dates (2025-07-02 vs. 02/07/2025), currencies ($100 vs. ¥100), and numbers (1,000 vs. 1.000).
- Cultural Adaptation: Managing text direction (LTR/RTL), plural rules (e.g., “1 book” vs. “2 books” in English), and sorting rules.
- Dynamic Switching: Allowing users to switch languages at runtime.
Frontend i18n typically relies on translation files (JSON/YAML) and libraries like i18next or Vue I18n. We’ll start with a plain JavaScript solution and move to React and Vue implementations.
Plain JavaScript i18n: i18next
i18next is a powerful, flexible i18n library for plain JavaScript, React, Vue, and more. It supports translation, formatting, plural rules, and dynamic language file loading. Let’s build a simple multi-language page with i18next.
Installation and Setup
Create an HTML project:
mkdir i18n-demo
cd i18n-demo
npm init -y
npm install i18next
Create translation files in public/locales/en/translation.json
:
{
"welcome": "Welcome to my app!",
"greeting": "Hello, {{name}}!",
"items": {
"one": "1 item",
"other": "{{count}} items"
}
}
And public/locales/es/translation.json
:
{
"welcome": "¡Bienvenido a mi aplicación!",
"greeting": "¡Hola, {{name}}!",
"items": {
"one": "1 artículo",
"other": "{{count}} artículos"
}
}
These JSON files define translations for English (en) and Spanish (es), with {{name}}
and {{count}}
as interpolation placeholders for dynamic content.
Initializing i18next
Set up i18next in index.html
:
<!DOCTYPE html>
<html>
<head>
<title>i18n Demo</title>
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/dist/umd/i18next.min.js"></script>
</head>
<body>
<div id="app">
<h1 id="welcome"></h1>
<p id="greeting"></p>
<p id="items"></p>
<select id="language">
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
<script>
i18next.init({
lng: 'en',
resources: {
en: {
translation: {
welcome: 'Welcome to my app!',
greeting: 'Hello, {{name}}!',
items: {
one: '1 item',
other: '{{count}} items'
}
}
},
es: {
translation: {
welcome: '¡Bienvenido a mi aplicación!',
greeting: '¡Hola, {{name}}!',
items: {
one: '1 artículo',
other: '{{count}} artículos'
}
}
}
}
}, (err, t) => {
if (err) return console.error(err);
function updateContent() {
document.getElementById('welcome').innerText = t('welcome');
document.getElementById('greeting').innerText = t('greeting', { name: 'Alice' });
document.getElementById('items').innerText = t('items', { count: 2 });
}
updateContent();
document.getElementById('language').addEventListener('change', (e) => {
i18next.changeLanguage(e.target.value, () => {
updateContent();
});
});
});
</script>
</body>
</html>
Run npx http-server
and visit localhost:8080
. The page shows English text; switching to Spanish updates the content. t('greeting', { name: 'Alice' })
handles dynamic interpolation, and t('items', { count: 2 })
picks the correct plural form.
Dynamic Translation File Loading
Hardcoding translations in JavaScript isn’t practical for large projects. Use JSON files with i18next-http-backend
:
npm install i18next-http-backend
Update index.html
:
<script src="https://cdn.jsdelivr.net/npm/i18next-http-backend@2.5.0/i18nextHttpBackend.min.js"></script>
<script>
i18next
.use(i18nextHttpBackend)
.init({
lng: 'en',
backend: {
loadPath: '/locales/{{lng}}/translation.json'
}
}, (err, t) => {
if (err) return console.error(err);
function updateContent() {
document.getElementById('welcome').innerText = t('welcome');
document.getElementById('greeting').innerText = t('greeting', { name: 'Alice' });
document.getElementById('items').innerText = t('items', { count: 2 });
}
updateContent();
document.getElementById('language').addEventListener('change', (e) => {
i18next.changeLanguage(e.target.value, () => {
updateContent();
});
});
});
</script>
Place en/translation.json
and es/translation.json
in public/locales
. i18next loads translations via HTTP, ideal for large projects. Language switches trigger async JSON requests.
Formatting Dates and Numbers
i18next supports formatting with browser APIs like Intl
. Install @formatjs/intl
for advanced formatting:
npm install @formatjs/intl
Update index.html
:
<body>
<div id="app">
<h1 id="welcome"></h1>
<p id="greeting"></p>
<p id="items"></p>
<p id="date"></p>
<p id="price"></p>
<select id="language">
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
<script src="https://cdn.jsdelivr.net/npm/i18next-http-backend@2.5.0/i18nextHttpBackend.min.js"></script>
<script>
i18next
.use(i18nextHttpBackend)
.init({
lng: 'en',
backend: {
loadPath: '/locales/{{lng}}/translation.json'
},
interpolation: {
format: (value, format, lng) => {
if (format === 'date') {
return new Intl.DateTimeFormat(lng).format(value);
}
if (format === 'currency') {
return new Intl.NumberFormat(lng, {
style: 'currency',
currency: 'USD'
}).format(value);
}
return value;
}
}
}, (err, t) => {
function updateContent() {
document.getElementById('welcome').innerText = t('welcome');
document.getElementById('greeting').innerText = t('greeting', { name: 'Alice' });
document.getElementById('items').innerText = t('items', { count: 2 });
document.getElementById('date').innerText = t('date', { val: new Date(), format: 'date' });
document.getElementById('price').innerText = t('price', { val: 99.99, format: 'currency' });
}
updateContent();
document.getElementById('language').addEventListener('change', (e) => {
i18next.changeLanguage(e.target.value, () => {
updateContent();
});
});
});
</script>
</body>
Update en/translation.json
:
{
"welcome": "Welcome to my app!",
"greeting": "Hello, {{name}}!",
"items": {
"one": "1 item",
"other": "{{count}} items"
},
"date": "{{val, date}}",
"price": "{{val, currency}}"
}
And es/translation.json
similarly. Run the page: English shows dates as “7/2/2025” and currency as “$99.99”; Spanish shows “02/07/2025” and “US$99,99”. Intl.DateTimeFormat
and Intl.NumberFormat
handle locale-specific formatting.
RTL Support
Languages like Arabic require right-to-left (RTL) layouts. i18next supports direction detection. Add ar/translation.json
:
{
"welcome": "مرحبًا بك في تطبيقي!",
"greeting": "مرحبًا، {{name}}!",
"items": {
"zero": "لا توجد عناصر",
"one": "عنصر واحد",
"two": "عنصران",
"few": "{{count}} عناصر",
"many": "{{count}} عنصرًا",
"other": "{{count}} عنصر"
},
"date": "{{val, date}}",
"price": "{{val, currency}}"
}
Update index.html
:
<select id="language">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="ar">Arabic</option>
</select>
<script>
i18next
.use(i18nextHttpBackend)
.init({
lng: 'en',
backend: { loadPath: '/locales/{{lng}}/translation.json' },
interpolation: { format: /* same as above */ }
}, (err, t) => {
function updateContent() {
document.getElementById('app').setAttribute('dir', i18next.dir());
document.getElementById('welcome').innerText = t('welcome');
document.getElementById('greeting').innerText = t('greeting', { name: 'Alice' });
document.getElementById('items').innerText = t('items', { count: 3 });
document.getElementById('date').innerText = t('date', { val: new Date(), format: 'date' });
document.getElementById('price').innerText = t('price', { val: 99.99, format: 'currency' });
}
updateContent();
document.getElementById('language').addEventListener('change', (e) => {
i18next.changeLanguage(e.target.value, () => {
updateContent();
});
});
});
</script>
Add CSS:
#app {
padding: 20px;
}
Switch to Arabic, and the page becomes RTL with right-to-left text. Plural rules adapt to Arabic (e.g., 3
shows “3 عناصر”). i18next.dir()
returns ltr
or rtl
.
React i18n: react-i18next
For React, react-i18next
integrates i18next with components and hooks for seamless translation management. Let’s build a multi-language app with Create React App.
Project Setup
npx create-react-app react-i18n-demo
cd react-i18n-demo
npm install i18next react-i18next i18next-http-backend
Use the same translation files in public/locales
.
Configuring react-i18next
Create src/i18n.js
:
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
i18next
.use(HttpBackend)
.use(initReactI18next)
.init({
lng: 'en',
backend: {
loadPath: '/locales/{{lng}}/translation.json'
},
interpolation: {
escapeValue: false, // React handles XSS
format: (value, format, lng) => {
if (format === 'date') return new Intl.DateTimeFormat(lng).format(value);
if (format === 'currency') return new Intl.NumberFormat(lng, { style: 'currency', currency: 'USD' }).format(value);
return value;
}
}
});
export default i18next;
Import in src/index.js
:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './i18n';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
Multi-Language Component
Update src/App.js
:
import React from 'react';
import { useTranslation } from 'react-i18next';
function App() {
const { t, i18n } = useTranslation();
return (
<div style={{ padding: 20, direction: i18n.dir() }}>
<h1>{t('welcome')}</h1>
<p>{t('greeting', { name: 'Alice' })}</p>
<p>{t('items', { count: 2 })}</p>
<p>{t('date', { val: new Date(), format: 'date' })}</p>
<p>{t('price', { val: 99.99, format: 'currency' })}</p>
<select onChange={(e) => i18n.changeLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="ar">Arabic</option>
</select>
</div>
);
}
export default App;
Run npm start
. The page shows English content, switching to Spanish or Arabic updates text and direction. The useTranslation
hook provides t
for translations and i18n
for language switching.
Nested Translations
Use the Trans
component for complex HTML:
import { Trans, useTranslation } from 'react-i18next';
function App() {
const { t, i18n } = useTranslation();
return (
<div style={{ padding: 20, direction: i18n.dir() }}>
<Trans i18nKey="welcomeWithHtml">
Welcome to my <strong>app</strong>!
</Trans>
<p>{t('greeting', { name: 'Alice' })}</p>
<select onChange={(e) => i18n.changeLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
);
}
Update en/translation.json
:
{
"welcomeWithHtml": "Welcome to my <1>app</1>!",
"greeting": "Hello, {{name}}!"
}
And es/translation.json
:
{
"welcomeWithHtml": "¡Bienvenido a mi <1>aplicación</1>!",
"greeting": "¡Hola, {{name}}!"
}
The Trans
component maps <1>
to <strong>
, supporting complex HTML structures.
Vue i18n: Vue I18n
Vue uses vue-i18n
for internationalization, offering a clean API and tight integration. Let’s build a project with Vue CLI.
Project Setup
npm install -g @vue/cli
vue create vue-i18n-demo
cd vue-i18n-demo
npm install vue-i18n@9
Use the same translation files in public/locales
.
Configuring Vue I18n
Update src/main.js
:
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import App from './App.vue';
const i18n = createI18n({
locale: 'en',
messages: {
en: {
welcome: 'Welcome to my app!',
greeting: 'Hello, {name}!',
items: {
one: '1 item',
other: '{count} items'
},
date: '{val, date}',
price: '{val, currency}'
},
es: {
welcome: '¡Bienvenido a mi aplicación!',
greeting: '¡Hola, {name}!',
items: {
one: '1 artículo',
other: '{count} artículos'
},
date: '{val, date}',
price: '{val, currency}'
}
},
datetimeFormats: {
en: { short: { year: 'numeric', month: '2-digit', day: '2-digit' } },
es: { short: { year: 'numeric', month: '2-digit', day: '2-digit' } }
},
numberFormats: {
en: { currency: { style: 'currency', currency: 'USD' } },
es: { currency: { style: 'currency', currency: 'USD' } }
}
});
const app = createApp(App);
app.use(i18n);
app.mount('#app');
Multi-Language Component
Update src/App.vue
:
<template>
<div :dir="$i18n.locale === 'ar' ? 'rtl' : 'ltr'" style="padding: 20px;">
<h1>{{ $t('welcome') }}</h1>
<p>{{ $t('greeting', { name: 'Alice' }) }}</p>
<p>{{ $tc('items', 2, { count: 2 }) }}</p>
<p>{{ $d(new Date(), 'short') }}</p>
<p>{{ $n(99.99, 'currency') }}</p>
<select @change="changeLanguage">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="ar">Arabic</option>
</select>
</div>
</template>
<script>
export default {
methods: {
changeLanguage(event) {
this.$i18n.locale = event.target.value;
}
}
};
</script>
Run npm run serve
. The page shows English content, switching to Spanish or Arabic updates text and direction. $t
translates text, $tc
handles plurals, and $d
/$n
format dates and numbers.
Dynamic Loading
Use i18next-http-backend
for dynamic loading:
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import HttpBackend from 'i18next-http-backend';
import i18next from 'i18next';
import App from './App.vue';
i18next.use(HttpBackend).init({
lng: 'en',
backend: { loadPath: '/locales/{{lng}}/translation.json' }
});
const i18n = createI18n({
locale: 'en',
legacy: false,
globalInjection: true,
messages: {}
});
i18next.on('loaded', (loaded) => {
Object.keys(loaded).forEach(locale => {
i18n.global.setLocaleMessage(locale, loaded[locale].translation);
});
});
const app = createApp(App);
app.use(i18n);
app.mount('#app');
Place translation files in public/locales
. Language switches load JSON files asynchronously.
Complex Scenarios: Nested Translations and Components
React Nested Translations
Use Trans
for nested HTML:
import { Trans, useTranslation } from 'react-i18next';
function App() {
const { t, i18n } = useTranslation();
return (
<div style={{ padding: 20, direction: i18n.dir() }}>
<Trans i18nKey="nested">
Welcome to <a href="/about">my app</a>!
</Trans>
<select onChange={(e) => i18n.changeLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
);
}
Update en/translation.json
:
{
"nested": "Welcome to <1>my app</1>!"
}
And es/translation.json
:
{
"nested": "¡Bienvenido a <1>mi aplicación</1>!"
}
Trans
maps <1>
to <a>
, supporting complex HTML.
Vue Nested Translations
Use Vue I18n’s v-t
directive:
<template>
<div :dir="$i18n.locale === 'ar' ? 'rtl' : 'ltr'" style="padding: 20px;">
<h1 v-t="'welcome'"></h1>
<p v-t="{ path: 'nested', args: { name: 'Alice' } }"></p>
<select @change="changeLanguage">
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
</template>
Update en/translation.json
:
{
"welcome": "Welcome to my app!",
"nested": "Hello, <1>{{name}}</1>!"
}
And es/translation.json
:
{
"welcome": "¡Bienvenido a mi aplicación!",
"nested": "¡Hola, <1>{{name}}</1>!"
}
<1>
maps to HTML tags, and Vue I18n handles interpolation.
Dynamic Language Detection
Automatically detect user language with i18next-browser-languagedetector
:
npm install i18next-browser-languagedetector
Update React’s i18n.js
:
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18next
.use(HttpBackend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
backend: { loadPath: '/locales/{{lng}}/translation.json' },
interpolation: { escapeValue: false }
});
LanguageDetector
checks navigator.language
to load the appropriate translation. Vue I18n offers similar plugins.
Conclusion (Technical Details)
Frontend i18n handles translation, formatting, and cultural adaptation. i18next works for plain JavaScript and React (via react-i18next
), while Vue I18n is tailored for Vue. The examples demonstrated:
- i18next for translations, plurals, formatting, and RTL support.
-
react-i18next
with hooks andTrans
component. - Vue I18n with
$t
,$tc
, and dynamic loading. - Dynamic language detection and nested translations.
Run these examples, switch languages, and experience the power of i18n!
Top comments (0)