Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Localization Generator Release Notes

Dots in resource file names. Only .resx files with exactly one dot in the name were processed, so Errors.Validation.resx was skipped without any message. Culture files are now recognised by their culture, not by counting dots, and the extra dots in a default file’s name become namespace segments: Errors.Validation.resx generates MyApp.Errors.ValidationLocalized, paired with MyApp.Errors.Validation. That is the name MSBuild gives the embedded resource, so IStringLocalizer<T> finds it, and Errors.Validation.fr-CA.resx is still its French-Canadian translation. The culture decision comes from MSBuild itself (WithCulture on the EmbeddedResource), so it always matches the satellite assemblies the build produces. If that information is missing, the last segment is checked against the cultures the machine knows, or, with no culture data at all, against whether the default file exists. In GeneratedLocalizers.All, Name is the file name without .resx (Errors.Validation), so two Validation.resx files in different places don’t collide.

Enhancement
Warning for a culture file with no default file. Orphan.de.resx with no Orphan.resx used to produce nothing and say nothing. It now raises SHINY0001 on that file: “‘Orphan.de.resx’ is a ‘de’ culture file, but ‘Orphan.resx’ does not exist, so no localizer was generated for it.”

GeneratedLocalizers.All: list every localizer without reflection. The generator now writes a GeneratedLocalizers class next to AddStronglyTypedLocalizations(), with one (Name, ResourceSource, LocalizedType) entry per .resx file. Name is the resource class name (HomeViewModel, Common), ResourceSource is the T for IStringLocalizer<T>, and LocalizedType is the generated wrapper. Apps that look localizers up by name, such as a XAML markup extension, no longer have to scan assemblies or the IServiceCollection, so this works under trimming and NativeAOT: GeneratedLocalizers.All.ToDictionary(x => x.Name, x => factory.Create(x.ResourceSource)). It uses the same root namespace and GenerateLocalizersInternal visibility as the extension method, and an assembly with no .resx files gets an empty array.

Feature
No more empty stub classes. IStringLocalizer<T> needs a T, so every .resx needed a matching class, often an empty one written only for that. If no class with the .resx file’s name exists in the namespace, the generator now creates an empty partial class for it. Everything still goes through DI: inject the generated *Localized class as before. Declare the class yourself, partial or not, whenever you want to, and the generator stops creating it. Only the project being compiled is checked, so a class with the same name in a referenced project doesn’t count.

Enhancement
Generated classes match the visibility of their class. GenerateLocalizersInternal applied to every generated class, so a project couldn’t have both internal services and public Blazor components: one or the other failed to compile with CS0051 or CS0053. Each *Localized class is now public when its class is public and internal otherwise. GenerateLocalizersInternal only applies when the generator creates the class itself. A public class in a project that sets GenerateLocalizersInternal to true now gets a public localizer where it used to get an internal one.

Feature
If you have format index parameters in your string, we now generate a method MyKeyFormat with the number of parameters to match indexes
Enhancement
Code comments are now generated with properties and methods so you can see what the default value is
Feature
Initial Public Release