Globalization e localization no .NET
Globalization e localization no .NET são dois problemas que toda aplicação internacional enfrenta e que costumam ser tratados como um só, embora sejam tecnicamente distintos. O primeiro é exibir números, datas e moedas segundo as convenções do usuário, enquanto o segundo é apresentar textos no idioma dele.
O .NET separa esses dois problemas de forma explícita. Entender essa separação evita a maior parte dos defeitos difíceis de reproduzir em produção.
Este artigo detalha como a plataforma trata cada um deles, tomando como base o .NET 10. Essa é a versão de suporte de longo prazo lançada em novembro de 2025.
Globalization e localization no .NET não são sinônimos
Globalização é o processo de escrever código que se comporta corretamente diante de convenções culturais diferentes. Ela trata de formatação de valores, ordenação e comparação de strings, mapeamento de maiúsculas e minúsculas e cálculo de calendários.
Localização, por sua vez, é o processo de traduzir os recursos da aplicação para cada cultura suportada. Ela cuida de textos de interface, mensagens de erro, rótulos e conteúdos que precisam existir em mais de um idioma.
A documentação oficial recomenda localizar apenas depois da revisão de localizabilidade. Ou seja, a aplicação precisa estar preparada para receber traduções antes que alguém comece a traduzir.
Uma aplicação pronta para localização se divide em dois blocos: um com os elementos de interface e outro com o código executável. Por isso, você troca traduções sem recompilar a lógica de negócio.
CultureInfo na globalization e localization no .NET
A classe System.Globalization.CultureInfo representa uma cultura. Além disso, ela carrega todos os dados de formatação associados, incluindo NumberFormat, DateTimeFormat e CompareInfo.
Toda thread no .NET possui dois objetos de cultura distintos, e essa é a distinção mais importante do assunto. O CurrentCulture define a formatação de data, hora, número e moeda, além da ordenação de texto, da capitalização e das comparações de strings.
Já o CurrentUICulture determina quais strings traduzidas o ResourceManager busca nos arquivos de recurso. Ele serve exclusivamente para recuperar recursos localizados.
Na prática, globalization e localization no .NET formam eixos independentes. Assim, uma aplicação pode exibir a interface em inglês enquanto formata valores monetários em real, ou o inverso.
As culturas se dividem entre neutras e específicas. A cultura pt é neutra, pois define apenas o idioma, enquanto pt-BR é específica, porque define idioma e região.
Essa hierarquia importa porque o símbolo monetário depende da região. Portanto, ao formatar um valor com uma cultura que informa apenas o idioma, o resultado pode não trazer o símbolo esperado.
Esse comportamento mudou com a adoção do ICU e já gerou incidentes em migrações. Se o seu sistema lida com valores financeiros em vários países, vale ler também como lidar com dinheiro em aplicações globalizadas.
Formatação explícita com IFormatProvider
Em globalization e localization no .NET, um risco comum é a cultura implícita, já que várias operações usam a cultura corrente sem que isso fique visível no código. Isso vale para strings interpoladas, concatenações, String.Format e os métodos ToString dos tipos numéricos e de data.
O ponto de extensão para controlar esse comportamento é a interface System.IFormatProvider. De fato, quase todo método de formatação e conversão tem uma sobrecarga que recebe esse provedor.
using System.Globalization;
var amount = 1250.50m;
var date = DateTime.UtcNow;
var ptBr = new CultureInfo("pt-BR");
var enUs = new CultureInfo("en-US");
Console.WriteLine(amount.ToString("C", ptBr)); // R$ 1.250,50
Console.WriteLine(amount.ToString("C", enUs)); // $1,250.50
Console.WriteLine(date.ToString("D", ptBr));
Console.WriteLine(date.ToString("D", enUs));
O especificador C produz o formato monetário da cultura informada, com símbolo, separador de milhar e separador decimal. Já o especificador D gera a data por extenso conforme as convenções locais.
Para obter consistência entre sistemas operacionais, a documentação sugere o construtor de CultureInfo com o parâmetro useUserOverride definido como falso. Dessa forma, configurações personalizadas do usuário não sobrescrevem os dados da cultura.
InvariantCulture: exibição versus persistência
A regra que resolve a maioria dos problemas de globalization e localization no .NET é simples. Se o valor vai aparecer para o usuário, use a cultura corrente; se vai ser armazenado ou consumido por software, use a cultura invariante.
A CultureInfo.InvariantCulture é uma cultura independente de localidade, baseada no inglês. Seus formatos são estáveis e não mudam entre plataformas ou versões do sistema operacional.
Para elementos de intercâmbio, como um campo JSON de uma chamada de API, a recomendação oficial vai além. Você deve usar a cultura invariante e também especificar um formato de ida e volta, como o O.
// Persistência e intercâmbio: formato estável e independente de cultura
var isoDate = date.ToString("O", CultureInfo.InvariantCulture);
var storedAmount = amount.ToString(CultureInfo.InvariantCulture);
// Leitura de volta com a mesma garantia
var parsedDate = DateTime.Parse(isoDate, CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind);
var parsedAmount = decimal.Parse(storedAmount, NumberStyles.Number,
CultureInfo.InvariantCulture);
Embora esses formatos sejam estáveis, a documentação orienta deixar a string de formato explícita. Com isso, a intenção do código fica evidente para quem for mantê-lo.
O erro clássico aparece no caminho inverso. Um decimal.Parse("10.50") executado em um servidor com pt-BR interpreta o ponto como separador de milhar e produz mil e cinquenta, e não dez e meio.
Pior do que o resultado errado é o fato de que esse código funciona na máquina do desenvolvedor. Ele quebra apenas quando a cultura do ambiente muda, ou seja, é um defeito silencioso e dependente de infraestrutura.
Os analisadores de código cobrem exatamente esse cenário de globalization e localization no .NET. A regra CA1304 exige a especificação de CultureInfo, e a CA1305 exige a especificação de IFormatProvider.
Além disso, as regras CA1307, CA1309 e CA1310 identificam comparações linguísticas usadas sem intenção. Você pode promover essas regras a erro de compilação no arquivo de projeto e, assim, transformar uma convenção de equipe em uma garantia da build.
ICU e o modo de globalização invariante
Desde o .NET 5, a plataforma usa a biblioteca International Components for Unicode em todos os sistemas suportados para comparações linguísticas. Antes disso, o Windows dependia das APIs NLS, o que gerava divergências entre plataformas.
Existe também um modo opcional chamado globalização invariante. Nesse modo, o runtime não carrega o ICU e se apoia apenas nos dados embutidos da cultura invariante.
Esse detalhe afeta globalization e localization no .NET diretamente para quem publica em contêineres. Segundo a documentação das imagens de contêiner do .NET, as imagens Alpine, distroless e chiseled não incluem ICU nem tzdata.
Por isso, essas imagens só funcionam com aplicações configuradas para o modo invariante:
<PropertyGroup>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
Se a aplicação precisar de culturas específicas, a Microsoft oferece variantes com o sufixo extra. Elas incluem ICU e tzdata, embora aumentem o tamanho da imagem.
CultureNotFoundException desde o .NET 6
A partir do .NET 6, criar qualquer cultura que não seja a invariante nesse modo lança CultureNotFoundException. Antes, a operação tinha sucesso, mas devolvia silenciosamente os dados da cultura invariante.
A mudança veio justamente para tornar o problema visível. Afinal, muitos desenvolvedores não sabiam que suas aplicações rodavam com o modo invariante ativo.
Se o comportamento anterior for necessário, a opção de runtime System.Globalization.PredefinedCulturesOnly permite voltar a criar culturas arbitrárias nesse modo.
Arquivos de recurso na globalization e localization no .NET
Do lado da tradução, o mecanismo primário de globalization e localization no .NET para isolar strings é o arquivo de recurso, um XML com extensão .resx. Os recursos são traduzidos antes da execução, isto é, representam conteúdo traduzido em repouso.
A convenção de nomes combina o nome completo do tipo com o identificador de localidade, no formato FullTypeName.Locale.resx. O segmento de localidade define o idioma e, opcionalmente, a região.
No Visual Studio, há um detalhe importante. Se você criar o arquivo sem cultura no nome, o ambiente gera uma classe C# com uma propriedade para cada string, o que normalmente não é o desejado no ASP.NET Core.
A recomendação, então, é criar os arquivos já com a cultura no nome, como Welcome.fr.resx. Nesse caso, o Visual Studio não gera a classe, e cada combinação de idioma e cultura exige um arquivo próprio.
Fallback de recursos
O runtime resolve os recursos por meio de um fallback em cadeia. Cada cultura tem exatamente um pai, definido em CultureInfo.Parent, e a busca sobe essa hierarquia até encontrar o recurso.
A busca termina quando Parent retorna a cultura invariante. Para fins de fallback, a cultura invariante não conta como pai nem como cultura capaz de conter recursos.
Isso permite reaproveitamento deliberado. Por exemplo, ao fornecer recursos para en, en-GB e en-US, o arquivo neutro concentra a terminologia comum e os regionais sobrescrevem apenas o que diverge.
Se nenhum recurso aparecer em toda a cadeia, o runtime lança MissingManifestResourceException ou MissingSatelliteAssemblyException. Portanto, inclua sempre um conjunto padrão de recursos no assembly principal.
Assemblies satélite
Para distribuir as traduções, globalization e localization no .NET seguem um modelo de eixo e raios. O eixo é o assembly principal, que contém o código executável e os recursos da cultura padrão.
Cada raio, por sua vez, é um assembly satélite com os recursos de uma única cultura e nenhum código. Como os satélites ficam fora do assembly principal, você atualiza as traduções sem tocar no binário da aplicação.
O atributo NeutralResourcesLanguageAttribute designa a cultura padrão da aplicação. Além de documentar a intenção, ele traz um ganho concreto de desempenho.
Quando o ResourceManager precisa de recursos da cultura padrão embutidos no assembly principal, ele usa esses recursos diretamente, sem sondar satélites. Com isso, o primeiro carregamento fica mais barato.
IStringLocalizer e injeção de dependência
Trabalhar diretamente com ResourceManager é possível, mas verboso para tarefas de globalization e localization no .NET moderno. Por isso, o ASP.NET Core oferece as abstrações IStringLocalizer e IStringLocalizer<T>.
A implementação padrão é a ResourceManagerStringLocalizer, que usa ResourceManager e ResourceReader em tempo de execução. A interface expõe um indexador e um IEnumerable para retornar as strings localizadas.
O registro acontece com uma única chamada no contêiner de serviços. Ela adiciona IStringLocalizer<T> e IStringLocalizerFactory e define a pasta dos recursos.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
var app = builder.Build();
Em uma Minimal API, o consumo acontece por injeção de dependência direto na assinatura do endpoint. Se você ainda não conhece esse modelo, o artigo sobre Minimal APIs no ASP.NET é um bom ponto de partida.
using Microsoft.Extensions.Localization;
app.MapGet("/welcome", (IStringLocalizer<Program> localizer) =>
{
var message = localizer["WelcomeMessage"];
return Results.Ok(new { Message = message.Value });
});
app.Run();
A chave como valor padrão
Um detalhe útil de globalization e localization no .NET é que o IStringLocalizer não exige arquivo de recurso para o idioma padrão. Quando não encontra o valor localizado da chave, ele devolve o próprio indexador.
Na prática, isso permite envolver os literais do idioma padrão no localizador e seguir desenvolvendo. Assim, a aplicação fica preparada para a localização sem que você crie arquivos de recurso desde o início.
Em projetos maiores, uma classe marcadora compartilhada costuma ser melhor do que usar Program como tipo do localizador. O mesmo princípio vale para DataAnnotations, cujas mensagens de validação podem apontar para um recurso compartilhado.
Vale lembrar que o .resx não é obrigatório. Ao implementar IStringLocalizer, qualquer fonte de dados serve, e existem implementações de referência com Entity Framework, JSON e banco relacional.
Globalization e localization no .NET por requisição
Em uma aplicação web, a cultura precisa ser resolvida por requisição, e não por processo. Esse é o papel do middleware de localização, que fecha o ciclo de globalization e localization no .NET para a web.
A configuração declara a cultura padrão e as culturas suportadas para formatação e para interface:
var supportedCultures = new[] { "pt-BR", "en-US", "es-ES" };
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
Há uma restrição de ordem que não pode ser ignorada. Você precisa registrar o middleware antes de qualquer outro que consulte a cultura da requisição.
A cada requisição, o middleware percorre a lista de provedores, e vence o primeiro que conseguir determinar a cultura. Se nenhum tiver sucesso, ele aplica a cultura padrão configurada.
Os três provedores padrão
O RequestLocalizationOptions registra, nesta ordem, os provedores QueryStringRequestCultureProvider, CookieRequestCultureProvider e AcceptLanguageHeaderRequestCultureProvider. A lista vai do mais específico para o menos específico.
O provedor de query string aceita os parâmetros culture e ui-culture. Se só um deles chegar, ele aplica o valor aos dois, o que torna esse provedor útil para depuração e testes.
O provedor de cookie usa, por padrão, o cookie .AspNetCore.Culture, que combina cultura e cultura de interface em um único valor. Para montar esse valor, use o método MakeCookieValue:
app.MapPost("/set-language", (HttpContext context, string culture) =>
{
context.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
return Results.NoContent();
});
Por fim, o provedor de cabeçalho lê o Accept-Language enviado pelo navegador. Segundo o guia de seleção de cultura por requisição, esse cabeçalho não é uma forma infalível de detectar o idioma preferido.
Assim, aplicações em produção devem oferecer um mecanismo explícito para o usuário escolher a cultura. Em cenários de Blazor com WebSockets, o cookie é a abordagem recomendada.
Além disso, a propriedade RequestCultureProviders permite mudar a ordem dos provedores e adicionar novos. Já o método AddInitialRequestCultureProvider insere um provedor na posição zero, com precedência sobre os demais.
Esse é o ponto de extensão ideal quando a preferência de idioma fica no banco de dados do usuário autenticado. Dessa forma, a escolha acompanha a pessoa em qualquer dispositivo.
Content-Language na resposta
Além de resolver a cultura de entrada, o middleware pode anunciar a cultura de saída. A propriedade ApplyCurrentCultureToResponseHeaders faz o RequestLocalizationMiddleware definir o cabeçalho Content-Language a partir do CurrentUICulture.
app.UseRequestLocalization(new RequestLocalizationOptions
{
ApplyCurrentCultureToResponseHeaders = true
});
Com isso, você não precisa definir o cabeçalho manualmente. Além disso, o cliente e os caches intermediários sabem qual idioma a resposta realmente usou.
Boas práticas de globalization e localization no .NET
A primeira boa prática de globalization e localization no .NET é formatar para exibição com a cultura do usuário e persistir com a cultura invariante. Essa única regra elimina a classe mais comum de defeitos.
Em seguida, nunca chame sobrecargas de formatação ou conversão sem especificar a cultura ou o provedor. Mesmo quando o padrão parece adequado, o código explícito se documenta sozinho.
Também inclua um conjunto padrão de recursos no assembly principal e declare a cultura neutra com NeutralResourcesLanguageAttribute. Assim, você evita exceções de recurso ausente e melhora o carregamento.
Antes de publicar, verifique se a imagem de contêiner suporta as culturas necessárias. Por exemplo, uma aplicação multilíngue em uma imagem chiseled sem ICU falha ao criar a primeira cultura específica.
Por fim, habilite as regras de análise relacionadas a cultura. Em projetos que manipulam dados financeiros ou datas de forma intensiva, considere promovê-las a erro de compilação.
Conclusão sobre globalization e localization no .NET
Globalization e localization no .NET são duas responsabilidades diferentes que compartilham o mesmo vocabulário. A primeira se resolve com CultureInfo, IFormatProvider e disciplina no uso de InvariantCulture.
A segunda, por outro lado, se resolve com arquivos de recurso, assemblies satélite e as abstrações de IStringLocalizer. No ASP.NET Core, o middleware de localização conecta as duas dimensões a cada requisição.
Dessa forma, a escolha entre query string, cookie e cabeçalho deixa de ser um detalhe de implementação e vira uma decisão de produto. O que separa uma aplicação que atende vários mercados não é a quantidade de traduções, mas o respeito à fronteira entre dados exibidos e dados armazenados.
FAQ
Qual a diferença prática entre CurrentCulture e CurrentUICulture?
O CurrentCulture controla formatação, ordenação e comparação de strings, enquanto o CurrentUICulture controla apenas a busca de recursos traduzidos. As duas propriedades são independentes e podem ter valores diferentes na mesma requisição.
Por que meu decimal.Parse funciona em desenvolvimento e falha em produção?
Provavelmente a cultura do sistema operacional difere entre os ambientes. Sempre informe CultureInfo.InvariantCulture ao converter dados vindos de JSON, arquivos ou banco de dados.
Preciso criar um arquivo .resx para o idioma padrão?
Não, porque o IStringLocalizer retorna a própria chave quando não encontra o valor localizado. Ainda assim, incluir recursos padrão no assembly principal evita exceções de fallback.
Por que minha aplicação lança CultureNotFoundException em contêiner?
A imagem provavelmente ativa o modo de globalização invariante, comum em Alpine, distroless e chiseled. Use uma imagem com ICU, como as variantes extra, ou revise a necessidade de culturas específicas.
Posso guardar as traduções em banco de dados?
Sim, basta implementar IStringLocalizer com a fonte de dados desejada. Existem implementações públicas com Entity Framework, JSON e SQL.
Em que posição do pipeline registro o UseRequestLocalization?
Antes de qualquer middleware que consulte a cultura da requisição, incluindo o roteamento de endpoints. Caso contrário, parte do pipeline roda com a cultura padrão do processo.
Qual provedor de cultura usar em produção?
O cookie tende a ser o mais robusto, pois persiste a escolha explícita do usuário e funciona com WebSockets. O Accept-Language serve como padrão inicial, e a query string ajuda em testes.