Globalizace .NET a ICU

Před .NET 5 používala rozhraní API globalizace .NET různé podkladové knihovny na různých platformách. V unixu používala rozhraní API mezinárodní komponenty pro kódování Unicode (ICU) a ve Windows používala podporu národních jazyků (NLS). Výsledkem jsou některé rozdíly v chování několika rozhraní API globalizace při spouštění aplikací na různých platformách. Rozdíly v chování byly zřejmé v těchto oblastech:

  • Jazykové verze a data jazykové verze
  • Velikost písmen řetězců
  • Řazení a vyhledávání řetězců
  • Řazení klíčů
  • Normalizace řetězců
  • Podpora internationalizovaných názvů domén (IDN)
  • Zobrazovaný název časového pásma v Linuxu

Počínaje platformou .NET 5 mají vývojáři větší kontrolu nad tím, jakou podkladovou knihovnu používáte, což umožňuje aplikacím vyhnout se rozdílům na různých platformách.

Poznámka:

Data jazykové verze, která řídí chování knihovny ICU, se obvykle spravují úložištěm CLDR (Common Locale Data Repository), nikoli modulem runtime.

ICU ve Windows

Systém Windows teď obsahuje předinstalovanou verzi icu.dll jako součást svých funkcí, které se automaticky používají pro úlohy globalizace. Tato úprava umožňuje rozhraní .NET používat tuto knihovnu ICU pro podporu globalizace. V případech, kdy je knihovna ICU nedostupná nebo nejde načíst, stejně jako u starších verzí Windows, .NET 5 a dalších verzí se vrátí k použití implementace založené na službě NLS.

Následující tabulka uvádí, které verze rozhraní .NET dokážou načíst knihovnu ICU napříč různými verzemi klienta a serveru Windows:

Verze .NET Verze Windows
.NET 5 nebo .NET 6 Klient Windows 10 verze 1903 nebo novější
.NET 5 nebo .NET 6 Windows Server 2022 nebo novější
.NET 7 nebo novější Klient Windows 10 verze 1703 nebo novější
.NET 7 nebo novější Windows Server 2019 nebo novější

Poznámka:

.NET 7 a novější verze mají možnost načíst ICU ve starších verzích Windows na rozdíl od .NET 6 a .NET 5.

Poznámka:

I když používáte ICU, CurrentCultureCurrentUICulturea CurrentRegion členové stále používají rozhraní API operačního systému Windows k zachování uživatelských nastavení.

Rozdíly v chování

Pokud aplikaci upgradujete tak, aby cílila na .NET 5 nebo novější, můžou se v aplikaci zobrazit změny, i když si neuvědomíte, že používáte zařízení globalizace. V následující části najdete některé změny chování, ke které může dojít.

Řazení řetězců a System.Globalization.CompareOptions

CompareOptions je výčet možností, které lze předat, aby String.Compare ovlivnily porovnání dvou řetězců.

Porovnání řetězců pro rovnost a určení jejich pořadí řazení se liší mezi NLS a ICU. Zejména jde o toto:

  • Výchozí pořadí řazení řetězců se liší, takže to bude zřejmé i v případě, že ho nepoužíváte CompareOptions přímo. Při použití ICU se None výchozí možnost provede stejně jako StringSort. StringSort seřadí jiné než alfanumerické znaky před alfanumerickými znaky (například "faktura" seřadí před "faktury"). Pokud chcete obnovit předchozí None funkce, musíte použít implementaci založenou na vyrovnávání zatížení sítě.
  • Výchozí zpracování znaků ligatury se liší. V rámci NLS se ligatury a jejich protějšky bez ligatur (například "oeuf" a "ğuf") považují za stejné, ale nejedná se o případ ICU v .NET. Důvodem je jiná síla kolace mezi dvěma implementacemi. Pokud chcete obnovit chování nlS při použití ICU, použijte CompareOptions.IgnoreNonSpace hodnotu.

String.IndexOf

Představte si následující kód, který volá String.IndexOf(String) vyhledání indexu znaku \0 null v řetězci.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • V .NET Core 3.1 a starších verzích ve Windows se fragment kódu vytiskne 3 na každý ze tří řádků.
  • V případě .NET 5 a novějších verzí spuštěných ve verzích Windows uvedených v tabulce oddílů ICU v systému Windows se fragment kódu vytiskne 00a 3 (pro řadové vyhledávání).

Ve výchozím nastavení String.IndexOf(String) provede lingvistické vyhledávání pracující s jazykovou verzí. ICU považuje znak null za znak \0nulové hmotnosti, a proto se v řetězci při použití lingvistického vyhledávání v .NET 5 a novějším nenajde. Vyrovnávání zatížení sítě však nepovažuje znak null za znak \0 nulové hmotnosti a lingvistické vyhledávání v .NET Core 3.1 a starší vyhledá znak na pozici 3. Řadové vyhledávání najde znak na pozici 3 ve všech verzích .NET.

Pravidla analýzy kódu CA1307: Zadejte StringComparison pro přehlednost a CA1309: Pomocí řadového StringComparisonu vyhledejte weby volání ve vašem kódu, kde není zadané porovnání řetězců nebo není pořadové.

Další informace najdete v tématu Změny chování při porovnávání řetězců v .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'));

Důležité

V .NET 5 a novějších spuštěných ve verzích Windows uvedených v tabulce ICU ve Windows se vytiskne předchozí fragment kódu:

True
True
True
False
False

Chcete-li se tomuto chování vyhnout, použijte přetížení parametru char nebo 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'));

Důležité

V .NET 5 a novějších spuštěných ve verzích Windows uvedených v tabulce ICU ve Windows se vytiskne předchozí fragment kódu:

True
True
True
False
False

Chcete-li se tomuto chování vyhnout, použijte přetížení parametru char nebo StringComparison.Ordinal.

TimeZoneInfo.FindSystemTimeZoneById

ICU poskytuje flexibilitu při vytváření TimeZoneInfo instancí pomocí ID časových pásem IANA , a to i v případě, že aplikace běží ve Windows. Podobně můžete vytvářet TimeZoneInfo instance s ID časových pásem Windows, i když běží na jiných platformách než Windows. Je však důležité si uvědomit, že tato funkce není dostupná při použití režimu nlS nebo globalizace invariantní režim.

Zkratky dnů v týdnu

Metoda DateTimeFormatInfo.GetShortestDayName(DayOfWeek) získá nejkratší zkrácený název dne pro zadaný den v týdnu.

  • V .NET Core 3.1 a starších verzích ve Windows se tyto zkratky v týdnu skládají ze dvou znaků, například "Su".
  • V .NET 5 a novějších verzích se tyto zkratky dnů v týdnu skládají jenom z jednoho znaku, například "S".

Rozhraní API závislá na ICU

Rozhraní .NET zavedla rozhraní API, která jsou závislá na ICU. Tato rozhraní API můžou být úspěšná pouze při použití ICU. Několik příkladů:

Ve verzích Windows uvedených v tabulce oddílů ICU ve Windows jsou uvedená rozhraní API úspěšná. Ve starších verzích Windows však tato rozhraní API selžou. V takových případech můžete povolit funkci ICU místní aplikace, abyste zajistili úspěch těchto rozhraní API. Na jiných platformách než Windows jsou tato rozhraní API vždy úspěšná bez ohledu na verzi.

Kromě toho je pro aplikace důležité zajistit, aby neběží v globalizačním režimu invariantního režimu nebo v režimu NLS, aby se zajistil úspěch těchto rozhraní API.

Místo ICU použijte službu NLS.

Použití ICU místo NLS může vést k rozdílům v chování některých operací souvisejících s globalizací. Pokud se chcete vrátit zpět k používání nlS, můžete vyjádřit výslovný nesouhlas s implementací ICU. Aplikace můžou povolit režim služby NLS některým z následujících způsobů:

  • V souboru projektu:

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

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Nastavením proměnné DOTNET_SYSTEM_GLOBALIZATION_USENLS prostředí na hodnotu true nebo 1.

Poznámka:

Hodnota nastavená v projektu nebo v runtimeconfig.json souboru má přednost před proměnnou prostředí.

Další informace naleznete v tématu Nastavení konfigurace modulu runtime.

Určení, jestli vaše aplikace používá ICU

Následující fragment kódu vám může pomoct určit, jestli je vaše aplikace spuštěná s knihovnami ICU (a ne 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;
}

K určení verze rozhraní .NET použijte RuntimeInformation.FrameworkDescription.

Místní ICU pro aplikace

Každá verze ICU může obsahovat opravy chyb a aktualizovaná data cLDR (Common Locale Data Repository), která popisují jazyky světa. Přechod mezi verzemi ICU může subtálně ovlivnit chování aplikace, pokud jde o operace související s globalizací. Aby vývojáři aplikací zajistili konzistenci napříč všemi nasazeními, umožňují .NET 5 a novějším verzím aplikacím ve Windows i Unixu přenášet a používat vlastní kopii ICU.

Aplikace se můžou přihlásit k režimu implementace ICU v místní aplikaci jedním z následujících způsobů:

  • V souboru projektu nastavte odpovídající RuntimeHostConfigurationOption hodnotu:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • Nebo v souboru runtimeconfig.json nastavte odpovídající runtimeOptions.configProperties hodnotu:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • Nebo nastavením proměnné DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU prostředí na hodnotu <suffix>:<version> nebo <version>.

    <suffix>: Volitelná přípona kratší než 36 znaků, která následuje po konvencích balení veřejných jednotek ICU. Při vytváření vlastní jednotky ICU ji můžete přizpůsobit tak, aby vznikly názvy libů a exportované názvy symbolů, které budou obsahovat příponu, libicuucmyappnapříklad , kde myapp je přípona.

    <version>: Platná verze ICU, například 67.1. Tato verze slouží k načtení binárních souborů a získání exportovaných symbolů.

Pokud je některý z těchto možností nastavený, můžete do projektu přidat Microsoft.ICU.ICU4C.RuntimePackageReference, který odpovídá nakonfigurované version a to vše, co je potřeba.

Pokud chcete načíst ICU, když je nastavený místní přepínač aplikace, použije .NET metodu NativeLibrary.TryLoad , která testuje více cest. Metoda se nejprve pokusí najít knihovnu NATIVE_DLL_SEARCH_DIRECTORIES ve vlastnosti, která je vytvořena hostitelem dotnet na deps.json základě souboru aplikace. Další informace najdete v tématu Výchozí zkušební verze.

V případě samostatných aplikací nevyžaduje uživatel žádnou zvláštní akci, kromě zajištění, že je ICU v adresáři aplikace (u samostatných aplikací je výchozí NATIVE_DLL_SEARCH_DIRECTORIESpracovní adresář).

Pokud icU využíváte prostřednictvím balíčku NuGet, funguje to v aplikacích závislých na architektuře. NuGet vyřeší nativní prostředky a zahrne je do deps.json souboru a do výstupního adresáře pro aplikaci v rámci runtimes adresáře. .NET ho odtud načte.

U aplikací závislých na architektuře (ne v samostatném prostředí), které spotřebovávají JEDNOTKY ICU z místního sestavení, musíte provést další kroky. Sada .NET SDK zatím nemá funkci pro "volné" nativní binární soubory, do deps.json kterých se mají začlenit (viz tento problém se sadou SDK). Místo toho to můžete povolit přidáním dalších informací do souboru projektu aplikace. Příklad:

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

To se musí provést pro všechny binární soubory ICU pro podporované moduly runtime. NuGetPackageId Metadata ve RuntimeTargetsCopyLocalItems skupině položek musí také odpovídat balíčku NuGet, na který projekt ve skutečnosti odkazuje.

Chování macOS

macOS má jiné chování při překladu závislých dynamických knihoven z příkazů pro načtení zadaných v Mach-O souboru, než je zavaděč Linuxu. V zavaděče Linuxu může .NET vyzkoušet libicudatalibicuuc, a libicui18n (v takovém pořadí) splňovat graf závislostí ICU. V systému macOS to ale nefunguje. Při vytváření ICU v systému macOS ve výchozím nastavení získáte dynamickou knihovnu s těmito příkazy pro načtení v libicuuc. Následující fragment kódu ukazuje příklad.

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

Tyto příkazy pouze odkazují na název závislých knihoven pro ostatní komponenty ICU. Zavaděč provádí vyhledávání podle dlopen konvencí, které zahrnují použití těchto knihoven v systémových adresářích nebo nastavení LD_LIBRARY_PATH hodnot env vars nebo s ICU v adresáři na úrovni aplikace. Pokud nemůžete nastavit LD_LIBRARY_PATH nebo zajistit, aby binární soubory ICU byly v adresáři na úrovni aplikace, budete muset udělat další práci.

Pro zavaděč existují určité direktivy, například @loader_path, které zavaděče říkají, že má vyhledat danou závislost ve stejném adresáři jako binární soubor s tímto příkazem pro načtení. Existují dva způsoby, jak toho dosáhnout:

  • install_name_tool -change

    Spusťte následující příkazy:

    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
    
  • Oprava ICU pro vytvoření názvů instalací pomocí @loader_path

    Před spuštěním příkazu autoconf (./runConfigureICU) změňte tyto řádky na:

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

K dispozici je verze ICU určená speciálně pro úlohy WebAssembly. Tato verze poskytuje kompatibilitu globalizace s profily plochy. Pokud chcete zmenšit velikost datového souboru ICU z 24 MB na 1,4 MB (nebo ~0,3 MB v případě komprimace pomocí Brotli), má tato úloha několik omezení.

Následující rozhraní API nejsou podporována:

Následující rozhraní API jsou podporována s omezeními:

Kromě toho se podporuje méně národních prostředí. Podporovaný seznam najdete v úložišti dotnet/icu.