Home

Awesome

ResXGenerator

ResXGenerator is a C# source generator to generate strongly-typed resource classes for looking up localized strings.

NOTE: This is an independent fork of VocaDb/ResXFileCodeGenerator.

Usage

Install the Aigamo.ResXGenerator package:

dotnet add package Aigamo.ResXGenerator

Generated source from ActivityEntrySortRuleNames.resx:

// ------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
namespace Resources
{
    using System.Globalization;
    using System.Resources;

    public static class ActivityEntrySortRuleNames
    {
        private static ResourceManager? s_resourceManager;
        public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof(ActivityEntrySortRuleNames).Assembly);
        public static CultureInfo? CultureInfo { get; set; }

        /// <summary>
        /// Looks up a localized string similar to Oldest.
        /// </summary>
        public static string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo);

        /// <summary>
        /// Looks up a localized string similar to Newest.
        /// </summary>
        public static string? CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo);
    }
}

New in version 3

New in version 3.1

Options

PublicClass (per file or globally)

Use cases: https://github.com/VocaDB/ResXFileCodeGenerator/issues/2.

Since version 2.0.0, ResXGenerator generates internal classes by default. You can change this behavior by setting PublicClass to true.

<ItemGroup>
  <EmbeddedResource Update="Resources\ArtistCategoriesNames.resx">
    <PublicClass>true</PublicClass>
  </EmbeddedResource>
</ItemGroup>

or

<ItemGroup>
  <EmbeddedResource Update="Resources\ArtistCategoriesNames.resx" PublicClass="true" />
</ItemGroup>

If you want to apply this globally, use

<PropertyGroup>
  <ResXGenerator_PublicClass>true</ResXGenerator_PublicClass>
</PropertyGroup>

NullForgivingOperators (globally)

Use cases: https://github.com/VocaDB/ResXFileCodeGenerator/issues/1.

<PropertyGroup>
  <ResXGenerator_NullForgivingOperators>true</ResXGenerator_NullForgivingOperators>
</PropertyGroup>

By setting ResXGenerator_NullForgivingOperators to true, ResXGenerator generates

public static string CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo)!;

instead of

public static string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo);

Non-static classes (per file or globally)

To use generated resources with Microsoft.Extensions.Localization IStringLocalizer<T> and resource manager, the resolved type cannot be a static class. You can disable default behavior per file by setting the value to false.

<ItemGroup>
  <EmbeddedResource Update="Resources\ArtistCategoriesNames.resx">
    <StaticClass>false</StaticClass>
  </EmbeddedResource>
</ItemGroup>

or globally

<PropertyGroup>
  <ResXGenerator_StaticClass>false</ResXGenerator_StaticClass>
</PropertyGroup>

With global non-static class you can also reset StaticClass per file by setting the value to anything but false.

Partial classes (per file or globally)

To extend an existing class, you can make your classes partial.

<ItemGroup>
  <EmbeddedResource Update="Resources\ArtistCategoriesNames.resx">
    <PartialClass>true</PartialClass>
  </EmbeddedResource>
</ItemGroup>

or globally

<PropertyGroup>
  <ResXGenerator_PartialClass>true</ResXGenerator_PartialClass>
</PropertyGroup>

Static Members (per file or globally)

In some rare cases it might be useful for the members to be non-static.

<ItemGroup>
  <EmbeddedResource Update="Resources\ArtistCategoriesNames.resx">
    <StaticMembers>false</StaticMembers>
  </EmbeddedResource>
</ItemGroup>

or globally

<PropertyGroup>
  <ResXGenerator_StaticMembers>false</ResXGenerator_StaticMembers>
</PropertyGroup>

Postfix class name (per file or globally)

In some cases the it is useful if the name of the generated class doesn't follow the filename.

A clear example is Razor pages that always generates a class for the code-behind named "-Model". This example configuration allows you to use Resources.MyResource in your model, or @Model.Resources.MyResource in your cshtml file.

<ItemGroup>
  <EmbeddedResource Update="**/Pages/*.resx">
    <ClassNamePostfix>Model</ClassNamePostfix>
    <StaticMembers>false</StaticMembers>
    <StaticClass>false</StaticClass>
    <PartialClass>true</PartialClass>
    <PublicClass>true</PublicClass>
    <InnerClassVisibility>public</InnerClassVisibility>
    <PartialClass>false</PartialClass>
    <InnerClassInstanceName>Resources</InnerClassInstanceName>
    <InnerClassName>_Resources</InnerClassName>
  </EmbeddedResource>
</ItemGroup>

or just the postfix globally

<PropertyGroup>
  <ResXGenerator_ClassNamePostfix>Model</ResXGenerator_ClassNamePostfix>
</PropertyGroup>

Inner classes (per file or globally)

If your resx files are organized along with code files, it can be quite useful to ensure that the resources are not accessible outside the specific class the resx file belong to.

<ItemGroup>
    <EmbeddedResource Update="**/*.resx">
        <DependentUpon>$([System.String]::Copy('%(FileName).cs'))</DependentUpon>
        <InnerClassName>MyResources</InnerClassName>
        <InnerClassVisibility>private</InnerClassVisibility>
        <InnerClassInstanceName>EveryoneLikeMyNaming</InnerClassInstanceName>
        <StaticMembers>false</StaticMembers>
        <StaticClass>false</StaticClass>
        <PartialClass>true</PartialClass>
    </EmbeddedResource>
    <EmbeddedResource Update="**/*.??.resx;**/*.??-??.resx">
        <DependentUpon>$([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx</DependentUpon>
    </EmbeddedResource>
</ItemGroup>

or globally

<PropertyGroup>
  <ResXGenerator_InnerClassName>MyResources</ResXGenerator_InnerClassName>
  <ResXGenerator_InnerClassVisibility>private</ResXGenerator_InnerClassVisibility>
  <ResXGenerator_InnerClassInstanceName>EveryoneLikeMyNaming</InnerClassInstanceName>
  <ResXGenerator_StaticMembers>false</ResXGenerator_StaticMembers>
  <ResXGenerator_StaticClass>false</ResXGenerator_StaticClass>
  <ResXGenerator_PartialClass>true</ResXGenerator_PartialClass>
</PropertyGroup>

This example would generate files like this:

// ------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
// ------------------------------------------------------------------------------
#nullable enable
namespace Resources
{
    using System.Globalization;
    using System.Resources;

    public partial class ActivityEntryModel
    {
        public MyResources EveryoneLikeMyNaming { get; } = new();

        private class MyResources
        {
            private static ResourceManager? s_resourceManager;
            public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntryModel", typeof(ActivityEntryModel).Assembly);
            public CultureInfo? CultureInfo { get; set; }

            /// <summary>
            /// Looks up a localized string similar to Oldest.
            /// </summary>
            public string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo);

            /// <summary>
            /// Looks up a localized string similar to Newest.
            /// </summary>
            public string? CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo);
        }
    }
}

Inner Class Visibility (per file or globally)

By default inner classes are not generated, unless this setting is one of the following:

Case is ignored, so you could use "private".

It is also possible to use "NotGenerated" to override on a file if the global setting is to generate inner classes.

<ItemGroup>
    <EmbeddedResource Update="**/*.resx">
        <InnerClassVisibility>private</InnerClassVisibility>
    </EmbeddedResource>
</ItemGroup>

or globally

<PropertyGroup>
  <ResXGenerator_InnerClassVisibility>private</ResXGenerator_InnerClassVisibility>
</PropertyGroup>

Inner Class name (per file or globally)

By default the inner class is named "Resources", which can be overridden with this setting:

<ItemGroup>
    <EmbeddedResource Update="**/*.resx">
        <InnerClassName>MyResources</InnerClassName>
    </EmbeddedResource>
</ItemGroup>

or globally

<PropertyGroup>
  <ResXGenerator_InnerClassName>MyResources</ResXGenerator_InnerClassName>
</PropertyGroup>

Inner Class instance name (per file or globally)

By default no instance is available of the class, but that can be made available if this setting is given.

<ItemGroup>
    <EmbeddedResource Update="**/*.resx">
        <InnerClassInstanceName>EveryoneLikeMyNaming</InnerClassInstanceName>
    </EmbeddedResource>
</ItemGroup>

or globally

<PropertyGroup>
  <ResXGenerator_InnerClassInstanceName>EveryoneLikeMyNaming</ResXGenerator_InnerClassInstanceName>
</PropertyGroup>

For brevity, settings to make everything non-static is omitted.

Generate Code (per file or globally)

By default the ancient System.Resources.ResourceManager is used.

Benefits of using System.Resources.ResourceManager:

Disadvantages of using System.Resources.ResourceManager

Benefits of using GenerateCode code generation:

Disadvantages of using GenerateCode code generation

Notice, it is required to set GenerateResource to false for all resx files to prevent the built-in resgen.exe from running.

<ItemGroup>
    <EmbeddedResource Update="**/*.resx">
        <GenerateCode>true</GenerateCode>
        <GenerateResource>false</GenerateResource>
    </EmbeddedResource>
</ItemGroup>

or globally

<PropertyGroup>
  <ResXGenerator_GenerateCode>true</ResXGenerator_GenerateCode>
</PropertyGroup>
<ItemGroup>
    <EmbeddedResource Update="@(EmbeddedResource)">
        <GenerateResource>false</GenerateResource>
    </EmbeddedResource>
</ItemGroup>

If you get build error MSB3030, add this to your csproj to prevent it from trying to copy satellite dlls that no longer exists

<Target Name="PreventMSB3030" DependsOnTargets="ComputeIntermediateSatelliteAssemblies" BeforeTargets="GenerateSatelliteAssemblies" >
  <ItemGroup>
    <IntermediateSatelliteAssembliesWithTargetPath Remove="@(IntermediateSatelliteAssembliesWithTargetPath)"></IntermediateSatelliteAssembliesWithTargetPath>
  </ItemGroup>
</Target>

Resource file namespaces

Linked resources namespace follow Link if it is set. The Link setting is also used by msbuild built-in 'resgen.exe' to determine the embedded filename.

Use-case: Linking .resx files from outside source (e.g. generated in a localization sub-module by translators) and expose them as "Resources" namespace.

<ItemGroup>
  <EmbeddedResource Include="..\..\Another.Project\Translations\*.resx">
    <Link>Resources\%(FileName)%(Extension)</Link>
    <PublicClass>true</PublicClass>
    <StaticClass>false</StaticClass>
  </EmbeddedResource>
  <EmbeddedResource Update="..\..\Another.Project\Translations\*.*.resx">
    <DependentUpon>$([System.IO.Path]::GetFilenameWithoutExtension([System.String]::Copy('%(FileName)'))).resx</DependentUpon>
  </EmbeddedResource>
</ItemGroup>

You can also use the TargetPath to just overwrite the namespace

<ItemGroup>
  <EmbeddedResource Include="..\..\Another.Project\Translations\*.resx">
    <TargetPath>Resources\%(FileName)%(Extension)</TargetPath>
    <PublicClass>true</PublicClass>
    <StaticClass>false</StaticClass>
  </EmbeddedResource>
  <EmbeddedResource Update="..\..\Another.Project\Translations\*.*.resx">
    <DependentUpon>$([System.IO.Path]::GetFilenameWithoutExtension([System.String]::Copy('%(FileName)'))).resx</DependentUpon>
  </EmbeddedResource>
</ItemGroup>

It is also possible to set the namespace using the CustomToolNamespace setting. Unlike the Link and TargetPath, which will prepend the assemblies namespace and includes the filename, the CustomToolNamespace is taken verbatim.

<ItemGroup>
  <EmbeddedResource Update="**\*.resx">
    <CustomToolNamespace>MyNamespace.AllMyResourcesAreBelongToYouNamespace</CustomToolNamespace>
  </EmbeddedResource>
</ItemGroup>

Excluding resx files

Individual resx files can also be excluded from being processed by setting the SkipFile metadata to true.

<ItemGroup>
    <EmbeddedResource Update="ExcludedFile.resx">
        <SkipFile>true</SkipFile>
    </EmbeddedResource>
</ItemGroup>

Alternatively it can be set with the attribute SkipFile="true".

<ItemGroup>
	<EmbeddedResource Update="ExcludedFile.resx" SkipFile="true" />
</ItemGroup>

References