Generated keys: what qualifies, and what you get

The source generator ships inside both UI packages as an analyzer, together with the MSBuild wiring that feeds it. Nothing has to be configured for it to run.

Which files qualify

Every neutral .resx file produces a typed key class: AppStrings.resx, not AppStrings.de.resx. The neutral file defines the key set; a satellite translates it.

Whether a file is neutral or a satellite is MSBuild's own culture detection, the one that decides which files go into satellite assemblies. So a neutral file with a dot in its name, such as My.Strings.resx, qualifies, while Strings.de.resx does not.

No .Designer.cs accessor is needed. Files with a designer, files without one, and the files of a project that uses the SDK's GenerateResxSource all qualify.

The generator learns each file's resource name - the name MSBuild embeds its resources under - from the build, through the analyzer config file the SDK writes into obj/. The packages' build wiring passes it; a .resx that reaches the generator any other way gets no keys and reports RXLGEN003.

What becomes a key

Only string entries. An entry carrying a type or mimetype attribute — a binary resource, an image, a colour, an icon — is skipped, because its value is not a string a lookup could return.

Resource names that are not valid C# identifiers are sanitized into ones that are, and a collision that sanitizing would create is resolved by appending a number. The class is named after the file, with Keys appended and sanitized the same way: My.Strings.resx produces My_StringsKeys.

// <auto-generated/> - for Resources/AppStrings.resx, with or without AppStrings.Designer.cs
namespace MyApp.Resources;

/// <summary>Typed, compile-validated keys generated from AppStrings.resx.</summary>
public static partial class AppStringsKeys
{
    /// <summary>
    /// The resource manager of AppStrings.resx - the instance every key of this class uses. Register this
    /// instance for search-all lookups, and pass it for scoped lookups.
    /// </summary>
    public static global::System.Resources.ResourceManager ResourceManager { get; } =
        new("MyApp.Resources.AppStrings", typeof(AppStringsKeys).Assembly);

    public static readonly global::RentADeveloper.ResXLocalization.ResourceKey Greeting = new("Greeting", ResourceManager);
    public static readonly global::RentADeveloper.ResXLocalization.ResourceKey WindowTitle = new("WindowTitle", ResourceManager);
    // …one ResourceKey per string entry, sorted by name.
}

Each ResourceKey carries both the key name and the ResourceManager it belongs to. That is what makes a typed lookup direct: it never searches, and two files may use the same key name without colliding.

ResourceManager is the one instance of that file every key uses. Use it wherever the file's resource manager is needed:

Localizer.Current.RegisterResourceManager(AppStringsKeys.ResourceManager);          // search-all
Localizer.Current.Get("Greeting", resourceManager: AppStringsKeys.ResourceManager); // scoped

A resource key named ResourceManager gets a numeric suffix (ResourceManager2), like any other name collision. The resource manager is created from the file's exact resource name, without reflection, so the generated code is trimming and Native AOT safe.

The class is partial, so you can add members of your own to it in a file of your own.

The namespace

The file Namespace of the keys class
has a paired classic designer the designer's namespace
has none, and sets CustomToolNamespace its CustomToolNamespace
has none, and no CustomToolNamespace the resource name without its last segment

A designer pairs when it is a .Designer.cs in the same folder with the same base name that declares a type with a static ResourceManager property — the accessor Visual Studio's and Rider's classic resx tooling writes. So a project with designers keeps its keys classes where they are, including with CustomToolNamespace.

Without a designer, CustomToolNamespace decides when the project sets it on the .resx - the same setting the designer tools use for the designer's namespace:

<ItemGroup>
  <EmbeddedResource Update="Resources\AppStrings.resx" CustomToolNamespace="MyApp.Texts" />
</ItemGroup>

It changes only the namespace: the resource manager still loads the name MSBuild embeds the resources under. With a designer, the designer's namespace wins, which is normally the same value. A blank CustomToolNamespace counts as not set.

Otherwise the resource name decides. MSBuild names Resources/AppStrings.resx in a project whose root namespace is MyApp MyApp.Resources.AppStrings, so the class lands in MyApp.Resources. A LogicalName replaces that name: with LogicalName="Totally.Custom.Name.resources" the resource manager loads Totally.Custom.Name and the class lands in Totally.Custom. Each namespace segment is sanitized into a valid identifier (event becomes @event, My-App becomes My_App), and so is each segment of a CustomToolNamespace; a resource name without a dot means the global namespace.

File, resource name Namespace Class ResourceManager base name
Resources/Strings.resx, N.Resources.Strings, no designer N.Resources StringsKeys N.Resources.Strings
same, designer declares namespace Custom.Ns Custom.Ns StringsKeys N.Resources.Strings
same, no designer, CustomToolNamespace=Texts.Custom Texts.Custom StringsKeys N.Resources.Strings
Logical.resx, LogicalName=Totally.Custom.Name.resources Totally.Custom LogicalKeys Totally.Custom.Name
My.Strings.resx, N.My.Strings N.My My_StringsKeys N.My.Strings

WPF projects. If the XAML build of a WPF project cannot find a keys class of a .resx without a designer - CS0234 or CS0103, reported for a project named <YourProject>_<random>_wpftmp - the project computes its RootNamespace in a way the build cannot map back to the real one, for example with ToUpper(). Set a literal RootNamespace, or a CustomToolNamespace on the .resx.

Projects with designers

Designers keep working, and the generator uses them for the namespace only. Their string properties, such as AppStrings.Greeting, read in CultureInfo.CurrentUICulture, so they do not follow Localizer.CurrentCulture or live language switching. Use the typed keys instead: Localizer.Current.Get(AppStringsKeys.Greeting).

Diagnostics

Id Meaning
RXLGEN001 A neutral .resx file is not well-formed XML. The diagnostic points at the file
RXLGEN003 A .resx file reached the generator without its MSBuild resource name, so no keys were generated. Include it as an EmbeddedResource of a project that references ResXLocalization.Avalonia or ResXLocalization.WPF
RXLGEN004 Two neutral .resx files produce the same keys class - for example two Strings.resx in different folders whose designers declare the same namespace. Both files report it and neither class is generated. Rename one of the files, or change its designer's namespace or its CustomToolNamespace

A file reports at most one of them, checked in this order: RXLGEN003, then RXLGEN004, then RXLGEN001. There is no RXLGEN002: a .resx no longer needs a designer.

The compiler floor

The generator is built against Roslyn 4.8 — the compiler in the .NET 8 SDK. A generator built against a newer Roslyn simply does not load in an older compiler: the build reports CS9057, skips generation, and then fails on the missing …Keys types. Building the generator against the floor is what keeps the packages usable on every SDK from .NET 8 upward, and CI proves it on every run by consuming the packed packages with nothing but the .NET 8 SDK installed.