Интернационализация приложений WinForms/WPF — resx, сателлитные сборки и переключение культуры на практике

· · CSharp, .NET, WinForms, WPF, Интернационализация, Локализация, Ресурсы, UI, Разработка Windows, Техническая консультация

«Хотим использовать то же приложение и в зарубежных офисах», «На заводе стало больше сотрудников, говорящих на других языках, поэтому нужно перевести интерфейс на английский» — интернационализация десктопных приложений Windows звучит как простое пожелание, но как только вы приступаете к работе, возникает целая серия вопросов, требующих решения: «как использовать resx», «во WPF есть несколько подходов, и непонятно, какой из них правильный», «можно ли переключать язык во время работы приложения».

В этой статье, ориентируясь на бизнес-приложения WinForms/WPF, мы разберём основы локализации в .NET (культуры и откат ресурсов), устройство resx и сателлитных сборок, реалистичные подходы для каждого фреймворка, переключение языка во время выполнения, а также вопросы за пределами строк — форматирование, вёрстку и поддержку RTL — в том порядке, в котором на практике обычно возникают сложности с принятием решений.

1. Сначала выводы (таблица решений)

Способ интернационализации определяется сочетанием фреймворка и требований.

Ситуация Рекомендуемый подход Причина
WinForms, в основном подписи/кнопки на форме Localizable = true у формы + resx для каждого языка (подход через дизайнер) Позволяет настраивать строки, размер и положение элементов управления отдельно для каждого языка, и всё делается средствами Visual Studio
Общее для WinForms/WPF, много сообщений и общих текстов Общий resx (Resources.resx + Resources.ja.resx и т. д.) + автоматически сгенерированный типизированный класс Строки можно типобезопасно получать из кода. Одни и те же тексты можно использовать и в интерфейсе, и в логике
WPF Общий resx + привязка через x:Static Подход с LocBaml не работает в WPF на .NET (Core)1. Подход на основе resx позволяет использовать те же знания, что и в WinForms
Обязательно переключение языка во время работы resx + механизм уведомления об изменениях (либо пересоздание экрана) Прежде чем выбирать подход, нужно как требование уточнить, действительно ли необходимо переключение без перезапуска (глава 6)
Перевод отдаётся на аутсорсинг Проектирование на основе resx, передача через таблицу соответствия ключей и переводов resx — это XML, поэтому его можно передавать как есть, но качество перевода определяется тем, снабжаете ли вы его контекстом (на каком экране и на какой кнопке используется строка)

Сформулируем выводы заранее.

  • CurrentCulture (форматирование) и CurrentUICulture (язык ресурсов) — это разные вещи. Это различие лежит в основе всего проектирования интернационализации (глава 2).
  • Ресурсы физически представлены как «ресурсы нейтрального языка + сателлитные сборки для каждого языка», а откат (fallback) происходит автоматически. Поиск идёт в порядке ja-JPja → нейтральный язык, поэтому даже при отсутствии перевода приложение не падает, а просто показывает текст на языке по умолчанию2.
  • Для WinForms практическое решение — сочетание подхода через дизайнер (Localizable = true) и подхода с общим resx. Экраны локализуются через дизайнер, а диалоговые окна сообщений и общие тексты выносятся в общий resx (глава 4).
  • Подход с LocBaml, описанный в официальном пошаговом руководстве по WPF, не работает в .NET (Core)1. На сегодняшний день основной практический вариант — resx + x:Static (либо привязка) (глава 5).
  • В качестве основного варианта переключения языка во время работы рекомендуется «применение при следующем запуске». Ресурсы уже открытых экранов автоматически не заменяются (глава 6).
  • Перевод строк — это лишь половина работы по интернационализации. Оставшаяся половина — это адаптация вёрстки под длину переведённого текста, форматирование дат и чисел, шрифты и поддержка языков с письмом справа налево (RTL) (глава 7).

2. Основы — различие между CurrentCulture и CurrentUICulture

В .NET за настройки культуры (региональные параметры) отвечают два свойства с разными ролями3.

Свойство Что определяет Пример
CultureInfo.CurrentCulture Формат даты, чисел и валюты Отображать 2026/07/07 или 7/7/2026
CultureInfo.CurrentUICulture На каком языке загружать ресурсы Показывать на кнопке «保存» или «Save»

Эти два свойства можно настраивать независимо друг от друга. Например, сочетание «интерфейс на английском, но даты и валюта — в японском формате» (частое требование для бизнес-приложений, которыми в Японии пользуются иностранные сотрудники) естественным образом выражается через CurrentUICulture = en при CurrentCulture = ja-JP.

Оба значения по умолчанию берутся из настроек ОС, поэтому отправная точка — «если ничего не менять, приложение следует настройкам ОС». Если язык нужно переключать явно для всего приложения, вместо настройки для каждого потока в отдельности меняйте именно значения по умолчанию.

using System.Globalization;

// Заменяем культуру по умолчанию для всех потоков, которые будут созданы далее
// (выполняется до создания UI-потока, то есть до Application.Run)
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en"); // язык ресурсов
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("ja-JP"); // формат

Присваивание значения Thread.CurrentThread.CurrentUICulture действует только на этот конкретный поток. В бизнес-приложениях, где async/await и фоновая обработка постоянно переходят между потоками, безопаснее менять значения по умолчанию целиком — через DefaultThreadCurrentUICulture / DefaultThreadCurrentCulture3.

Обратите внимание: форматы для логов, файлового вывода и обмена данными между системами не должны зависеть от культуры пользователя — их нужно жёстко закрепить за CultureInfo.InvariantCulture. Подробнее о проектировании форматов дат см. в статье «Проектирование даты, времени и часовых поясов в бизнес-приложениях».

3. Устройство ресурсов — resx, сателлитные сборки и откат (fallback)

Локализация в .NET построена по модели «ступица и спицы» (hub and spoke). Ресурсы языка по умолчанию (нейтрального) встраиваются прямо в основную сборку, а ресурсы для каждого языка выносятся в отдельные DLL, называемые сателлитными сборками4.

После сборки resx для каждого языка образуют примерно такую структуру выходных файлов.

MyApp.exe                     ← основная сборка (содержит нейтральные ресурсы)
MyApp.dll
ja/MyApp.resources.dll        ← сателлитная сборка для японского языка
en/MyApp.resources.dll        ← сателлитная сборка для английского языка

Во время выполнения менеджер ресурсов, отталкиваясь от CurrentUICulture, ищет ресурсы в следующем порядке: конкретная культура (ja-JP) → нейтральная культура (ja) → значение по умолчанию (основная сборка)2. Благодаря этому автоматическому откату приложение не падает даже при отсутствии одного перевода — просто отображается строка на языке по умолчанию. И наоборот: если на «английском» экране вдруг проскакивают отдельные японские строки, это признак пропущенного перевода, и данный факт можно использовать как критерий при тестировании (глава 8).

На практике стоит учитывать два момента.

  • Если объявить, какая культура является языком по умолчанию, через атрибут NeutralResourcesLanguage, то при совпадении с культурой по умолчанию не будет лишнего поиска сателлитной сборки, а конечная точка отката станет однозначной4.
  • Сателлитные сборки «работают только тогда, когда распространяются вместе с основной сборкой», поэтому в структуру инсталлятора или простого копирования файлов обязательно нужно включать языковые папки (глава 8).

4. Практика WinForms — подход через дизайнер и подход с общим resx

В WinForms есть два подхода, и на практике их обычно сочетают.

4.1 Экраны — через дизайнер (Localizable = true)

Если установить свойство формы Localizable в true, переключить свойство Language на нужный язык (например, английский) и после этого отредактировать Text у надписей и кнопок, Visual Studio автоматически создаст resx для этого языка, например Form1.en.resx5.

Сила этого подхода в том, что для каждого языка сохраняются не только строки, но и размер, и положение элементов управления. Проблему, когда японское «登録» превращается в английское «Register» и вылезает за пределы кнопки, можно устранить, подстроив размер отдельно для каждого языка прямо в дизайнере. С другой стороны, это создаёт затраты на сопровождение: количество resx растёт как произведение числа языков на число форм, и при каждом изменении вёрстки формы приходится проверять все языки. Если ожидается рост числа языков, лучше в первую очередь применить меры по вёрстке (глава 7) и строить экраны так, чтобы они «не ломались без ручной подгонки».

4.2 Сообщения и общие тексты — через общий resx

Строки, которые используются из кода — тексты диалоговых окон сообщений, окон подтверждения, строки состояния — стоит собирать в общем для проекта Resources.resx, а для каждого языка добавлять, например, Resources.en.resx. Поскольку из resx автоматически генерируется типизированный класс, обращаться к строкам из кода можно без риска опечататься в ключе.

// Обращаемся через сгенерированный типизированный класс.
// Возвращаемая строка автоматически меняется в зависимости от CurrentUICulture
MessageBox.Show(
    Properties.Resources.ConfirmDeleteMessage,
    Properties.Resources.ConfirmTitle,
    MessageBoxButtons.YesNo);

Здесь важнейшее правило — никогда не писать строки, показываемые пользователю, прямо в коде. Основные трудозатраты на интернационализацию уходят не на внедрение самого механизма, а на «сбор разбросанных по коду строк». Именно поэтому имеет смысл заранее выносить строки в resx, даже если приложение пока работает только на одном языке.

5. Практика WPF — resx вместо LocBaml как основной вариант

Интернационализация WPF — область, где информация особенно легко путается. В официальной документации описан подход, при котором UICulture задаётся в файле проекта, генерируются ресурсы BAML, а перевод подставляется через инструмент LocBaml. Однако LocBaml — это демонстрационный инструмент, предназначенный только для WPF на .NET Framework, и он не работает в WPF на .NET (Core)1. Считайте, что сегодня это не тот подход, который стоит выбирать для новых проектов.

Основной практический вариант — использовать тот же подход с общим resx, что и в WinForms, обращаясь к нему из XAML.

<!-- Устанавливаем модификатор доступа resx на Public и обращаемся через x:Static -->
<Window xmlns:res="clr-namespace:MyApp.Properties"
        Title="{x:Static res:Resources.MainWindowTitle}">
    <Button Content="{x:Static res:Resources.SaveButton}" />
</Window>

Преимущество этого подхода в том, что механизм работы с ресурсами (сателлитные сборки, откат) полностью совпадает с WinForms, поэтому даже в смешанном решении тексты можно вести централизованно. x:Static — это статическая ссылка, разрешаемая при запуске, поэтому если требуется переключение во время выполнения, подход нужно развивать в сторону привязки с уведомлением об изменениях (глава 6).

Также стоит задать xml:lang у корневого элемента в XAML — тогда корректно заработают зависящие от языка функции WPF: перенос слов по слогам, откат шрифтов, отображение цифр и прочее1.

Сам выбор между WinForms и WPF рассмотрен в статье «Таблица решений: когда использовать WinForms, WPF или WinUI».

6. Переключение языка во время выполнения — «применение при следующем запуске» как основной вариант

Если по требованиям пользователь должен выбирать язык в настройках, момент применения выбирается из двух вариантов.

  • Вариант A: применить при следующем запуске (рекомендуется). Выбранный язык сохраняется в файле настроек, а DefaultThreadCurrentUICulture устанавливается до Application.Run при запуске (в WPF — до старта Application). Реализация простая, и структурно исключены проблемы, связанные с пересозданием экранов (потеря вводимых данных, повторная регистрация обработчиков событий и т. п.).
  • Вариант B: немедленное применение. Даже при изменении CurrentUICulture строки на уже открытых экранах автоматически не заменяются. При использовании подхода через дизайнер в WinForms нужно повторно применить ресурсы к открытым формам через ComponentResourceManager.ApplyResources; в WPF — подавать строки через привязку с уведомлением об изменениях. В любом случае реализация строится на предпосылке, что «все экраны поддерживают повторное применение ресурсов».

В бизнес-приложениях частота смены языка обычно невысока (как правило, один раз при внедрении), и затраты на реализацию и тестирование варианта B часто себя не оправдывают. Рекомендуется брать вариант A за основу и применять вариант B только тогда, когда это действительно необходимо по требованиям. Поскольку языковая настройка индивидуальна для каждого пользователя, для её хранения подходит каталог %LOCALAPPDATA%. О том, где хранить настройки, см. таблицу решений в статье «Не только appsettings.json — практика управления конфигурацией в бизнес-приложениях Windows».

7. Интернационализация за пределами строк — вёрстка, форматирование, шрифты, RTL

Даже когда перевод готов, остаётся ещё половина работы по интернационализации.

  • Растяжение и сжатие вёрстки. Даже при одинаковом смысле длина текста в разных языках может сильно отличаться (японское «保存» → немецкое «Speichern»). Вёрстка с фиксированными координатами и размерами будет ломаться при каждом переводе, поэтому в WinForms официально рекомендуется сочетать TableLayoutPanel и AutoSize, чтобы вёрстка подстраивалась под длину строки6. В WPF система вёрстки изначально гибкая, поэтому в большинстве случаев достаточно просто избегать жёстко заданной ширины. Имеет смысл проектировать это одновременно с мерами против искажений при высоком DPI (см. «Руководство по поддержке высокого DPI в WinForms» и «Руководство по поддержке высокого DPI в WPF»), чтобы избежать доработок задним числом.
  • Формат даты, чисел и валюты. Отображение на экране стоит доверить CurrentCulture, сократив в коде экранов количество жёстко заданных вызовов вроде ToString("yyyy/MM/dd"). И наоборот, для файлов, логов и данных интеграции формат нужно жёстко закрепить за InvariantCulture (глава 2).
  • Шрифты. Нужно проверить, содержит ли шрифт символы нужного языка. При добавлении китайского (упрощённого/традиционного) или корейского языка использование шрифта, рассчитанного на японский текст, может привести к неестественному начертанию (так называемая проблема начертания CJK). Если централизовать указание UI-шрифта в одном месте, добавление нового языка в дальнейшем будет проще.
  • Языки с письмом справа налево (RTL). Для поддержки арабского и иврита, помимо перевода строк, требуется зеркальное отражение экрана по горизонтали. В WinForms этим управляют свойства RightToLeft / RightToLeftLayout, в WPF — свойство FlowDirection. Добавление поддержки RTL «задним числом» превращается в пересмотр всех экранов, поэтому если такой язык потенциально может понадобиться, включайте эту поддержку в требования с самого начала.
  • Текст внутри изображений и иконок. Строки, «впечённые» в изображение, механизмом ресурсов не переключаются. Безопаснее избегать изображений с текстом и использовать конструкцию «изображение + текстовая подпись».

Общие вопросы проектирования UI рассмотрены также в статье «Таблица решений по UX-проектированию приложений Windows».

8. Эксплуатация — передача переводов, обнаружение пропусков и распространение

Напоследок — об эксплуатационных аспектах после того, как механизм уже создан.

  • При передаче перевода прикладывайте контекст. Если передавать только ключи resx и исходный текст, качество перевода коротких фраз вроде «OK» или «Открыть» будет нестабильным. Реалистичный подход — обмениваться таблицей соответствия (например, в электронной таблице), где к ключу, исходному тексту и переводу добавлено «на каком экране и для чего используется строка», а после получения перевода вносить его в resx.
  • Пропущенные переводы можно обнаружить через откат. Благодаря откату ресурсов пропущенный перевод проявляется как «на экране этого языка проскакивает язык по умолчанию»2. Если добавить в CI простую проверку (скрипт или тестовый код), сверяющую список ключей resx для каждого языка с нейтральным resx, такие пропуски можно выявлять автоматически ещё до релиза.
  • Включайте сателлитные сборки в дистрибутив. Если не распространять языковые папки вроде ja/ и en/ целиком, для соответствующего языка сработает откат, и возникнет ситуация «перевод сделан, но не отображается». Пропуск языковой папки при ручном копировании — классическая типовая ошибка, поэтому структуру каталогов нужно включать в инсталлятор (MSI/MSIX и т. д.) целиком. При распространении через ClickOnce сателлитные сборки всех языков по умолчанию включаются в развёртывание7. Общий выбор способа распространения рассмотрен в статье «Как выбрать способ распространения приложения Windows».

Итог

Интернационализация десктопных приложений строится на давно отработанном механизме resx и сателлитных сборок, и сам по себе он несложен. Успех на практике определяют проектирование с чётким разделением CurrentCulture и CurrentUICulture, дисциплина, не допускающая жёстко закодированных строк, показываемых пользователю, вёрстка, выдерживающая длину переведённого текста, и трезвая оценка требования «действительно ли нужно немедленное переключение». Для WPF не стоит опираться на устаревшую информацию о подходе LocBaml — на сегодняшний день практическое решение — строить всё на основе resx.

Выбор подхода к интернационализации существующего приложения и оценка объёма затрагиваемых изменений, а также адаптация экранов, отчётов и данных интеграции для выхода на зарубежные рынки часто требуют решений, принимаемых с учётом текущего состояния кодовой базы, — если сомневаетесь, обращайтесь к нам за консультацией.

Связанные статьи

Похожие направления консультаций

Komura Soft LLC (合同会社小村ソフト) оказывает технические консультации по проектированию и реализации поддержки нескольких языков в приложениях WinForms/WPF, а также по выбору подхода и оценке объёма изменений при интернационализации уже существующих приложений.

Источники

  1. Microsoft Learn, WPF Globalization and Localization Overview. О генерации сателлитных сборок через настройку UICulture в файле проекта, о роли атрибута xml:lang у корневого элемента, а также о том, что LocBaml — это демонстрационный инструмент, предназначенный только для WPF на .NET Framework, и он не работает в WPF на .NET.  2 3 4

  2. Microsoft Learn, Retrieve resources in .NET apps. О процессе отката ресурсов (поиск в порядке: конкретная культура → нейтральная культура → ресурсы по умолчанию в основной сборке).  2 3

  3. Microsoft Learn, CultureInfo.CurrentUICulture Property. О том, что CurrentUICulture используется менеджером ресурсов для поиска ресурсов, специфичных для культуры, что значение по умолчанию берётся из языка интерфейса ОС, и о настройке значения по умолчанию для всех потоков через DefaultThreadCurrentUICulture 2

  4. Microsoft Learn, Resources in .NET apps. О создании и упаковке ресурсов resx, о модели «ступица и спицы» с основной и сателлитными сборками, а также об атрибуте NeutralResourcesLanguage 2

  5. Microsoft Learn, Walkthrough: Localizing a Hybrid Application. О процедуре создания resx для конкретного языка (например, Form1.es-ES.resx) путём установки свойства Localizable в true и переключения свойства Language в дизайнере форм Windows Forms, а также о примере установки CurrentUICulture до вызова InitializeComponent

  6. Microsoft Learn, How to: Design a Windows Forms Layout that Responds Well to Localization. О том, как с помощью TableLayoutPanel построить вёрстку, в которой размер элементов управления подстраивается под изменение длины строки в свойстве Text при локализации. 

  7. Microsoft Learn, Localize ClickOnce applications. О том, что включение всех сателлитных сборок в одно развёртывание — поведение Visual Studio по умолчанию, и что во время выполнения используется сателлитная сборка, соответствующая культуре ОС. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Не оборачивайте HttpClient в using ── практика HTTP-взаимодействия в бизнес-приложениях на C# (паттерны создания, тайм-ауты, повторные попытки)

HttpClient в C#, создаваемый через using при каждом запросе, приводит к исчерпанию сокетов, а статический HttpClient перестаёт учитывать ...

Не только appsettings.json — практическое руководство по управлению конфигурацией Windows-бизнес-приложений (настройки окружений, секреты и место записи)

Практический разбор управления конфигурацией Windows-бизнес-приложений: слои appsettings.json, выбор между IConfiguration и семейством IO...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Можно ли интернационализировать уже существующее приложение, изначально сделанное только на японском языке?
Можно, но основная часть работы — это не «внедрение механизма ресурсов», а «выявление и замена строк, жёстко закодированных прямо в коде». Нужно провести инвентаризацию не только текстов на экранах, но и сообщений в диалоговых окнах, логов, текстов исключений, отчётов и строк для отображения, хранящихся в базе данных — объём работы определяется не количеством экранов, а тем, насколько разбросаны строки по коду. Если уже на этапе новой разработки выносить через resx хотя бы те строки, которые видит пользователь, будущие затраты на интернационализацию снизятся на порядок.
Можно ли ограничиться машинным переводом?
Для внутренних инструментов или временного решения начать с машинного перевода вполне реалистично. Однако в бизнес-приложениях много коротких слов на кнопках и в меню (например, «Регистрация», «Отмена»), и без контекста доля ошибок машинного перевода в этой области возрастает. Рекомендуется как минимум один раз провести проверку экранов человеком, который реально пользуется этим языком. Ещё важнее, чем качество перевода, заранее обеспечить с помощью TableLayoutPanel и AutoSize, что вёрстка не сломается из-за более длинных строк после перевода.
Нужно ли переводить логи и сообщения об ошибках?
Стандартная практика — переводить сообщения, которые видит пользователь на экране, но не переводить содержимое, записываемое в лог-файлы и журнал событий (фиксировать его на одном языке). Логи читает не пользователь, а разработчик или специалист по эксплуатации, и многоязычные логи разрушают возможность поиска при расследовании инцидентов. Форматы (даты, числа) в логах тоже нужно жёстко закрепить за CultureInfo.InvariantCulture. Если изначально разделить обработку CurrentCulture и CurrentUICulture, такое разделение реализуется естественным образом.
Стоит ли делать переключение языка в самом приложении отдельно от языка интерфейса ОС?
Для выхода в зарубежные офисы или для бизнес-приложений, которыми в Японии пользуются иностранные сотрудники, наличие собственной языковой настройки в приложении очень ценно. Разумный подход — двухступенчатая схема: по умолчанию следовать языку интерфейса ОС (значение CurrentUICulture по умолчанию) и переопределять его только тогда, когда пользователь явно выбрал язык в настройках. Самый безопасный вариант — сделать так, чтобы переключение применялось «при следующем запуске»: это упрощает и реализацию, и объяснение поведения пользователям.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог