Возможно ли одноуровневое многоязычное развертывание Windows Forms (ILMerge и спутниковые сборки / локализация)?

У меня есть простое приложение Windows Forms (C #, .NET 2.0), созданное с помощью Visual Studio 2008.

Я хотел бы поддерживать несколько языков пользовательского интерфейса и использовать свойство Localizable формы, а также файлы .resx, относящиеся к культуре, аспект локализации работает легко и просто. Visual Studio автоматически компилирует файлы resx, относящиеся к культуре, в спутниковые сборки, поэтому в моей скомпилированной папке приложения есть вложенные в культуру подпапки, содержащие эти спутниковые сборки.

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

Используя ILMerge (или ILRepack ), я могу объединить спутниковые сборки в основную исполняемую сборку, но стандартные резервные механизмы .NET ResourceManager не находят ресурсы, специфичные для культуры, которые были скомпилированы в основную сборку.

Интересно, что если я возьму свою объединенную (исполняемую) сборку и поместил ее в соответствующие вложенные в культуру подпапки, тогда все будет работать! Точно так же я вижу основные и культурные ресурсы в объединенной сборке, когда я использую Reflector (или ILSpy ). Но копирование основной сборки в зависимые от культуры подпапки наносит ущерб цели слияния в любом случае – мне действительно нужно, чтобы она была единственной копией единственной сборки …

Мне интересно , есть ли способ захватить или повлиять на резервные механизмы ResourceManager, чтобы искать ресурсы, специфичные для культуры, в одной и той же сборке, а не в подпапках GAC и в названии культуры . Я вижу механизм резервного копирования, описанный в следующих статьях, но не знаю, как он будет изменен: BCL Team Blog Article on ResourceManager .

Кто-нибудь есть идеи? Это, по-видимому, довольно частый вопрос онлайн (например, еще один вопрос здесь о переполнении стека: « ILMerge и локализованные сборки ресурсов »), но я нигде не нашел никакого авторитетного ответа.


ОБНОВЛЕНИЕ 1: основное решение

Следуя приведенной ниже рекомендации casperOne , я наконец смог выполнить эту работу.

Я помещаю код решения здесь в вопрос, потому что casperOne предоставил единственный ответ, я не хочу добавлять свои собственные.

Я смог заставить его работать, вытащив кишки из механизмов резервного восстановления ресурсов Framework, реализованных в методе «InternalGetResourceSet», и сделав наш аналогичный compilation первым используемым механизмом. Если ресурс не найден в текущей сборке, мы вызываем базовый метод для инициирования механизмов поиска по умолчанию (благодаря комментарию @ Wouter ниже).

Для этого я получил class «ComponentResourceManager» и переопределил только один метод (и повторно реализовал метод private framework):

class SingleAssemblyComponentResourceManager : System.ComponentModel.ComponentResourceManager { private Type _contextTypeInfo; private CultureInfo _neutralResourcesCulture; public SingleAssemblyComponentResourceManager(Type t) : base(t) { _contextTypeInfo = t; } protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) { ResourceSet rs = (ResourceSet)this.ResourceSets[culture]; if (rs == null) { Stream store = null; string resourceFileName = null; //lazy-load default language (without caring about duplicate assignment in race conditions, no harm done); if (this._neutralResourcesCulture == null) { this._neutralResourcesCulture = GetNeutralResourcesLanguage(this.MainAssembly); } // if we're asking for the default language, then ask for the // invariant (non-specific) resources. if (_neutralResourcesCulture.Equals(culture)) culture = CultureInfo.InvariantCulture; resourceFileName = GetResourceFileName(culture); store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); //If we found the appropriate resources in the local assembly if (store != null) { rs = new ResourceSet(store); //save for later. AddResourceSet(this.ResourceSets, culture, ref rs); } else { rs = base.InternalGetResourceSet(culture, createIfNotExists, tryParents); } } return rs; } //private method in framework, had to be re-specified here. private static void AddResourceSet(Hashtable localResourceSets, CultureInfo culture, ref ResourceSet rs) { lock (localResourceSets) { ResourceSet objA = (ResourceSet)localResourceSets[culture]; if (objA != null) { if (!object.Equals(objA, rs)) { rs.Dispose(); rs = objA; } } else { localResourceSets.Add(culture, rs); } } } } 

Чтобы на самом деле использовать этот class, вам нужно заменить System.ComponentModel.ComponentResourceManager в файлах «XXX.Designer.cs», созданных Visual Studio, – и вам нужно будет делать это каждый раз при изменении оформленной формы – Visual Studio заменяет это код автоматически. (Проблема обсуждалась в « Настроить конструктор Windows Forms для использования MyResourceManager », я не нашел более элегантного решения – я использую fart.exe на этапе предварительной сборки для автоматической замены).


ОБНОВЛЕНИЕ 2: другое практическое рассмотрение – более 2-х языков

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

Недавно я начал работать над аналогичным проектом, где есть несколько вторичных языков, и поэтому несколько спутниковых сборок, и ILMerge делал что-то очень странное: вместо того, чтобы слить несколько сборок со спутника, которые я запросил, это было объединение первой сборки спутника в несколько раз !

например, командной строки:

 "c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1InputProg.exe %1es\InputProg.resources.dll %1fr\InputProg.resources.dll 

С этой командной строкой я собирал следующие сборы ресурсов в объединенной сборке (наблюдался с декомпилятором ILSpy):

 InputProg.resources InputProg.es.resources InputProg.es.resources <-- Duplicated! 

После некоторых игр я понял, что это просто ошибка в ILMerge, когда он встречает несколько файлов с тем же именем в одном вызове командной строки. Решение состоит в том, чтобы просто объединить каждую сборку спутника в другом вызове командной строки:

 "c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1es\InputProg.resources.dll "c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1fr\InputProg.resources.dll 

Когда я это сделаю, результирующие ресурсы в последней сборке верны:

 InputProg.resources InputProg.es.resources InputProg.fr.resources 

Итак, наконец, если это поможет прояснить, вот полный пакетный файл после сборки:

 "%ProgramFiles%\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1es\InputProg.resources.dll IF %ERRORLEVEL% NEQ 0 GOTO END "%ProgramFiles%\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1fr\InputProg.resources.dll IF %ERRORLEVEL% NEQ 0 GOTO END del %1InputProg.exe del %1InputProg.pdb del %1TempProg.exe del %1TempProg.pdb del %1es\*.* /Q del %1fr\*.* /Q :END 

ОБНОВЛЕНИЕ 3: ILRepack

Еще одно замечание. Одной из вещей, которые беспокоили меня с помощью ILMerge, было то, что это дополнительный собственный инструмент Microsoft, не установленный по умолчанию в Visual Studio, и, следовательно, дополнительная зависимость, которая делает его немного сложнее для третьей стороны с моими проектами с открытым исходным кодом.

Недавно я обнаружил эквивалент ILRepack , эквивалентный Open Source (Apache 2.0), который до сих пор работает для меня (замена на замену) и может свободно распространяться с вашими источниками проекта.


Надеюсь, это поможет кому-то!

Единственный способ увидеть это – создать class, который происходит из ResourceManager а затем переопределить методы InternalGetResourceSet и GetResourceFileName . Оттуда вы сможете переопределить, где будут получены ресурсы, с учетом экземпляра CultureInfo .

Другой подход:

1) добавьте свои ресурсы.DLL как вложенные ресурсы в свой проект.

2) добавьте обработчик событий для AppDomain.CurrentDomain.ResourceResolve. Этот обработчик срабатывает, когда ресурс не может быть найден.

  internal static System.Reflection.Assembly CurrentDomain_ResourceResolve(object sender, ResolveEventArgs args) { try { if (args.Name.StartsWith("your.resource.namespace")) { return LoadResourcesAssyFromResource(System.Threading.Thread.CurrentThread.CurrentUICulture, "name of your the resource that contains dll"); } return null; } catch (Exception ex) { return null; } } 

3) Теперь вам нужно реализовать LoadResourceAssyFromResource что-то вроде

 private Assembly LoadResourceAssyFromResource( Culture culture, ResourceName resName) { //var x = Assembly.GetExecutingAssembly().GetManifestResourceNames(); using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resName)) { if (stream == null) { //throw new Exception("Could not find resource: " + resourceName); return null; } Byte[] assemblyData = new Byte[stream.Length]; stream.Read(assemblyData, 0, assemblyData.Length); var ass = Assembly.Load(assemblyData); return ass; } 

}

У меня есть предложение по части вашей проблемы. В частности, решение для этапа обновления файлов .Designer.cs для замены ComponentResourceManager с помощью SingleAssemblyComponentResourceManager.

  1. Переместите метод InitializeComponent () из .Designer.cs и в файл реализации (включая #region). Visual Studio продолжит автоматическое создание этого раздела, без каких-либо проблем, насколько я могу судить.

  2. Используйте псевдоним C # в верхней части файла реализации, чтобы ComponentResourceManager был псевдонимом SingleAssemblyComponentResourceManager.

К сожалению, я не смог полностью проверить это. Мы нашли другое решение нашей проблемы и поэтому двинулись дальше. Надеюсь, вам это поможет.

Просто мысль.

Вы сделали шаг и создали свой SingleAssemblyComponentResourceManager

Итак, почему вы принимаете боль, чтобы включить ваши спутниковые сборки в объединенную сборку?

Вы можете добавить ResourceName.es.resx как двоичный файл в другой ресурс вашего проекта.

Чем вы можете переписать свой код

  store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); //If we found the appropriate resources in the local assembly if (store != null) { rs = new ResourceSet(store); 

с этим кодом (не проверено, но должно работать)

 // we expect the "main" resource file to have a binary resource // with name of the local (linked at compile time of course) // which points to the localized resource var content = Properties.Resources.ResourceManager.GetObject("es"); if (content != null) { using (var stream = new MemoryStream(content)) using (var reader = new ResourceReader(stream)) { rs = new ResourceSet(reader); } } 

Это должно приложить усилия для включения устаревших сборщиков в процессе ilmerge.

Добавлено как ответ, так как комментарии не обеспечивали достаточного пространства:

Я не мог найти ресурсы для нейтральных культур ( en вместо en-US ) с решением OPs. Поэтому я распространил InternalGetResourceSet на поиск нейтральных культур, которые сделали для меня работу. С этим вы также можете найти ресурсы, которые не определяют регион. На самом деле это то же поведение, что и нормальный ресурсореформатор, когда не выполняется ILMerging файлов ресурсов.

 //Try looking for the neutral culture if the specific culture was not found if (store == null && !culture.IsNeutralCulture) { resourceFileName = GetResourceFileName(culture.Parent); store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); } 

Это приводит к следующему коду для SingleAssemblyComponentResourceManager

 class SingleAssemblyComponentResourceManager : System.ComponentModel.ComponentResourceManager { private Type _contextTypeInfo; private CultureInfo _neutralResourcesCulture; public SingleAssemblyComponentResourceManager(Type t) : base(t) { _contextTypeInfo = t; } protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) { ResourceSet rs = (ResourceSet)this.ResourceSets[culture]; if (rs == null) { Stream store = null; string resourceFileName = null; //lazy-load default language (without caring about duplicate assignment in race conditions, no harm done); if (this._neutralResourcesCulture == null) { this._neutralResourcesCulture = GetNeutralResourcesLanguage(this.MainAssembly); } // if we're asking for the default language, then ask for the // invariant (non-specific) resources. if (_neutralResourcesCulture.Equals(culture)) culture = CultureInfo.InvariantCulture; resourceFileName = GetResourceFileName(culture); store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); //Try looking for the neutral culture if the specific culture was not found if (store == null && !culture.IsNeutralCulture) { resourceFileName = GetResourceFileName(culture.Parent); store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); } //If we found the appropriate resources in the local assembly if (store != null) { rs = new ResourceSet(store); //save for later. AddResourceSet(this.ResourceSets, culture, ref rs); } else { rs = base.InternalGetResourceSet(culture, createIfNotExists, tryParents); } } return rs; } //private method in framework, had to be re-specified here. private static void AddResourceSet(Hashtable localResourceSets, CultureInfo culture, ref ResourceSet rs) { lock (localResourceSets) { ResourceSet objA = (ResourceSet)localResourceSets[culture]; if (objA != null) { if (!object.Equals(objA, rs)) { rs.Dispose(); rs = objA; } } else { localResourceSets.Add(culture, rs); } } } } 
  • .NET Assembly Diff / Compare Tool - Что доступно?
  • В чем разница между MOV и LEA?
  • Сколько циклов процессора требуется для каждой инструкции сборки?
  • Как определить, была ли assembly .NET построена для x86 или x64?
  • Определите, были ли сборки .NET построены из одного источника
  • Вызов функции стандартной библиотеки C из asm в Visual Studio
  • .Net: Запуск кода при загрузке сборки
  • Использование разных версий одной и той же сборки в одной папке
  • Ошибки CocoaPods при сборке проекта
  • Visual Studio «Не удалось скопировать» ... во время сборки
  • Как объединить несколько сборок в один?
  • Давайте будем гением компьютера.