Conceptos básicos de i18n
10 ejemplos para empezar con internacionalización en Expo SDK 57 - externalización de cadenas, detección de idioma al iniciar por primera vez, y un proveedor mínimo que puedas expandir a i18next más adelante.
Busca en todas las páginas de la documentación
10 ejemplos para empezar con internacionalización en Expo SDK 57 - externalización de cadenas, detección de idioma al iniciar por primera vez, y un proveedor mínimo que puedas expandir a i18next más adelante.
Crea un andamio de una aplicación Expo lista para producción con un pin SDK 57 explícito. Estos ejemplos asumen Expo Router, TypeScript y expo-localization para el predeterminado del dispositivo.
npx create-expo-app@latest MyApp --template default@sdk-57
cd MyApp
npm installnpx expo install expo-localization @react-native-async-storage/async-storage{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Tooling: Estos ejemplos tienen como objetivo Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 y React 19.2.3.
Cada cadena visible para el usuario debe estar en archivos de traducción - no en línea en componentes. Los literales bloquean a los traductores, rompen las reglas de plurales e imposibilitan las pruebas de truncación de RTL.
// ❌ Bloquea i18n
<Text>Welcome back, {name}</Text>
// ✅ La clave se resuelve en tiempo de renderizado
<Text>{t("home.welcome", { name })}</Text>// src/i18n/locales/en/common.json
{
"home": {
"welcome": "Welcome back, {{name}}"
}
}"Welcome back," y name en búsquedas separadas{{name}}) permanecen en el archivo de traducción para que el orden de las palabras pueda cambiar según el idiomacommon; divídelo por función (auth, settings) a medida que el catálogo creceRelacionado: Mejores prácticas de i18n - nunca concatenes oraciones
Mantén los archivos de idioma en paralelo - las mismas claves en cada idioma. Las claves faltantes deberían fallar en CI, no renderizar silenciosamente inglés en producción.
src/i18n/
locales/
en/
common.json
auth.json
es/
common.json
auth.json
ar/
common.json
auth.json
resolveLocale.ts
LocaleProvider.tsx
useT.ts// src/i18n/locales/es/common.json
{
"home": {
"welcome": "Bienvenido de nuevo, {{name}}"
},
"actions": {
"save": "Guardar",
"cancel": "Cancelar"
}
}t()Antes de agregar i18next, un resolutor de 20 líneas prueba el patrón de externalización y habilita el cambio de idioma en Context.
// src/i18n/useT.ts
import { useLocale } from "./LocaleProvider";
import enCommon from "./locales/en/common.json";
import esCommon from "./locales/es/common.json";
import arCommon from "./locales/ar/common.json";
const catalogs: Record<string, Record<string, unknown>> = {
en: enCommon,
es: esCommon,
ar: arCommon,
};
function getNested(obj: Record<string, unknown>, path: string): string | undefined {
return path.split(".").reduce<unknown>((acc, key) => {
if (acc && typeof acc === "object" && key in (acc as object)) {
return (acc as Record<string, unknown>)[key];
}
return undefined;
}, obj) as string | undefined;
}
export function useT(namespace = "common") {
const { locale } = useLocale();
const table = catalogs[locale] ?? catalogs.en;
return function t(key: string, vars?: Record<string, string | number>) {
const template = getNested(table as Record<string, unknown>, key) ?? key;
if (!vars) return template;
return Object.entries(vars).reduce(
(out, [k, v]) => out.replaceAll(`{{${k}}}`, String(v)),
template,
);
};
}useT como el límite del componente - nunca importes JSON directamente en las pantallasUtiliza expo-localization sincrónica al arrancar para leer el languageTag preferido del usuario. Asígnalo a un idioma de aplicación compatible.
// src/i18n/resolveLocale.ts
import { getLocales } from "expo-localization";
export const SUPPORTED_LOCALES = ["en", "es", "ar"] as const;
export type AppLocale = (typeof SUPPORTED_LOCALES)[number];
export function resolveDeviceLocale(): AppLocale {
const [primary] = getLocales();
const tag = primary?.languageTag ?? "en-US";
const language = tag.split("-")[0]?.toLowerCase() ?? "en";
return (SUPPORTED_LOCALES as readonly string[]).includes(language)
? (language as AppLocale)
: "en";
}// src/i18n/localeStorage.ts
import AsyncStorage from "@react-native-async-storage/async-storage";
import type { AppLocale } from "./resolveLocale";
const KEY = "user:locale:v1";
export async function loadSavedLocale(): Promise<AppLocale | null> {
const raw = await AsyncStorage.getItem(KEY);
if (!raw) return null;
return raw as AppLocale;
}
export async function saveLocale(locale: AppLocale) {
await AsyncStorage.setItem(KEY, locale);
}getLocales() devuelve la lista de preferencias ordenadas del usuario - la primera entrada es el predeterminadoen con graciaResuelve la anulación guardada + el predeterminado del dispositivo antes de renderizar la navegación. Una pantalla nula previene que el texto en inglés destelle en dispositivos árabes.
// src/i18n/LocaleProvider.tsx
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { loadSavedLocale, saveLocale } from "./localeStorage";
import { resolveDeviceLocale, type AppLocale } from "./resolveLocale";
type LocaleContextValue = {
locale: AppLocale;
setLocale: (next: AppLocale) => Promise<void>;
isReady: boolean;
};
const LocaleContext = createContext<LocaleContextValue | null>(null);
export function LocaleProvider({ children }: { children: ReactNode }) {
const [locale, setLocaleState] = useState<AppLocale>("en");
const [isReady, setIsReady] = useState(false);
useEffect(() => {
let cancelled = false;
(async () => {
const saved = await loadSavedLocale();
if (!cancelled) {
setLocaleState(saved ?? resolveDeviceLocale());
setIsReady(true);
}
})();
return () => {
cancelled = true;
};
}, []);
const value = useMemo<LocaleContextValue>(
() => ({
locale,
isReady,
setLocale: async (next) => {
setLocaleState(next);
await saveLocale(next);
},
}),
[locale, isReady],
);
if (!isReady) return null;
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;
}
export function useLocale() {
const ctx = useContext(LocaleContext);
if (!ctx) throw new Error("useLocale must be used within LocaleProvider");
return ctx;
}// app/_layout.tsx (excerpt)
import { LocaleProvider } from "@/i18n/LocaleProvider";
export default function RootLayout() {
return (
<LocaleProvider>
<Stack />
</LocaleProvider>
);
}isReady - devuelve null o una pantalla de marca hasta que se resuelva el idiomasetLocaleExpone idiomas compatibles en configuración. Persiste inmediatamente - la selección solo sobrevivirá a una reinstalación si la sincronizas también con el servidor.
// src/features/settings/LanguagePicker.tsx
import { Pressable, Text, View } from "react-native";
import { useLocale } from "@/i18n/LocaleProvider";
import { SUPPORTED_LOCALES, type AppLocale } from "@/i18n/resolveLocale";
import { useT } from "@/i18n/useT";
const LABELS: Record<AppLocale, string> = {
en: "English",
es: "Español",
ar: "العربية",
};
export function LanguagePicker() {
const { locale, setLocale } = useLocale();
const t = useT();
return (
<View accessibilityRole="radiogroup" accessibilityLabel={t("settings.language")}>
{SUPPORTED_LOCALES.map((code) => (
<Pressable
key={code}
accessibilityRole="radio"
accessibilityState={{ selected: locale === code }}
onPress={() => setLocale(code)}
>
<Text>{LABELS[code]}</Text>
</Pressable>
))}
</View>
);
}Español, no "Spanish") - los usuarios reconocen su idioma más rápidoPasa valores dinámicos como un objeto vars - nunca uses template literals alrededor de fragmentos traducidos.
// src/components/OrderSummary.tsx
import { Text } from "react-native";
import { useT } from "@/i18n/useT";
export function OrderSummary({ itemCount, total }: { itemCount: number; total: number }) {
const t = useT();
return (
<Text>
{t("cart.summary", { count: itemCount, total: total.toFixed(2) })}
</Text>
);
}{
"cart": {
"summary": "{{count}} items - {{total}} total"
}
}// es - word order changes
{
"cart": {
"summary": "{{count}} artículos - total {{total}}"
}
}Intl - pasa solo cadenas ya formateadas cuando la frase lo requiera1 item vs 5 items), pasa a i18next - ver i18next / react-i18nextLa misma palabra en inglés, diferentes traducciones - usa claves distintas por contexto de UI. Los traductores necesitan pistas semánticas, no homógrafos.
{
"actions": {
"close_dialog": "Close",
"close_account": "Close account"
},
"status": {
"pending": "Pending approval",
"order_pending": "Order pending"
}
}<Text>{t("actions.close_dialog")}</Text>
<Text>{t("actions.close_account")}</Text>t("close") para un botón de diálogo y eliminación de cuenta_verb, _noun o un prefijo de pantalla cuando la palabra en inglés tiene múltiples significadosA medida que los catálogos crecen, limita las claves con TypeScript para que los errores tipográficos fallen en tiempo de compilación.
// src/i18n/keys.ts
import type enCommon from "./locales/en/common.json";
type NestedKeyOf<T, P extends string = ""> = T extends string
? P extends ""
? never
: P
: {
[K in keyof T & string]: NestedKeyOf<T[K], P extends "" ? K : `${P}.${K}`>;
}[keyof T & string];
export type CommonKey = NestedKeyOf<typeof enCommon>;// useT.ts (excerpt) - overload for common namespace
export function useT(): (key: CommonKey, vars?: Record<string, string | number>) => string;CustomTypeOptions para la misma garantía a escalaVincula el proveedor, búsqueda y predeterminado del dispositivo en una pantalla de inicio que los equipos puedan copiar.
// src/features/home/HomeScreen.tsx
import { Text, View } from "react-native";
import { useLocale } from "@/i18n/LocaleProvider";
import { useT } from "@/i18n/useT";
import { LanguagePicker } from "@/features/settings/LanguagePicker";
export function HomeScreen({ userName }: { userName: string }) {
const { locale } = useLocale();
const t = useT();
return (
<View style={{ padding: 16 }}>
<Text accessibilityRole="header">{t("home.welcome", { name: userName })}</Text>
<Text>{t("home.current_locale", { locale })}</Text>
<LanguagePicker />
</View>
);
}// en/common.json (excerpt)
{
"home": {
"welcome": "Welcome back, {{name}}",
"current_locale": "App language: {{locale}}"
},
"settings": {
"language": "Language"
}
}Lista de verificación de arranque de i18n:
✓ expo-localization instalado (npx expo install expo-localization)
✓ LocaleProvider cierra el diseño raíz hasta que se resuelvan el almacenamiento + dispositivo
✓ Sin literales visibles para el usuario en componentes de características
✓ Anulación del usuario persistida en AsyncStorage
✓ Dirección RTL conectada cuando locale es ar/he - ver Diseños RTL
✓ CI verifica claves entre idiomas antes de la versión - ver CI de traducciónRelacionado: expo-localization - API de idioma del dispositivo | i18next / react-i18next - librería de producción
Cuando necesites reglas de plurales, formato ICU, packs de idioma cargados perezosamente o flujos de trabajo de traductores con espacios de nombres. El ayudante mínimo en esta página es suficiente para 2-3 idiomas y <100 claves.
Context está bien - el idioma cambia raramente y no debería re-renderizar filas de listas. Separa el idioma del estado de sesión de alta volatilidad - ver Context Without Storms.
No para la mayoría de las aplicaciones Expo - envía todo JSON de idioma en el paquete o carga perezosamente packs. Sabores de marca de marca blanca pueden variar idiomas compatibles por inquilino sin bifurcar código - ver Temas y sabores de marca.
Cambia Configuración - General - Idioma y región en iOS o Configuración - Sistema - Idiomas en Android. Para RTL, agrega árabe o hebreo y verifica el diseño - Diseños RTL.
Resuelve títulos con useT() dentro de componentes de pantalla o callbacks de options de Stack.Screen - no codifiques title: "Home" en archivos de diseño.
getLocales, calendarios, hooks de monedaI18nManager e iconos reflejadosVersiones de stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0 y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026