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 ?
CodePudding user response:
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
and Errors.resx
, in a Properties
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 :
- We will use 2 programs that are part of the .NET SDK :
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. - The location of these files on your system may vary. I used the ones in
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 x64
- We use PublishSingleFile to avoid having a huge mess in our app's folder, so we'd like to avoid having 20 folders in there if the app is localized in 20 languages.
- In order to see what resources are embedded in what assembly, it can be useful to inspect assemblies, for example with ILSpy.
Let's take this step by step.
Step 1: Create satellite assemblies with VS
- While keeping your translation data intact, remove the default configuration for handling .resx files
- Set all resx files' properties to
None/Do not copy
- Remove the custom Tool from
Resources.resx
andErrors.resx
- Delete
Errors.Designer.cs
andResources.Designer.cs
- Set all resx files' properties to
- Generate
.resources
files from your default language.resx
.- Set the pre-build event to (the path to your resgen command might differ):
set resgen = "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\ResGen.exe" resgen Properties\Resources.resx /str:cs,$(ProjectName).Properties,Resources /publicClass resgen Properties\Errors.resx /str:cs,$(ProjectName).Properties,Errors /publicClass
- Try building the application. It will create the files
Resources.resources
andErrors.resources
in yourProperties
folder. Ignore the warning, everything is generated just fine. - Set
Resources.resources
andErrors.resources
' properties toEmbedded Resource/Do not copy
- Rebuild the application. The program can't find the french and german translations, but has the english defaults embedded into it.
- Set the pre-build event to (the path to your resgen command might differ):
- Have Visual Studio generate satellite assemblies
In the pre-build event, add the following lines
echo "fr-FR" %resgen% Properties\Errors.fr-FR.resx %resgen% Properties\Resources.fr-FR.resx echo "de-DE" %resgen% Properties\Errors.de-DE.resx %resgen% Properties\Resources.de-DE.resx echo "en-US" echo F|xcopy Properties\Errors.resources Properties\Errors.en-US.resources /Y echo F|xcopy Properties\Resources.resources Properties\Resources.en-US.resources /Y
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 :
- Generate
.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). - Generate
.resources
files for the french and german cultures. - Copy the culture-neutral
.resources
files into english.resources
files. - By setting those to Embedded resources, visual studio will automatically:
- Embed the culture-neutral resources into the application's dll, so that texts are always translated even if the satellite assemblies can't be found.
- Create a satellite assembly for each culture that it finds, and embed the
.resources
files specific to that culture into that assembly. - Therefore, we end up with 3 satellite assemblies, for the en-US, fr-FR, and de-DE cultures.
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.
- Build the application
- Modify the translations directly in your
.resx
files. Do not build again. - Make a script (in the github project, it's called
updateDll.bat
) to generate the satellite assemblies. The following assumes we are building and testing inDebug|x64
.set resgen="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\ResGen.exe" set al="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\al.exe" %resgen% Properties\Resources.resx %resgen% Properties\Errors.resx %resgen% Properties\Resources.fr-FR.resx %resgen% Properties\Errors.fr-FR.resx %resgen% Properties\Resources.de-DE.resx %resgen% Properties\Errors.de-DE.resx %al% -target:lib -embed:Properties\Resources.resources,SatelliteLocDemo.Properties.Resources.en-US.resources -embed:Properties\Errors.resources,SatelliteLocDemo.Properties.Errors.en-US.resources -template:bin\x64\Debug\SatelliteLocDemo.dll -culture:en-US -out:bin\x64\Debug\en-US\SatelliteLocDemo.resources.dll %al% -target:lib -embed:Properties\Resources.fr-FR.resources,SatelliteLocDemo.Properties.Resources.fr-FR.resources -embed:Properties\Errors.fr-FR.resources,SatelliteLocDemo.Properties.Errors.fr-FR.resources -template:bin\x64\Debug\SatelliteLocDemo.dll -culture:fr-FR -out:bin\x64\Debug\fr-FR\SatelliteLocDemo.resources.dll %al% -target:lib -embed:Properties\Resources.de-DE.resources,SatelliteLocDemo.Properties.Resources.de-DE.resources -embed:Properties\Errors.de-DE.resources,SatelliteLocDemo.Properties.Errors.de-DE.resources -template:bin\x64\Debug\SatelliteLocDemo.dll -culture:de-DE -out:bin\x64\Debug\de-DE\SatelliteLocDemo.resources.dll
- Run the script, then navigate to your build folder and run your application from the explorer (if you run from VS, it will first rebuild the entire application).
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
- Remove all 6
.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). - Avoid creating the culture-specific
.resources
files in the pre-build event. Keep only the culture-neutral ones (remove everything except the first 3 lines). - Generate the satellite assemblies manually in a post-build event, similarly to how we did it with
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.set resgen="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\ResGen.exe" set al="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\al.exe" echo "Compile resx" SET resourcesPath="obj\$(PlatformName)\$(ConfigurationName)\Properties" if not exist %resourcesPath% mkdir %resourcesPath% %resgen% Properties\Resources.fr-FR.resx %resourcesPath%\Resources.fr-FR.resources %resgen% Properties\Resources.de-DE.resx %resourcesPath%\Resources.de-DE.resources %resgen% Properties\Errors.fr-FR.resx %resourcesPath%\Errors.fr-FR.resources %resgen% Properties\Errors.de-DE.resx %resourcesPath%\Errors.de-DE.resources echo "en-US" SET enusPath="$(TargetDir)\Languages\en-US" if not exist %enusPath% mkdir %enusPath% %al% -target:lib -embed:Properties\Resources.resources,$(ProjectName).Properties.Resources.en-US.resources -embed:Properties\Errors.resources,$(ProjectName).Properties.Errors.en-US.resources -template:$(TargetPath) -culture:en-US -platform:x64 -out:%enusPath%\$(TargetName).resources.dll echo "fr-FR" SET frfrPath="$(TargetDir)\Languages\fr-FR" if not exist %frfrPath% mkdir %frfrPath% %al% -target:lib -embed:%resourcesPath%\Resources.fr-FR.resources,$(ProjectName).Properties.Resources.fr-FR.resources -embed:%resourcesPath%\Errors.fr-FR.resources,$(ProjectName).Properties.Errors.fr-FR.resources -template:$(TargetPath) -culture:fr-FR -platform:x64 -out:%frfrPath%\$(TargetName).resources.dll echo "de-DE" SET dedePath="$(TargetDir)\Languages\de-DE" if not exist
- Remove all 6