i18n
Um motor de internacionalização independente de framework: um núcleo reativo pequeno (I18nCore) com um singleton global opcional (createI18n / useI18n). Nada aqui toca no DOM: ligue-o à interface como preferir.
import { createI18n, useI18n } from 'ranuts/i18n';Ele também é reexportado de ranuts/utils. Importe de ranuts/i18n quando a i18n for tudo de que você precisa: essa entrada leva só o motor e os dois auxiliares dele, em vez do que o largo barril de utils acabar puxando junto.
Uso
import { createI18n, useI18n } from 'ranuts/i18n';
createI18n({
messages: {
en: { 'hero.title': 'Hello, {name}', 'nav.docs': 'Docs' },
zh: { 'hero.title': '你好,{name}', 'nav.docs': '文档' },
},
fallbackLocale: 'en',
persist: true,
detectNavigator: true,
});
const i18n = useI18n()!;
i18n.t('hero.title', { name: 'Ada' }); // "Hello, Ada"
i18n.setLocale('zh');
i18n.t('hero.title', { name: 'Ada' }); // "你好,Ada"Os dicionários são planos: t() faz uma busca direta de messages[locale][key], então as chaves são strings literais como 'hero.title', não objetos aninhados.
Idioma inicial
Resolvido uma única vez no construtor, nesta ordem:
- A escolha guardada no
localStorage(só compersistligado, e só se aquele idioma tiver dicionário) config.locale- Os idiomas do navegador (só com
detectNavigatorligado) fallbackLocale
O passo 3 passa pelo resolveLocale, que lê a lista ordenada navigator.languages inteira em vez de só navigator.language: quem não tem a primeira escolha entre os seus dicionários ainda recebe a segunda, em vez de cair direto no idioma de reserva.
Interpolação
t(key, params) substitui os marcadores {param} numa única passagem da esquerda para a direita, seguindo a convenção de strings de formato do format! do Rust, do str.format do Python e do String.Format do .NET:
| Entrada | Saída |
|---|---|
{{ |
uma { literal |
}} |
uma } literal |
{name} |
params.name, convertido em texto |
{name} sem esse parâmetro |
fica intacto, então um marcador solto aparece em vez de sumir em branco |
Uma { / } sozinha, ou um grupo com espaços como { x }, não é um marcador e sai como está, então CSS, JSON ou trechos de código dentro de uma mensagem passam ilesos. Para envolver um valor em chaves literais, duplique o par externo: {{{name}}}.
Dicionários tipados
Passe o formato do seu dicionário como argumento de tipo e cada chamada a t() é conferida em tempo de compilação. Sem isso, uma chave renomeada ou digitada errado degrada em silêncio para "desenhe a própria chave": a pessoa vê agentModelFirstDownlaod onde deveria haver uma frase, e nada falha até ali.
interface Messages {
save: string;
cancel: string;
}
const i18n = createI18n<Messages>({
messages: {
en: { save: 'Save', cancel: 'Cancel' },
'zh-CN': { save: '保存' }, // ainda em tradução — tudo bem
},
fallbackLocale: 'en',
});
i18n.t('save'); // ok
i18n.t('saev'); // erro de compilação
useI18n<Messages>()?.t('cancel'); // passe o mesmo tipo de volta para manter a conferênciaTrês detalhes fazem isso ser usável e não apenas possível:
- Cada idioma é
Partial. Uma tradução em andamento é o estado normal; o idioma de reserva cobre o que um idioma ainda não preencheu. - O tipo vem do argumento de tipo, nunca dos dados.
messagesé embrulhado emNoInfer, então idiomas com conjuntos de chaves diferentes não conseguem fazer o TypeScript inferir a interseção deles. Do contrário, uma chave que só o idioma de reserva define seria rejeitada em cada ponto de chamada, e uma tradução incompleta quebraria a compilação em vez de recair na reserva em tempo de execução. - Uma
interfacefunciona, não só umtype. A restrição éStringValues<T>({ [K in keyof T]: string }) e nãoRecord<string, string>, porque o TypeScript só dá assinaturas de índice implícitas a apelidos de tipo: restringir do jeito óbvio teria obrigado todo mundo a reescrever o dicionário como umtype.
Omitir o argumento de tipo mantém o comportamento sem tipos exatamente igual: o MessageDict padrão é Record<string, string>, cujo keyof é string.
Configuração
| Campo | Descrição | Tipo | Padrão |
|---|---|---|---|
locale |
Idioma inicial. Uma escolha guardada o substitui quando persist está ligado |
string |
- |
fallbackLocale |
Idioma usado quando falta uma chave no idioma ativo | string |
'en' |
messages |
Idioma → chave → string | LocaleMessages |
{} |
persist |
Guarda o idioma ativo no localStorage |
boolean |
false |
storageKey |
Chave do localStorage usada quando persist está ligado |
string |
'ran-locale' |
detectNavigator |
Define o idioma inicial pelas preferências do navegador | boolean |
false |
API
createI18n
Cria e registra o singleton global.
Parâmetros
| Parâmetro | Descrição | Tipo | Padrão |
|---|---|---|---|
config |
Veja Configuração | I18nConfig |
{} |
Retorna
| Argumento | Descrição | Tipo |
|---|---|---|
i18n |
A nova instância | I18nCore |
useI18n
Devolve a instância global ativa, ou null quando nenhuma foi criada.
Retorna
| Argumento | Descrição | Tipo |
|---|---|---|
i18n |
A instância ativa ou null |
I18nCore | null |
I18nCore
| Membro | Descrição |
|---|---|
t(key, params?) |
Traduz; recai no idioma de reserva e depois na própria chave |
locale / getLocale() |
O idioma ativo |
setLocale(locale) |
Troca de idioma, guarda (quando ligado) e avisa. Não faz nada se não mudar |
addMessages(locale, dict) |
Funde um dicionário num idioma, criando-o se preciso |
getMessages(locale?) |
O dicionário de um idioma, ou {} |
availableLocales |
Os idiomas que têm um dicionário registrado |
onChange(fn) |
Assina as mudanças de idioma; devolve uma função para cancelar |
destroy() |
Remove todos os assinantes |
SSR
Seguro. Todo acesso a localStorage e a navigator é protegido, então construir uma instância durante a renderização no servidor recai em config.locale ou no fallbackLocale.