Globalisation et ICU .NET

Avant .NET 5, les API de globalisation .NET utilisaient différentes bibliothèques sous-jacentes sur différentes plateformes. Sur Unix, les API utilisaient International Components for Unicode (ICU) et sur Windows, elles utilisaient National Language Support (NLS). Cette situation a entraîné des différences de comportement dans quelques API de globalisation lors de l’exécution d’applications sur différentes plateformes. Les différences de comportement étaient évidentes dans ces domaines :

  • Cultures et données de culture
  • Casse de chaîne
  • Tri et recherche de chaînes
  • Tri des clés
  • Normalisation des chaînes
  • Prise en charge des noms de domaine internationalisés (IDN)
  • Nom d’affichage des fuseaux horaires sur Linux

À compter de .NET 5, les développeurs ont davantage de contrôle sur la bibliothèque sous-jacente utilisée, ce qui permet aux applications d’éviter les différences entre les plateformes.

ICU sur Windows

Windows intègre désormais une version de icu.dll préinstallée dans le cadre de ses fonctionnalités qui sont automatiquement utilisées pour les tâches de globalisation. Cette modification permet à .NET de tirer parti de cette bibliothèque ICU pour sa prise en charge de la globalisation. Dans les cas où la bibliothèque ICU n’est pas disponible ou ne peut pas être chargée, comme c’est le cas avec les versions antérieures de Windows, .NET version 5 et ultérieure revient à l’utilisation de l’implémentation basée sur NLS.

Le tableau suivant indique quelles versions de .NET sont capables de charger la bibliothèque ICU sur différentes versions du client et du serveur Windows :

Version de .NET Version de Windows
.NET 5 ou .NET 6 Client Windows 10 version 1903 ou ultérieure
.NET 5 ou .NET 6 Windows Server 2022 ou ultérieur
.NET 7 ou version ultérieure Client Windows 10 version 1703 ou ultérieure
.NET 7 ou version ultérieure Windows Server 2019 ou ultérieur

Remarque

.NET version 7 et ultérieure a la possibilité de charger ICU sur les versions antérieures de Windows, contrairement à .NET 6 et .NET 5.

Remarque

Même lors de l’utilisation d’ICU, les membres CurrentCulture, CurrentUICulture et CurrentRegion utilisent toujours les API du système d’exploitation Windows pour honorer les paramètres utilisateur.

Différences de comportement

Si vous mettez à niveau votre application pour cibler .NET version 5 ou ultérieure, vous pourriez voir des modifications dans votre application même si vous ne réalisez pas que vous utilisez des fonctionnalités de globalisation. Cette section répertorie l’un des changements de comportement que vous pourriez voir, mais il en existe également d’autres.

String.IndexOf

Examinez le code suivant qui appelle String.IndexOf(String) pour rechercher l’index du caractère Null \0 dans une chaîne.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • Dans .NET Core 3.1 et les versions antérieures sur Windows, l’extrait de code affiche 3 sur chacune des trois lignes.
  • Pour .NET version 5 et ultérieure s’exécutant sur les versions Windows répertoriées dans la table de la section ICU sur Windows, l’extrait de code imprime 0, 0 et 3 (pour la recherche ordinale).

Par défaut, String.IndexOf(String) effectue une recherche linguistique prenant en compte les cultures. ICU considère que le caractère Null \0 est un caractère de poids nul, et par conséquent, celui-ci n’est pas trouvé dans la chaîne lors de l’utilisation d’une recherche linguistique sur .NET 5 et les versions ultérieures. Toutefois, NLS ne considère pas le caractère Null \0 comme un caractère de poids nul, et une recherche linguistique sur .NET Core 3.1 et les versions antérieures localise le caractère à la position 3. Une recherche ordinale recherche le caractère à la position 3 sur toutes les versions de .NET.

Vous pouvez exécuter les règles d’analyse du code CA1307 : spécification de StringComparison pour plus de clarté et CA1309 : utilisation de StringComparison ordinale pour rechercher des sites d’appel dans votre code où la comparaison de chaînes n’est pas spécifiée ou n’est pas ordinale.

Pour plus d’informations, consultez Changements de comportement lors de la comparaison de chaînes sur .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'));

Important

Dans .NET 5+ s’exécutant sur des versions de Windows répertoriées dans la table ICU sur Windows, l’extrait de code précédent imprime :

True
True
True
False
False

Pour éviter ce comportement, utilisez la surcharge du paramètre char ou utilisez 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'));

Important

Dans .NET 5+ s’exécutant sur des versions de Windows répertoriées dans la table ICU sur Windows, l’extrait de code précédent imprime :

True
True
True
False
False

Pour éviter ce comportement, utilisez la surcharge du paramètre char ou utilisez StringComparison.Oridinal.

TimeZoneInfo.FindSystemTimeZoneById

L’ICU offre la possibilité de créer des instances TimeZoneInfo à l’aide d’ID de fuseau horaire IANA, même lorsque l’application s’exécute sur Windows. De même, vous pouvez créer des instances TimeZoneInfo avec des ID de fuseau horaire Windows, même en cas d’exécution sur des plateformes non Windows. Toutefois, il est important de noter que cette fonctionnalité n’est pas disponible lors de l’utilisation du mode NLS ou du mode invariant de globalisation.

API dépendantes de l’ICU

.NET a introduit des API qui dépendent de l’ICU. Ces API peuvent réussir uniquement lors de l’utilisation de l’ICU. Voici quelques exemples :

Dans les versions de Windows répertoriées dans la table de la section ICU sur Windows, les API mentionnées réussissent constamment. Toutefois, sur les versions antérieures de Windows, ces API échouent systématiquement. Dans ce cas, vous pouvez activer la fonctionnalité ICU app-local pour garantir la réussite de ces API. Sur les plateformes non Windows, ces API réussissent toujours quelle que soit la version.

En outre, il est essentiel que les applications s’assurent qu’elles ne s’exécutent pas en mode invariant de globalisation ou en mode NLS pour garantir la réussite de ces API.

Utiliser NLS au lieu d’ICU

L’utilisation d’ICU au lieu de NLS peut entraîner des différences de comportement avec certaines opérations liées à la globalisation. Pour revenir à l’utilisation de NLS, un développeur peut refuser l’implémentation d’ICU. Les applications peuvent activer le mode NLS de l’une des manières suivantes :

  • Dans le fichier projet :

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • Dans le fichier runtimeconfig.json :

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • En définissant la variable d’environnement DOTNET_SYSTEM_GLOBALIZATION_USENLS sur la valeur true ou 1.

Notes

Une valeur définie dans le projet ou dans le fichier runtimeconfig.json est prioritaire sur la variable d’environnement.

Pour plus d’informations, consultez les paramètres de configuration du runtime.

Déterminer si votre application utilise ICU

L’extrait de code suivant peut vous aider à déterminer si votre application s’exécute avec des bibliothèques ICU (et non 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;
}

Pour déterminer la version de .NET, utilisez RuntimeInformation.FrameworkDescription.

ICU local de l’application

Chaque version d’ICU peut inclure des correctifs ainsi que des données CLDR (Common Locale Data Repository) mises à jour qui décrivent les langues du monde. La transition entre des versions d’ICU peut impacter subtilement le comportement de l’application en ce qui concerne les opérations liées à la globalisation. Pour aider les développeurs d’application à garantir la cohérence entre tous les déploiements, .NET 5 et les versions ultérieures permettent aux applications sur Windows et Unix d’emporter et d’utiliser leur propre copie d’ICU.

Les applications peuvent activer un mode d’implémentation d’ICU local d’application de l’une des manières suivantes :

  • Dans le fichier de projet, définissez la valeur RuntimeHostConfigurationOption appropriée :

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • Ou dans le fichier runtimeconfig.json, définissez la valeur runtimeOptions.configProperties appropriée :

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • Ou en définissant la variable d’environnement DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU sur la valeur <suffix>:<version> ou <version>.

    <suffix> : suffixe facultatif d’une longueur inférieur à 36 caractères, qui suit les conventions d’empaquetage d’ICU publiques. Lorsque vous créez un ICU, vous pouvez le personnaliser pour produire les noms de bibliothèques et les noms de symboles exportés pour contenir un suffixe (par exemple, libicuucmyappmyapp est le suffixe).

    <version> : version d’ICU valide (par exemple, 67.1). Cette version est utilisée pour charger les fichiers binaires et obtenir les symboles exportés.

Lorsque l’une de ces options est définie, vous pouvez ajouter un Microsoft.ICU.ICU4C.RuntimePackageReference à votre projet qui correspond à la version configurée et c’est tout ce qui est nécessaire.

Sinon, pour charger ICU lorsque le commutateur local d’application est défini, .NET utilise la méthode NativeLibrary.TryLoad, qui sonde plusieurs chemins. La méthode tente d’abord de trouver la bibliothèque dans la propriété NATIVE_DLL_SEARCH_DIRECTORIES, qui est créée par l’hôte dotnet en fonction du fichier deps.json de l’application. Pour plus d’informations, consultez Sondage par défaut.

Pour les applications autonomes, aucune action spéciale n’est requise par l’utilisateur, à part s’assurer que l’ICU figure dans le répertoire de l’application (pour les applications autonomes, le répertoire de travail par défaut est défini sur NATIVE_DLL_SEARCH_DIRECTORIES).

Si vous consommez ICU via un package NuGet, cela fonctionne dans les applications dépendantes du framework. NuGet résout les ressources natives et les inclut dans le fichier deps.json et dans le répertoire de sortie de l’application sous le répertoire runtimes. .NET le charge à partir de là.

Pour les applications dépendantes du framework (non autonomes) où ICU est consommé à partir d’une build locale, vous devez effectuer des étapes supplémentaires. Le Kit de développement logiciel (SDK) .NET n’a pas encore de fonctionnalité pour les fichiers binaires natifs « libres » à incorporer dans deps.json (voir ce problème du SDK). Au lieu de cela, vous pouvez l’activer en ajoutant des informations supplémentaires dans le fichier projet de l’application. Par exemple :

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

Cette opération doit être effectuée pour tous les fichiers binaires d’ICU pour les runtimes pris en charge. En outre, les métadonnées NuGetPackageId du groupe d’éléments RuntimeTargetsCopyLocalItems doivent correspondre à un package NuGet référencé par le projet.

Comportement de macOS

macOS présente un comportement différent pour résoudre les bibliothèques dynamiques dépendantes à partir des commandes de chargement spécifiées dans le fichier Mach-O que le chargeur Linux. Dans le chargeur Linux, .NET peut essayer libicudata, libicuuc et libicui18n (dans cet ordre) pour satisfaire le graphique des dépendances d’ICU. Toutefois, sur macOS, cela ne fonctionne pas. Lors de la génération d’ICU sur macOS, vous obtenez par défaut une bibliothèque dynamique avec ces commandes de chargement dans libicuuc. L’extrait de code suivant montre un exemple.

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

Ces commandes référencent simplement le nom des bibliothèques dépendantes pour les autres composants d’ICU. Le chargeur effectue la recherche suivant les conventions dlopen, ce qui implique d’avoir ces bibliothèques dans les répertoires système ou de définir la variable d’environnement LD_LIBRARY_PATH, ou de disposer d’ICU dans le répertoire au niveau de l’application. Si vous ne pouvez pas définir LD_LIBRARY_PATH ou vérifier que les fichiers binaires ICU se trouvent dans le répertoire au niveau de l’application, vous devez effectuer un travail supplémentaire.

Il existe certaines directives pour le chargeur, comme @loader_path, qui indiquent au chargeur de rechercher cette dépendance dans le même répertoire que le fichier binaire avec cette commande de chargement. Il existe deux moyens de parvenir à cet objectif :

  • install_name_tool -change

    Exécutez les commandes suivantes :

    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
    
  • Réparer ICU pour produire les noms d’installation avec @loader_path

    Avant d’exécuter autoconf (./runConfigureICU), remplacez ces lignes par :

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

Une version d’ICU est disponible spécifiquement pour les charges de travail WebAssembly. Cette version assure la compatibilité de la globalisation avec les profils de bureau. Pour réduire la taille du fichier de données d’ICU de 24 Mo à 1,4 Mo (ou ~0,3 Mo s’il est compressé avec Brotli), cette charge de travail présente quelques limitations.

Les API suivantes ne sont pas prises en charge :

Les API suivantes sont prises en charge avec des limitations :

Par ailleurs, moins de paramètres régionaux sont pris en charge. La liste prise en charge se trouve dans le dépôt dotnet/icu.