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.