I have an existing localized WPF application, and my localizations are stored in a bunch of .resx files, and accessed through the ".Designer.cs" files generated by the default resx custom tool. Each supported language has its own version of every .resx file. It works perfectly fine, but i have to recompile the application everytime we want to adjust the translations, which is not the most practical thing to do once the application has been shipped to multiple customers.
My application gets published in PublishSingleFile mode, and my setup adds some configuration files along with it. The user is expected to access to the configuration files at some point, so i'd like to keep that directory as clean as possible.
It seems that the .NET way to do that is through satellite assemblies, but their interaction with published apps and the PublishSingleFile option is not very well documented.
How can one go about it ?
I made a test project on github to try and solve that. There is a tag for the base project, and different tags for the steps described in (the original version of) this answer. None of this is too complicated, but in order to make everything work there are quite a few steps. The steps described in this answer are based on that project.
It's a very basic WPF app with 1 windows and a couple controls, 2 resource files
Resources.resx
andErrors.resx
, in aProperties
subfolder, and their translations in french and german into.{culture}.resx
files (so 6 files in total). There's a button to switch the UI from english to french, then to french from german, and from german back to english.Before we get to explaining how to do it, here are a few things to consider :
resgen.exe
andal.exe
. AFAIK, the version used does not matter too much, i think i was able to make it work with the .net framework 2 version of these files, at some point.C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\
resgen.exe
x64\al.exe
<- Make sure you use the x64 version if you compile in x64Let's take this step by step.
Step 1: Create satellite assemblies with VS
None/Do not copy
Resources.resx
andErrors.resx
Errors.Designer.cs
andResources.Designer.cs
.resources
files from your default language.resx
.Resources.resources
andErrors.resources
in yourProperties
folder. Ignore the warning, everything is generated just fine.Resources.resources
andErrors.resources
' properties toEmbedded Resource/Do not copy
In the pre-build event, add the following lines
Build once. The pre-build event will generate .resources files for both files, for all 3 languages.
Set all
.resources
files' properties toEmbedded Resource/Do not copy
Build again, visual studio will now generate satellite assemblies for all 3 languages.
Explanation
The "neutral"
.resources
file gets embedded in the application's dll. If no satellite assembly is found, the texts will be translated based on that file. In order to modify the default translations, we would have to recompile the application's dll, by rebuilding the entire application. However, cutlure-specific translations have been embedded into satellite assemblies, which can be compiled and shipped individually, without having to touch the application.The pre-build event does the following :
.resources
files for the neutral culture, while automatically creating a.cs
file which maps each resource string to a static property for easy use ("strongly typed resources", just like the.Designer.cs
files automatically created by the default Custom Tool for.resx
files)..resources
files for the french and german cultures..resources
files into english.resources
files..resources
files specific to that culture into that assembly.Testing the satellite assemblies
The application has a button that switches the culture. To test that the satellite assemblies work, you can simply remove one culture, say de-DE, and check that it translates to french but reverts to neutral (english) when german is selected.
A more thorough way to test it would be to generate new satellite assemblies. You can make a script for that.
.resx
files. Do not build again.updateDll.bat
) to generate the satellite assemblies. The following assumes we are building and testing inDebug|x64
.Step 2: Create the satellite assemblies manually and clean up the program's directory
Having a folder for each language next to your application can look pretty bad when the user is expected to interact with that folder (for editing configuration files for example). We will instead put all translations in a single
Languages
directory, to keep things clean.Don't let Visual Studio generate satellite assemblies
.culture.resources
files from the solution (keep the neutral ones,Resources.resources
andErrors.resources
, so that the application's assembly remains bundled with a default translation)..resources
files in the pre-build event. Keep only the culture-neutral ones (remove everything except the first 3 lines).updateDll.bat
. The.culture.resources
files will be generated into theobj\
folder. We do not need one for the english language, the en-US satellite assembly will be generated directly from the neutral.resources
files.Tell the resource manager to look for satellite assemblies in the
Languages
folder. We will need to do that in code.App.xaml.cs
, in theApp
constructor, handle theAppDomain.AssemblyResolve
event for SatelliteLocDemo.resources:Delete the
bin\
directory, build, and test your app. If using theupdateDll.bat
script to generate the satellite assemblies, you'll have to adapt it to the new structure, or generate everything elsewhere and copy-paste the satellite assemblies into the Languages folder.Step 3: Publish and PublishSingleFile
When publishing your app, you need to publish the satellite assemblies aswell. I suppose you could generate all the satellite assemblies in the pre-build event, directly into your projet's structure, and set their properties to
Content/Copy if newer
. This would copy them both into your build directory and into your publish directory. This won't work well withPublishSingleFile
(or maybe it would work withExcludeFromSingleFile
, maybe not), so i chose a different way.We will add a script on the
Publish
event. This one is not accessible from Visual Studio directly, you have to set it in your.csproj
file manually. Simply add the following line at the end, after yourPostBuild
section :Add the publish profile for the app
bin\publish
Publish and test your app. The
Languages
folder should be present inbin\publish
.Enable PublishSingleFile. For .NET6, that's all you have to do, you can ignore the rest of this section. For .NET core 3.1, the published application no longer finds your satellite assemblies, because the build is extracted to a temp directory but the satellite assemblies remain in their original directory.
Modify the
AssemblyResolve
event callback to look for satellite assemblies next to the published .exe instead of the temp locationCurrentDomain_AssemblyResolve
, replace the call toAppDomain.CurrentDomain.BaseDirectory
with a method based onProcess.GetCurrentProcess().MainModule
:Delete your
bin\
directory, then publish again and test your app and translations. Now finally everything's good !