ResxAnalyzer 1.0.8-pre

This is a prerelease version of ResxAnalyzer.
dotnet add package ResxAnalyzer --version 1.0.8-pre
                    
NuGet\Install-Package ResxAnalyzer -Version 1.0.8-pre
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ResxAnalyzer" Version="1.0.8-pre" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ResxAnalyzer" Version="1.0.8-pre" />
                    
Directory.Packages.props
<PackageReference Include="ResxAnalyzer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ResxAnalyzer --version 1.0.8-pre
                    
#r "nuget: ResxAnalyzer, 1.0.8-pre"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ResxAnalyzer@1.0.8-pre
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ResxAnalyzer&version=1.0.8-pre&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ResxAnalyzer&version=1.0.8-pre&prerelease
                    
Install as a Cake Tool

ResxAnalyzer

Version Downloads Buy Me a Coffee

ResxAnalyzer is a small .NET library for validating .resx translation resources. It is designed to find .resx files and run a selection of checks upon them. Read the quick start guide below to get an impression of the functionality delivered.

Download and Install ResxAnalyzer

This library is available on NuGet: https://www.nuget.org/packages/ResxAnalyzer/ Use the following command to install ResxAnalyzer using the NuGet Package Manager Console:

PM> Install-Package ResxAnalyzer

Or with the .NET CLI:

dotnet add package ResxAnalyzer

ResxAnalyzer supports .NET Standard 2.1 and higher.

Quick Start

ResxAnalyzer is intended to be used from unit tests as a utility for finding and analyzing .resx files. It can run multiple checks all at once, or you can run individual checks in separate tests when you want more focused failures. Use one unit test for a complete resource validation suite, or a series of test cases to validate specific rules one by one.

using System.Resources;
using System.Resources.Checks;

public sealed class StringsTests
{
    [Fact]
    public void ShouldAnalyzeStringResources()
    {
        // Arrange
        var resxAnalyzer = ResxAnalyzer
            .ForResource("MyProject.Contracts/Resources/Strings.resx")
            .Build();

        // Act
        var result = resxAnalyzer.Analyze();

        // Assert
        result.Succeeded.Should().BeTrue(result.Report);
    }
}

Built-in checks are registered by default. Use WithChecks(...) to add configured checks, remove built-in checks, or clear the registry for a custom-only suite of checks.

The analyzer returns a simple result:

public sealed record ResxAnalysisResult(
    bool Succeeded,
    string Report,
    IReadOnlyList<ResxCheckResult> Checks);

Tests can print Report to ITestOutputHelper and assert Succeeded. ToString() returns Report, so Console.WriteLine(result) is also useful.

Available Checks

Check Default Description
AcceleratorKeyCheck Localized values preserve accelerator key markers. Supports common markers such as &File and _File; escaped && is ignored.
CompletenessCheck Every non-invariant neutral key exists with a non-empty value in every localized resource. Keys marked with the configured invariant comment, default @Invariant, are excluded.
CultureFileCoverageCheck Every configured culture has a localized resource file. Works with cultures configured through WithCultures(...) or explicit localized resources.
DuplicateKeyCheck Resource files do not contain duplicate <data name="..."> entries. Uses parsed file-order entries, so duplicate keys can be reported even though the public dictionary exposes one entry per key.
EmptyNeutralValueCheck Neutral resource values are not empty or whitespace. Localized empties are handled by CompletenessCheck.
KeyMaxLengthCheck Resource keys do not exceed a configured maximum length. Construct with new KeyMaxLengthCheck(maxLength).
NewlineConsistencyCheck Localized values preserve leading and trailing newline characters from the neutral value.
NewlineStartEndCheck Resource values do not start or end with newline characters. Applies to neutral and localized resources.
OrphanLocalizedKeyCheck Localized resources do not contain keys missing from the neutral resource. Catches stale translated keys after neutral keys are removed or renamed.
PlaceholderConsistencyCheck Localized values use the same placeholders as the neutral value. Compares exact placeholder identity, supports numeric placeholders like {0} and named placeholders like {name}, and reports malformed placeholders.
PunctuationConsistencyCheck Localized values preserve terminal punctuation from the neutral value. Checks punctuation such as ., ?, !, :, ;, and ellipses.
SuspiciousSameAsNeutralCheck Localized values are not identical to the neutral value for non-invariant keys. Useful for copied, untranslated values.
UnusedCultureFileCheck Discovered localized resource files are part of the configured culture set. Useful when stale culture files remain beside the neutral resource.
UnusedKeysCheck Resource keys appear in configured source files unless explicitly ignored. Configure source scanning with new UnusedKeysCheck(scan => ...); scans .cs and .xaml, excluding bin, obj, *.Designer.cs, and .resx.
WhitespaceConsistencyCheck Localized values preserve leading and trailing non-newline whitespace from the neutral value. Reports spaces and tabs at the edges.
WhitespaceStartEndCheck Resource values do not start or end with non-newline whitespace. Applies to neutral and localized resources.

Resource Selection

ForResource(...) is the single entry point for selecting resources. It accepts:

  • A direct .resx file path.
  • A directory containing one or more neutral .resx files.
  • A glob pattern with * for one path segment.
  • A glob pattern with ** for any subfolder depth.

Localized resource selection has three modes:

Configuration Localized resources used
Only ForResource(...) Auto-discovers localized sibling files such as Strings.de.resx, Strings.fr.resx, and Strings.it.resx.
ForResource(...).WithCultures(...) Infers expected sibling files from the configured cultures, for example de-CH maps to Strings.de.resx.
ForResource(...).WithLocalizedResource(...) Uses only the explicitly configured localized files. This is useful for non-standard file names or locations.

When a directory or glob is used, localized resources such as Strings.de.resx are treated as localized siblings, not as separate neutral resource groups.

var result = ResxAnalyzer
    .ForResource("MyProject.Contracts/Resources")
    .Build()
    .Analyze<CompletenessCheck>();

If the directory contains multiple resource groups, all neutral resources are analyzed:

Resources/Strings.resx
Resources/Strings.de.resx
Resources/OtherStrings.resx
Resources/OtherStrings.de.resx

Use a direct file or glob when you want to limit which groups are analyzed:

var result = ResxAnalyzer
    .ForResource("MyProject.Contracts/Resources/Other*.resx")
    .Build()
    .Analyze<CompletenessCheck>();

Recursive globs can scan resource folders across a project:

var result = ResxAnalyzer
    .ForResource("**/Resources/*.resx")
    .Build()
    .Analyze<CompletenessCheck>();

Culture Discovery

WithCultures(...) is optional. If no cultures and no explicit localized resources are configured, the analyzer discovers sibling resources next to the neutral file:

Resources/Strings.resx
Resources/Strings.de.resx
Resources/Strings.fr.resx
Resources/Strings.it.resx

This is enough:

var result = ResxAnalyzer
    .ForResource("Resources/Strings.resx")
    .Build()
    .Analyze<CompletenessCheck>();

That call analyzes Strings.resx together with discovered siblings such as Strings.de.resx and Strings.fr.resx.

Explicit Cultures

Use WithCultures(...) when the supported languages should be defined by the application rather than by available files:

using System.Globalization;

var result = ResxAnalyzer
    .ForResource("Resources/Strings.resx")
    .WithCultures(new[]
    {
        new CultureInfo("de-CH"),
        new CultureInfo("fr-CH"),
        new CultureInfo("it-CH")
    })
    .Build()
    .Analyze<CompletenessCheck>();

When a specific culture has a parent culture, the analyzer looks for the parent resource file. For example, de-CH maps to Strings.de.resx.

This mode is stricter than discovery: if a configured culture maps to a file that does not exist, checks such as CultureFileCoverageCheck and CompletenessCheck can report that explicitly.

Explicit Localized Resources

Use WithLocalizedResource(...) when localized files do not follow the default sibling naming convention:

using System.Globalization;

var result = ResxAnalyzer
    .ForResource("Resources/Strings.resx")
    .WithLocalizedResource(new CultureInfo("de-CH"), "Translations/German.resx")
    .WithLocalizedResource(new CultureInfo("fr-CH"), "Translations/French.resx")
    .Build()
    .Analyze<CompletenessCheck>();

Do not combine WithCultures(...) and WithLocalizedResource(...) in the same analyzer configuration. Once explicit localized resources are configured, the analyzer uses those files as the expected localized resources instead of auto-discovering Strings.*.resx siblings.

Built-In Checks

Most built-in checks are registered by default:

var result = ResxAnalyzer
    .ForResource("Resources/Strings.resx")
    .Build()
    .Analyze();

Run a focused check by selecting it at analysis time:

var result = ResxAnalyzer
    .ForResource("Resources/Strings.resx")
    .Build()
    .Analyze<CompletenessCheck>();

Checks that need project-specific configuration are added explicitly:

.WithChecks(checks => checks
    .Add(new KeyMaxLengthCheck(80))
    .Add(new UnusedKeysCheck(scan => scan
        .In("MyApp")
        .IgnoreKeys("^Dynamic_"))))

Remove a default built-in check when it is not useful for a project:

.WithChecks(checks => checks.Remove<UnusedCultureFileCheck>())

Clear all defaults when you want a custom-only or fully manual suite:

.WithChecks(checks => checks
    .Clear()
    .Add(new CompletenessCheck()))

Each check produces a ResxCheckResult:

public sealed record ResxCheckResult(
    string CheckName,
    bool Succeeded,
    string Report);

The final ResxAnalysisResult.Report aggregates all failed check reports.

Custom Checks

Custom checks implement IResxCheck:

public sealed class NoTodoTranslationsCheck : IResxCheck
{
    public string Description => "Checks that translations do not contain TODO markers.";

    public ResxCheckResult Analyze(ResxAnalysisContext context)
    {
        var matches = context.AllResources
            .SelectMany(resource => resource.Entries.Values.Select(entry => new
            {
                Culture = resource.CultureName ?? "neutral",
                entry.Key,
                entry.Value
            }))
            .Where(entry => entry.Value.Contains("TODO", StringComparison.OrdinalIgnoreCase))
            .Select(entry => $"> CultureInfo \"{entry.Culture}\", Key='{entry.Key}'")
            .ToArray();

        return new ResxCheckResult(
            nameof(NoTodoTranslationsCheck),
            matches.Length == 0,
            matches.Length == 0
                ? string.Empty
                : $"Following translations contain TODO:{Environment.NewLine}{string.Join(Environment.NewLine, matches)}");
    }
}

Register custom checks together with the default built-in checks:

var result = ResxAnalyzer
    .ForResource("Resources/Strings.resx")
    .WithChecks(checks => checks.Add(new NoTodoTranslationsCheck()))
    .Build()
    .Analyze();

For custom-only analysis, clear the default registry first:

var result = ResxAnalyzer
    .ForResource("Resources/Strings.resx")
    .WithChecks(checks => checks
        .Clear()
        .Add(new NoTodoTranslationsCheck()))
    .Build()
    .Analyze();

Custom checks receive a ResxAnalysisContext with:

  • NeutralResource
  • LocalizedResources
  • AllResources
  • AllKeys
  • InvariantKeys

The public resource model intentionally uses simple values:

public sealed record ResxResource(
    string FilePath,
    string? CultureName,
    IReadOnlyDictionary<string, ResxResourceEntry> Entries);

File usage scanning is owned by UnusedKeysCheck. Custom checks receive parsed resource data, not file-scanning internals.

JSON-Serializable Options

For standalone tools or persisted configuration, use ResxAnalyzerOptions. It contains only JSON-friendly values: strings, dictionaries, and lists. Default checks are still registered when building from options.

{
  "ResourcePath": "MyProject.Contracts/Resources/Strings.resx",
  "LocalizedResourceFiles": {
    "de-CH": "MyProject.Contracts/Resources/Strings.de.resx",
    "fr-CH": "MyProject.Contracts/Resources/Strings.fr.resx"
  },
  "InvariantComment": "@Invariant"
}

For discovery-based analysis, ResourcePath can be a directory or glob and LocalizedResourceFiles can be omitted:

{
  "ResourcePath": "MyProject.Contracts/**/Resources/*.resx"
}

You can also configure one neutral file directly:

{
  "ResourcePath": "MyProject.Contracts/Resources/Strings.resx"
}

Run from deserialized options:

var options = JsonSerializer.Deserialize<ResxAnalyzerOptions>(json)!;
var result = ResxAnalyzer
    .ForOptions(options)
    .WithChecks(checks => checks.Add(new UnusedKeysCheck(scan => scan
        .In("MyProject.Api")
        .In("MyProject.Mobile/MyProject")
        .In("MyProject.Contracts")
        .IgnoreKeys("^CultureInfo_"))))
    .Build()
    .Analyze();

Console.WriteLine(result.Report);
return result.Succeeded ? 0 : 1;

If LocalizedResourceFiles is empty, localized sibling resources are discovered automatically. Check-specific configuration, such as source directories for UnusedKeysCheck, lives on the check itself rather than in ResxAnalyzerOptions.

Result Report

When a check succeeds, the report includes:

Check "CompletenessCheck" succeeded
Checks that every non-invariant resource key exists with a non-empty value in every localized resource.

When a check fails, the report includes the check name plus the explanation, for example:

Check "CompletenessCheck" failed
Checks that every non-invariant resource key exists with a non-empty value in every localized resource.
Following translation keys are missing:
CultureInfo "de" (1):
> MissingButtonText

Check "PlaceholderConsistencyCheck" failed
Checks that translated values use the same placeholder tokens as the neutral resource.
Following translation keys have inconsistent placeholders:
CultureInfo "fr" (1):
> TotalMessage (neutral: {0}, {1:N2}, localized: {0})

Check "UnusedKeysCheck" failed
Checks that every resource key appears in the configured source files unless explicitly ignored.
Usage scan:
searchDirectories:
> MyProject.Api
> MyProject.Mobile/MyProject
searchFileExtensions: .cs, .xaml
ignoredUsageKeyPatterns: ^CultureInfo_

Following keys are not used (1):
> OldUnusedText

Thank You

Thanks to everyone who has contributed to this project. If you find a bug or want to propose a feature, feel free to open an issue on GitHub.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.8-pre 106 6/3/2026
1.0.7-pre 71 5/28/2026
1.0.6-pre 69 5/28/2026
1.0.5-pre 64 5/28/2026
1.0.4-pre 91 5/27/2026
1.0.3-pre 84 5/27/2026
1.0.2-pre 74 5/27/2026

1.0
- Initial release.