Introduction
Over the past three months, I’ve been working solo on a Mega SaaS idea. While it’s been an exciting journey, the challenges have been immense. As I approach the last two weeks, delivering high-priority use cases while maintaining quality has been my top priority.
One of the key decisions I faced was whether to integrate internationalization (i18n) to support multiple languages. Initially, I leaned toward launching with an English-only version, leveraging LLMs for translations in the future. However, as a one-person team, I chose to focus on a single lucrative market for now.
Although optional for my project, internationalization is essential in professional settings due to legal and regulatory requirements. This blog explores how to design a scalable and efficient i18n architecture, avoiding pitfalls like high complexity or poor structure—insights that can benefit both solo developers and teams.
Since I decided not to implement i18n in my project, I’m sharing this guide to help others (and my future self!).
Goals
A good internationalization system should:
- Scalability: Support seamless collaboration across teams for translations and language updates.
- Modularity: Maintain a simple structure that is easy to extend without overhead.
- Predictability: Follow consistent, enforceable patterns for localization.
- Beginner-Friendly: Be accessible to developers of varying skill levels.
Available Tools
For internationalization in JavaScript, here are some popular tools:
- i18next: A mature, feature-rich library ideal for scalable, professional-grade localization.
- Alternatives: FormatJS, Polyglot.js, LinguiJS, GlobalizeJS, Fluent by Mozilla.
Each tool has its pros and cons. For simplicity, this guide focuses on i18next.
Designing the Architecture
Folder Structure for Internationalization
The architecture centers around an i18n
folder containing three key components:
Translations Folder: Stores JSON files for each language (e.g.,
en.json
,ar.json
,ch.json
) and abase.json
template for new languages.index.js
: Configures and initializes the i18n library (e.g.,i18next
), setting fallback languages and other options.keys.js
: A centralized structure defining translation keys, ensuring consistency and avoiding duplication.
Example Folder Structure:
src/
├── i18n/
│ ├── translations/
│ │ ├── en.json # English translations
│ │ ├── ar.json # Arabic translations
│ │ ├── ch.json # Chinese translations
│ │ └── base.json # Template for new languages
│ ├── index.js # i18n configuration
│ └── keys.js # Centralized keys for consistency
keys.js
as the Central Hub
The keys.js
file mirrors the project’s structure, grouping keys by feature or module. This structure makes managing keys intuitive and scalable.
Example keys.js
:
const keys = {
components: {
featureA: {
buttonText: "components.featureA.buttonText",
label: "components.featureA.label",
},
featureB: {
header: "components.featureB.header",
},
},
};
export default keys;
Translation Files
Translation JSON files align with the structure in keys.js
, ensuring consistency.
Example en.json
:
{
"components": {
"featureA": {
"buttonText": "Submit",
"label": "Enter your name"
},
"featureB": {
"header": "Welcome to Feature B"
}
}
}
Example ar.json
:
{
"components": {
"featureA": {
"buttonText": "إرسال",
"label": "أدخل اسمك"
},
"featureB": {
"header": "مرحبًا بكم في الميزة ب"
}
}
}
Setting Up i18next
Install i18next and its React integration:
npm install i18next react-i18next
i18n/index.js
:
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "./translations/en.json";
import ar from "./translations/ar.json";
i18n.use(initReactI18next).init({
resources: { en: { translation: en }, ar: { translation: ar } },
lng: "en", // Default language
fallbackLng: "en",
interpolation: { escapeValue: false }, // React handles escaping
});
export default i18n;
Integrating i18n into Components
Example Component (FeatureA
):
import React from "react";
import { useTranslation } from "react-i18next";
import keys from "../../i18n/keys";
const FeatureA = () => {
const { t } = useTranslation();
return (
<div>
<h2>Feature A</h2>
<button>{t(keys.components.featureA.buttonText)}</button>
<label>{t(keys.components.featureA.label)}</label>
</div>
);
};
export default FeatureA;
Adding a Language Switcher
A language switcher allows users to toggle between languages.
LanguageSwitcher.jsx
:
import React from "react";
import { useTranslation } from "react-i18next";
const LanguageSwitcher = () => {
const { i18n } = useTranslation();
const changeLanguage = (lang) => {
i18n.changeLanguage(lang);
};
return (
<div>
<button onClick={() => changeLanguage("en")}>English</button>
<button onClick={() => changeLanguage("ar")}>العربية</button>
</div>
);
};
export default LanguageSwitcher;
Final Folder Structure
src/
├── components/
│ ├── featureA/
│ │ ├── index.jsx
│ │ └── featureAStyles.css
│ └── shared/
│ └── LanguageSwitcher.jsx
├── i18n/
│ ├── translations/
│ │ ├── en.json
│ │ ├── ar.json
│ │ └── base.json
│ ├── keys.js
│ └── index.js
├── App.jsx
├── index.js
Going Beyond
Leverage AI for Translations: Use LLMs for rapid translations. For example, prompt:
"Translate the following JSON to Chinese: {contents ofen.json
}."Backend-Driven Translations: Centralize translations on the backend to enable dynamic updates without code deployments. Options include GitOps or a dedicated backend service.
Demo
Sandbox: https://codesandbox.io/p/sandbox/785hpz
Conclusion
Internationalization is a critical step for scaling applications globally. By following this guide, you’ll have a scalable, modular, and beginner-friendly architecture that supports seamless localization for solo projects or large teams.
Happy coding!
— Ahmed R. Aldhafeeri
Top comments (0)