Noções básicas de i18n
10 exemplos para você começar com internacionalização no Expo SDK 57 - externalização de strings, detecção de localidade no primeiro lançamento e um provedor mínimo que você pode expandir para i18next mais tarde.
Busque em todas as páginas da documentação
10 exemplos para você começar com internacionalização no Expo SDK 57 - externalização de strings, detecção de localidade no primeiro lançamento e um provedor mínimo que você pode expandir para i18next mais tarde.
Crie um aplicativo Expo com formato de produção e um pino explícito do SDK 57. Estes exemplos assumem Expo Router, TypeScript e expo-localization para o padrão do 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"
}
}Ferramentas: Estes exemplos visam o Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 e React 19.2.3.
Toda string visível ao usuário pertence a arquivos de tradução - não inline nos componentes. Literais bloqueiam tradutores, quebram regras de plural e tornam o QA de truncamento RTL impossível.
// ❌ Bloqueia i18n
<Text>Welcome back, {name}</Text>
// ✅ A chave é resolvida no tempo de renderização
<Text>{t("home.welcome", { name })}</Text>// src/i18n/locales/en/common.json
{
"home": {
"welcome": "Welcome back, {{name}}"
}
}"Welcome back," e name em buscas separadas{{name}}) permanecem no arquivo de tradução para que a ordem das palavras possa mudar por idiomacommon; divida por recurso (auth, settings) à medida que o catálogo cresceRelacionado: Melhores Práticas de i18n - nunca concatene frases
Mantenha os arquivos de localidade paralelos - mesmas chaves em todos os idiomas. Chaves ausentes devem falhar no CI, não renderizar silenciosamente em inglês na produção.
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 adicionar i18next, um resolvedor de 20 linhas prova o padrão de externalização e desbloqueia a troca de localidade no Contexto.
// 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 limite do componente - nunca importe JSON diretamente em telasUse expo-localization síncronamente no boot para ler a languageTag preferida do usuário. Mapeie-a para uma localidade de aplicativo suportada.
// 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() retorna a lista de preferência ordenada do usuário - o primeiro item é o padrãoen graciosamenteResolva a substituição salva + o padrão do dispositivo antes de renderizar a navegação. Um splash nulo evita que a cópia em inglês apareça em 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 (trecho)
import { LocaleProvider } from "@/i18n/LocaleProvider";
export default function RootLayout() {
return (
<LocaleProvider>
<Stack />
</LocaleProvider>
);
}isReady - retorne null ou um splash de marca até que a localidade seja resolvidasetLocaleExponha idiomas suportados nas configurações. Persista imediatamente para que a escolha sobreviva à reinstalação apenas se você também sincronizar com o 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, não "Spanish") - os usuários reconhecem seu idioma mais rapidamentePasse valores dinâmicos como um objeto de variáveis - nunca use literais de template em torno de fragmentos traduzidos.
// 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 - a ordem das palavras muda
{
"cart": {
"summary": "{{count}} artículos - total {{total}}"
}
}Intl - passe strings já formatadas apenas quando a frase exigir1 item vs 5 items), passe para i18next - veja i18next / react-i18nextMesma palavra em inglês, traduções diferentes - use chaves distintas por contexto de UI. Tradutores precisam de dicas semânticas, não 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 um botão de diálogo e exclusão de conta_verb, _noun ou prefixo de tela quando o inglês colidirÀ medida que os catálogos crescem, restrinja as chaves com TypeScript para que erros de digitação falhem em tempo de compilação.
// 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 (trecho) - sobrecarga para namespace comum
export function useT(): (key: CommonKey, vars?: Record<string, string | number>) => string;CustomTypeOptions para a mesma garantia em escalaConecte o provedor, a busca e o padrão do dispositivo em uma tela inicial que as equipes possam 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 (trecho)
{
"home": {
"welcome": "Welcome back, {{name}}",
"current_locale": "App language: {{locale}}"
},
"settings": {
"language": "Language"
}
}Lista de verificação de boot de i18n:
✓ expo-localization instalado (npx expo install expo-localization)
✓ LocaleProvider bloqueia o layout raiz até que o armazenamento + dispositivo resolvam
✓ Sem literais visíveis ao usuário em componentes de recursos
✓ Substituição do usuário persistida no AsyncStorage
✓ Direção RTL conectada quando a localidade é ar/he - veja Layouts RTL
✓ CI verifica chaves entre localidades antes do lançamento - veja CI de TraduçãoRelacionado: expo-localization - API de localidade do dispositivo | i18next / react-i18next - biblioteca de produção
Quando você precisar de regras de plural, formatação ICU, pacotes de idioma carregados preguiçosamente ou fluxos de trabalho de tradutor com namespaces. O helper mínimo nesta página é suficiente para 2–3 localidades e <100 chaves.
Contexto é bom - a localidade muda raramente e não deve re-renderizar linhas de lista. Separe a localidade do estado de sessão de alta rotação - veja Contexto Sem Tempestades.
Não para a maioria dos aplicativos Expo - envie todo o JSON de localidade no pacote ou carregue pacotes preguiçosamente. Sabores de marca white-label podem variar localidades suportadas por locatário sem bifurcar o código - veja Temas e Sabores de Marca.
Altere Configurações → Geral → Idioma e Região no iOS ou Configurações → Sistema → Idiomas no Android. Para RTL, adicione árabe ou hebraico e verifique o layout - Layouts RTL.
Resolva títulos com useT() dentro dos componentes de tela ou callbacks de options de Stack.Screen - não codifique title: "Home" em arquivos de layout.
getLocales, calendários, hooks de moedaI18nManager e ícones espelhadosVersões do Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026