Skip to content

Localization pt BR

Edgar Mesquita edited this page Aug 22, 2026 · 11 revisions

Localização

🌐 Esta página em: English · Português

Multi-idioma no eQuantic.UI segue uma lei: o desenvolvedor localiza com o que o próprio .NET oferece: .resx, o acessor fortemente tipado, CultureInfo. Ele nunca vê um catálogo JavaScript nem uma DSL do framework. O design completo (Track L: como o eqc faz a ponte do ResourceManager até o browser, catálogos por cultura, o subconjunto de formatação, as cercas honestas) vive no repositório em docs/I18N-PLAN.md.

As strings do próprio SDK

Desde 0.2.0-preview.24

Componentes nunca embutem uma string de interface. Todo rótulo interno (os anúncios que um leitor de tela ouve ("Marcado", "Ativado"), as afordâncias de dispensar, o placeholder de busca, a barra de localizar do editor) lê SdkStrings, que hoje é um resx com satélites. Um app com zero resx próprio já anuncia "Marcado" para um leitor de tela em pt-BR: as chaves do SDK entram no catálogo de toda cultura que o app emite.

Janelas Photon adotam o locale da máquina ao abrir

Desde 0.2.0-preview.24

Um processo de interface aberto pelo Finder não carrega LANG/LC_*, então o .NET começa invariante mesmo numa máquina pt-BR. Os shells resolvem a verdade da plataforma antes do primeiro frame: NSLocale.preferredLanguages[0] como cultura de interface e currentLocale como cultura de formato na Apple (a mesma divisão que o .NET modela como CurrentUICulture versus CurrentCulture), Locale.getDefault() alimentando as duas no Android, e o PhotonCultureController copia o par para os statics do processo, repintando num Apply posterior exatamente como o controller de tema repinta numa troca de modo.

Uma página fala resx no web, de ponta a ponta

Desde 0.2.0-preview.26

Um app escreve localização exatamente como qualquer app .NET: um Resources/Strings.resx mais um Strings.pt-BR.resx, os acessores comuns do Designer, string.Format(Strings.Greeting, nome), e a página responde na cultura do request nas DUAS metades do alvo web.

sequenceDiagram
  participant B as Browser
  participant S as Servidor
  participant R as Runtime
  B->>S: GET /pagina + Accept-Language
  S->>S: escolhe catalogo (exata, pai, neutra)
  S-->>B: shell + window.__EQ_CULTURE__
  B->>R: boot instala a cultura
  R->>R: hidrata com as MESMAS strings
Loading

As peças, espelhando a ponte de tema encaixe por encaixe:

  • O eqc REESCREVE os acessores, nunca os inline. Strings.Hero_Title compila para $eq.str("Strings", "Hero.Title"), porque fazer inline gravaria a cultura da máquina de build dentro do bundle. A detecção é por FORMA (as propriedades estáticas ResourceManager + Culture do Designer), então qualquer resx em qualquer lugar do projeto funciona, com o nome que o desenvolvedor quiser.
  • O build emite catálogos a partir das chaves que o app realmente usa. wwwroot/_equantic/strings/neutral.json mais um {cultura}.json por variante autorada, cada um já ACHATADO contra a cadeia de fallback do .NET: o servidor escolhe um arquivo, nunca mescla.
  • O servidor embute a resposta. O UseRequestLocalization (o APP liga a negociação, o SDK só lê o que o middleware definiu) alimenta o <html lang> e um slot window.__EQ_CULTURE__ = { name, formatName, strings }; o boot o instala ANTES da hidratação, então o cliente resolve exatamente as strings que o servidor renderizou.

Trocar de idioma re-renderiza, nunca recarrega

Desde 0.2.0-preview.27

ICultureController é a mão do app sobre o IDIOMA, na mesma forma do IThemeController, resolvido por nome de interface, então um componente que oferece o seletor nunca aprende qual alvo respondeu. Ele fala nomes BCP-47, nunca CultureInfo: o contrato atravessa para um browser onde esse tipo não existe, e um nome é a única moeda que .NET, web e as duas plataformas móveis compartilham.

  • Web: o setCulture busca o catálogo da cultura só quando ele ainda não está em memória, escolhendo o arquivo do mesmo jeito que o SERVIDOR escolhe (exata → pais → neutra), troca, e re-renderiza pelo mesmo agendador que o SetState usa. O estado sobrevive; só o texto que mudou é tocado. A troca também sobrevive à página: o controller escreve o cookie de cultura do próprio ASP.NET, no formato dele, então o próximo request já chega traduzido.
  • Nativo: o PhotonCultureController realiza a mesma interface: os statics do processo mudam e a janela repinta.
  • CultureSwitcher é a superfície embutida: um segmented control para dois ou três idiomas, um menu acima disso, acendendo a opção ativa por nome exato e depois pela parte do idioma (uma página servida como pt-BR precisa acender uma entrada pt).
new CultureSwitcher([
    new("en", "English"),
    new("pt-BR", "Português"),
    new("es", "Español"),
])

Um especificador de formato sobrevive à viagem

Desde 0.2.0-preview.27

{0:C2} escrito uma vez em C# imprime R$ 1.234,50 numa página pt-BR e 1234,50 € numa es, com SSR e cliente concordando caractere por caractere, e uma troca de cultura reformatando ao vivo junto com as strings.

A prova é o método: a fixture pinada é gerada do .NET real (value.ToString(spec, culture), três culturas escolhidas para discordar) e o runtime transpilado precisa reproduzi-la exatamente. Essa direção pegou todas as divergências em tempo de build. O Intl arredonda o meio para longe do zero onde o .NET arredonda para o par, então o formatador pré-arredonda com o helper banqueiro; os presets do Intl derrubam a data curta de en-US para ano de dois dígitos, então os padrões próprios de cada cultura viajam no catálogo dela, junto com o código ISO da moeda (o Intl exige um código, e nenhuma API do browser deriva um a partir do locale).

O que não consegue concordar é recusado no build, nunca aproximado: EQ2100 rejeita alinhamento e especificadores fora do subconjunto num template resx; EQ2101 confere os placeholders de cada TRADUÇÃO contra o resx neutro, então uma string pt-BR que pede {2} onde a neutra tem {0}/{1} falha o build em vez de estourar só para os leitores brasileiros.

Nativo: satélites provados sob AOT

O Photon roda .NET de verdade, sem transpilação: ResourceManager e assemblies satélite já funcionam lá: um app nativo localiza hoje com resx comum. O risco não é o mecanismo, é o EMPACOTAMENTO: um publish com trimming/NativeAOT que derruba os satélites não quebra, ele responde inglês em silêncio. Por isso a prova é executável (scripts/verify-aot-satellites.sh): um binário NativeAOT que referencia a biblioteca só passa se en/pt-BR/es responderem com a TRADUÇÃO, e se es-AR cair em es pelo caminho de pai real.

Duas culturas numa janela

Desde 0.2.0-preview.29

A metade web desta história é uma requisição: uma cultura entra, SSR e hidratação concordam. Uma janela não tem requisição, então uma troca ali tem três juntas que o web nunca tem... a plataforma decide o locale inicial, alguém o copia para os statics do .NET, e a árvore precisa RECONSTRUIR para a mudança alcançar os pixels.

Um componente não diz nada disso. A seção Language do sample Studio é um CultureSwitcher comum, que resolve o ICultureController pelo contexto e não nomeia plataforma nenhuma... a mesma classe que o browser renderiza. Aplicar uma cultura escreve os dois statics (o par do D13: recursos e formatos são decisões separadas) e repinta a janela, então a troca cai no frame seguinte com todo o estado da página intacto.

O caminho de screenshot a renderiza dos dois jeitos, que foi o que fechou o marco: sem argumento a janela mostra o par da MÁQUINA (interface em inglês com formatos portugueses, num Mac configurado assim), e com --culture pt-BR ela mostra Marcado, Ativado, Planilha ao lado de R$ 1.234,50.

Escrever o sample achou o bug que os testes não achavam: o host registrava só o controller concreto, então um componente que pedia a INTERFACE resolvia nulo e o switcher não trocava nada. Dois registros, uma instância. Uma capacidade que ninguém resolve numa janela de verdade é uma capacidade que não funciona.

Um app novo nasce localizado

Desde 0.2.0-preview.28

O dotnet new equantic-app agora scaffolda o que esta página prega: um Resources/Strings.resx com o satélite pt-BR e o acessor comum do Designer, um CultureSwitcher na página inicial, e a história inteira numa linha, string.Format(Strings.CountedTimes, _count). O primeiro projeto de uma pessoa responde em dois idiomas antes de ela escrever qualquer código, que é o momento mais barato de aprender que string de interface nunca se fixa no código. Veja Primeiros passos para o scaffold completo.

Um buscador sabe qual URL é qual idioma

Desde 0.2.0-preview.31

Traduzir um site e nunca declarar o grupo de tradução é como um site localizado passa a competir contra si mesmo em silêncio. Uma política do app inteiro, options.UseAlternateLinks(AlternateUrls.PathPrefix(), "en", "pt-BR", "es"), coloca o conjunto rel="alternate" hreflang inteiro mais o x-default no head de toda página, com as regras de reciprocidade e de URL absoluta garantidas em vez de deixadas para o app. Veja Integração com o servidor.

O idioma viaja na URL

Desde 0.2.0-preview.36

Um site público quer cada tradução no seu próprio endereço — /pt-BR/pricing — porque é a única forma que um buscador indexa como tradução e que um leitor consegue compartilhar. Uma declaração resolve:

builder.Services.AddUI(options => options
    .ScanAssembly(typeof(Program).Assembly)
    .UseCultureRoutes("en", "pt-BR", "es"));   // a primeira é servida sem prefixoapp.UseRequestLocalization();                  // o middleware do próprio ASP.NET, já configurado
app.MapUI();

Nada ali é maquinaria própria do SDK. UseCultureRoutes preenche as RequestLocalizationOptions da plataforma a partir dessa lista — cultura padrão, culturas suportadas, e o RouteDataRequestCultureProvider do ASP.NET em primeiro lugar — e registra uma route constraint culture, de modo que toda página (um [Page] ou um MapPage<T>) também é mapeada em /{culture:culture}/…. A partir daí:

  • /pt-BR/pricing renderiza em pt-BR, e o segmento vence o cookie e o Accept-Language: uma URL que nomeia um idioma é uma promessa sobre o que a página diz.
  • Todo href interno carrega o idioma — o autor escreve /about uma vez e servidor e navegador emitem /pt-BR/about enquanto o leitor está em português. Link e links de Markdown igual; URLs absolutas, âncoras e mailto: ficam intactos.
  • /fr/pricing é 404, não inglês num endereço francês: a constraint aceita exatamente os prefixos que você nomeou.
  • Um /pricing nu pedido em português (o cookie que o CultureSwitcher grava, ou o Accept-Language) é redirecionado (302) para /pt-BR/pricing, para que o que se vê, compartilha e salva sempre nomeie o seu idioma. A cultura padrão mantém as URLs nuas.
  • Trocar de idioma é uma navegação: o CultureSwitcher vai para a mesma página no outro endereço, pelo router, sem reload.
  • hreflang e x-default saem do mesmo mapa sem segunda declaração, e um canonical que a página escreveu à mão é localizado na saída, para nunca apontar o buscador para a tradução errada.

Um app que configura o middleware pela sobrecarga com lambda (app.UseRequestLocalization(o => …)) monta as opções inline e não vê nada da DI — passe a mesma lista ali com o.UseCultureRoutes("en", "pt-BR", "es").

Cercas honestas

  • Chave ausente nunca lança. Renderiza a chave e avisa uma vez. As chaves do próprio SDK têm uma rede a mais: o inglês neutro viaja no runtime como dado gerado, então uma página sem catálogo nenhum lê "Search…" em vez de "SearchPlaceholder".
  • Uma cultura negociada sem catálogo autorado cai para os fatos NEUTROS (formatos invariantes, determinísticos, nunca dependentes do browser).
  • Plurais em v1 são chaves explícitas (correto para as línguas de 2 formas); 3+ formas é uma cerca documentada, não uma resposta errada em silêncio.
  • RTL e cobertura de scripts (CJK, árabe) são outros trilhos, explicitamente.
  • O fluxo do tradutor é .resx/XLIFF, e o framework não vai crescer uma UI de tradução.

Clone this wiki locally