Package and deploy resources in .NET apps

Applications rely on the .NET Framework Resource Manager, represented by the ResourceManager class, to retrieve localized resources. The Resource Manager assumes that a hub and spoke model is used to package and deploy resources. The hub is the main assembly that contains the nonlocalizable executable code and the resources for a single culture, called the neutral or default culture. The default culture is the fallback culture for the application; it is the culture whose resources are used if localized resources cannot be found. Each spoke connects to a satellite assembly that contains the resources for a single culture, but does not contain any code.

There are several advantages to this model:

  • You can incrementally add resources for new cultures after you have deployed an application. Because subsequent development of culture-specific resources can require a significant amount of time, this allows you to release your main application first, and deliver culture-specific resources at a later date.
  • You can update and change an application's satellite assemblies without recompiling the application.
  • An application needs to load only those satellite assemblies that contain the resources needed for a particular culture. This can significantly reduce the use of system resources.

However, there are also disadvantages to this model:

  • You must manage multiple sets of resources.
  • The initial cost of testing an application increases, because you must test several configurations. Note that in the long term it will be easier and less expensive to test one core application with several satellites, than to test and maintain several parallel international versions.

Resource naming conventions

When you package your application's resources, you must name them using the resource naming conventions that the common language runtime expects. The runtime identifies a resource by its culture name. Each culture is given a unique name, which is usually a combination of a two-letter, lowercase culture name associated with a language and, if required, a two-letter, uppercase subculture name associated with a country or region. The subculture name follows the culture name, separated by a dash (-). Examples include ja-JP for Japanese as spoken in Japan, en-US for English as spoken in the United States, de-DE for German as spoken in Germany, or de-AT for German as spoken in Austria. See the Language tag column in the list of language/region names supported by Windows. Culture names follow the standard defined by BCP 47.

Note

There are some exceptions for the two-letter culture names, such as zh-Hans for Chinese (Simplified).

For more information, see Create resource files and Create satellite assemblies.

The resource fallback process

The hub and spoke model for packaging and deploying resources uses a fallback process to locate appropriate resources. If an application requests a localized resource that's unavailable, the common language runtime searches the hierarchy of cultures for an appropriate fallback resource that most closely matches the user's application's request, and throws an exception only as a last resort. At each level of the hierarchy, if an appropriate resource is found, the runtime uses it. If the resource is not found, the search continues at the next level.

To improve lookup performance, apply the NeutralResourcesLanguageAttribute attribute to your main assembly, and pass it the name of the neutral language that will work with your main assembly.

.NET Framework resource fallback process

The .NET Framework resource fallback process involves the following steps:

Tip

You may be able to use the <relativeBindForResources> configuration element to optimize the resource fallback process and the process by which the runtime probes for resource assemblies. For more information, see Optimizing the resource fallback process.

  1. The runtime first checks the global assembly cache for an assembly that matches the requested culture for your application.

    The global assembly cache can store resource assemblies that are shared by many applications. This frees you from having to include specific sets of resources in the directory structure of every application you create. If the runtime finds a reference to the assembly, it searches the assembly for the requested resource. If it finds the entry in the assembly, it uses the requested resource. If it doesn't find the entry, it continues the search.

  2. The runtime next checks the directory of the currently executing assembly for a subdirectory that matches the requested culture. If it finds the subdirectory, it searches that subdirectory for a valid satellite assembly for the requested culture. The runtime then searches the satellite assembly for the requested resource. If it finds the resource in the assembly, it uses it. If it doesn't find the resource, it continues the search.

  3. The runtime next queries the Windows Installer to determine whether the satellite assembly is to be installed on demand. If so, it handles the installation, loads the assembly, and searches it or the requested resource. If it finds the resource in the assembly, it uses it. If it doesn't find the resource, it continues the search.

  4. The runtime raises the AppDomain.AssemblyResolve event to indicate that it is unable to find the satellite assembly. If you choose to handle the event, your event handler can return a reference to the satellite assembly whose resources will be used for the lookup. Otherwise, the event handler returns null and the search continues.

  5. The runtime next searches the global assembly cache again, this time for the parent assembly of the requested culture. If the parent assembly exists in the global assembly cache, the runtime searches the assembly for the requested resource.

    The parent culture is defined as the appropriate fallback culture. Consider parents as fallback candidates, because providing any resource is preferable to throwing an exception. This process also allows you to reuse resources. You should include a particular resource at the parent level only if the child culture doesn't need to localize the requested resource. For example, if you supply satellite assemblies for en (neutral English), en-GB (English as spoken in the United Kingdom), and en-US (English as spoken in the United States), the en satellite would contain the common terminology, and the en-GB and en-US satellites could provide overrides for only those terms that differ.

  6. The runtime next checks the directory of the currently executing assembly to see if it contains a parent directory. If a parent directory exists, the runtime searches the directory for a valid satellite assembly for the parent culture. If it finds the assembly, the runtime searches the assembly for the requested resource. If it finds the resource, it uses it. If it doesn't find the resource, it continues the search.

  7. The runtime next queries the Windows Installer to determine whether the parent satellite assembly is to be installed on demand. If so, it handles the installation, loads the assembly, and searches it or the requested resource. If it finds the resource in the assembly, it uses it. If it doesn't find the resource, it continues the search.

  8. The runtime raises the AppDomain.AssemblyResolve event to indicate that it is unable to find an appropriate fallback resource. If you choose to handle the event, your event handler can return a reference to the satellite assembly whose resources will be used for the lookup. Otherwise, the event handler returns null and the search continues.

  9. The runtime next searches parent assemblies, as in the previous three steps, through many potential levels. Each culture has only one parent, which is defined by the CultureInfo.Parent property, but a parent might have its own parent. The search for parent cultures stops when a culture's Parent property returns CultureInfo.InvariantCulture; for resource fallback, the invariant culture is not considered a parent culture or a culture that can have resources.

  10. If the culture that was originally specified and all parents have been searched and the resource is still not found, the resource for the default (fallback) culture is used. Typically, the resources for the default culture are included in the main application assembly. However, you can specify a value of Satellite for the Location property of the NeutralResourcesLanguageAttribute attribute to indicate that the ultimate fallback location for resources is a satellite assembly, rather than the main assembly.

    Note

    The default resource is the only resource that can be compiled with the main assembly. Unless you specify a satellite assembly by using the NeutralResourcesLanguageAttribute attribute, it is the ultimate fallback (final parent). Therefore, we recommend that you always include a default set of resources in your main assembly. This helps prevent exceptions from being thrown. By including a default resource, file you provide a fallback for all resources and ensure that at least one resource is always present for the user, even if it is not culturally specific.

  11. Finally, if the runtime doesn't find a resource for a default (fallback) culture, a MissingManifestResourceException or MissingSatelliteAssemblyException exception is thrown to indicate that the resource could not be found.

For example, suppose the application requests a resource localized for Spanish (Mexico) (the es-MX culture). The runtime first searches the global assembly cache for the assembly that matches es-MX, but doesn't find it. The runtime then searches the directory of the currently executing assembly for an es-MX directory. Failing that, the runtime searches the global assembly cache again for a parent assembly that reflects the appropriate fallback culture — in this case, es (Spanish). If the parent assembly is not found, the runtime searches all potential levels of parent assemblies for the es-MX culture until it finds a corresponding resource. If a resource isn't found, the runtime uses the resource for the default culture.

Optimize the .NET Framework resource fallback process

Under the following conditions, you can optimize the process by which the runtime searches for resources in satellite assemblies:

  • Satellite assemblies are deployed in the same location as the code assembly. If the code assembly is installed in the Global Assembly Cache, satellite assemblies are also installed in the global assembly cache. If the code assembly is installed in a directory, satellite assemblies are installed in culture-specific folders of that directory.

  • Satellite assemblies are not installed on demand.

  • Application code does not handle the AppDomain.AssemblyResolve event.

You optimize the probe for satellite assemblies by including the <relativeBindForResources> element and setting its enabled attribute to true in the application configuration file, as shown in the following example.

<configuration>
   <runtime>
      <relativeBindForResources enabled="true" />
   </runtime>
</configuration>

The optimized probe for satellite assemblies is an opt-in feature. That is, the runtime follows the steps documented in The resource fallback process unless the <relativeBindForResources> element is present in the application's configuration file and its enabled attribute is set to true. If this is the case, the process of probing for a satellite assembly is modified as follows:

  • The runtime uses the location of the parent code assembly to probe for the satellite assembly. If the parent assembly is installed in the global assembly cache, the runtime probes in the cache but not in the application's directory. If the parent assembly is installed in an application directory, the runtime probes in the application directory but not in the global assembly cache.

  • The runtime doesn't query the Windows Installer for on-demand installation of satellite assemblies.

  • If the probe for a particular resource assembly fails, the runtime does not raise the AppDomain.AssemblyResolve event.

.NET Core resource fallback process

The .NET Core resource fallback process involves the following steps:

  1. The runtime attempts to load a satellite assembly for the requested culture.

    • Checks the directory of the currently executing assembly for a subdirectory that matches the requested culture. If it finds the subdirectory, it searches that subdirectory for a valid satellite assembly for the requested culture and loads it.

      Note

      On operating systems with case-sensitive file systems (that is, Linux and macOS), the culture name subdirectory search is case-sensitive. The subdirectory name must exactly match the case of the CultureInfo.Name (for example, es or es-MX).

      Note

      If the programmer has derived a custom assembly load context from AssemblyLoadContext, the situation is complicated. If the executing assembly was loaded into the custom context, the runtime loads the satellite assembly into the custom context. The details are out of scope for this document. See AssemblyLoadContext.

    • If a satellite assemble has not been found, the AssemblyLoadContext raises the AssemblyLoadContext.Resolving event to indicate that it is unable to find the satellite assembly. If you choose to handle the event, your event handler can load and return a reference to the satellite assembly.

    • If a satellite assembly still has not been found, the AssemblyLoadContext causes the AppDomain to trigger an AppDomain.AssemblyResolve event to indicate that it is unable to find the satellite assembly. If you choose to handle the event, your event handler can load and return a reference to the satellite assembly.

  2. If a satellite assembly is found, the runtime searches it for the requested resource. If it finds the resource in the assembly, it uses it. If it doesn't find the resource, it continues the search.

    Note

    To find a resource within the satellite assembly, the runtime searches for the resource file requested by the ResourceManager for the current CultureInfo.Name. Within the resource file it searches for the requested resource name. If either is not found, the resource is treated as not found.

  3. The runtime next searches the parent culture assemblies through many potential levels, each time repeating steps 1 & 2.

    The parent culture is defined as an appropriate fallback culture. Consider parents as fallback candidates, because providing any resource is preferable to throwing an exception. This process also allows you to reuse resources. You should include a particular resource at the parent level only if the child culture doesn't need to localize the requested resource. For example, if you supply satellite assemblies for en (neutral English), en-GB (English as spoken in the United Kingdom), and en-US (English as spoken in the United States), the en satellite contains the common terminology, and the en-GB and en-US satellites provides overrides for only those terms that differ.

    Each culture has only one parent, which is defined by the CultureInfo.Parent property, but a parent might have its own parent. The search for parent cultures stops when a culture's Parent property returns CultureInfo.InvariantCulture. For resource fallback, the invariant culture is not considered a parent culture or a culture that can have resources.

  4. If the culture that was originally specified and all parents have been searched and the resource is still not found, the resource for the default (fallback) culture is used. Typically, the resources for the default culture are included in the main application assembly. However, you can specify a value of Satellite for the Location property to indicate that the ultimate fallback location for resources is a satellite assembly rather than the main assembly.

    Note

    The default resource is the only resource that can be compiled with the main assembly. Unless you specify a satellite assembly by using the NeutralResourcesLanguageAttribute attribute, it is the ultimate fallback (final parent). Therefore, we recommend that you always include a default set of resources in your main assembly. This helps prevent exceptions from being thrown. By including a default resource file, you provide a fallback for all resources and ensure that at least one resource is always present for the user, even if it is not culturally specific.

  5. Finally, if the runtime doesn't find a resource file for a default (fallback) culture, a MissingManifestResourceException or MissingSatelliteAssemblyException exception is thrown to indicate that the resource could not be found. If the resource file is found but the requested resource is not present the request returns null.

Ultimate fallback to satellite assembly

You can optionally remove resources from the main assembly and specify that the runtime should load the ultimate fallback resources from a satellite assembly that corresponds to a specific culture. To control the fallback process, you use the NeutralResourcesLanguageAttribute(String, UltimateResourceFallbackLocation) constructor and supply a value for the UltimateResourceFallbackLocation parameter that specifies whether Resource Manager should extract the fallback resources from the main assembly or from a satellite assembly.

The following .NET Framework example uses the NeutralResourcesLanguageAttribute attribute to store an application's fallback resources in a satellite assembly for the French (fr) language. The example has two text-based resource files that define a single string resource named Greeting. The first, resources.fr.txt, contains a French language resource.

Greeting=Bon jour!

The second, resources,ru.txt, contains a Russian language resource.

Greeting=Добрый день

These two files are compiled to .resources files by running Resource File Generator (resgen.exe) from the command line. For the French language resource, the command is:

resgen.exe resources.fr.txt

For the Russian language resource, the command is:

resgen.exe resources.ru.txt

The .resources files are embedded into dynamic link libraries by running Assembly Linker (al.exe) from the command line for the French language resource as follows:

al /t:lib /embed:resources.fr.resources /culture:fr /out:fr\Example1.resources.dll

and for the Russian language resource as follows:

al /t:lib /embed:resources.ru.resources /culture:ru /out:ru\Example1.resources.dll

The application source code resides in a file named Example1.cs or Example1.vb. It includes the NeutralResourcesLanguageAttribute attribute to indicate that the default application resource is in the fr subdirectory. It instantiates the Resource Manager, retrieves the value of the Greeting resource, and displays it to the console.

using System;
using System.Reflection;
using System.Resources;

[assembly:NeutralResourcesLanguage("fr", UltimateResourceFallbackLocation.Satellite)]

public class Example
{
   public static void Main()
   {
      ResourceManager rm = new ResourceManager("resources",
                                               typeof(Example).Assembly);
      string greeting = rm.GetString("Greeting");
      Console.WriteLine(greeting);
   }
}
Imports System.Reflection
Imports System.Resources

<Assembly: NeutralResourcesLanguage("fr", UltimateResourceFallbackLocation.Satellite)>
Module Example
    Public Sub Main()
        Dim rm As New ResourceManager("resources", GetType(Example).Assembly)
        Dim greeting As String = rm.GetString("Greeting")
        Console.WriteLine(greeting)
    End Sub
End Module

You can then compile C# source code from the command line as follows:

csc Example1.cs

The command for the Visual Basic compiler is very similar:

vbc Example1.vb

Because there are no resources embedded in the main assembly, you do not have to compile by using the /resource switch.

When you run the example from a system whose language is anything other than Russian, it displays the following output:

Bon jour!

Suggested packaging alternative

Time or budget constraints might prevent you from creating a set of resources for every subculture that your application supports. Instead, you can create a single satellite assembly for a parent culture that all related subcultures can use. For example, you can provide a single English satellite assembly (en) that is retrieved by users who request region-specific English resources, and a single German satellite assembly (de) for users who request region-specific German resources. For example, requests for German as spoken in Germany (de-DE), Austria (de-AT), and Switzerland (de-CH) would fall back to the German satellite assembly (de). The default resources are the final fallback and therefore should be the resources that will be requested by the majority of your application's users, so choose these resources carefully. This approach deploys resources that are less culturally specific, but can significantly reduce your application's localization costs.

See also