Condividi tramite


Globalizzazione .NET e ICU

Prima di .NET 5, le API di globalizzazione .NET usavano librerie sottostanti diverse su piattaforme diverse. In Unix, le API usavano componenti internazionali per Unicode (ICU)e in Windows usavano il supporto per il linguaggio nazionale (NLS). Ciò ha comportato alcune differenze comportamentali in una manciata di API di globalizzazione durante l'esecuzione di applicazioni su piattaforme diverse. Le differenze di comportamento sono state evidenti in queste aree:

  • Impostazioni cultura e dati relativi alle impostazioni cultura
  • Maiuscole/minuscole di stringa
  • Ordinamento e ricerca di stringhe
  • Chiavi ordinamento
  • Normalizzazione delle stringhe
  • Supporto di IDN (Internationalized Domain Names)
  • Nome visualizzato del fuso orario in Linux

A partire da .NET 5, gli sviluppatori hanno maggiore controllo sulla libreria sottostante usata, consentendo alle applicazioni di evitare differenze tra le piattaforme.

Nota

I dati delle impostazioni cultura che determinano il comportamento della libreria ICU sono in genere gestiti dal Common Locale Data Repository (CLDR), non dal runtime.

ICU in Windows

Windows ora incorpora una versione di icu.dll preinstallata come parte delle funzionalità usate automaticamente per le attività di globalizzazione. Questa modifica consente a .NET di usare questa libreria di ICU per il supporto della globalizzazione. Nei casi in cui la libreria di ICU non è disponibile o non può essere caricata, come avviene con le versioni precedenti di Windows, .NET 5 e versioni successive ripristinano l'uso dell'implementazione basata su NLS.

La tabella seguente illustra le versioni di .NET in grado di caricare la libreria di ICU in versioni client e server Windows diverse:

Versione di .NET Versione Windows
.NET 5 o .NET 6 Client Windows 10 versione 1903 o successiva
.NET 5 o .NET 6 Windows Server 2022 o versioni successive
.NET 7 o versione successiva Client Windows 10 versione 1703 o successiva
.NET 7 o versione successiva Windows Server 2019 o versione successiva

Nota

.NET 7 e versioni successive hanno la possibilità di caricare l'ICU nelle versioni precedenti di Windows, a differenza di .NET 6 e .NET 5.

Nota

Anche quando si usa l'ICU, i membri CurrentCulture, CurrentUICulture e CurrentRegion usano ancora le API del sistema operativo Windows per rispettare le impostazioni utente.

Differenze di comportamento

Se si aggiorna l'app a .NET 5 o versione successiva, è possibile che vengano visualizzate modifiche nell'app anche se non ci si rende conto che si usano le funzionalità di globalizzazione. La sezione seguente elenca alcune modifiche comportamentali che potrebbero verificarsi.

Ordinamento di stringhe e System.Globalization.CompareOptions

CompareOptions è l'enumerazione delle opzioni che può essere passata a String.Compare per influenzare il modo in cui vengono confrontate due stringhe.

Il confronto delle stringhe per l'uguaglianza e la determinazione del relativo ordinamento differiscono tra NLS e ICU. In particolare:

  • L'ordinamento predefinito delle stringhe è diverso, per cui ciò sarà evidente anche se non si usa direttamente CompareOptions. Quando si usa ICU, l'opzione predefinita None funziona come StringSort. StringSort ordina i caratteri non alfanumerici prima di quelli alfanumerici (ad esempio, "bill's" appare nell'ordine prima di "bills"). Per ripristinare la funzionalità None precedente, è necessario usare l'implementazione basata su NLS.
  • La gestione predefinita dei caratteri di legatura è diversa. In NLS, le legature e le loro controparti non legature (ad esempio, "oeuf" e "œuf") sono considerate uguali, ma ciò non avviene con ICU in .NET. Ciò è dovuto a un peso della regola di confronto diverso tra le due implementazioni. Per ripristinare il comportamento NLS quando si usa ICU, usare il valore CompareOptions.IgnoreNonSpace.

String.IndexOf

Si consideri il codice seguente che chiama String.IndexOf(String) per trovare l'indice del carattere \0 Null in una stringa.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • In .NET Core 3.1 e versioni precedenti in Windows il frammento di codice stampa 3 su ognuna delle tre righe.
  • Per .NET 5 e le versioni successive in esecuzione nelle versioni di Windows elencate nella tabella della sezione ICU in Windows, il frammento di codice stampa 0, 0 e 3 (per la ricerca ordinale).

Per impostazione predefinita, String.IndexOf(String) esegue una ricerca linguistica compatibile con le impostazioni cultura. L'ICU considera il carattere \0 Null come carattere di peso zero e quindi il carattere non viene trovato nella stringa quando si usa una ricerca linguistica in .NET 5 e versioni successive. Tuttavia, NLS non considera il carattere \0 Null come carattere zero e una ricerca linguistica in .NET Core 3.1 e versioni precedenti individua il carattere nella posizione 3. Una ricerca ordinale trova il carattere nella posizione 3 in tutte le versioni .NET.

È possibile eseguire regole di analisi del codice CA1307: Specificare StringComparison per maggiore chiarezza e CA1309: Usare lo StringComparison ordinale per trovare i siti di chiamata nel codice in cui il confronto di stringhe non è specificato o non è ordinale.

Per altre informazioni, vedere Modifiche al comportamento durante il confronto di stringhe in .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

In .NET 5+ in esecuzione nelle versioni di Windows elencate nella tabella ICU in Windows, il frammento di codice precedente stampa:

True
True
True
False
False

Per evitare questo comportamento, usare l'overload del parametro char 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

In .NET 5+ in esecuzione nelle versioni di Windows elencate nella tabella ICU in Windows, il frammento di codice precedente stampa:

True
True
True
False
False

Per evitare questo comportamento, usare l'overload del parametro char o StringComparison.Ordinal.

TimeZoneInfo.FindSystemTimeZoneById

L'ICU offre la flessibilità necessaria per creare istanze di TimeZoneInfo usando ID di fuso orario IANA, anche quando l'applicazione è in esecuzione in Windows. Analogamente, è possibile creare istanze TimeZoneInfo con ID fuso orario di Windows, anche se in esecuzione su piattaforme non Windows. Tuttavia, è importante notare che questa funzionalità non è disponibile quando si usa la modalità NLS o globalizzazione in modalità invariante.

Abbreviazioni dei giorni della settimana

Il metodo DateTimeFormatInfo.GetShortestDayName(DayOfWeek) ottiene il nome del giorno abbreviato più corto per un giorno della settimana specificato.

  • In .NET Core 3.1 e versioni precedenti in Windows, queste abbreviazioni dei giorni della settimana sono formate da due caratteri, ad esempio "Do".
  • In .NET 5 e versioni successive, queste abbreviazioni dei giorni della settimana sono formate da un solo carattere, ad esempio "D".

API dipendenti dall'ICU

.NET ha introdotto API dipendenti dall'ICU. Queste API possono avere esito positivo solo quando si usa l'ICU. Di seguito sono riportati alcuni esempi.

Nelle versioni di Windows elencate nella tabella della sezione ICU su Windows le API menzionate avranno esito positivo. Tuttavia, nelle versioni precedenti di Windows queste API avranno esito negativo. In questi casi, è possibile abilitare la funzionalità di ICU locale dell'app per garantire il successo di queste API. Nelle piattaforme non Windows queste API hanno sempre esito positivo indipendentemente dalla versione.

Inoltre, è fondamentale per le app assicurarsi che non siano in esecuzione in modalità globalizzazione invariante o modalità NLS per garantire il successo di queste API.

Usare NLS invece di ICU

L'uso di ICU invece di NLS potrebbe comportare differenze comportamentali con alcune operazioni correlate alla globalizzazione. Per ripristinare l'uso di NLS, è possibile rifiutare esplicitamente l'implementazione dell'ICU. Le applicazioni possono abilitare la modalità NLS in uno dei modi seguenti:

  • Nel file di progetto:

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

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Impostando la variabile di ambiente DOTNET_SYSTEM_GLOBALIZATION_USENLS sul valore true o 1.

Nota

Un valore impostato nel progetto o nel file runtimeconfig.json ha la precedenza sulla variabile di ambiente.

Per altre informazioni, vedere Impostazioni di configurazione del runtime.

Determinare se l'app usa l'ICU

Il frammento di codice seguente consente di determinare se l'app è in esecuzione con librerie di ICU (e 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;
}

Per determinare la versione di .NET, usare RuntimeInformation.FrameworkDescription.

ICU locale dell'app

Ogni versione di ICU potrebbe includere correzioni di bug e dati CLDR (Common Locale Data Repository) aggiornati che descrivono le lingue del mondo. Lo spostamento tra versioni di ICU può influire negativamente sul comportamento delle app quando si tratta di operazioni correlate alla globalizzazione. Per aiutare gli sviluppatori di applicazioni a garantire la coerenza tra tutte le distribuzioni, .NET 5 e versioni successive consentono alle app in Windows e Unix di usare la propria copia di ICU.

Le applicazioni possono acconsentire esplicitamente a una modalità di implementazione di ICU locale dell'app in uno dei modi seguenti:

  • Nel file di progetto impostare il valore RuntimeHostConfigurationOption appropriato:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • In alternativa, nel file runtimeconfig.json impostare il valore runtimeOptions.configProperties appropriato:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • In alternativa, impostando la variabile di ambiente DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU sul valore <suffix>:<version> o <version>.

    <suffix>: suffisso facoltativo di lunghezza inferiore a 36 caratteri, seguendo le convenzioni pubbliche di creazione di pacchetti di ICU. Quando si compila un ICU personalizzato, è possibile personalizzarlo per produrre i nomi lib e i nomi dei simboli esportati in modo da contenere un suffisso, ad esempio libicuucmyapp, dove myapp è il suffisso.

    <version>: versione valida dell'ICU, ad esempio 67.1. Questa versione viene usata per caricare i file binari e per ottenere i simboli esportati.

Quando una di queste opzioni è impostata, è possibile aggiungere Microsoft.ICU.ICU4C.RuntimePackageReference al progetto che corrisponde a version configurato ed è tutto ciò che è necessario.

In alternativa, per caricare l'ICU quando è impostata l'opzione locale dell'app, .NET usa il metodo NativeLibrary.TryLoad, che esegue il probe di più percorsi. Il metodo tenta prima di tutto di trovare la libreria nella proprietà NATIVE_DLL_SEARCH_DIRECTORIES, creata dall'host dotnet in base al file deps.json per l'app. Per altre informazioni, vedere Probe predefinito.

Per le app autonome, non è richiesta alcuna azione speciale da parte dell'utente, oltre ad assicurarsi che l'ICU si trova nella directory dell'app (per le app autonome, per impostazione predefinita la directory di lavoro è NATIVE_DLL_SEARCH_DIRECTORIES).

Se si usa l'ICU tramite un pacchetto NuGet, questo funziona nelle applicazioni dipendenti dal framework. NuGet risolve gli asset nativi e li include nel file deps.json e nella directory di output per l'applicazione nella directory runtimes. .NET lo carica da questa posizione.

Per le app dipendenti dal framework (non autonome) in cui l'ICU viene usata da una compilazione locale, è necessario eseguire dei passaggi aggiuntivi. .NET SDK non dispone ancora di una funzionalità per i file binari nativi "separati" da incorporare in deps.json (vedere questo problema dell'SDK). È invece possibile abilitare questa operazione aggiungendo informazioni aggiuntive nel file di progetto dell'applicazione. Ad esempio:

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

Questa operazione deve essere eseguita per tutti i file binari di ICU per i runtime supportati. Inoltre, i metadati NuGetPackageId nel gruppo di elementi RuntimeTargetsCopyLocalItems devono corrispondere a un pacchetto NuGet a cui fa effettivamente riferimento il progetto.

Caricare una versione di ICU specifica in Linux

Per impostazione predefinita, quando si usa L'ICU in Linux, .NET tenta di caricare la versione più recente di ICU installata dal sistema. Tuttavia, è possibile specificare una versione specifica dell'ICU da caricare impostando la variabile di ambiente DOTNET_ICU_VERSION_OVERRIDE.

Ad esempio, se la variabile di ambiente è impostata su un numero di versione specifico, ad esempio 67.1, .NET tenta di caricare tale versione di ICU. Ad esempio, .NET cerca le librerie libicuuc.so.67.1 e libicui18n.so.67.1.

Nota

Questa variabile di ambiente è supportata solo nelle build .NET fornite da Microsoft e non è supportata nelle build fornite dalle distribuzioni Linux. Per le versioni di .NET precedenti a .NET 10, la variabile di ambiente viene chiamata CLR_ICU_VERSION_OVERRIDE.

Se la versione specificata non viene trovata, .NET passa alla versione di ICU installata più alta sul sistema.

Questa configurazione offre flessibilità nel controllo dell'utilizzo delle versioni di ICU, garantendo la compatibilità con le versioni di ICU specifiche dell'applicazione o fornite dal sistema.

Comportamento di macOS

macOS ha un comportamento diverso per la risoluzione delle librerie dinamiche dipendenti dai comandi di caricamento specificati nel file Mach-O rispetto al caricatore Linux. Nel caricatore Linux .NET può provare libicudata, libicuuc e libicui18n (in questo ordine) per soddisfare il grafico delle dipendenze di ICU. Tuttavia, in macOS, questo non funziona. Quando si compila l'ICU in macOS, per impostazione predefinita, si ottiene una libreria dinamica con questi comandi di caricamento in libicuuc. Il frammento di codice seguente mostra un esempio.

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

Questi comandi fanno riferimento solo al nome delle librerie dipendenti per gli altri componenti dell'ICU. Il caricatore esegue la ricerca seguendo le convenzioni di dlopen, che implica la presenza di queste librerie nelle directory di sistema o l'impostazione delle variabili di ambiente LD_LIBRARY_PATH o l'ICU a livello di app. Se non è possibile impostare LD_LIBRARY_PATH o assicurarsi che i file binari di ICU si trovino nella directory a livello di app, è necessario eseguire alcune operazioni aggiuntive.

Esistono alcune direttive per il caricatore, ad esempio @loader_path, che indicano al caricatore di cercare tale dipendenza nella stessa directory del file binario con tale comando di caricamento. A questo scopo è possibile procedere in due modi:

  • install_name_tool -change

    Eseguire i comandi seguenti:

    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 per produrre i nomi di installazione con @loader_path

    Prima di eseguire autoconf (./runConfigureICU), modificare queste righe in:

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

È disponibile una versione di ICU specifica per i carichi di lavoro WebAssembly. Questa versione offre la compatibilità della globalizzazione con i profili desktop. Per ridurre le dimensioni del file di dati di ICU da 24 MB a 1,4 MB (o ~0,3 MB se compresso con Brotli), questo carico di lavoro presenta alcune limitazioni.

Le API seguenti non sono supportate:

Le API seguenti sono supportate con limitazioni:

Sono inoltre supportate meno impostazioni locali. L'elenco supportato è disponibile nel repository dotnet/icu.

Configurazione della globalizzazione nelle app .NET

L'inizializzazione della globalizzazione .NET è un processo complesso che comporta il caricamento della libreria di globalizzazione appropriata, la configurazione dei dati culturali e la configurazione delle impostazioni di globalizzazione. Le sezioni seguenti descrivono il funzionamento dell'inizializzazione della globalizzazione su piattaforme diverse.

Windows

Su Windows, .NET segui i seguenti passaggi per inizializzare la globalizzazione del sistema:

  • Controllare se modalità invariante globalizzazione è abilitata. Quando questa modalità è attiva, .NET ignora il caricamento della libreria di ICU ed evita l'uso delle API NLS. Si basa invece sui dati culturali invarianti predefiniti, garantendo che il comportamento rimanga completamente indipendente dal sistema operativo e dalla libreria ICU.

  • Controllare se la modalità NLS è abilitata. Se abilitato, .NET ignorerà il caricamento della libreria di ICU e si affiderà invece alle API NLS per il supporto della globalizzazione.

  • Controllare se la funzionalità locale dell'app ICU è abilitata. In caso affermativo, .NET tenterà di caricare la libreria di ICU dalla directory dell'applicazione aggiungendo la versione specificata ai nomi della libreria. Ad esempio, se la versione è 72.1, .NET tenterà prima di tutto di caricare icuuc72.dll, icuin72.dlle icudt72.dll. Se queste librerie non possono essere caricate, tenterà di caricare icuuc72.1.dll, icuin72.1.dlle icudt72.1.dll. Se nessuna delle librerie viene trovata, il processo terminerà con un messaggio di errore, ad esempio: Failed to load app-local ICU: {library name}.

  • Se nessuna delle condizioni precedenti viene soddisfatta, .NET tenta di caricare la libreria di ICU dalla directory di sistema. Tenta prima di tutto di caricare icu.dll. Se questa libreria non è disponibile, tenta di caricare icuuc.dll e icuin.dll dalla directory di sistema. Se una di queste librerie non viene trovata, il runtime ritorna all'uso delle API NLS per il supporto della globalizzazione.

Nota

Le API NLS sono sempre disponibili in tutte le versioni di Windows, quindi .NET può sempre ricorrere a esse per il supporto della globalizzazione.

Linux

  • Controllare se modalità invariante globalizzazione è abilitata. Quando questa modalità è attiva, .NET ignora il caricamento della libreria di ICU. Si basa invece sui dati culturali invarianti predefiniti, garantendo che il comportamento rimanga completamente indipendente dal sistema operativo e dalla libreria ICU.
  • Controllare se la funzionalità locale dell'app ICU è abilitata. In caso affermativo, .NET tenterà di caricare la libreria di ICU dalla directory dell'applicazione aggiungendo la versione specificata ai nomi della libreria. Ad esempio, se la versione è 68.2.0.9, .NET tenterà di caricare libicuuc.so.68.2.0.9 e libicui18n.so.68.2.0.9. Se nessuna delle librerie viene trovata, il processo terminerà con un messaggio di errore, ad esempio: Failed to load app-local ICU: {library name}.
  • Controllare se è impostata la variabile di ambiente DOTNET_ICU_VERSION_OVERRIDE. In caso affermativo, .NET tenterà di caricare la versione specificata di ICU come descritto in Caricare una versione di ICU specifica in Linux.
  • Se nessuna delle condizioni precedenti viene soddisfatta, .NET tenta di caricare la versione più recente installata della libreria di ICU dal sistema. Tenta di caricare le librerie libicuuc.so.[version] e libicui18n.so.[version], dove [version] è la versione più recente installata di ICU nel sistema. Se le librerie non vengono trovate, il processo termina con un messaggio di errore, ad esempio: Failed to load system ICU: {library name}.

macOS

  • Controllare se modalità invariante globalizzazione è abilitata. Quando questa modalità è attiva, .NET ignora il caricamento della libreria di ICU. Si basa invece sui dati culturali invarianti predefiniti, garantendo che il comportamento rimanga completamente indipendente dal sistema operativo e dalla libreria ICU.
  • Controllare se la funzionalità locale dell'app ICU è abilitata. In caso affermativo, .NET tenterà di caricare la libreria di ICU dalla directory dell'applicazione aggiungendo la versione specificata ai nomi della libreria. Ad esempio, se la versione è 68.2.0.9, .NET tenterà di caricare libicuuc68.2.0.9.dylib e libicui18n68.2.0.9.dylib. Se nessuna delle librerie viene trovata, il processo termina con un messaggio di errore, ad esempio: Failed to load app-local ICU: {library name}.
  • Se nessuna delle condizioni precedenti viene soddisfatta, .NET tenterà di caricare la versione installata della libreria di ICU, come descritto in comportamento di macOS.