Глобализация .NET и ICU

До .NET 5 API глобализации .NET использовали различные базовые библиотеки на разных платформах. В Unix API-интерфейсы использовали международные компоненты для Юникода (ICU), а в Windows — многоязыковую поддержку (NLS). Это привело к некоторым различиям в поведении API-интерфейсов глобализации при запуске приложений на разных платформах. Различия в работе были очевидны в следующих областях:

  • Региональные параметры и данные языка и региональных параметров
  • Регистр строк
  • Сортировка и поиск строк
  • Сортировка ключей
  • Нормализация строк
  • Поддержка международных доменных имен (IDN)
  • Отображаемое имя часового пояса в Linux

Начиная с .NET 5 разработчики имеют больше контроля над использованием базовой библиотеки, что позволяет приложениям избежать различий между платформами.

ICU в Windows

Windows теперь включает предварительно установленную версию icu.dll как часть ее функций, которые автоматически используются для задач глобализации. Это изменение позволяет .NET использовать эту библиотеку ICU для поддержки глобализации. В случаях, когда библиотека ICU недоступна или не может быть загружена, как и в случае с более старыми версиями Windows, .NET 5 и последующими версиями, отменить изменения использовать реализацию на основе NLS.

В следующей таблице показано, какие версии .NET могут загружать библиотеку ICU в разных версиях клиента и сервера Windows:

Версия .NET Версия Windows
.NET 5 или .NET 6 Клиент Windows 10 версии 1903 или более поздней версии
.NET 5 или .NET 6 Windows Server 2022 или более поздней версии
.NET 7 или более поздней версии Клиент Windows 10 версии 1703 или более поздней версии
.NET 7 или более поздней версии Windows Server 2019 или более поздней версии;

Примечание.

В .NET 7 и более поздних версиях есть возможность загружать ICU в более ранних версиях Windows, в отличие от .NET 6 и .NET 5.

Примечание.

Даже при использовании ICU элементы CurrentCulture, CurrentUICulture и CurrentRegion по-прежнему используют API операционной системы Windows, чтобы применить параметры пользователя.

Различия в поведении

Если вы обновляете приложение до целевого объекта .NET 5 или более поздней версии, вы можете увидеть изменения в приложении, даже если вы не понимаете, что используете средства глобализации. В этом разделе описано одно из возможных изменений поведения.

String.IndexOf

Рассмотрим следующий код, который вызывает String.IndexOf(String) поиск индекса символа NULL \0 в строке.

const string greeting = "Hel\0lo";
Console.WriteLine($"{greeting.IndexOf("\0")}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.CurrentCulture)}");
Console.WriteLine($"{greeting.IndexOf("\0", StringComparison.Ordinal)}");
  • В .NET Core 3.1 и более ранних версиях в Windows фрагмент кода печатается 3 на каждой из трех строк.
  • Для .NET 5 и последующих версий, работающих в версиях Windows, перечисленных в таблице разделов ICU в Windows, фрагмент кода печатается 0и 03 (для порядкового поиска).

По умолчанию выполняет лингвистическое поиск с String.IndexOf(String) учетом языка и региональных параметров. ICU считает символ null \0 символом нулевого веса, поэтому символ не найден в строке при использовании лингвистического поиска в .NET 5 и более поздних версиях. Однако NLS не считает символ null \0 символом нулевого веса, а лингвистическое поиск в .NET Core 3.1 и более ранних версиях находит символ в позиции 3. Порядковый поиск находит символ в позиции 3 во всех версиях .NET.

Правила анализа кода CA1307: укажите StringComparison для ясности и CA1309: используйте порядковый код StringComparison для поиска сайтов вызовов в коде, где сравнение строк не указано или не является порядковым.

Дополнительные сведения см. в статье Изменения поведения при сравнении строк в .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'));

Внимание

В .NET 5+ работает в версиях Windows, перечисленных в таблице ICU в Windows , предыдущий фрагмент кода выводит:

True
True
True
False
False

Чтобы избежать этого поведения, используйте перегрузку char параметра или 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'));

Внимание

В .NET 5+ работает в версиях Windows, перечисленных в таблице ICU в Windows , предыдущий фрагмент кода выводит:

True
True
True
False
False

Чтобы избежать этого поведения, используйте перегрузку char параметра или StringComparison.Oridinal.

TimeZoneInfo.FindSystemTimeZoneById

ICU обеспечивает гибкость для создания TimeZoneInfo экземпляров с помощью идентификаторов часовых поясов IANA , даже если приложение работает в Windows. Аналогичным образом можно создавать TimeZoneInfo экземпляры с идентификаторами часовых поясов Windows, даже если они работают на платформах, отличных от Windows. Однако важно отметить, что эта функция недоступна при использовании режима NLS или инвариантного режима глобализации.

API-интерфейсы, зависимые от ICU

В .NET появились API- интерфейсы, зависящие от ICU. Эти API могут успешно выполняться только при использовании ICU. Далее приводятся некоторые примеры.

В версиях Windows, перечисленных в таблице разделов ICU в Windows, интерфейсы API упоминание будут последовательно успешными. Однако в более старых версиях Windows эти API будут последовательно завершатся ошибкой. В таких случаях можно включить функцию локального ICU приложения, чтобы обеспечить успешность этих API. На платформах, отличных от Windows, эти API всегда будут успешными независимо от версии.

Кроме того, важно обеспечить, чтобы приложения не работали в режиме глобализации инвариантного режима или режима NLS , чтобы гарантировать успешность этих API.

Использование NLS вместо ICU

Использование ICU вместо NLS может привести к различиям в работе некоторых операций, связанных с глобализацией. Чтобы вернуться к использованию NLS, разработчик может отказаться от реализации ICU. Приложения могут включать режим NLS одним из следующих способов:

  • В файле проекта:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
    </ItemGroup>
    
  • В файле runtimeconfig.json:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.UseNls": true
          }
      }
    }
    
  • Путем присвоения переменной среды DOTNET_SYSTEM_GLOBALIZATION_USENLS значения true или 1.

Примечание.

Значение, заданное в проекте или файле runtimeconfig.json, имеет приоритет над переменной среды.

Дополнительные сведения см. в статье Настройки конфигурации среды выполнения.

Определение того, использует ли приложение ICU

Следующий фрагмент кода поможет определить, работает ли приложение с библиотеками ICU (а не 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;
}

Чтобы определить версию .NET, используйте RuntimeInformation.FrameworkDescription.

ICU с параметром app-local

Каждый выпуск ICU включает исправления ошибок, а также обновленные данные репозитория данных общего языкового стандарта (CLDR), которые описывают языки мира. Использование разных версий ICU может незначительно повлиять на работу приложения, когда дело доходит до операций, связанных с глобализацией. Чтобы помочь разработчикам приложений обеспечить согласованность во всех развертываниях, .NET 5 и более поздних версий позволяют приложениям в Windows и Unix переносить и использовать собственную копию ICU.

Приложения могут принять участие в режиме реализации локального ICU приложения одним из следующих способов:

  • В файле проекта задайте соответствующее RuntimeHostConfigurationOption значение:

    <ItemGroup>
      <RuntimeHostConfigurationOption Include="System.Globalization.AppLocalIcu" Value="<suffix>:<version> or <version>" />
    </ItemGroup>
    
  • Или в файле runtimeconfig.json задайте соответствующее runtimeOptions.configProperties значение:

    {
      "runtimeOptions": {
         "configProperties": {
           "System.Globalization.AppLocalIcu": "<suffix>:<version> or <version>"
         }
      }
    }
    
  • Или, задав переменную DOTNET_SYSTEM_GLOBALIZATION_APPLOCALICU среды значением <suffix>:<version> или <version>.

    <suffix>: необязательный суффикс длиной менее 36 символов, следуя соглашениям об упаковке общедоступных ICU. При создании настраиваемых ICU можно указать, чтобы имена библиотек и имена экспортированных символов содержали суффикс (например, libicuucmyapp, где myapp является суффиксом).

    <version>: допустимая версия ICU, например 67.1. Эта версия используется для загрузки двоичных файлов и получения экспортированных символов.

Если задан любой из этих параметров, вы можете добавить microsoft.ICU.ICU4C.RuntimePackageReference в проект, соответствующий настроенной конфигурации version , и это все, что необходимо.

Кроме того, чтобы загрузить ICU при установке локального коммутатора приложения, .NET использует NativeLibrary.TryLoad метод, который проверяет несколько путей. Сначала метод пытается найти библиотеку в свойстве NATIVE_DLL_SEARCH_DIRECTORIES, которое создается узлом .NET на основе файла deps.json для приложения. Дополнительные сведения см. в разделе Проверка по умолчанию.

В случае с автономными приложениями пользователю не требуется выполнять никаких особых действий, кроме помещения ICU в каталог приложения (для автономных приложений по умолчанию используется рабочий каталог NATIVE_DLL_SEARCH_DIRECTORIES).

Если вы используете ICU через пакет NuGet, это сработает в приложениях, зависящих от платформы. NuGet разрешает собственные активы и включает их в файл deps.json и в выходной каталог для приложения в каталоге runtimes. .NET загружает его отсюда.

Для приложений, зависимых от платформы (не автономных), где ICU используется из локальной сборки, необходимо выполнить дополнительные действия. В пакете SDK для .NET пока еще нет функции для включения "свободных" собственных двоичных файлов в deps.json (см. сведения об этойпроблеме с пакетом SDK). Это можно сделать, добавив дополнительные сведения в файл проекта приложения. Например:

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

Это необходимо сделать для всех двоичных файлов ICU для поддерживаемых сред выполнения. Кроме того, метаданные NuGetPackageId в группе элементов RuntimeTargetsCopyLocalItems должны соответствовать пакету NuGet, на который фактически ссылается проект.

Поведение в macOS

MacOS имеет другое поведение для разрешения зависимых динамических библиотек из команд загрузки, указанных в Mach-O файле, чем загрузчик Linux. В загрузчике Linux .NET пробует использовать libicudata, libicuuc и libicui18n (в этом порядке) для обеспечения соответствия графу зависимостей ICU. Однако в macOS это не работает. При создании ICU в macOS вы по умолчанию получаете динамическую библиотеку с помощью этих команд загрузки в libicuuc. Фрагмент кода приведен ниже.

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

Эти команды просто ссылаются на имена зависимых библиотек для других компонентов ICU. Загрузчик выполняет поиск, следуя соглашениям dlopen, которые подразумевают использование этих библиотек в системных каталогах или задание переменных среды LD_LIBRARY_PATH или наличие ICU в каталоге уровня приложения. Если не удается задать LD_LIBRARY_PATH или убедиться, что двоичные файлы ICU находятся в каталоге уровня приложения, необходимо выполнить некоторую дополнительную работу.

Существует несколько директив для загрузчика, таких как @loader_path, которые указывают загрузчику выполнять поиск этой зависимости в том же каталоге, в котором находится двоичный файл с этой командой загрузки. Это достигается двумя способами.

  • install_name_tool -change

    Выполните следующие команды:

    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
    
  • Исправление ICU таким образом, чтобы создавались имена установки с помощью @loader_path

    Перед выполнением автонастройки (./runConfigureICU) измените эти строки на следующие:

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

Доступна версия ICU, специально предназначенная для рабочих нагрузок WebAssembly. Она обеспечивает совместимость глобализации с профилями рабочих столов. Чтобы уменьшить размер файла данных ICU с 24 МБ до 1,4 МБ (или около 0,3 МБ при сжатии с помощью Brotli), к этой рабочей нагрузке применяется ряд ограничений.

Не поддерживаются следующие API:

Следующие API поддерживаются с ограничениями:

Кроме того, поддерживаются меньше языковых стандартов. Поддерживаемый список можно найти в репозитории dotnet/icu.