Правильная работа с локалью в C#/.NET

Правильная работа с локалью в C#/.NET #

В этой статье описано, как сделать код устойчивым к смене локали и как проверить это в тестах.

Суть проблемы #

В C# / .NET почти все методы форматирования строк зависимы от текущей локали:

  • метод double.ToString() форматирует с учётом текущей локали
  • метод string.Format(string format, ...) форматирует с учётом текущей локали
  • и даже интерполяция строк работает с учётом текущей локали

Это означает, что форматирование чисел в строку зависит от языка и страны, установленных в операционной системе (или в контейнере):

ЛокальКак форматируется число
en-US3.14
fr-FR3,14
ru-RU3,14

Использование запятой для разделения целой и дробной части характерно для всех европейских локалей, включая русскую.

Как это проявляется #

Допустим, разработчик запускал свою программу или её тесты на системе, где стоит локаль “en-US”, и все ожидания настроены на форматирование по правилам английского языка и США: например, в проверках ожидается “3.14159” вместо “3,14159”.

Другой разработчик запустит программу или тесты на системе, где стоит локаль “ru-RU”. В результате поведение программы изменится, а тесты упадут.

Пример ошибки для XUnit (тест ожидает 6.28, а в системе установлена локаль 6.28):

   /home/user/my-compiler/tests/Interpreter.IntegrationTests/ExpressionsTest.cs(18): error TESTERROR: 
      Interpreter.IntegrationTests.ExpressionsTest.Can_evaluate_expressions(code: "fn main(): int { printf(3.14 * 2, 2); return 0; }", expected: "6.28") (< 1ms): Сообщение об ошибке: Assert.Equal() Failure: Strings differ
                  ↓ (pos 1)
      Expected: "6.28"
      Actual:   "6,28"
                  ↑ (pos 1)
      Трассировка стека:
         at Interpreter.IntegrationTests.ExpressionsTest.Can_evaluate_expressions(String code, String expected) in /home/dev/teaching/compilers2026/rrtry-PS-VM/tests/Interpreter.IntegrationTests/ExpressionsTest.cs:line 18
         at InvokeStub_ExpressionsTest.Can_evaluate_expressions(Object, Span`1)
         at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)

Как выявить проблему в тестах #

Плохой способ: глобальная смена локали #

Можно поменять локаль глобально:

  • поменять локаль в операционной системе
  • или установить переменную окружения DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 перед запуском dotnet test для проверки с CultureInfo.InvariantCulture

Однако, глобальная смена локали может лишь замаскировать проблему.

Хороший способ: атрибуты XUnit v3 #

В XUnit v3 появились атрибуты CulturedTheory и CulturedFact, позволяющие запустить тест с одной или несколькими локалями для проверки способности кода работать в любых условиях.

Если проект ещё использует XUnit v2, то можно отредактировать *.csproj для обновления на XUnit v3.

Допустим, у вас в проекте указано:


  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.2" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
    <PackageReference Include="xunit" Version="2.9.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
  </ItemGroup>

Замените пакет xunit на xunit.v3 и обновите версию до актуальной (не забудьте обновить версию xunit.runner.visualstudio):

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.2" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
    <PackageReference Include="xunit.v3" Version="3.2.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
  </ItemGroup>

Теперь можно дописать тесты, заменяя:

  • атрибут [Theory] на [CulturedTheory(["ru-RU", "en-US"])]
  • атрибут [Fact] на [CulturedFact(["ru-RU", "en-US"])]

Пример теста, который будет запущен дважды с двумя кульутрами:

    [CulturedTheory(["ru-RU", "en-US"])]
    [MemberData(nameof(GetTypeMismatch))]
    public void Can_interpret_variable_type_mismatch(string code, Type expected)
    {
        FakeEnvironment environment = new();
        Interpreter interpreter = new(environment);
        Assert.Throws(expected, () => interpreter.Execute(code));
    }

Как исправить код #

Для компилятора или интерпретатора обычно требуется независимое от локали поведение: например, литерал числа с плавающей точкой всегда записывается в стиле локали en-US — то есть “3.14159”.

В таких случаях используйте CultureInfo.InvariantCulture для всех методов форматирования и разбора строк.

Примеры:

  • value.ToString(CultureInfo.InvariantCulture)
  • double.Parse(s, CultureInfo.InvariantCulture)
  • _outputBuffer.Append(value.ToString(CultureInfo.InvariantCulture))

Перегрузки, принимающие объект CultureInfo, есть у всех методов форматирования и разбора, включая классы вроде StringBuilder.

Ссылки #

Дополнительная информация есть по ссылкам:

  1. What does CultureInfo.InvariantCulture mean?
  2. A Primer on CultureInfo and Internationalisation in C#/.NET
  3. Обновление XUnit, в котором появились CulturedTheory и CulturedFact: Core Framework v3 3.0.1