Globalização e ICU do .NET

Antes do .NET 5, as APIs de globalização do .NET usavam diferentes bibliotecas subjacentes em diferentes plataformas. No Unix, as APIs usaram Componentes Internacionais para Unicode (ICU) e, no Windows, usaram o NLS (National Language Support). Isso resultou em algumas diferenças comportamentais em um várias APIs de globalização ao executar aplicativos em diferentes plataformas. Diferenças de comportamento eram evidentes nessas áreas:

  • Culturas e dados de cultura
  • Invólucro de cadeia de caracteres
  • Classificação e pesquisa
  • Chaves de classificação
  • Normalização de cadeia de caracteres
  • Suporte a nomes de domínio internacionalizados (IDN)
  • Nome de exibição de fuso horário no Linux

A partir do .NET 5, os desenvolvedores têm mais controle sobre qual biblioteca subjacente é usada, permitindo que os aplicativos evitem diferenças entre plataformas.

Observação

Os dados de cultura que impulsionam o comportamento da biblioteca de UTI geralmente são mantidos pelo Common Locale Data Repository (CLDR), não pelo runtime.

ICU no Windows

Agora, o Windows incorpora uma versão icu.dll pré-instalada como parte de seus recursos que é automaticamente aplicada a tarefas de globalização. Esta modificação permite que o .NET use esta biblioteca ICU para seu suporte à globalização. Nos casos em que a biblioteca ICU não estiver disponível ou não puder ser carregada, como é o caso das versões mais antigas do Windows, o .NET 5 e as versões subsequentes são revertidos para usar a implementação baseada em NLS.

A tabela a seguir mostra quais versões do .NET são capazes de carregar a biblioteca de ICU em diferentes versões de cliente e servidor do Windows:

Versão do .NET Versão do Windows
.NET 5 ou .NET 6 Cliente Windows 10 versão 1903 ou posterior
.NET 5 ou .NET 6 Windows Server 2022 ou posterior
.NET 7 ou posterior Cliente Windows 10 versão 1703 ou posterior
.NET 7 ou posterior Windows Server 2019 ou posterior

Observação

O .NET 7 e versões posteriores têm a capacidade de carregar a ICU em versões mais antigas do Windows, ao contrário do .NET 6 e .NET 5.

Observação

Mesmo ao usar a ICU, os membros CurrentCulture, CurrentUICulture e CurrentRegion ainda usam APIs do sistema operacional Windows para respeitar as configurações do usuário.

Diferenças de comportamento

Se você atualizar seu aplicativo para usar o .NET 5 ou posterior como destino, poderá ver alterações em seu aplicativo mesmo que não perceba que está usando instalações de globalização. A seção a seguir lista algumas alterações de comportamento que você poderá ver.

Classificação de cadeias de caracteres e System.Globalization.CompareOptions

CompareOptions é a enumeração de opções que pode ser transmitida para String.Compare a fim de influenciar como duas cadeias de caracteres são comparadas.

Comparar cadeias de caracteres quanto à igualdade e determinar a ordem de classificação é diferente entre o NLS e o ICU. Especialmente:

  • A ordem de classificação de cadeias de caracteres padrão é diferente, portanto, isso ficará evidente mesmo se você não usar CompareOptions diretamente. Quando o ICU é usado, a opção padrão None faz o mesmo que StringSort. StringSort classifica caracteres não alfanuméricos antes dos alfanuméricos (portanto, “bill's” é classificado antes de “bills”, por exemplo). Para restaurar a funcionalidade anterior de None, é preciso usar a implementação baseada em NLS.
  • O tratamento padrão de caracteres de ligadura é diferente. No NLS, as ligaturas e os equivalentes que não são ligaturas (por exemplo, “oeuf” e “œuf”) são considerados iguais, mas esse não é o caso do ICU no .NET. Isso ocorre devido a uma força de ordenação diferente entre as duas implementações. Para restaurar o comportamento do NLS ao usar o ICU, use o valor CompareOptions.IgnoreNonSpace.

String.IndexOf

Considere o código a seguir que chama String.IndexOf(String) para localizar o índice do caractere nulo \0 em uma cadeia de caracteres.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • No .NET Core 3.1 e versões anteriores no Windows, o snippet é imprime 3 em cada uma das três linhas.
  • Para .NET 5 e versões posteriores em execução nas versões do Windows listadas na tabela de seção ICU no Windows, o snippet é impresso 0, 0, e 3 (para a pesquisa ordinal).

Por padrão, String.IndexOf(String) executa uma pesquisa linguística com reconhecimento de cultura. A UTI considera que o caractere nulo \0 é um caractere de peso zero e, portanto, o caractere não é encontrado na cadeia de caracteres ao usar uma pesquisa linguística no .NET 5 e posterior. No entanto, o NLS não considera o caractere nulo \0 como um caractere de peso zero, e uma pesquisa linguística no .NET Core 3.1 e anterior localiza o caractere na posição 3. Uma pesquisa ordinal localiza o caractere na posição 3 em todas as versões do .NET.

Você pode executar as regras de análise de código CA1307: especifcar StringComparison para maior clareza e CA1309: usar ordinal StringComparison para localizar sites de chamadas em seu código em que a comparação de cadeia de caracteres não é especificada ou não é ordinal.

Para obter mais informações, consulte Alterações de comportamento ao comparar cadeias de caracteres no .NET 5+.

String.EndsWith

const string foo = "abc";

Console.WriteLine(foo.EndsWith("\0"));
Console.WriteLine(foo.EndsWith("c"));
Console.WriteLine(foo.EndsWith("\0", StringComparison.CurrentCulture));
Console.WriteLine(foo.EndsWith("\0", StringComparison.Ordinal));
Console.WriteLine(foo.EndsWith('\0'));

Importante

No .NET 5+ em execução nas versões do Windows listadas na tabela ICU no Windows, o trecho de código anterior imprime:

True
True
True
False
False

Para evitar esse comportamento, use a sobrecarga de parâmetro char ou StringComparison.Oridinal.

String.StartsWith

const string foo = "abc";

Console.WriteLine(foo.StartsWith("\0"));
Console.WriteLine(foo.StartsWith("a"));
Console.WriteLine(foo.StartsWith("\0", StringComparison.CurrentCulture));
Console.WriteLine(foo.StartsWith("\0", StringComparison.Ordinal));
Console.WriteLine(foo.StartsWith('\0'));

Importante

No .NET 5+ em execução nas versões do Windows listadas na tabela ICU no Windows, o trecho de código anterior imprime:

True
True
True
False
False

Para evitar esse comportamento, use a sobrecarga de parâmetro char ou StringComparison.Ordinal.

TimeZoneInfo.FindSystemTimeZoneById

A ICU fornece a flexibilidade para criar instâncias TimeZoneInfo usando IDs de fuso horário IANA, mesmo quando o aplicativo está em execução no Windows. Da mesma forma, você pode criar instâncias TimeZoneInfo com IDs de fuso horário do Windows, mesmo quando executadas em plataformas que não sejam do Windows. No entanto, é importante observar que essa funcionalidade não está disponível ao usar o modo NLS ou o modo invariável de globalização.

Abreviatura do dia da semana

O método DateTimeFormatInfo.GetShortestDayName(DayOfWeek) obtém o nome do dia abreviado mais curto para um dia da semana especificado.

  • No .NET Core 3.1 e em versões anteriores no Windows, essas abreviações dos dias da semana consistiam em dois caracteres, por exemplo, "Do".
  • No .NET 5 e versões posteriores, essas abreviaturas do dia da semana consistem em apenas um caractere, por exemplo, "D".

APIs dependentes da ICU

O .NET introduziu APIs que dependem da ICU. Essas APIs só podem ser bem-sucedidas ao usar a ICU. Estes são alguns exemplos:

Nas versões do Windows listadas na tabela da seção ICU no Windows, as APIs mencionadas tiveram sucesso. No entanto, em versões mais antigas do Windows, essas APIs costumam falhar. Nesses casos, você pode habilitar o recurso de ICU local do aplicativo para garantir o sucesso dessas APIs. Em plataformas que não são Windows, essas APIs sempre terão sucesso, independentemente da versão.

Além disso, é crucial que os aplicativos garantam que elas não estejam em execução no modo invariável de globalização ou no modo NLS para garantir o sucesso dessas APIs.

Usar NLS em vez de ICU

Usar a ICU em vez de NLS pode resultar em diferenças comportamentais com algumas operações relacionadas à globalização. Para voltar a usar o NLS, um desenvolvedor pode recusar a implementação da ICU. Os aplicativos podem habilitar o modo invariável usando um dos seguintes métodos:

  • No arquivo de projeto:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • No arquivo runtimeconfig.json:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Definindo a variável de ambiente DOTNET_SYSTEM_GLOBALIZATION_USENLS para o valor true ou 1.

Observação

Um valor definido no projeto ou no arquivo runtimeconfig.json tem precedência sobre a variável de ambiente.

Para obter mais informações, consulte Configurações de runtime.

Determinar se seu aplicativo está usando a ICU

O snippet de código a seguir pode ajudá-lo a determinar se seu aplicativo está em execução com bibliotecas de ICU (e não NLS).

public static bool ICUMode()
{
    SortVersion sortVersion = CultureInfo.InvariantCulture.CompareInfo.Version;
    byte[] bytes = sortVersion.SortId.ToByteArray();
    int version = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0];
    return version != 0 && version == sortVersion.FullVersion;
}

Para determinar a versão do .NET, use RuntimeInformation.FrameworkDescription.

ICU local do aplicativo

Cada versão da ICU pode trazer correções de bug e dados do Common Locale Data Repository (CLDR) atualizados que descrevem os idiomas do mundo todo. Migrar entre versões da ICU pode afetar sutilmente o comportamento do aplicativo quando se trata de operações relacionadas à globalização. Para ajudar os desenvolvedores de aplicativos a garantir a consistência em todas as implantações, o .NET 5 e versões posteriores permitem que aplicativos no Windows e no Unix carreguem e usem sua própria cópia de ICU.

Os aplicativos podem aceitar um modo de implementação da ICU local do aplicativo de uma das seguintes maneiras:

  • No arquivo de projeto, defina o valor RuntimeHostConfigurationOption apropriado:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • Ou no arquivo runtimeconfig.json, defina o valor de runtimeOptions.configProperties apropriado:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • Ou definindo a variável de ambiente DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU para o valor <suffix>:<version> ou <version>.

    <suffix>: sufixo opcional de menos de 36 caracteres de comprimento, seguindo as convenções de empacotamento de ICU pública. Ao criar uma ICU personalizada, você pode personalizá-la para produzir os nomes de lib e os nomes de símbolo exportados para conter um sufixo, por exemplo, libicuucmyapp, em que myapp é o sufixo.

    <version>: uma versão válida da ICU, por exemplo, 67.1. Essa versão é usada para carregar os binários e obter os símbolos exportados.

Quando uma dessas opções estiver definida, você poderá adicionar um Microsoft.ICU.ICU4C.RuntimePackageReference ao seu projeto que corresponde à version configurada e isso é tudo o que é necessário.

Como outra opção, para carregar uma ICU quando a opção local do aplicativo é definida, o .NET usa o método NativeLibrary.TryLoad, que investiga vários caminhos. Primeiro, o método tenta localizar a biblioteca na propriedade NATIVE_DLL_SEARCH_DIRECTORIES, que é criada pelo host dotnet com base no arquivo deps.json do aplicativo. Para obter mais informações, consulte Investigação padrão.

Para aplicativos independentes, nenhuma ação especial é exigida pelo usuário, além de verificar se a ICU está no diretório do aplicativo (para aplicativos autossuficientes, o diretório de trabalho é padrão NATIVE_DLL_SEARCH_DIRECTORIES).

Se você estiver consumindo ICU por meio de um pacote NuGet, isso funcionará em aplicativos dependentes de estrutura. O NuGet resolve os ativos nativos e os inclui no arquivo deps.json e no diretório de saída do aplicativo no diretório runtimes. O .NET o carrega de lá.

Para aplicativos dependentes de estrutura (não independentes) em que a ICU é consumida a partir de uma compilação local, você deve executar etapas adicionais. O .NET SDK ainda não tem um recurso para binários nativos "soltos" a serem incorporados em deps.json (consulte Esse problema do SDK). Em vez disso, você pode habilitar isso adicionando informações adicionais ao arquivo de projeto do aplicativo. Por exemplo:

<ItemGroup>
  <IcuAssemblies Include="icu\*.so*" />
  <RuntimeTargetsCopyLocalItems Include="@(IcuAssemblies)" AssetType="native" CopyLocal="true"
    DestinationSubDirectory="runtimes/linux-x64/native/" DestinationSubPath="%(FileName)%(Extension)"
    RuntimeIdentifier="linux-x64" NuGetPackageId="System.Private.Runtime.UnicodeData" />
</ItemGroup>

Isso deve ser feito para todos os binários de UTI para os runtimes com suporte. Além disso, os metadados NuGetPackageId no grupo de itens RuntimeTargetsCopyLocalItems precisam corresponder a um pacote NuGet que o projeto realmente faz referência.

Comportamento do macOS

o macOS tem um comportamento diferente para resolver bibliotecas dinâmicas dependentes dos comandos de carga especificados no arquivo Mach-O em comparação com o carregador do Linux. No carregador do Linux, o .NET pode tentar libicudata, libicuuc e libicui18n (nessa ordem) atender ao grafo de dependência da ICU. No entanto, no macOS, isso não funciona. Ao criar a ICU no macOS, você, por padrão, obtém uma biblioteca dinâmica com esses comandos de carga em libicuuc. O snippet a seguir é um exemplo.

~/ % otool -L /Users/santifdezm/repos/icu-build/icu/install/lib/libicuuc.67.1.dylib
/Users/santifdezm/repos/icu-build/icu/install/lib/libicuuc.67.1.dylib:
 libicuuc.67.dylib (compatibility version 67.0.0, current version 67.1.0)
 libicudata.67.dylib (compatibility version 67.0.0, current version 67.1.0)
 /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.100.1)
 /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 902.1.0)

Esses comandos fazem referência apenas ao nome das bibliotecas dependentes para os outros componentes da ICU. O carregador executa a pesquisa seguindo as convenções dlopen, o que envolve ter essas bibliotecas nos diretórios do sistema ou definir os env vars LD_LIBRARY_PATH ou ter ICU no diretório no nível do aplicativo. Se você não puder definir LD_LIBRARY_PATH ou garantir que os binários da ICU estejam no diretório no nível do aplicativo, você precisará fazer algum trabalho extra.

Há algumas diretivas para o carregador, como @loader_path, que dizem ao carregador para pesquisar essa dependência no mesmo diretório que o binário com esse comando de carga. Há duas maneiras de fazer isso:

  • install_name_tool -change

    Execute os seguintes comandos:

    install_name_tool -change "libicudata.67.dylib" "@loader_path/libicudata.67.dylib" /path/to/libicuuc.67.1.dylib
    install_name_tool -change "libicudata.67.dylib" "@loader_path/libicudata.67.dylib" /path/to/libicui18n.67.1.dylib
    install_name_tool -change "libicuuc.67.dylib" "@loader_path/libicuuc.67.dylib" /path/to/libicui18n.67.1.dylib
    
  • ICU do patch para produzir os nomes de instalação com @loader_path

    Antes de executar autoconf (./runConfigureICU), altere estas linhas para:

    LD_SONAME = -Wl,-compatibility_version -Wl,$(SO_TARGET_VERSION_MAJOR) -Wl,-current_version -Wl,$(SO_TARGET_VERSION) -install_name @loader_path/$(notdir $(MIDDLE_SO_TARGET))
    

ICU no WebAssembly

Uma versão da ICU está disponível especificamente para cargas de trabalho do WebAssembly. Essa versão fornece compatibilidade de globalização com perfis de área de trabalho. Para reduzir o tamanho do arquivo de dados da ICU de 24 MB para 1,4 MB (ou ~0,3 MB se compactado com Brotli), essa carga de trabalho tem algumas limitações.

A APIs a seguir não têm suporte:

As seguintes APIs têm suporte com limitações:

Além disso, há suporte para menos localidades. A lista com suporte pode ser encontrada no repositório dotnet/icu.