Compartir a través de


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.

Nota:

Los datos de referencia cultural que impulsan el comportamiento de la biblioteca ICU normalmente los mantiene el Repositorio de datos de configuración regional común (CLDR), no el tiempo de ejecución.

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 usar esta biblioteca de ICU para su compatibilidad con la 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 la sección siguiente se enumeran algunos cambios de comportamiento que puede experimentar.

Orden de cadenas y System.Globalization.CompareOptions

CompareOptions es la enumeración de opciones que se pueden pasar a String.Compare para influir en cómo se comparan dos cadenas.

La comparación de cadenas de igualdad y la determinación de su criterio de ordenación difieren entre NLS e ICU. En concreto:

  • El criterio de ordenación de cadenas predeterminado es diferente, por lo que será evidente incluso si no se usa CompareOptions directamente. Al usar ICU, la opción predeterminada None realiza lo mismo que StringSort. StringSort ordena los caracteres no alfanuméricos antes que los alfanuméricos (por lo tanto, "bill's" se ordena antes que "bills", por ejemplo). Para restaurar la funcionalidad None anterior, debe usar la implementación basada en NLS.
  • El control predeterminado de los caracteres de ligadura difiere. En NLS, las ligaduras y sus homólogos sin ligadura (por ejemplo, "oeuf" y "œuf") se consideran iguales, pero esto no es el caso con ICU en .NET. Esto se debe a una intensidad de intercalación diferente entre las dos implementaciones. Para restaurar el comportamiento de NLS al usar ICU, utilice el valor CompareOptions.IgnoreNonSpace.

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.

Puede 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.Ordinal.

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

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.

Abreviaturas del día de la semana

El método DateTimeFormatInfo.GetShortestDayName(DayOfWeek) obtiene el nombre de día abreviado más corto para un día especificado de la semana.

  • En .NET Core 3.1 y versiones anteriores de Windows, estas abreviaturas diarias de semana constan de dos caracteres, por ejemplo, "Su".
  • En .NET 5 y versiones posteriores, estas abreviaturas de día de semana constan de solo un carácter, por ejemplo, "S".

Las API dependientes de la ICU

.NET introdujo API que dependen de ICU. Estas API solo se pueden realizar correctamente cuando se usa ICU. Estos son algunos ejemplos:

En las versiones de Windows enumeradas en la tabla de secciones ICU en Windows, las API mencionadas se realizan correctamente. Sin embargo, en versiones anteriores de Windows, se produce un error en estas API. 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 realizan 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 volver a usar NLS, puede optar por no participar en 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 una Microsoft.ICU.ICU4C.Runtime PackageReference al proyecto que corresponda al version configurado y eso es todo lo que se necesita.

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.