Building Bilingual Web Apps with i18next (EN/JA)
Practical patterns for internationalizing React applications with i18next, with a focus on English and Japanese — from initial setup to translation management at scale.
We have built several bilingual web applications that serve both English and Japanese-speaking users. Internationalization is one of those things that is straightforward if you set it up correctly from the start, and painful to retrofit later. After iterating across multiple projects, here is the approach we have settled on using i18next with React.
Initial Setup
Install the dependencies:
npm install i18next react-i18next
Create your i18n configuration:
// src/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import ja from './locales/ja.json';
import en from './locales/en.json';
const savedLanguage = localStorage.getItem('language') || 'ja';
i18n.use(initReactI18next).init({
resources: {
ja: { translation: ja },
en: { translation: en },
},
lng: savedLanguage,
fallbackLng: 'ja',
interpolation: {
escapeValue: false,
},
});
export default i18n;
Then import it at the top of your app entry point:
// App.tsx
import './i18n'; // Must be imported before any component that uses translations
That is it. Every component in your React tree now has access to translations via the useTranslation hook.
A few decisions worth explaining:
We bundle translations directly rather than loading them asynchronously. For two languages with a few thousand keys each, the JSON files add maybe 30-50 KB to your bundle. The complexity of async loading, loading states, and error handling is not worth it at this scale.
The fallback language is Japanese, not English. Set the fallback to whichever language your primary user base speaks. When a translation key is missing, falling back to a language your users actually read is far better than showing English text in the middle of a Japanese interface.
Language preference persists to localStorage. When a user selects their language, it sticks across sessions. The one-liner localStorage.getItem('language') || 'ja' handles both returning users and first-time visitors.
Structuring Translation Files
Organize your translations by feature, not by UI element:
{
"common": {
"loading": "Loading...",
"error": "An error occurred",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"confirm": "Confirm"
},
"auth": {
"login": "Log In",
"logout": "Log Out",
"email": "Email Address",
"password": "Password",
"forgotPassword": "Forgot your password?"
},
"events": {
"title": "Events",
"create": "Create Event",
"rsvp": "RSVP",
"attendees": "Attendees",
"noEvents": "No upcoming events"
},
"admin": {
"dashboard": "Dashboard",
"totalCount": "Total: {{count}}"
}
}
And the Japanese equivalent:
{
"common": {
"loading": "読み込み中...",
"error": "エラーが発生しました",
"save": "保存",
"cancel": "キャンセル",
"delete": "削除",
"confirm": "確認"
},
"auth": {
"login": "ログイン",
"logout": "ログアウト",
"email": "メールアドレス",
"password": "パスワード",
"forgotPassword": "パスワードをお忘れですか?"
},
"events": {
"title": "イベント",
"create": "イベント作成",
"rsvp": "参加登録",
"attendees": "参加者",
"noEvents": "予定されているイベントはありません"
},
"admin": {
"dashboard": "ダッシュボード",
"totalCount": "合計: {{count}}"
}
}
The nested structure keeps related strings together and makes it easy to hand off a specific section to a translator. When your app has hundreds of keys, this organization is essential.
Using Translations in Components
The useTranslation hook is your primary interface:
import { useTranslation } from 'react-i18next';
export default function EventsPage() {
const { t } = useTranslation();
return (
<div>
<h1>{t('events.title')}</h1>
<button>{t('events.create')}</button>
<p>{t('admin.totalCount', { count: 42 })}</p>
</div>
);
}
For the language switcher:
import { useTranslation } from 'react-i18next';
export default function LanguageSwitcher() {
const { i18n } = useTranslation();
const changeLanguage = (lang: string) => {
i18n.changeLanguage(lang);
localStorage.setItem('language', lang);
};
return (
<select
value={i18n.language}
onChange={(e) => changeLanguage(e.target.value)}
>
<option value="ja">日本語</option>
<option value="en">English</option>
</select>
);
}
Notice that we save to localStorage on every change. This keeps the i18next state and the persisted preference in sync.
Japanese-Specific Considerations
Building for Japanese users introduces challenges that do not exist in English-only applications:
Text length varies dramatically. Japanese text is often shorter than its English equivalent because of kanji density, but not always. “Forgot your password?” becomes “パスワードをお忘れですか?” — roughly the same visual width in a proportional font, but it depends heavily on the phrase. Design your UI with flexible layouts rather than fixed widths.
Line breaking rules differ. Japanese text does not have spaces between words, so the browser’s line-breaking algorithm needs the word-break: break-all or overflow-wrap: break-word CSS property to wrap correctly in narrow containers. Without this, long Japanese strings can overflow their containers.
.text-content {
overflow-wrap: break-word;
word-break: normal;
line-break: strict; /* Follows Japanese typographic rules */
}
Date and number formatting. Japanese users expect dates in YYYY年MM月DD日 format and numbers with comma separators (same as English). Use Intl.DateTimeFormat and Intl.NumberFormat with the appropriate locale rather than hardcoding formats:
const formatDate = (date: Date, locale: string) =>
new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
formatDate(new Date(), 'ja'); // "2026年3月16日"
formatDate(new Date(), 'en'); // "March 16, 2026"
Honorifics and formality levels. Japanese has formal (敬語) and casual registers. For a business application, use the polite form consistently. “Delete” is “削除” (neutral), but confirmation prompts should use polite phrasing like “削除してもよろしいですか?” rather than the casual “削除する?”.
Adding a Third Language
When you need to add another language — we have added Spanish to some projects — the pattern scales cleanly:
import es from './locales/es.json';
i18n.use(initReactI18next).init({
resources: {
ja: { translation: ja },
en: { translation: en },
es: { translation: es },
},
// ...
});
The challenge is not the code; it is the translations. A partial translation (say, 15% of keys translated) creates a terrible user experience where the interface randomly switches between languages. Our approach is to only expose a language option in the UI once it reaches at least 90% coverage. Until then, untranslated keys fall back to the fallback language.
Translation Management at Scale
With 2,000+ translation keys across multiple languages, you need a process:
Keep English and Japanese in sync. When a developer adds a new key to en.json, they should add the Japanese key in the same commit — even if it is a rough translation that gets refined later. Empty keys are easy to miss; a rough translation at least communicates intent.
Use interpolation instead of concatenation. Never do this:
// Bad: breaks in Japanese where word order differs
t('greeting') + name + t('welcome')
Always use interpolation:
// Good: translators control the word order
t('greeting', { name })
// en: "Hello, {{name}}! Welcome back."
// ja: "{{name}}さん、おかえりなさい。"
Japanese sentence structure is Subject-Object-Verb (SOV), while English is Subject-Verb-Object (SVO). Interpolation lets translators place variables wherever they belong in the target language.
Review translations with native speakers. Machine translation gets you 80% of the way, but the remaining 20% — natural phrasing, appropriate formality, domain-specific terminology — requires a native speaker’s review. Budget for this.
The Payoff
Internationalization done right is invisible to users. They select their language once, and everything just works — every button, every error message, every date format adapts. The investment in setting up i18next properly from the start pays off every time you add a new feature, because the pattern is established and there is no temptation to hardcode strings “just this once.”
For us, the English/Japanese combination is a core competency. Many of our clients operate across both markets, and delivering applications that feel native in both languages — not just translated, but culturally appropriate — is a significant differentiator.
KeyQ builds bilingual and multilingual web applications for businesses operating across borders. Contact us if you need software that speaks your customers’ language.