Skip to content

Localization Generator

Microsoft.Extensions.Localization provides great abstractions, but you lose the strong typing and IDE tooling that .resx files originally gave you. This library fixes that — it source-generates a strongly-typed class for each .resx file, giving you compile-time safety, IntelliSense, and format-string methods with the correct number of parameters.

  • GitHub stars for shinyorg/localizegen
  • Strongly-typed localization classes generated from .resx files at compile time
  • Properties for simple strings, methods for format strings ({0}, {1}, etc.)
  • XML documentation comments generated from .resx comment fields — see default values in IntelliSense
  • Full XAML and Razor binding support with IntelliSense
  • Namespace follows folder structure automatically
  • DI registration generated via AddStronglyTypedLocalizations()
  • Supports internal accessor generation
  1. Install the NuGet packages:

    Terminal window
    dotnet add package Microsoft.Extensions.Localization
    dotnet add package Shiny.Extensions.Localization.Generator
  2. Register in your DI container:

    builder.Services.AddStronglyTypedLocalizations(); // also calls AddLocalization() internally
  3. Create a .resx file alongside an existing class (the names must match):

    MyViewModel.cs
    MyViewModel.resx ← default locale
    MyViewModel.fr-ca.resx ← French-Canadian locale
  4. Inject the generated {ClassName}Localized class:

    public class MyViewModel
    {
    public MyViewModel(MyViewModelLocalized localizer)
    => this.Localizer = localizer;
    public MyViewModelLocalized Localizer { get; }
    }

Every .resx file must have a matching C# class with the same name in the same namespace. If the class doesn’t exist, the generated code will produce a compile error.

Create a .resx file with your localization keys:

<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="Welcome" xml:space="preserve">
<value>Welcome to our app!</value>
<comment>Greeting shown on the home page</comment>
</data>
<data name="UserGreeting" xml:space="preserve">
<value>Hello {0}, welcome to {1}!</value>
<comment>Personalized greeting with user name and app name</comment>
</data>
<data name="ItemCount" xml:space="preserve">
<value>You have {0} items in your cart</value>
</data>
</root>

The generator produces a class with properties for simple strings and methods for format strings:

public partial class MyViewModelLocalized
{
readonly IStringLocalizer localizer;
public MyViewModelLocalized(IStringLocalizer<MyViewModel> localizer)
=> this.localizer = localizer;
/// <summary>
/// Greeting shown on the home page
/// Default: Welcome to our app!
/// </summary>
public string Welcome => this.localizer["Welcome"];
/// <summary>
/// Personalized greeting with user name and app name
/// Default: Hello {0}, welcome to {1}!
/// </summary>
public string UserGreetingFormat(object parameter0, object parameter1)
=> string.Format(this.localizer["UserGreeting"], parameter0, parameter1);
/// <summary>
/// Default: You have {0} items in your cart
/// </summary>
public string ItemCountFormat(object parameter0)
=> string.Format(this.localizer["ItemCount"], parameter0);
/// <summary>
/// Access the underlying IStringLocalizer for edge cases
/// </summary>
public IStringLocalizer Localizer => this.localizer;
}
  • Dots (.), hyphens (-), and spaces in resource keys are converted to underscores (_)
  • Format parameters ({0}, {1}, etc.) are detected automatically and generate a method
  • Method names get a Format suffix unless the key already ends with Format

The folder structure determines the generated namespace:

MyApp/
├── MyViewModel.cs
├── MyViewModel.resx → MyApp.MyViewModelLocalized
├── MyViewModel.fr-ca.resx
├── Features/
│ ├── Settings/
│ │ ├── SettingsPage.cs
│ │ ├── SettingsPage.resx → MyApp.Features.Settings.SettingsPageLocalized
│ │ └── SettingsPage.fr-ca.resx