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.exeandal.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.exex64\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.resxandErrors.resx - Delete
Errors.Designer.csandResources.Designer.cs
- Set all resx files' properties to
- Generate
.resourcesfiles 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.resourcesandErrors.resourcesin yourPropertiesfolder. Ignore the warning, everything is generated just fine. - Set
Resources.resourcesandErrors.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 /YBuild once. The pre-build event will generate .resources files for both files, for all 3 languages.
Set all
.resourcesfiles' properties toEmbedded Resource/Do not copyBuild 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
.resourcesfiles for the neutral culture, while automatically creating a.csfile which maps each resource string to a static property for easy use ("strongly typed resources", just like the.Designer.csfiles automatically created by the default Custom Tool for.resxfiles). - Generate
.resourcesfiles for the french and german cultures. - Copy the culture-neutral
.resourcesfiles into english.resourcesfiles. - 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
.resourcesfiles 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
.resxfiles. 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.resourcesfiles from the solution (keep the neutral ones,Resources.resourcesandErrors.resources, so that the application's assembly remains bundled with a default translation). - Avoid creating the culture-specific
.resourcesfiles 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.resourcesfiles 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.resourcesfiles.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
