Globalização do .NET e UTI

Antes do .NET 5, as APIs de globalização do .NET usavam bibliotecas subjacentes diferentes em plataformas diferentes. No Unix, as APIs usavam International Components for Unicode (ICU) e, no Windows, usavam National Language Support (NLS). Isso resultou em algumas diferenças comportamentais em um punhado de APIs de globalização ao executar aplicativos em plataformas diferentes. As diferenças de comportamento foram evidentes nestas áreas:

  • Culturas e dados sobre culturas
  • Invólucro de corda
  • Classificação e pesquisa de cadeias de caracteres
  • Chaves de classificação
  • Normalização de cadeias 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.

Nota

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

UTI no Windows

O Windows agora incorpora uma versão icu.dll pré-instalada como parte de seus recursos que é automaticamente empregada para tarefas de globalização. Essa modificação permite que o .NET use essa biblioteca de UTI para seu suporte à globalização. Nos casos em que a biblioteca da UTI não está disponível ou não pode ser carregada, como é o caso das versões mais antigas do Windows, o .NET 5 e as versões subsequentes voltam a usar a implementação baseada em NLS.

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

Versão .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

Nota

O .NET 7 e versões posteriores têm a capacidade de carregar a UTI em versões mais antigas do Windows, em contraste com o .NET 6 e o .NET 5.

Nota

Mesmo ao usar a ICU, o , CurrentUICulturee CurrentRegion os membros ainda usam APIs do CurrentCulturesistema operacional Windows para honrar as configurações do usuário.

Diferenças comportamentais

Se você atualizar seu aplicativo para o .NET 5 ou posterior de destino, poderá ver alterações em seu aplicativo mesmo que não perceba que está usando recursos de globalização. A seção a seguir lista algumas mudanças comportamentais que você pode experimentar.

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

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

Comparar cadeias de caracteres para igualdade e determinar sua ordem de classificação difere entre NLS e UTI. Em particular:

  • A ordem de classificação de cadeia de caracteres padrão é diferente, portanto, isso será aparente mesmo se você não usar CompareOptions diretamente. Ao usar a UTI, a None opção padrão executa o mesmo que StringSort. StringSort classifica caracteres não alfanuméricos antes dos alfanuméricos (assim, "bill's" classifica antes de "bills", por exemplo). Para restaurar a funcionalidade anterior None , você deve usar a implementação baseada em NLS.
  • A manipulação padrão de caracteres de ligadura é diferente. Sob NLS, ligaduras e suas contrapartes não-ligaduras (por exemplo, "oeuf" e "œuf") são consideradas iguais, mas este não é o caso da UTI em .NET. Isso ocorre devido a uma força de agrupamento diferente entre as duas implementações. Para restaurar o comportamento do NLS ao usar a UTI, use o CompareOptions.IgnoreNonSpace valor.

String.IndexOf

Considere o código a seguir que chama String.IndexOf(String) para localizar o índice do caractere \0 nulo 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 trecho é 3 impresso em cada uma das três linhas.
  • Para o .NET 5 e versões posteriores em execução nas versões do Windows listadas na tabela de seção ICU no Windows , o trecho imprime 0, 0e 3 (para a pesquisa ordinal).

Por padrão, String.IndexOf(String) executa uma pesquisa linguística sensível à cultura. A ICU considera o caractere \0 nulo como 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 \0 nulo como um caractere de peso zero, e uma pesquisa linguística no .NET Core 3.1 e anteriores localiza o caractere na posição 3. Uma pesquisa ordinal encontra o caractere na posição 3 em todas as versões do .NET.

Você pode executar regras de análise de código CA1307: Especifique StringComparison para maior clareza e CA1309: Use ordinal StringComparison para localizar sites de chamada em seu código onde 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 em versões do Windows listadas na tabela ICU on Windows , o trecho anterior é impresso:

True
True
True
False
False

Para evitar esse comportamento, use o char parâmetro overload 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 em versões do Windows listadas na tabela ICU on Windows , o trecho anterior é impresso:

True
True
True
False
False

Para evitar esse comportamento, use o char parâmetro overload ou StringComparison.Ordinal.

TimeZoneInfo.FindSystemTimeZoneById

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

Abreviaturas do dia da semana

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

  • No .NET Core 3.1 e versões anteriores no Windows, essas abreviaturas de dia da semana consistiam em dois caracteres, por exemplo, "Su".
  • No .NET 5 e versões posteriores, essas abreviaturas de dia da semana consistem em apenas um caractere, por exemplo, "S".

APIs dependentes de UTI

O .NET introduziu APIs que dependem da UTI. Essas APIs só podem ter êxito ao usar a UTI. Seguem-se alguns exemplos:

Nas versões do Windows listadas na tabela de seção ICU on Windows , as APIs mencionadas são bem-sucedidas. No entanto, em versões mais antigas do Windows, essas APIs falham. Nesses casos, você pode habilitar o recurso de UTI local do aplicativo para garantir o sucesso dessas APIs. Em plataformas que não sejam Windows, essas APIs sempre são bem-sucedidas, independentemente da versão.

Além disso, é crucial que os aplicativos garantam que não estejam sendo executados no modo invariante de globalização ou no modo NLS para garantir o sucesso dessas APIs.

Use NLS em vez de UTI

Usar UTI em vez de NLS pode resultar em diferenças comportamentais com algumas operações relacionadas à globalização. Para voltar a usar o NLS, você pode desativar a implementação da UTI. Os aplicativos podem habilitar o modo NLS de qualquer uma das seguintes maneiras:

  • No ficheiro do projeto:

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

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

Nota

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

Para obter mais informações, consulte Configurações de configuração de tempo de execução.

Determinar se seu aplicativo está usando a UTI

O trecho de código a seguir pode ajudá-lo a determinar se seu aplicativo está sendo executado com bibliotecas 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.

UTI local por aplicativo

Cada versão da UTI pode trazer correções de bugs e dados CLDR (Common Locale Data Repository) atualizados que descrevem os idiomas do mundo. Alternar entre versões da UTI 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 Unix carreguem e usem sua própria cópia da ICU.

Os aplicativos podem optar por um modo de implementação de UTI local do aplicativo de uma das seguintes maneiras:

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

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

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

    <suffix>: Sufixo opcional com menos de 36 caracteres, seguindo as convenções públicas de embalagem da UTI. Ao criar uma UTI personalizada, você pode personalizá-la para produzir os nomes lib e nomes de símbolos exportados para conter um sufixo, por exemplo, libicuucmyapponde myapp é o sufixo.

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

Quando qualquer uma dessas opções é definida, você pode adicionar um Microsoft.ICU.ICU4C.Runtime PackageReference ao seu projeto que corresponde ao configurado version e que é tudo o que é necessário.

Como alternativa, para carregar a UTI quando a opção app-local é definida, o .NET usa o NativeLibrary.TryLoad método, que investiga vários caminhos. O método primeiro tenta localizar a NATIVE_DLL_SEARCH_DIRECTORIES biblioteca na propriedade, que é criada pelo host dotnet com base no deps.json arquivo para o aplicativo. Para obter mais informações, consulte Sondagem padrão.

Para aplicativos autônomos, nenhuma ação especial é exigida pelo usuário, além de certificar-se de que a UTI está no diretório do aplicativo (para aplicativos autônomos, o padrão do diretório de trabalho é ).NATIVE_DLL_SEARCH_DIRECTORIES

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

Para aplicativos dependentes da estrutura (não autônomos) em que a UTI é consumida a partir de uma compilação local, você deve executar etapas adicionais. O SDK do .NET ainda não tem um recurso para binários nativos "soltos" a serem incorporados ( deps.json consulte este 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 da UTI para os tempos de execução suportados. Além disso, os NuGetPackageId metadados no grupo de RuntimeTargetsCopyLocalItems itens precisam corresponder a um pacote NuGet ao qual o projeto realmente faz referência.

Comportamento do macOS

O macOS tem um comportamento diferente para resolver bibliotecas dinâmicas dependentes a partir dos comandos de carregamento especificados no Mach-O arquivo do que o carregador Linux. No carregador Linux, o .NET pode tentar libicudata, libicuuce libicui18n (nessa ordem) satisfazer o gráfico de dependência da UTI. 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 carregamento no libicuuc. O trecho a seguir mostra 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 apenas fazem referência ao nome das bibliotecas dependentes para os outros componentes da UTI. O carregador realiza a pesquisa seguindo as dlopen convenções, o que envolve ter essas bibliotecas nos diretórios do sistema ou definir os LD_LIBRARY_PATH vars env, ou ter UTI no diretório no nível do aplicativo. Se você não puder definir LD_LIBRARY_PATH ou garantir que os binários da UTI estejam no diretório no nível do aplicativo, será necessário fazer algum trabalho extra.

Existem algumas diretivas para o carregador, como @loader_path, que dizem ao carregador para procurar essa dependência no mesmo diretório que o binário com esse comando load. Existem duas formas de o conseguir:

  • 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
    
  • Patch ICU para produzir os nomes de instalação com @loader_path

    Antes de executar o 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))
    

UTI em WebAssembly

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

As seguintes APIs não são suportadas:

As seguintes APIs são suportadas com limitações:

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