Интернационализация приложений 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-JP→ja→ нейтральный язык, поэтому даже при отсутствии перевода приложение не падает, а просто показывает текст на языке по умолчанию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.
Выбор подхода к интернационализации существующего приложения и оценка объёма затрагиваемых изменений, а также адаптация экранов, отчётов и данных интеграции для выхода на зарубежные рынки часто требуют решений, принимаемых с учётом текущего состояния кодовой базы, — если сомневаетесь, обращайтесь к нам за консультацией.
Связанные статьи
- Таблица решений: когда использовать WinForms, WPF или WinUI
- Таблица решений по UX-проектированию приложений Windows
- Проектирование даты, времени и часовых поясов в бизнес-приложениях
- Руководство по поддержке высокого DPI в WinForms
- Руководство по поддержке высокого DPI в WPF
- Не только appsettings.json — практика управления конфигурацией в бизнес-приложениях Windows
- Как выбрать способ распространения приложения Windows (MSI / MSIX / ClickOnce / xcopy / собственный обновлятор)
- Устройство профилей пользователей Windows
Похожие направления консультаций
Komura Soft LLC (合同会社小村ソフト) оказывает технические консультации по проектированию и реализации поддержки нескольких языков в приложениях WinForms/WPF, а также по выбору подхода и оценке объёма изменений при интернационализации уже существующих приложений.
Источники
-
Microsoft Learn, WPF Globalization and Localization Overview. О генерации сателлитных сборок через настройку
UICultureв файле проекта, о роли атрибутаxml:langу корневого элемента, а также о том, что LocBaml — это демонстрационный инструмент, предназначенный только для WPF на .NET Framework, и он не работает в WPF на .NET. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Retrieve resources in .NET apps. О процессе отката ресурсов (поиск в порядке: конкретная культура → нейтральная культура → ресурсы по умолчанию в основной сборке). ↩ ↩2 ↩3
-
Microsoft Learn, CultureInfo.CurrentUICulture Property. О том, что
CurrentUICultureиспользуется менеджером ресурсов для поиска ресурсов, специфичных для культуры, что значение по умолчанию берётся из языка интерфейса ОС, и о настройке значения по умолчанию для всех потоков черезDefaultThreadCurrentUICulture. ↩ ↩2 -
Microsoft Learn, Resources in .NET apps. О создании и упаковке ресурсов resx, о модели «ступица и спицы» с основной и сателлитными сборками, а также об атрибуте
NeutralResourcesLanguage. ↩ ↩2 -
Microsoft Learn, Walkthrough: Localizing a Hybrid Application. О процедуре создания resx для конкретного языка (например, Form1.es-ES.resx) путём установки свойства
Localizableвtrueи переключения свойстваLanguageв дизайнере форм Windows Forms, а также о примере установкиCurrentUICultureдо вызоваInitializeComponent. ↩ -
Microsoft Learn, How to: Design a Windows Forms Layout that Responds Well to Localization. О том, как с помощью
TableLayoutPanelпостроить вёрстку, в которой размер элементов управления подстраивается под изменение длины строки в свойствеTextпри локализации. ↩ -
Microsoft Learn, Localize ClickOnce applications. О том, что включение всех сателлитных сборок в одно развёртывание — поведение Visual Studio по умолчанию, и что во время выполнения используется сателлитная сборка, соответствующая культуре ОС. ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Печать и вывод PDF в Windows-бизнес-приложениях — выбор между System.Drawing.Printing, WPF и библиотеками отчётов
В виде таблицы выбора по требованиям собраны печать WinForms через PrintDocument, печать WPF через FlowDocument/FixedDocument и варианты ...
Аутсорсинг и контрактная разработка Windows-приложения: что стоит прояснить перед заказом
Перед тем как заказать аутсорсинг или контрактную разработку Windows-приложения, разберём, что нужно прояснить: доработка существующего П...
Не оборачивайте HttpClient в using ── практика HTTP-взаимодействия в бизнес-приложениях на C# (паттерны создания, тайм-ауты, повторные попытки)
HttpClient в C#, создаваемый через using при каждом запросе, приводит к исчерпанию сокетов, а статический HttpClient перестаёт учитывать ...
Не только appsettings.json — практическое руководство по управлению конфигурацией Windows-бизнес-приложений (настройки окружений, секреты и место записи)
Практический разбор управления конфигурацией Windows-бизнес-приложений: слои appsettings.json, выбор между IConfiguration и семейством IO...
CSV — не «просто текст»: практика работы с CSV в бизнес-приложениях на C# (кодировки, совместимость с Excel, защита от инъекций)
Разбираем типичные ошибки при вводе-выводе CSV в бизнес-приложениях — самописный парсинг через Split(','), искажение текста в Excel из-за...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Поток UI и таймеры
Поток UI WPF / WinForms, асинхронные операции, Dispatcher и проектирование таймеров.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Проектирование и реализация поддержки нескольких языков в десктопных приложениях относится к практической области разработки приложений Windows.
Технические консультации и ревью дизайна
Выбор подхода и оценка объёма изменений при интернационализации уже существующего приложения требуют технической консультации с обзором проектных решений.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Можно ли интернационализировать уже существующее приложение, изначально сделанное только на японском языке?
- Можно, но основная часть работы — это не «внедрение механизма ресурсов», а «выявление и замена строк, жёстко закодированных прямо в коде». Нужно провести инвентаризацию не только текстов на экранах, но и сообщений в диалоговых окнах, логов, текстов исключений, отчётов и строк для отображения, хранящихся в базе данных — объём работы определяется не количеством экранов, а тем, насколько разбросаны строки по коду. Если уже на этапе новой разработки выносить через resx хотя бы те строки, которые видит пользователь, будущие затраты на интернационализацию снизятся на порядок.
- Можно ли ограничиться машинным переводом?
- Для внутренних инструментов или временного решения начать с машинного перевода вполне реалистично. Однако в бизнес-приложениях много коротких слов на кнопках и в меню (например, «Регистрация», «Отмена»), и без контекста доля ошибок машинного перевода в этой области возрастает. Рекомендуется как минимум один раз провести проверку экранов человеком, который реально пользуется этим языком. Ещё важнее, чем качество перевода, заранее обеспечить с помощью TableLayoutPanel и AutoSize, что вёрстка не сломается из-за более длинных строк после перевода.
- Нужно ли переводить логи и сообщения об ошибках?
- Стандартная практика — переводить сообщения, которые видит пользователь на экране, но не переводить содержимое, записываемое в лог-файлы и журнал событий (фиксировать его на одном языке). Логи читает не пользователь, а разработчик или специалист по эксплуатации, и многоязычные логи разрушают возможность поиска при расследовании инцидентов. Форматы (даты, числа) в логах тоже нужно жёстко закрепить за CultureInfo.InvariantCulture. Если изначально разделить обработку CurrentCulture и CurrentUICulture, такое разделение реализуется естественным образом.
- Стоит ли делать переключение языка в самом приложении отдельно от языка интерфейса ОС?
- Для выхода в зарубежные офисы или для бизнес-приложений, которыми в Японии пользуются иностранные сотрудники, наличие собственной языковой настройки в приложении очень ценно. Разумный подход — двухступенчатая схема: по умолчанию следовать языку интерфейса ОС (значение CurrentUICulture по умолчанию) и переопределять его только тогда, когда пользователь явно выбрал язык в настройках. Самый безопасный вариант — сделать так, чтобы переключение применялось «при следующем запуске»: это упрощает и реализацию, и объяснение поведения пользователям.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки