Globalización de .NET e ICU

Antes de .NET 5, las API de globalización de .NET usaban diferentes bibliotecas subyacentes en distintas plataformas. En UNIX, las API usaban Componentes internacionales para Unicode (ICU) y, en Windows, usaban Compatibilidad con el idioma nacional (NLS). Esto producía algunas diferencias de comportamiento en varias API de globalización al ejecutar aplicaciones en distintas plataformas. Las diferencias de comportamiento eran evidentes en estas áreas:

  • Referencias culturales y datos culturales
  • Uso de mayúsculas y minúsculas en cadenas
  • Ordenación y búsqueda de cadenas
  • Criterios de ordenación
  • Normalización de cadenas
  • Compatibilidad con nombres de dominio internacionalizados (IDN)
  • Nombre para mostrar de la zona horaria en Linux

A partir de .NET 5, los desarrolladores tienen más control sobre la biblioteca subyacente que se usa, lo que permite a las aplicaciones evitar diferencias entre plataformas.

ICU en Windows

Windows incorpora ahora una versión icu.dll preinstalada como parte de sus características que se emplea automáticamente para tareas de globalización. Esta modificación permite a .NET aprovechar esta biblioteca ICU para su soporte de globalización. En los casos en los que la biblioteca ICU no está disponible o no se puede cargar, como ocurre con las versiones antiguas de Windows, .NET 5 y las versiones posteriores vuelven a utilizar la implementación basada en NLS.

La siguiente tabla muestra qué versiones de .NET son capaces de cargar la biblioteca ICU en las distintas versiones de cliente y servidor de Windows:

Versión de .NET Versión de Windows
.NET 5 o .NET 6 Cliente Windows 10 versión 1903 o posterior
.NET 5 o .NET 6 Windows Server 2022 o posterior
.NET 7 o una versión posterior Cliente Windows 10 versión 1703 o posterior
.NET 7 o una versión posterior Windows Server 2019 o posterior

Nota:

.NET 7 y versiones posteriores tienen la capacidad de cargar ICU en versiones antiguas de Windows, a diferencia de .NET 6 y .NET 5.

Nota:

Incluso cuando se usa ICU, los miembros de CurrentCulture, CurrentUICulture y CurrentRegion siguen usando las API del sistema operativo Windows para respetar la configuración de usuario.

Diferencias de comportamiento

Si actualiza su aplicación a .NET 5 o posterior, es posible que vea cambios en su aplicación aunque no se de cuenta de que está utilizando facilidades de globalización. En esta sección se enumera uno de los cambios de comportamiento que podría ver, pero también hay otros.

String.IndexOf

Considera el código siguiente que llama a String.IndexOf(String) para buscar el índice del carácter null \0 en una cadena.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • En .NET Core 3.1 y versiones anteriores en Windows, el fragmento de código imprime 3 en cada una de las tres líneas.
  • Para .NET 5 y versiones posteriores que se ejecutan en las versiones de Windows enumeradas en la ICU en Windows tabla de secciones, el fragmento de código imprime 0, 0. y 3 (para la búsqueda ordinal).

De forma predeterminada, String.IndexOf(String) realiza una búsqueda lingüística compatible con la referencia cultural. ICU considera que el carácter \0 null es un carácter de peso cero y, por tanto, el carácter no se encuentra en la cadena cuando se usa una búsqueda lingüística en .NET 5 y versiones posteriores. Sin embargo, NLS no considera que el carácter NULL sea un carácter \0 de peso cero y una búsqueda lingüística en .NET Core 3.1 y anteriormente localiza el carácter en la posición 3. Una búsqueda ordinal busca el carácter en la posición 3 en todas las versiones de .NET.

Puedes ejecutar reglas de análisis de código CA1307: Especificar StringComparison para mayor claridad y CA1309: Usar ordinal StringComparison para buscar sitios de llamada en el código donde no se especifica la comparación de cadenas o no es ordinal.

Para más información, consulte Cambios de comportamiento al comparar cadenas en .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

En .NET 5+ que se ejecuta en versiones de Windows enumeradas en la tabla ICU en Windows, el fragmento de código anterior imprime:

True
True
True
False
False

Para evitar este comportamiento, use la sobrecarga del parámetrochar o 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

En .NET 5+ que se ejecuta en versiones de Windows enumeradas en la tabla ICU en Windows, el fragmento de código anterior imprime:

True
True
True
False
False

Para evitar este comportamiento, use la sobrecarga del parámetrochar o StringComparison.Oridinal.

TimeZoneInfo.FindSystemTimeZoneById

ICU proporciona la flexibilidad de crear TimeZoneInfo instancias mediante identificadores de zona horaria IANA , incluso cuando la aplicación se ejecuta en Windows. De forma similar, puede crear TimeZoneInfo instancias con identificadores de zona horaria de Windows, incluso cuando se ejecutan en plataformas que no son de Windows. Sin embargo, es importante tener en cuenta que esta funcionalidad no está disponible al usar el modo NLS o el modo invariable de globalización.

API dependientes de ICU

.NET introdujo API que dependen de ICU. Estas API solo se pueden realizar correctamente cuando se usa ICU. A continuación se muestran algunos ejemplos:

En las versiones de Windows listadas en la tabla de la sección ICU en Windows, las API mencionadas tendrán éxito de forma consistente. Sin embargo, en versiones anteriores de Windows, estas API producirán errores de forma coherente. En tales casos, puede habilitar la característica ICU local de la aplicación para garantizar el éxito de estas API. En plataformas que no son de Windows, estas API siempre se realizarán correctamente independientemente de la versión.

Además, es fundamental que las aplicaciones se aseguren de que no se ejecutan en el modo invariable de globalización o en el modo NLS para garantizar el éxito de estas API.

Uso de NLS en lugar de ICU

Si se usa ICU en lugar de NLS, se pueden producir diferencias de comportamiento con algunas operaciones relacionadas con la globalización. Para revertir al uso de NLS, los desarrolladores pueden rechazar la implementación de ICU. Las aplicaciones pueden habilitar el modo NLS de cualquiera de estas maneras:

  • El archivo del proyecto:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • En el archivo runtimeconfig.json:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Al establecer la variable de entorno DOTNET_SYSTEM_GLOBALIZATION_USENLS en el valor true o 1.

Nota:

Un valor establecido en el proyecto o en el archivo runtimeconfig.json tiene prioridad sobre la variable de entorno.

Para obtener más información, consulte Valores de configuración de tiempo de ejecución.

Determinar si tu aplicación usa ICU

El siguiente fragmento de código puede ayudarte a determinar si la aplicación se ejecuta con bibliotecas de ICU (y no 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 la versión de .NET, usa RuntimeInformation.FrameworkDescription.

ICU local de la aplicación

Cada versión de ICU puede tener incorporadas correcciones de errores, así como datos del Repositorio de datos comunes de configuración regional (CLDR) que describen los idiomas del mundo. El cambio entre las versiones de ICU puede afectar sutilmente al comportamiento de la aplicación cuando se trata de operaciones relacionadas con la globalización. Para que los desarrolladores de aplicaciones puedan garantizar la coherencia entre todas las implementaciones, .NET 5 y las versiones posteriores permiten que las aplicaciones de Windows y UNIX transporten y usen su propia copia de ICU.

Las aplicaciones pueden participar en un modo de implementación de ICU local de la aplicación de alguna de estas maneras:

  • En el archivo del proyecto, establezca el valor de RuntimeHostConfigurationOption adecuado:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • O bien, en el archivo runtimeconfig.json, establezca el valor de runtimeOptions.configProperties adecuado:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • O establezca la variable de entorno DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU en el valor <suffix>:<version> o <version>.

    <suffix>: sufijo opcional de menos de 36 caracteres de longitud, según las convenciones de empaquetado públicas de ICU. Al compilar un ICU personalizado, puede personalizarlo para generar los nombres de lib y los nombres de símbolos exportados para que contengan un sufijo, por ejemplo, libicuucmyapp, donde myapp es el sufijo.

    <version>: versión de ICU válida, por ejemplo, 67.1. Esta versión se usa para cargar los archivos binarios y obtener los símbolos exportados.

Cuando se establece cualquiera de estas opciones, puede agregar un elemento Microsoft.ICU.ICU4C.RuntimePackageReference al proyecto que corresponde al version configurado; eso es todo lo necesario.

Como alternativa, para cargar el ICU cuando se establece el modificador local de la aplicación, .NET usa el método NativeLibrary.TryLoad, que sondea varias rutas de acceso. El método intenta buscar primero la biblioteca en la propiedad NATIVE_DLL_SEARCH_DIRECTORIES, que la crea el host dotnet basándose en el archivo deps.json de la aplicación. Para más información, vea Sondeo predeterminado.

En el caso de las aplicaciones independientes, el usuario no tiene que hacer ninguna acción especial, aparte de asegurarse de que ICU está en el directorio de la aplicación (para las aplicaciones independientes, el directorio de trabajo tiene como valor predeterminado NATIVE_DLL_SEARCH_DIRECTORIES).

Si está usando ICU a través de un paquete NuGet, esto funciona en aplicaciones dependientes del marco. NuGet resuelve los recursos nativos y los incluye en el archivo deps.json y en el directorio de salida de la aplicación en el directorio runtimes. .NET lo carga desde allí.

En el caso de las aplicaciones dependientes del marco (no independientes) en las que se usa ICU desde una compilación local, debe realizar unos cuantos pasos más. El SDK de .NET todavía no tiene una característica para que los binarios nativos "sueltos" se incorporen en deps.json (vea este problema del SDK). En su lugar, puede habilitarlo si agrega información adicional en el archivo de proyecto de la aplicación. Por ejemplo:

<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>

Debe realizar esto en todos los archivos binarios de ICU de los entornos de ejecución admitidos. Además, los metadatos de NuGetPackageId del grupo de elementos RuntimeTargetsCopyLocalItems deben coincidir con un paquete NuGet al que realmente hace referencia el proyecto.

Comportamiento de macOS

macOS tiene un comportamiento distinto para resolver bibliotecas dinámicas dependientes a partir de los comandos de carga especificados en el archivo Mach-O al que tiene el cargador de Linux. En el cargador de Linux, .NET puede intentar libicudata, libicuuc y libicui18n (en ese orden) para satisfacer el gráfico de dependencias de ICU. Pero esto no funciona en macOS. Al crear ICU en macOS, se obtiene de forma predeterminada una biblioteca dinámica con estos comandos de carga en libicuuc. En el fragmento de código siguiente se muestra un ejemplo.

~/ % 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)

Estos comandos simplemente hacen referencia al nombre de las bibliotecas dependientes para los demás componentes de ICU. El cargador realiza la búsqueda según las convenciones de dlopen, lo que implica tener estas bibliotecas en los directorios del sistema o configurar env vars de LD_LIBRARY_PATH o tener ICU en el directorio de nivel de aplicación. Si no puede establecer LD_LIBRARY_PATH o asegurarse de que los binarios de ICU se encuentran en el directorio de nivel de aplicación, deberá realizar algún trabajo adicional.

Hay algunas directivas para el cargador, como @loader_path, que indican al cargador que busque esa dependencia en el mismo directorio que el binario con ese comando de carga. Hay dos formas de lograrlo:

  • install_name_tool -change

    Ejecute los comandos siguientes:

    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
    
  • Revisión de ICU para generar los nombres de instalación con @loader_path

    Antes de ejecutar autoconf (./runConfigureICU), cambie estas líneas a:

    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 en WebAssembly

Hay disponible una versión de ICU que es específicamente para cargas de trabajo de WebAssembly. Esta versión proporciona compatibilidad de globalización con los perfiles de escritorio. Para reducir el tamaño del archivo de datos de ICU de 24 MB a 1,4 MB (o ~ 0,3 MB si está comprimido con Brotli), esta carga de trabajo tiene una serie de limitaciones.

No se admiten las siguientes API:

Las siguientes API se admiten con limitaciones:

Además, se admiten menos configuraciones regionales. La lista admitida se puede encontrar en el repositorio dotnet/icu.