Programmatic way to get all the available languages (in satellite assemblies)
Using what Rune Grimstad said I end up with this:
string executablePath = Path.GetDirectoryName(Application.ExecutablePath);
string[] directories = Directory.GetDirectories(executablePath);
foreach (string s in directories)
{
try
{
DirectoryInfo langDirectory = new DirectoryInfo(s);
cmbLanguage.Items.Add(CultureInfo.GetCultureInfo(langDirectory.Name));
}
catch (Exception)
{
}
}
or another way
int pathLenght = executablePath.Length + 1;
foreach (string s in directories)
{
try
{
cmbLanguage.Items.Add(CultureInfo.GetCultureInfo(s.Remove(0, pathLenght)));
}
catch (Exception)
{
}
}
I still don't think that this is a good idea ...
Programmatic way to get all the available languages (in satellite assemblies)
Using what Rune Grimstad said I end up with this:
string executablePath = Path.GetDirectoryName(Application.ExecutablePath);
string[] directories = Directory.GetDirectories(executablePath);
foreach (string s in directories)
{
try
{
DirectoryInfo langDirectory = new DirectoryInfo(s);
cmbLanguage.Items.Add(CultureInfo.GetCultureInfo(langDirectory.Name));
}
catch (Exception)
{
}
}
or another way
int pathLenght = executablePath.Length + 1;
foreach (string s in directories)
{
try
{
cmbLanguage.Items.Add(CultureInfo.GetCultureInfo(s.Remove(0, pathLenght)));
}
catch (Exception)
{
}
}
I still don't think that this is a good idea ...
Enumerate the languages supported by satellite assemblies
I have solve above problem with following solution:
This would be one of solution on basis of following statement:
Each satellite assembly for a specific language is named the same but lies in a sub-folder named after the specific culture e.g. fr or fr-CA.
public IEnumerable<CultureInfo> GetSupportedCulture()
{
//Get all culture
CultureInfo[] culture = CultureInfo.GetCultures(CultureTypes.AllCultures);
//Find the location where application installed.
string exeLocation = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path));
//Return all culture for which satellite folder found with culture code.
return culture.Where(cultureInfo => Directory.Exists(Path.Combine(exeLocation, cultureInfo.Name)));
}
Getting a list of available (languages) resx files
Well, even if you could get a list (and you can I guess, by inspecting the filesystem), depending on how you implemented your localisation, it may not be accurate (may have not provided a translation for the specific .resx you're looking for).
I'd suggest the best way to do this is in a .config somewhere (or database). That way you can disable languages independantly of the .resx system; which is something that will inevitably come up.
Get all available cultures from a .resx file group
Look for satellite assemblies in your application's directory: for each subdirectory, check if its name corresponds to a culture name, and if it contains a .resources.dll
file :
public IEnumerable<CultureInfo> GetAvailableCultures()
{
var programLocation = Process.GetCurrentProcess().MainModule.FileName;
var resourceFileName = Path.GetFileNameWithoutExtension(programLocation) + ".resources.dll";
var rootDir = new DirectoryInfo(Path.GetDirectoryName(programLocation));
return from c in CultureInfo.GetCultures(CultureTypes.AllCultures)
join d in rootDir.EnumerateDirectories() on c.IetfLanguageTag equals d.Name
where d.EnumerateFiles(resourceFileName).Any()
select c;
}
Invariant .resx file
You can use the NeutralResourcesLanguageAttribute assembly attribute to set the neutral language.
[assembly: NeutralResourcesLanguageAttribute("en-US", UltimateResourceFallbackLocation.Satellite)]
This also makes it "fallback" to other satellites if a key is not found in one resource (if you ask for a key which is not defined in resource.en-US.resx
, it'll search in resource.en.resx
and finally in resource.resx
)
I'm not sure how you are listing the language names, but in case this still gives InvariantCulture
, you could have an empty Localization.en.resx
, and with this method it'll find all missing resources in Localization.resx
, so no need to have them in sync.
How do i make satellite assemblies work with my WPF app in PublishSingleFile
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 /YBuild 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 %dedePath% mkdir %dedePath%
%al% -target:lib -embed:%resourcesPath%\Resources.de-DE.resources,$(ProjectName).Properties.Resources.de-DE.resources -embed:%resourcesPath%\Errors.de-DE.resources,$(ProjectName).Properties.Errors.de-DE.resources -template:$(TargetPath) -culture:de-DE -platform:x64 -out:%dedePath%\$(TargetName).resources.dll
- Remove all 6
Tell the resource manager to look for satellite assemblies in the
Languages
folder. We will need to do that in code.- In your
App.xaml.cs
, in theApp
constructor, handle theAppDomain.AssemblyResolve
event for SatelliteLocDemo.resources:public App()
{
AppDomain.CurrentDomain.AssemblyResolve += this.CurrentDomain_AssemblyResolve;
}
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
try
{
if (args.Name != null && args.Name.StartsWith("SatelliteLocDemo.resources"))
{
string assemblyPath = $"{AppDomain.CurrentDomain.BaseDirectory}\\Languages\\{Thread.CurrentThread.CurrentUICulture.Name}\\SatelliteLocDemo.resources.dll";
Assembly assembly = Assembly.LoadFrom(assemblyPath);
return assembly;
}
return null;
}
catch (Exception e)
{
Trace.WriteLine($"Error loading translations for {args.Name}");
Trace.WriteLine(e);
return null;
}
}
- In your
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 with PublishSingleFile
(or maybe it would work with ExcludeFromSingleFile
, 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 :<Target Name="PublishLanguages" AfterTargets="Publish">
<ItemGroup>
<LangFiles Include="$(OutDir)\Languages\**\*.*" />
</ItemGroup>
<Exec Command="echo Publishing Language files" />
<Copy SourceFiles="@(LangFiles)" DestinationFiles="@(LangFiles->'$(PublishDir)\Languages\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>Add the publish profile for the app
- Folder, into
bin\publish
- Framework-dependent, Debug, win-x64, readytorun
- Do not set it to single-file yet.
- Folder, into
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 location- In
CurrentDomain_AssemblyResolve
, replace the call toAppDomain.CurrentDomain.BaseDirectory
with a method based onProcess.GetCurrentProcess().MainModule
:
public static string GetBasePath()
{
using ProcessModule processModule = Process.GetCurrentProcess().MainModule;
return Path.GetDirectoryName(processModule?.FileName)!;
}- In
Delete your
bin\
directory, then publish again and test your app and translations. Now finally everything's good !
.NET Localization Satellite Assemblies
You can create another project that houses only the localized resource files - build these into Satellite Assemblies and then copy them into your main project.
For example:
Create a new project in your existing solution eg: myApp.Localized
Then change the assembly name to match the name of the main application (myApp) and change the default namespace (C#) or the root namespace (Visual Basic) to match the default or root namespace of the main application (myApp).
Now when you build the myApp.Localized project it will create the satellite assemblies for each language-culture in the bin folder.
A Satellite Assembly for each language will be located in a subfolder within the bin.
Copy each language folder into your main application bin when you are ready to deploy the project. (the assemblies must reside inside the named language folders)
Repeat the above for each project (DAL, BLL, PLL)
This allows you to work with the Neutral language in your main project and keep all other languages in a separate project. Thus reducing complexity, build time and clutter in the project!
You can find more information at:
http://visualstudiomagazine.com/articles/2005/01/01/make-the-best-of-net-resource-files.aspx
Related Topics
How to Get Text Between Nested Parentheses
Wpf Equivalent to Textrenderer
How to Have a Delegate as Attribute Parameter
How to Format Timespan in Xaml
Parsing HTML to Get Content Using C#
What Happens While Waiting on a Task's Result
Getting the Index of a Particular Item in Array
Relative Path to Absolute Path in C#
Reversible Shuffle Algorithm Using a Key
Can a Picturebox Show Animated Gif in Windows Application
Performance of Linq Any VS Firstordefault != Null
Date Difference in Years Using C#
Closing a File After File.Create
.Net Application Cannot Start and Receive Xamlparseexception