Internationalization (i18n)
This boilerplate uses i18next for internationalization, providing a robust solution for multi-language support with TypeScript type safety.
Setup
The i18n configuration is already set up in the boilerplate:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import * as Localization from 'expo-localization';
import en from './locales/en.json';
import pt from './locales/pt.json';
import es from './locales/es.json';
const resources = {
en: { translation: en },
pt: { translation: pt },
es: { translation: es },
};
i18n
.use(initReactI18next)
.init({
resources,
lng: Localization.locale.split('-')[0], // Get device language
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React already escapes
},
});
export default i18n;Translation Files
Create JSON files for each language:
{
"common": {
"welcome": "Welcome",
"loading": "Loading...",
"error": "Something went wrong",
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
"edit": "Edit"
},
"auth": {
"login": "Login",
"logout": "Logout",
"email": "Email",
"password": "Password",
"forgotPassword": "Forgot password?",
"noAccount": "Don't have an account?",
"signUp": "Sign up",
"invalidCredentials": "Invalid email or password"
},
"home": {
"title": "Home",
"greeting": "Hello, {{name}}!",
"taskCount": "You have {{count}} tasks",
"taskCount_plural": "You have {{count}} tasks"
},
"tasks": {
"title": "Tasks",
"newTask": "New Task",
"completed": "Completed",
"pending": "Pending",
"noTasks": "No tasks yet",
"createFirst": "Create your first task"
}
}{
"common": {
"welcome": "Bem-vindo",
"loading": "Carregando...",
"error": "Algo deu errado",
"save": "Salvar",
"cancel": "Cancelar",
"delete": "Excluir",
"edit": "Editar"
},
"auth": {
"login": "Entrar",
"logout": "Sair",
"email": "E-mail",
"password": "Senha",
"forgotPassword": "Esqueceu a senha?",
"noAccount": "Não tem uma conta?",
"signUp": "Cadastre-se",
"invalidCredentials": "E-mail ou senha inválidos"
},
"home": {
"title": "Início",
"greeting": "Olá, {{name}}!",
"taskCount": "Você tem {{count}} tarefa",
"taskCount_plural": "Você tem {{count}} tarefas"
},
"tasks": {
"title": "Tarefas",
"newTask": "Nova Tarefa",
"completed": "Concluídas",
"pending": "Pendentes",
"noTasks": "Nenhuma tarefa ainda",
"createFirst": "Crie sua primeira tarefa"
}
}Basic Usage
Use the useTranslation hook in your components:
import { View, Text } from 'react-native';
import { useTranslation } from 'react-i18next';
export function WelcomeScreen() {
const { t } = useTranslation();
return (
<View>
<Text>{t('common.welcome')}</Text>
<Text>{t('home.greeting', { name: 'John' })}</Text>
</View>
);
}In ViewModels
Use translations in your ViewModels for validation messages, etc:
import { useTranslation } from 'react-i18next';
export function useLoginViewModel() {
const { t } = useTranslation();
const form = useForm({
resolver: zodResolver(loginSchema),
});
const loginMutation = useMutation({
mutationFn: authApi.login,
onError: (error) => {
form.setError('root', {
message: t('auth.invalidCredentials'),
});
},
});
return {
form,
errorMessage: form.formState.errors.root?.message,
submitButtonText: t('auth.login'),
forgotPasswordText: t('auth.forgotPassword'),
};
}Interpolation
Pass variables to translations:
// Translation
{
"greeting": "Hello, {{name}}!",
"welcome": "Welcome back, {{name}}. You have {{count}} new messages."
}
// Usage
const { t } = useTranslation();
<Text>{t('greeting', { name: 'John' })}</Text>
<Text>{t('welcome', { name: 'John', count: 5 })}</Text>Pluralization
Handle singular and plural forms automatically:
{
"taskCount": "You have {{count}} task",
"taskCount_plural": "You have {{count}} tasks"
}const { t } = useTranslation();
// Automatically picks the right form based on count
<Text>{t('taskCount', { count: 0 })}</Text> // "You have 0 tasks"
<Text>{t('taskCount', { count: 1 })}</Text> // "You have 1 task"
<Text>{t('taskCount', { count: 5 })}</Text> // "You have 5 tasks"Changing Language
Create a language selector:
import { useTranslation } from 'react-i18next';
import { Pressable, Text, View } from 'react-native';
export function LanguageSelector() {
const { i18n, t } = useTranslation();
const currentLanguage = i18n.language;
const languages = [
{ code: 'en', name: 'English' },
{ code: 'pt', name: 'Português' },
{ code: 'es', name: 'Español' },
];
const handleChangeLanguage = (languageCode: string) => {
i18n.changeLanguage(languageCode);
};
return (
<View className="gap-2">
{languages.map((lang) => (
<Pressable
key={lang.code}
onPress={() => handleChangeLanguage(lang.code)}
className={cn(
'p-4 rounded-lg',
currentLanguage === lang.code
? 'bg-blue-500'
: 'bg-slate-200 dark:bg-slate-700'
)}
>
<Text
className={cn(
'font-semibold',
currentLanguage === lang.code
? 'text-white'
: 'text-slate-900 dark:text-white'
)}
>
{lang.name}
</Text>
</Pressable>
))}
</View>
);
}Persist Language Preference
Save the user's language choice:
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { storage } from '@/data/storage/mmkv';
import i18n from '@/i18n';
interface SettingsState {
language: string;
setLanguage: (language: string) => void;
}
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
language: 'en',
setLanguage: (language) => {
i18n.changeLanguage(language);
set({ language });
},
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => ({
getItem: (name) => storage.getString(name) ?? null,
setItem: (name, value) => storage.set(name, value),
removeItem: (name) => storage.delete(name),
})),
}
)
);// In your app initialization
import { useSettingsStore } from '@/stores/settings.store';
import i18n from '@/i18n';
export default function App() {
const language = useSettingsStore(state => state.language);
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
return <YourApp />;
}Namespace Organization
Organize translations by features:
{
"common.welcome": "Welcome",
"auth.login": "Login",
"tasks.title": "Tasks"
}{
"common": {
"welcome": "Welcome",
"actions": {
"save": "Save",
"cancel": "Cancel"
}
},
"auth": {
"login": "Login",
"form": {
"email": "Email",
"password": "Password"
}
}
}TypeScript Type Safety
Add type safety to your translations:
import en from './locales/en.json';
export type TranslationKeys = typeof en;
declare module 'react-i18next' {
interface CustomTypeOptions {
defaultNS: 'translation';
resources: {
translation: TranslationKeys;
};
}
}Now you get autocomplete and type checking:
const { t } = useTranslation();
// ✅ TypeScript knows this key exists
t('auth.login');
// ❌ TypeScript error: key doesn't exist
t('auth.invalidKey');Date & Time Formatting
Format dates according to locale:
import { format } from 'date-fns';
import { enUS, ptBR, es } from 'date-fns/locale';
import { useTranslation } from 'react-i18next';
const locales = {
en: enUS,
pt: ptBR,
es,
};
export function useDateFormatter() {
const { i18n } = useTranslation();
const locale = locales[i18n.language as keyof typeof locales];
const formatDate = (date: Date, pattern: string = 'PP') => {
return format(date, pattern, { locale });
};
return { formatDate };
}
// Usage
function TaskItem({ task }) {
const { formatDate } = useDateFormatter();
return (
<Text>{formatDate(task.createdAt, 'PPpp')}</Text>
);
}Currency Formatting
import { useTranslation } from 'react-i18next';
const currencyMap = {
en: 'USD',
pt: 'BRL',
es: 'EUR',
};
export function useCurrencyFormatter() {
const { i18n } = useTranslation();
const currency = currencyMap[i18n.language as keyof typeof currencyMap];
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat(i18n.language, {
style: 'currency',
currency,
}).format(amount);
};
return { formatCurrency };
}
// Usage
const { formatCurrency } = useCurrencyFormatter();
<Text>{formatCurrency(1299.99)}</Text> // $1,299.99 or R$ 1.299,99Best Practices
✅ Use namespaces
Organize translations by feature: auth., tasks., etc
✅ Keep translations in sync
Ensure all language files have the same keys
✅ Use interpolation
Don't concatenate strings, use t('key', { '{ value }' })
✅ Test all languages
Verify layout works with longer/shorter translations
Dynamic Content
For user-generated content that shouldn't be translated:
const { t } = useTranslation();
// Translate the label, not the user data
<View>
<Text className="font-semibold">{t('profile.name')}</Text>
<Text>{user.name}</Text> {/* User data, don't translate */}
</View>
// Translate static parts
<Text>
{t('tasks.createdBy', { author: task.author })}
</Text>
// Translation: "Created by {{author}}"