Поделиться через


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

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

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

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

Примечание.

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

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 или более поздней версии, вы можете увидеть изменения в приложении, даже если вы не понимаете, что используете средства глобализации. В следующем разделе перечислены некоторые изменения поведения, которые могут возникнуть.

Сортировка строк и System.Globalization.CompareOptions

CompareOptions — перечисление параметров, которое можно передать, чтобы повлиять String.Compare на сравнение двух строк.

Сравнение строк для равенства и определение порядка сортировки отличается от NLS и ICU. В частности:

  • Порядок сортировки строк по умолчанию отличается, поэтому это будет очевидно, даже если вы не используете CompareOptions напрямую. При использовании ICU параметр по умолчанию выполняет то же самое, что StringSortи при использовании ICUNone. StringSort сортирует не буквенно-цифровые символы перед буквенно-цифровыми символами (например, "счета" сортируются до "счетов"). Чтобы восстановить предыдущие None функциональные возможности, необходимо использовать реализацию на основе NLS.
  • Обработка символов лигатуры по умолчанию отличается. В NLS лигатуры и их нелигатурные аналоги (например, "oeuf" и "uf") считаются равными, но это не так с ICU в .NET. Это связано с другой силой сортировки между двумя реализациями. Чтобы восстановить поведение NLS при использовании ICU, используйте CompareOptions.IgnoreNonSpace значение.

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, фрагмент кода выводит 00и 3 (для порядкового поиска).

По умолчанию выполняет лингвистическое поиск с 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.Ordinal.

TimeZoneInfo.FindSystemTimeZoneById

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

Сокращение дня недели

Метод DateTimeFormatInfo.GetShortestDayName(DayOfWeek) получает короткое сокращенное имя дня для указанного дня недели.

  • В .NET Core 3.1 и более ранних версиях в Windows эти сокращения дня недели состоят из двух символов, например Su.
  • В .NET 5 и более поздних версиях эти сокращения дня недели состоят только из одного символа, например "S".

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.Runtime PackageReference в проект, соответствующий настроенной конфигурации 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.