Готовим C# код к глобальному миру: интернационализация приложений

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему вашему C# приложению нужна интернационализация?

Представьте: вы написали крутой сервис для управления заметками. Всё работает, интерфейс лаконичный, пользователи довольны. Но только русскоязычные. Стоит вам выйти на рынок США или Германии — и проект встанет. Не потому, что код плохой, а потому что кнопка «Сохранить» не превращается в «Save», а сообщение «Ошибка ввода» пугает иностранца.

Это и есть проблема локализации. В мире .NET её решение называется интернационализация C# (или i18n — от слова «internationalization», где 18 — количество букв между i и n). Если вы используете C# и платформу .NET (Core, 5/6/7/8/9), у вас под капотом уже есть мощнейший инструментарий, чтобы сделать приложение мультиязычным. В этой статье мы разберём его на практике: от простых ресурсов до смены языка «на лету».

Основы: что такое культура и нейтральный язык

Прежде чем писать код, разберёмся с терминами. В .NET за локализацию отвечают два ключевых класса из пространства имён System.Globalization:

  • CultureInfo — объект, описывающий язык и региональные стандарты (например, ru-RU, en-US, de-DE).
  • CurrentUICulture — текущая культура пользовательского интерфейса. Именно на неё ориентируется система при поиске переводов.
  • CurrentCulture — формат дат, чисел, валюты. Его тоже можно менять, но для переводов важен первый.

В C# приложениях мы обычно используем ресурсные файлы (.resx). Это XML-файлы, где хранятся строки, изображения и другие данные, привязанные к конкретной культуре. Например:

  • Messages.resx — строки по умолчанию (обычно английский).
  • Messages.ru.resx — русский перевод.
  • Messages.de.resx — немецкий перевод.

Когда приложение запускается, .NET автоматически выбирает файл, соответствующий текущей UICulture. Если точного совпадения нет — использует нейтральный (базовый) файл.

Создаём первый ресурсный файл в Visual Studio

Допустим, у нас есть консольное приложение или ASP.NET Core проект. Добавим папку Resources и внутри неё файл Strings.resx. Заполним его:

Имя          | Значение
-------------|-------------------
WelcomeMsg   | Welcome to our app!
GoodbyeMsg   | Goodbye!

Теперь создадим Strings.ru.resx:

Имя          | Значение
-------------|-------------------
WelcomeMsg   | Добро пожаловать в наше приложение!
GoodbyeMsg   | До свидания!

Обратите внимание: имена ключей должны совпадать. После сохранения Visual Studio автоматически сгенерирует класс Strings с доступом к строкам через статические свойства.

Программное переключение языка

Чтобы интернационализация C# работала, нужно в начале выполнения приложения (например, в Main или Startup) установить нужную культуру. Вот как это выглядит:

using System.Globalization;
using System.Threading;

// Устанавливаем русский язык интерфейса
CultureInfo culture = new CultureInfo("ru-RU");
Thread.CurrentThread.CurrentUICulture = culture;
Thread.CurrentThread.CurrentCulture = culture; // для форматов дат/чисел

// Теперь обращаемся к ресурсам
Console.WriteLine(Strings.WelcomeMsg);  // Выведет: Добро пожаловать в наше приложение!

Если вы хотите дать пользователю возможность переключать язык «на лету» (без перезапуска), просто вызывайте эти строки повторно после смены культуры. В ASP.NET Core для этого используют middleware:

app.UseRequestLocalization(new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en-US"),
    SupportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("ru-RU") },
    SupportedUICultures = new[] { new CultureInfo("en-US"), new CultureInfo("ru-RU") }
});

Этот middleware автоматически определяет язык браузера пользователя (заголовок Accept-Language) и устанавливает соответствующую культуру для каждого запроса.

Локализация строк с параметрами

Часто в сообщениях нужно подставлять динамические данные. Например: «Привет, Иван! У вас 3 новых сообщения.». В ресурсном файле мы пишем шаблон:

Имя               | Значение
------------------|--------------------------------------------
GreetingWithName  | Hello, {0}! You have {1} new messages.

А в коде используем String.Format:

string userName = "John";
int msgCount = 3;
string greeting = string.Format(Strings.GreetingWithName, userName, msgCount);
Console.WriteLine(greeting);  // Hello, John! You have 3 new messages.

Для русского перевода в Strings.ru.resx пишем:

Имя               | Значение
------------------|--------------------------------------------
GreetingWithName  | Привет, {0}! У вас {1} новых сообщений.

Обратите внимание: порядок подстановок ({0}, {1}) не меняется, а сами фразы могут строиться по-разному. Это нормально — грамматика языков отличается.

Сложные случаи: плюрализация (множественное число)

В русском языке есть три формы: 1 сообщение, 2-4 сообщения, 5+ сообщений. В английском — две: 1 message, 2 messages. Простым String.Format тут не обойтись.

Решение: используйте библиотеку Humanizer (NuGet: Humanizer) или пишите свою логику с условными конструкциями. Пример с Humanizer:

using Humanizer;

int count = 5;
string pluralized = "message".ToQuantity(count); // "5 messages"
// Для русского нужно указать культуру:
string russianPlural = "сообщение".ToQuantity(count, showQuantityAs: ShowQuantityAs.Numeric, 
                            format: "N0", culture: new CultureInfo("ru-RU")); 
// Выведет: "5 сообщений"

Но лучше хранить готовые строки для каждого числового варианта в ресурсах и выбирать через switch. Это надёжнее и проще для переводчиков.

Интернационализация в ASP.NET Core (веб-приложения)

Для веба интернационализация C# становится ещё важнее — пользователи заходят со всего мира. Помимо ресурсных файлов, в ASP.NET Core есть встроенная система локализации представлений и Data Annotations.

Локализация моделей и валидации

Предположим, у вас есть модель:

public class UserModel
{
    [Required(ErrorMessageResourceName = "FieldRequired", ErrorMessage                

Блоги

Рекомендаций по книгам