Programmatic Way to Get All the Available Languages (In Satellite Assemblies)

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 and al.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

  1. 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 and Errors.resx
    • Delete Errors.Designer.cs and Resources.Designer.cs
  2. 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 and Errors.resources in your Properties folder. Ignore the warning, everything is generated just fine.
    • Set Resources.resources and Errors.resources' properties to Embedded 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.
  3. 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 to Embedded 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.

  1. Build the application
  2. Modify the translations directly in your .resx files. Do not build again.
  3. 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 in Debug|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
  4. 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.

  1. Don't let Visual Studio generate satellite assemblies

    • Remove all 6 .culture.resources files from the solution (keep the neutral ones, Resources.resources and Errors.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 the obj\ 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
  2. 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 the App constructor, handle the AppDomain.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;
      }
      }
  3. Delete the bin\ directory, build, and test your app. If using the updateDll.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.

  1. 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 your PostBuild 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>
  2. 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.
  3. Publish and test your app. The Languages folder should be present in bin\publish.

  4. 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.

  5. 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 to AppDomain.CurrentDomain.BaseDirectory with a method based on Process.GetCurrentProcess().MainModule:
    public static string GetBasePath()
    {
    using ProcessModule processModule = Process.GetCurrentProcess().MainModule;
    return Path.GetDirectoryName(processModule?.FileName)!;
    }
  6. 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



Leave a reply



Submit