Building Multilingual React & React Native Apps: A Developer’s Guide to Type-Safe Localization
Building Multilingual React & React Native Apps: A Developer’s Guide to Type-Safe Localization

How we simplified internationalization in React and React Native with a lightweight, type-safe solution
The Problem We All Face
You’re building a React application, and your product manager walks in with that familiar smile. “Great news!” they say. “We’re expanding to three new markets. We need the app in French, Spanish, and German by next month.”
Your heart sinks a little. You’ve been here before. You know what’s coming: managing translation files, keeping them in sync, dealing with missing keys, and wrestling with type safety. The last time you did this, you spent more time debugging translation issues than building features.
If this sounds familiar, you’re not alone. Internationalization (i18n) is one of those necessary evils in modern web development. It’s crucial for reaching global audiences, but it can quickly become a maintenance nightmare.
Enter: A Better Way
After one too many late nights fixing translation bugs, I decided to build a solution that would make localization actually enjoyable. The result is @weprodev/ui-localization — a lightweight, type-safe localization package that makes working with translations feel natural and maintainable.
In this article, I’ll walk you through how to use it, why it’s different, and how it can save you hours of debugging time.
What Makes This Package Special?
Before we dive into the code, let me tell you what sets this package apart:
1.Type Safety: Your IDE knows exactly what translation keys exist, and it’ll yell at you if you try to use one that doesn’t.
2. Simple API: Just a few hooks, and you’re done. No complex configuration.
3. Built-in Tools: CLI tools to validate and sync translations automatically.
4. React & React Native: Works seamlessly with both platforms.
5. Developer Experience: Autocomplete, IntelliSense, and compile-time error checking.
Sounds good? Let’s get started.
Installation: Getting Set Up
First things first, let’s install the package:
npm install @weprodev/ui-localizationThat’s it! You’re ready to go.
Setting Up Your Translations
The first step is organizing your translation files. I recommend creating a translations directory in your project root.
Creating Translation Files
Let’s start with English as our base language:
// translations/en.ts
const en = {
common: {
hello: "Hello {{name}}!",
welcome: "Welcome {{name}}!",
goodbye: "Goodbye {{name}}!",
greeting: "Hello, {{name}}!"
},
auth: {
login: "Login",
signup: "Sign Up",
forgotPassword: "Forgot Password",
welcomeMessage: "Welcome <strong>{{name}}</strong>! Please <link>sign in</link> to continue."
},
dashboard: {
title: "Dashboard",
stats: {
today: "Today",
thisWeek: "This Week",
},
},
} as const;
export en;Now let’s add French:
// translations/fr.ts
const fr = {
common: {
hello: "Bonjour {{name}}!",
welcome: "Bienvenue {{name}}!",
goodbye: "Au revoir {{name}}!",
greeting: "Bonjour, {{name}}!"
},
auth: {
login: "Connexion",
signup: "S'inscrire",
forgotPassword: "Mot de passe oublié",
welcomeMessage: "Bienvenue <strong>{{name}}</strong>! Veuillez <link>vous connecter</link> pour continuer."
},
dashboard: {
title: "Tableau de bord",
stats: {
today: "Aujourd'hui",
thisWeek: "Cette semaine",
},
},
} as const;
export default fr;Notice how the structure is identical? That’s intentional. The package will help you keep it that way.
Configuring Localization
Now let’s set up the localization configuration. Create a file called localizationConfig.ts:
// src/localizationConfig.ts
import { LocalizationConfig, LanguageStore } from '@weprodev/ui-localization';
import en from './translations/en';
import fr from './translations/fr';
import es from './translations/es'; // Add more languages as needed
// Optional: Create a custom language store
// This is where the selected language is persisted
class MyLanguageStore implements LanguageStore {
getLanguage(): string | null {
// For web apps, use localStorage
return localStorage.getItem("language") || null;
// For React Native, you might use AsyncStorage:
// return await AsyncStorage.getItem("language");
}
setLanguage(language: string): void {
localStorage.setItem("language", language);
// For React Native:
// await AsyncStorage.setItem("language", language);
}
}
export const localizationConfig: LocalizationConfig = {
resources: {
en: { translation: en },
fr: { translation: fr },
es: { translation: es },
},
fallbackLng: 'en', // Default language if detection fails
languageStore: new MyLanguageStore(), // Optional: uses default if not provided
};The LanguageStore interface is what makes this flexible. You can store the user’s language preference anywhere: localStorage, AsyncStorage, a database, or even a cookie. The package doesn’t care — it just needs to know how to get and set it.
Initializing in Your App
Before your app renders, you need to initialize localization. This is typically done in your entry point:
// src/index.tsx
import React, { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { initLocalization } from '@weprodev/ui-localization';
import { localizationConfig } from './localizationConfig';
import App from './App';
const rootElement = document.getElementById('root');
// Initialize localization before rendering
initLocalization(localizationConfig).then(() => {
ReactDOM.createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>
);
});The initLocalization function returns a Promise, so we wait for it to complete before rendering. This ensures that translations are ready when your components mount.
Using Translations: The Fun Part
Now comes the part where you’ll fall in love with this package. Let’s see how easy it is to use translations in your components.
Creating a Type-Safe Hook
First, let’s create a type-safe translation hook. This is optional but highly recommended:
// src/hooks/useTranslation.ts
import { useTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
export const useAppTranslation = () => {
return useTranslation<typeof en>();
};This hook knows the exact structure of your English translations, which means TypeScript will provide autocomplete and catch errors at compile time.
Basic Usage
Now let’s use it in a component:
import { useAppTranslation } from '../hooks/useTranslation';
function Welcome({ name }: { name: string }) {
const { t } = useAppTranslation();
return (
<div>
<h1>{t('common.welcome', { name })}</h1>
<p>{t('common.hello', { name })}</p>
</div>
);
}Notice how t.common.welcome gives you autocomplete? That’s the magic of TypeScript. If you try to type t.common.wrongKey, TypeScript will immediately tell you it doesn’t exist.
Also notice that TypeScript requires the { name } parameter because the translation string contains {{name}}. If you forget it, you’ll get a compile-time error!
Changing Languages
Switching languages is just as simple:
import { useLanguage } from '@weprodev/ui-localization';
function LanguageSwitcher() {
const { currentLanguage, changeLanguage, availableLanguages } = useLanguage();
return (
<select
value={currentLanguage}
onChange={(e) => changeLanguage(e.target.value)}
>
{availableLanguages.map(lang => (
<option key={lang} value={lang}>
{lang.toUpperCase()}
</option>
))}
</select>
);
}The useLanguage hook gives you:
currentLanguage: The currently active language
changeLanguage: A function to switch languages
availableLanguages: An array of all configured languages
When you call changeLanguage, the entire app updates automatically. No manual re-renders needed.
Advanced Features
Dynamic Values: String Interpolation
The same t function handles variable injection seamlessly. TypeScript enforces that you provide all required parameters:
import { useAppTranslation } from '../hooks/useTranslation';
function Greeting({ name }: { name: string }) {
const { t } = useAppTranslation();
// ✅ TypeScript requires the 'name' parameter because translation has {{name}} placeholder
const greeting = t('common.greeting', { name });
// ❌ TypeScript error: missing required parameter 'name'
// const greeting = t('common.greeting');
// ❌ TypeScript error: wrong parameter name
// const greeting = t('common.greeting', { wrongName: name });
// ✅ No parameters needed for translations without placeholders
const title = t('dashboard.title');
return <p>{greeting}</p>;
}The {{name}} placeholder gets replaced with the actual name value. You can use multiple placeholders in a single string, and TypeScript will require all of them.
Component Interpolation: When You Need More
What if you want to include React components inside your translations? For example, making part of a sentence bold or adding a link:
The same unified t function handles component interpolation! Just pass a components object as the third argument:
import { useAppTranslation } from '../hooks/useTranslation';
function TermsAgreement({ name }: { name: string }) {
const { t } = useAppTranslation();
// Translation: "auth.welcomeMessage": "Welcome <strong>{{name}}</strong>! Please <link>sign in</link> to continue."
// The translation string must contain matching HTML-like tags (<strong>, <link>, etc.)
const welcomeElement = t(
'auth.welcomeMessage',
{ name },
{
strong: <strong className="highlight" />,
link: (props: { children?: React.ReactNode }) => (
<a href="#login" className="link-button">
{props.children}
</a>
)
}
);
// Returns JSX.Element when components are provided
return <div>{welcomeElement}</div>;
}This is incredibly powerful. You can inject any React component into your translations, making them flexible and reusable. The component keys in your components object must match the tag names in the translation (e.g., <strong> matches strong, <link> matches link).
Important Notes:
Component interpolation only works when the translation string contains matching HTML-like tags (e.g.,
<strong>,<link>)The component keys in your
componentsobject must match the tag names in the translationFor self-closing tags like
<strong />, use self-closing componentsFor tags with content like
<link>text</link>, use function components that acceptprops.children
Using Translations Outside React Components
Sometimes you need translations outside of React components — in utilities, API clients, or other non-component code. That’s where createTranslation comes in:
import { createTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
// Create a type-safe translation function
const t = createTranslation(en);
// Use it anywhere - no React hooks needed!
function sendEmail(userName: string) {
const subject = t('common.greeting', { name: userName });
// ... send email logic
}This is perfect for server-side code, utilities, or any place where React hooks aren’t available.
Keeping Translations in Sync: The CLI Tools
Here’s where the package really shines. Managing multiple translation files is error-prone. You add a key to English, forget to add it to French, and suddenly your app shows common.newKey instead of the actual translation.
The package includes two CLI tools to solve this:
Validating Translations
This tool checks if all language files have the same keys:
npx wpd-translation-validate --dir ./translations --source enIf any language is missing keys, it’ll tell you exactly which ones. Perfect for CI/CD pipelines!
Syncing Translations
This tool automatically adds missing keys from the source language to all other languages:
npx wpd-translation-sync --dir ./translations --source enMissing keys are added with empty strings, so you know exactly what needs to be translated.
Adding to Your Workflow
I recommend adding these to your package.json:
{
"scripts": {
"translation:validate": "wpd-translation-validate --dir ./translations --source en",
"translation:sync": "wpd-translation-sync --dir ./translations --source en"
}
}Then run npm run translation:validate before committing, or add it to your CI pipeline to catch issues automatically.
Best Practices
After using this package in production, here are some tips I’ve learned:
1. Organize by Feature: Group translations by feature (auth, dashboard, settings) rather than by component. It makes maintenance easier.
2. Use Nested Objects: Don’t flatten everything. dashboard.stats.today is more maintainable than dashboardStatsToday.
3. Validate in CI: Add translation validation to your CI pipeline. Catch missing keys before they reach production.
4. Sync Regularly: Run translation:sync after adding new features to keep all languages in sync.
5. Type Your Hook: Always create a typed useTranslation hook. The autocomplete is worth the extra file.
6. Fallback Language: Always set a fallbackLng. If a translation is missing, users will see the fallback language instead of a key.
Why This Approach Works
You might be wondering: why not just use i18next directly? You absolutely can, but this package adds a layer of developer experience that makes a huge difference:
- Type Safety: No more typos in translation keys
- Autocomplete: Your IDE suggests available keys as you type
- Validation Tools: Catch missing translations before deployment
- Simple API: Less boilerplate, more productivity
- Flexibility: Works with any storage mechanism you prefer
It’s built on top of i18next (the industry standard), so you get all the power with none of the complexity.
What Changed in v2.0
If you’re upgrading from v1.0, here’s what you need to know:
Breaking Changes:
- useTranslation now returns { t } instead of a proxy object
- Translation keys are accessed via function calls: t(‘common.welcome’) instead of t.common.welcome
- Removed useTranslationWithInterpolation and useTranslationInjection hooks — functionality merged into the unified t function
New Features:
createTranslationutility for use outside React componentsEnhanced type safety with better parameter validation
Unified API — one function handles both variables and components
The migration is straightforward: change from property access to function calls, and you’re done!
Conclusion
Localization doesn’t have to be painful. With the right tools and approach, it can be straightforward and even enjoyable.
@weprodev/ui-localization gives you:
Type-safe translations
Simple, intuitive API
Built-in validation and sync tools
Support for both React and React Native
Excellent developer experience
If you’re starting a new project or refactoring an existing one, give it a try. Your future self (and your product manager) will thank you.
Getting Started
Ready to try it out? Here’s a quick checklist:
Install the package
Create your translation files
3. Set up the configuration
4. Initialize in your app
5. Create a typed translation hook
6. Start using translations in your components
7. Add validation to your workflow
Questions? Found a bug? Want to contribute? Check out the GitHub repository.