A lightweight, professional localization package for React and React Native applications built on top of i18next and react-i18next. This package provides a clean, type-safe API for managing translations with built-in language switching, variable injection, and component interpolation capabilities.
- π Simple React Hooks - Clean, intuitive hooks for translations and language management
- π Language Switching - Built-in language switching with persistence
- π Type-Safe Variable Injection - Dynamic content insertion with compile-time parameter validation
- π§© Unified Component Interpolation - Embed React components within translations using the same
tfunction - π Translation Validation - CLI tools to ensure translation consistency
- π Translation Sync - Automated synchronization of translation files
- π± React Native Support - Full compatibility with React Native applications
- π‘οΈ Full Type Safety - TypeScript support with type-safe keys, parameters, and component interpolation
- β‘ Performance Optimized - Lightweight wrapper with minimal overhead
npm install @weprodev/ui-localizationCreate a translations directory in your project with language files. Important: Use as const to enable type-safe parameter validation:
// 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",
summary: "Summary",
recentActivity: "Recent Activity"
}
} as const;
export default en;// translations/es.ts
const es = {
common: {
hello: "Hola {{name}}!",
welcome: "Bienvenido {{name}}!",
goodbye: "AdiΓ³s {{name}}!",
greeting: "Β‘Hola, {{name}}!"
},
auth: {
login: "Iniciar sesiΓ³n",
signup: "Registrarse",
forgotPassword: "ContraseΓ±a olvidada",
welcomeMessage: "Bienvenido <strong>{{name}}</strong>! Por favor <link>inicia sesiΓ³n</link> para continuar."
},
dashboard: {
title: "Panel de control",
summary: "Resumen",
recentActivity: "Actividad reciente"
}
} as const;
export default es;// src/localizationConfig.ts
import { LocalizationConfig, LanguageStore } from '@weprodev/ui-localization';
import en from '../translations/en';
import es from '../translations/es';
// Optional: Create a custom language store for persistence
class CustomLanguageStore implements LanguageStore {
getLanguage(): string | null {
return localStorage.getItem("app-language") || null;
}
setLanguage(language: string): void {
localStorage.setItem("app-language", language);
}
}
export const localizationConfig: LocalizationConfig = {
resources: {
en: { translation: en },
es: { translation: es }
},
fallbackLng: 'en',
languageStore: new CustomLanguageStore()
};// 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 the app
initLocalization(localizationConfig).then(() => {
ReactDOM.createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>
);
});// src/components/Welcome.tsx
import React from 'react';
import { useTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
const Welcome: React.FC<{ name: string }> = ({ name }) => {
// Type-safe translation hook with path-based keys
const { t } = useTranslation<typeof en>();
return (
<div>
{/* TypeScript requires 'name' parameter because translation has {{name}} placeholder */}
<h1>{t('common.welcome', { name })}</h1>
<p>{t('common.hello', { name })}</p>
{/* TypeScript will error if you use invalid keys like t('common.invalid') */}
{/* TypeScript will error if you forget required parameters */}
</div>
);
};
export default Welcome;Alternative: Create a custom hook for better reusability
// src/hooks/useAppTranslation.ts
import { useTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
export const useAppTranslation = () => {
return useTranslation<typeof en>();
};
// Usage in components
import { useAppTranslation } from '../hooks/useAppTranslation';
const Welcome: React.FC = () => {
const { t } = useAppTranslation();
return (
<div>
<h1>{t('common.welcome')}</h1> {/* Full intellisense and type safety */}
<p>{t('common.hello')}</p>
{/* t('common.invalid') will show TypeScript error */}
</div>
);
};Important: For type-safe parameter validation to work, your translation files must use as const. This ensures TypeScript preserves literal string types, allowing the system to extract parameter names from placeholders like {{name}}.
Troubleshooting: If you encounter TypeScript errors with the type-safe hook, you can use useTranslationFallback() as an escape hatch. See the API Reference for details.
// src/components/LanguageSwitcher.tsx
import React from 'react';
import { useLanguage } from '@weprodev/ui-localization';
const LanguageSwitcher: React.FC = () => {
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>
);
};
export default LanguageSwitcher;Same structure as React applications (see above).
// src/localizationConfig.ts
import { LocalizationConfig, LanguageStore } from '@weprodev/ui-localization';
import AsyncStorage from '@react-native-async-storage/async-storage';
import en from '../translations/en';
import es from '../translations/es';
// React Native language store using AsyncStorage
class ReactNativeLanguageStore implements LanguageStore {
async getLanguage(): Promise<string | null> {
try {
return await AsyncStorage.getItem("app-language");
} catch {
return null;
}
}
async setLanguage(language: string): Promise<void> {
try {
await AsyncStorage.setItem("app-language", language);
} catch {
// Handle storage error silently
}
}
}
export const localizationConfig: LocalizationConfig = {
resources: {
en: { translation: en },
es: { translation: es }
},
fallbackLng: 'en',
languageStore: new ReactNativeLanguageStore()
};// src/App.tsx
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';
import { initLocalization } from '@weprodev/ui-localization';
import { localizationConfig } from './localizationConfig';
const App: React.FC = () => {
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
initLocalization(localizationConfig).then(() => {
setIsInitialized(true);
});
}, []);
if (!isInitialized) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Loading...</Text>
</View>
);
}
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
{/* Your app content */}
</View>
);
};
export default App;// src/components/Welcome.tsx
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
const Welcome: React.FC<{ name: string }> = ({ name }) => {
// Type-safe translation hook with path-based keys
const { t } = useTranslation<typeof en>();
return (
<View style={styles.container}>
<Text style={styles.title}>{t('common.welcome', { name })}</Text>
<Text style={styles.subtitle}>{t('common.hello', { name })}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 10,
},
subtitle: {
fontSize: 16,
color: '#666',
},
});
export default Welcome;// src/components/LanguageSwitcher.tsx
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { useLanguage } from '@weprodev/ui-localization';
const LanguageSwitcher: React.FC = () => {
const { currentLanguage, changeLanguage, availableLanguages } = useLanguage();
return (
<View style={styles.container}>
<Text style={styles.label}>Language:</Text>
<View style={styles.buttonContainer}>
{availableLanguages.map(lang => (
<TouchableOpacity
key={lang}
style={[
styles.button,
currentLanguage === lang && styles.activeButton
]}
onPress={() => changeLanguage(lang)}
>
<Text style={[
styles.buttonText,
currentLanguage === lang && styles.activeButtonText
]}>
{lang.toUpperCase()}
</Text>
</TouchableOpacity>
))}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 20,
},
label: {
fontSize: 16,
fontWeight: 'bold',
marginBottom: 10,
},
buttonContainer: {
flexDirection: 'row',
gap: 10,
},
button: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 4,
borderWidth: 1,
borderColor: '#ddd',
},
activeButton: {
backgroundColor: '#007AFF',
borderColor: '#007AFF',
},
buttonText: {
fontSize: 14,
color: '#333',
},
activeButtonText: {
color: '#fff',
},
});
export default LanguageSwitcher;The t function enforces type-safe parameters based on placeholders in your translation strings. Parameters are required when placeholders exist, and optional when they don't.
import { useTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
const Greeting: React.FC<{ name: string }> = ({ name }) => {
const { t } = useTranslation<typeof en>();
// Translation: "common.greeting": "Hello, {{name}}!"
// β
TypeScript requires the 'name' parameter
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 unified t function supports both string and component interpolation. When you pass a components object as the third argument, it returns a React element instead of a string.
import { useTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
const WelcomeMessage: React.FC<{ name: string }> = ({ name }) => {
const { t } = useTranslation<typeof en>();
// 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>;
};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 translation - For self-closing tags like
<strong />, use self-closing components - For tags with content like
<link>text</link>, use function components that acceptprops.children
For utility functions or other non-component files where hooks are not available, you can use createTranslation to get a type-safe t function.
import { createTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
// Create a standalone translation function
const t = createTranslation<typeof en>();
// Basic usage - Returns string
const greeting = t('common.hello');
// With parameters - Returns string
const message = t('common.welcome', { name: 'World' });
// With components - Returns JSX.Element
// Useful for creating localized constants with React elements
const content = t('common.info', undefined, {
link: <a href="/more">More</a>
});The package includes powerful CLI tools to help manage your translations. You can use these tools in multiple ways:
Add these commands to your project's package.json scripts:
{
"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
npm run translation:syncCustomizing paths:
Modify the scripts in your package.json to use different directories or source language:
{
"scripts": {
"translation:validate": "wpd-translation-validate --dir ./src/translations --source es",
"translation:sync": "wpd-translation-sync --dir ./src/translations --source es"
}
}Run the CLI tools directly via npx:
# Validate translations
npx wpd-translation-validate --dir ./translations --source en
# Sync translations
npx wpd-translation-sync --dir ./translations --source enValidate Translations (validate-translations)
- Checks if all language files have the same keys as the source language
- Reports missing keys for each language file
- Exits with error code if inconsistencies are found
- Perfect for CI/CD integration
Sync Translations (sync-translations)
- Adds missing keys from the source language to all other language files
- Preserves existing translations
- Sets empty string values for new keys (ready for translation)
- Updates translation files automatically
We recommend running translation:validate as part of your CI pipeline to ensure translation consistency:
# .github/workflows/ci.yml
- name: Validate Translations
run: npm run translation:validateReturns a type-safe translation function with intellisense support and type-safe parameter validation.
Parameters:
- None (the translation object type
Tis provided as a generic parameter for type safety)
Returns: Object with t function that accepts type-safe translation keys
The t function supports two modes:
- String interpolation (returns
string):
import en from '../translations/en';
const { t } = useTranslation<typeof en>();
// No parameters needed for translations without placeholders
const title = t('dashboard.title'); // β
Returns string
// Parameters required when placeholders exist
const greeting = t('common.greeting', { name: 'John' }); // β
Returns string
const greeting = t('common.greeting'); // β TypeScript error: missing required parameter- Component interpolation (returns
JSX.Element):
// Pass components as third argument
const welcomeElement = t(
'auth.welcomeMessage',
{ name: 'Alice' },
{
strong: <strong className="highlight" />,
link: (props) => <a href="#login">{props.children}</a>
}
); // β
Returns JSX.ElementType-Safe Parameters:
- Parameters are required when the translation string contains placeholders like
{{name}} - Parameters are optional when the translation has no placeholders
- TypeScript validates parameter names match the placeholders in the translation
Note: For better reusability, consider creating a custom hook:
// src/hooks/useAppTranslation.ts
import { useTranslation } from '@weprodev/ui-localization';
import en from '../translations/en';
export const useAppTranslation = () => {
return useTranslation<typeof en>();
};
// Usage
const { t } = useAppTranslation();
const text = t('common.welcome', { name: 'User' }); // Full type safety and intellisenseProvides language management functionality.
const { currentLanguage, changeLanguage, availableLanguages } = useLanguage();useTranslation hook has TypeScript issues.
Returns the raw i18next translation function without type safety. This should only be used in rare edge cases where the type-safe hook encounters problems.
// Only use when useTranslation has TypeScript errors
const t = useTranslationFallback();
const text = t('common.hello'); // No type safety - standard i18next usage
const withVars = t('greeting', { name: 'John' });Note: The main useTranslation hook should be preferred in 99% of cases.
Initializes the localization system.
await initLocalization({
resources: { en: { translation: enTranslations } },
fallbackLng: 'en',
languageStore: new CustomLanguageStore()
});Interface for custom language storage implementations.
interface LanguageStore {
getLanguage(): string | null;
setLanguage(language: string): void;
}Configuration object for localization initialization.
interface LocalizationConfig {
resources: Resource;
fallbackLng?: string;
compatibilityJSON?: "v4";
interpolation?: {
escapeValue?: boolean;
};
languageStore?: LanguageStore;
}Type for component interpolation map. Supports both React elements and function components.
type ComponentMap = {
[key: string]:
| React.ReactElement
| ((props: { children?: React.ReactNode; [key: string]: any }) => React.ReactElement)
}Type definition for the type-safe translation function.
type TranslateFunction<T extends NestedRecord>Return type of the useTranslation hook.
interface UseTranslationReturn<T extends NestedRecord> {
t: TranslateFunction<T>;
}Base type for translation resources, allowing recursive nesting of primitive values.
type NestedRecord = {
[key: string]: string | number | boolean | null | undefined | NestedRecord
};For support, bug reports, or feature requests, please contact the WeProDev team or create an issue in our internal repository.
This project is licensed under the MIT License.
@weprodev/ui-localization - Professional localization solution by WeProDev