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.
Quais idiomas viajam, e como levar menos. As strings do SDK estão traduzidas hoje para pt-BR e es, e vão DENTRO do pacote como assemblies satélite (lib/net10.0/<cultura>/eQuantic.UI.Components.resources.dll, cerca de 8 KB cada) — um pacote levando os seus idiomas, como o Roslyn e a plataforma de testes fazem, em vez de um pacote por idioma. Um app que quer só alguns deles não precisa de outro pacote, porque o .NET já é dono dessa decisão:
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
Medido num consumidor do pacote publicado: sem isso a saída do app leva es/ e pt-BR/; com isso, nenhum dos dois. O único sítio onde não se aplica é uma referência de PROJETO — os samples deste repositório mantêm os satélites de qualquer forma, porque a propriedade filtra o que chega por pacote.
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.
GET /pagina + Accept-Language
escolhe catalogo (exata, pai, neutra)
shell + window.__EQ_CULTURE__
hidrata com as MESMAS strings
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("pt-BR", "Português"),
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. 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 prefixo
app.UseRequestLocalization(); // o middleware do próprio ASP.NET, já configurado
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").
•
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.