.NET Reference

.NET satellite assemblies: folder layout, naming, and fallback.

A satellite assembly holds the resources of one language and no code. This page shows where .NET Framework 4.8 and .NET 5–10 look for them, how they must be named, what happens when a language is missing, and three ways to build them.

At startup UI culture de-DE from Windows or set by the app Release folder EXE MyApp.exe DE de\ loaded FR fr\ JA ja\ The window Neue Bestellung Kunde: Menge: Erstellen in German At startup UI culture de-DE Windows or app EXE MyApp.exe DEde\ FRfr\ JAja\ Neue Bestellung Kunde: Erstellen Menge:
File
MyApp.resources.dll, the same name for every language
Folder
A culture folder next to the assembly: de\, fr-CA\
Chosen by
CultureInfo.CurrentUICulture
Missing language
Parent culture, then the neutral resources
On this page

One main assembly, one satellite per language.

The main assembly carries the code and the neutral resources, usually English. Every other language ships as a separate assembly with only resources in it. At run time ResourceManager loads the satellite that matches the user's UI culture.

What you gain

  • Add a language after release: copy one more folder, the EXE stays the same.
  • Fix a translation by replacing one satellite, without a new build of the application.
  • One installer for every language, or one package per language.

What you take on

  • Each satellite must match its main assembly: name, resource names, and on .NET Framework the version and the key.
  • Each language is one more configuration to test.
  • A missing or misplaced folder fails quietly: the user sees English.

Folder layout and names.

Every assembly that has resources gets its own satellites, in culture folders next to it. A class library MyLib.dll ships de\MyLib.resources.dll, separate from the application's.

MyApp.exe
MyApp.dll                     main assembly: code + neutral resources
MyLib.dll
de\
    MyApp.resources.dll       German, for de, de-DE, de-AT, de-CH
    MyLib.resources.dll
fr-CA\
    MyApp.resources.dll       French (Canada) only
zh-Hans\
    MyApp.resources.dll       Chinese (Simplified)
  • Folder name is the culture name exactly as CultureInfo.Name writes it: de, pt-BR, sr-Latn-RS. Not German, not de_DE.
  • File name is the assembly name plus .resources.dll, in every folder. The folder tells the language, not the file.
  • Resource names inside carry the culture: MyApp.Strings.resources in the main assembly becomes MyApp.Strings.de.resources in the German satellite.
  • The satellite declares its culture (AssemblyCulture("de")); the main assembly declares none.

Where .NET looks for a satellite.

The rules differ between .NET Framework and .NET 5–10. Most "it works on my machine" reports come from the last two rows.

Satellite loading in .NET Framework 4.8 and .NET 5–10
.NET Framework 4.8.NET 5–10
WhereThe global assembly cache for strong-named assemblies, then <app folder>\de\MyApp.resources.dll and <app folder>\de\MyApp.resources\MyApp.resources.dllThe culture subfolder next to the file the main assembly was loaded from: de\MyApp.resources.dll; on Linux and macOS the folder name is case-sensitive
VersionMust equal the main assembly's version, or the version named in SatelliteContractVersionBuild satellites with each release of the main assembly
Strong nameA strong-named main assembly needs satellites signed with the same keyUnsigned satellites load
Diagnose withFusion Log Viewer (fuslogvw.exe)The AssemblyLoadContext.Default.Resolving event, which fires for every satellite the default probing did not find

Applications on .NET 1.x–3.5 use the .NET Framework rules with older tooling. To localize them with Lingobit, use Localizer 9.

Fallback: from the exact culture to the neutral resources.

ResourceManager looks for each resource separately: first in the satellite of the UI culture, then in its parent culture, and last in the neutral resources. A string missing from de-AT comes from de, then from English.

UI culture de-DE: each string is looked up on its own de-DE\not shippedde\neutralMyApp.exeCustomer:—Kunde:Customer:Quantity:—Menge:Quantity:Cancel—missingCancel Neue BestellungKunde:Menge:Cancel Cancel is missing in de\,so it comes from the neutralresources; the rest is German Each string on its own de-DE\not shippedde\neutralMyApp.exeCustomer:—Kunde:Customer:Quantity:—Menge:Quantity:Cancel—missingCancel Neue BestellungKunde:Menge:Cancel
  • Ship the parent culture. One de\ folder serves German in Germany, Austria, and Switzerland. Add de-CH\ only for the strings that differ.
  • Keep the neutral resources complete. They are the last stop. When a resource is missing there too, GetString returns null; when the whole resource file is missing, .NET throws MissingManifestResourceException.
  • Mark the neutral language with [assembly: NeutralResourcesLanguage("en")]. For English users .NET then skips the search for an en satellite.
  • Neutral resources in a satellite. With UltimateResourceFallbackLocation.Satellite the neutral resources live in a satellite too, for example en-US\, and that folder must ship. WPF projects with <UICulture> in the project file work this way.
// Set the UI culture before the first resource is read and before any window is created
var culture = CultureInfo.GetCultureInfo("de-DE");
CultureInfo.DefaultThreadCurrentUICulture = culture;
CultureInfo.CurrentUICulture = culture;

var rm = new ResourceManager("MyApp.Strings", typeof(Program).Assembly);
string title = rm.GetString("MainTitle");   // de-DE → de → neutral

Build

Three ways to build satellite assemblies.

Pick by what you have: the source with translated .resx files, single resource files, or only the compiled application.

1. From translated .resx files, with your build

Put Strings.de.resx next to Strings.resx in the project. MSBuild compiles every culture-suffixed .resx into bin\…\de\MyApp.resources.dll. The translated files live in source control and go through your normal build.

2. By hand, for one resource file

On .NET Framework you can compile a .resx and link it with the Assembly Linker. -template copies the version and other metadata of the main assembly; a strong-named application also needs -keyfile with its key:

resgen Strings.de.resx MyApp.Strings.de.resources
al -target:lib -embed:MyApp.Strings.de.resources -culture:de -template:MyApp.exe -out:de\MyApp.resources.dll

3. From the compiled application, with Lingobit Localizer

Add MyApp.exe or MyApp.dll to a project, translate the forms, windows, and strings it contains, and run Create Localized Files. Lingobit writes de\MyApp.resources.dll and the other languages next to your files; the EXE and DLLs are not changed and nothing is rebuilt. The same workflow covers WinForms, WPF (BAML), and plain resources, on .NET Framework 4.8 and .NET 5–10; for WPF it replaces LocBaml, which stops at .NET Framework.

  • Version: satellites get the version of the original assembly; another version can be set in the project.
  • .NET 5–10: building the satellites needs the .NET SDK of that version, or a newer one if the project allows it.
  • .NET Framework, strong-named: the satellites are signed with your key, set in the project.
  • Check the result: Run Localized Application starts the application in the chosen language.

Questions.

Can I add a language without rebuilding the application?

Yes. Copy the new culture folder next to the assembly. On .NET Framework the satellite's version must match the main assembly or its SatelliteContractVersion, and a strong-named application needs the satellite signed with its key.

Do NuGet packages ship satellites too?

Many do, for example localized exception messages. <SatelliteResourceLanguages>en;de</SatelliteResourceLanguages> in the project file limits which of the package satellites are copied to the output. It does not affect your own .resx files.

What about single-file and trimmed publishing?

Satellites are part of the published output like any other assembly. After changing publish settings, start the published application in each language once: that is the only check that covers every way a satellite can go missing.

I already have translated satellites from another tool.

Add the current build to a Lingobit project and use Import Translations from Localized Files: it takes the translations from your existing .resources.dll satellites where they still match the current build.