Skip to content

Enable nullable reference types and optimize performance for the release build #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
run: nuget restore ReswPlus.sln

- name: Build Solution
run: msbuild ReswPlus.sln /p:Configuration=${{ env.BUILD_CONFIG }} /p:Platform="Any CPU"
run: msbuild ReswPlus.sln /p:Configuration=${{ env.BUILD_CONFIG }} /p:Platform="Any CPU" /p:RuntimeIdentifierOverride=win-x64

- name: Run Unit Tests
run: dotnet test tests/ReswPlusUnitTests/ReswPlusUnitTests.csproj --configuration ${{ env.BUILD_CONFIG }} --no-build --verbosity normal
Expand Down
11 changes: 10 additions & 1 deletion nuget/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
Reswplus
**ReswPlus** is a C# Source Generator for Visual Studio that enhances `.resw` files with a powerful set of features, making localization easier, safer, and more efficient.

## Features

- **Strongly typed static properties** for safer and more efficient string access.
- **Automatic generation of string formatting methods**, supporting:
- Typed and named parameters, literal strings, string references, and macros.
- **Pluralization support** for *196 languages*, including handling empty states when the item count is zero.
- **Variant support** for managing multiple versions of a string.
- **Generation of a markup extension** for accessing strings with **compile-time verification**.
6 changes: 3 additions & 3 deletions nuget/ReswPlus.SourceGenerator.NugetPackage.props
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<FileVersion>0.3.0.1</FileVersion>
<FileVersion>0.3.1.1</FileVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Title>ReswPlus</Title>
<Title>ReswPlus - Source Generator</Title>
<PackageId>ReswPlus</PackageId>
<Version>$(FileVersion)</Version>
<Authors>Rudy Huyn</Authors>
<Description>ReswPlus - Advanced File Code Generator for Resw files</Description>
<Description>ReswPlus - Source Generator for Resw files</Description>
<PackageProjectUrl>https://github.com/reswplus/ReswPlus/</PackageProjectUrl>
<PackageIcon>Icon.png</PackageIcon>
<PackageCopyright>Copyright 2025</PackageCopyright>
Expand Down
10 changes: 9 additions & 1 deletion samples/UWP/ReswPlusUWPSample/ReswPlusUWPSample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WindowsXamlEnableOverview>true</WindowsXamlEnableOverview>
<PackageCertificateThumbprint>5E71F95FCAD47B85E52CC33EF85A3981DD6BE171</PackageCertificateThumbprint>
<PackageCertificateThumbprint>
</PackageCertificateThumbprint>
<PackageCertificateKeyFile>ReswPlusUWPSample_TemporaryKey.pfx</PackageCertificateKeyFile>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<Use64BitCompiler>true</Use64BitCompiler>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundle>Always</AppxBundle>
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,26 @@
<RootNamespace>ReswPlusWinAppSDKSample</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers Condition="$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)')) &gt;= 8">win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<RuntimeIdentifiers Condition="$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)')) &lt; 8">win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<PackageIcon>Icon.png</PackageIcon>
<PackageCertificateThumbprint>D1C162AC5FB617F237A6E3B015863C777293065E</PackageCertificateThumbprint>
<PackageCertificateKeyFile>ReswPlusWinAppSDKSample_TemporaryKey.pfx</PackageCertificateKeyFile>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxSymbolPackageEnabled>False</AppxSymbolPackageEnabled>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundle>Always</AppxBundle>
<AppxBundlePlatforms>x86|x64|arm64</AppxBundlePlatforms>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifierOverride)' != ''">
<RuntimeIdentifier>$(RuntimeIdentifierOverride)</RuntimeIdentifier>
</PropertyGroup>

<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
Expand All @@ -42,7 +50,10 @@
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\ReswPlus.SourceGenerator\ReswPlus.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" Private="false" />
<ProjectReference Include="..\..\..\src\ReswPlus.SourceGenerator\ReswPlus.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" Private="false">
<Project>{44d125b3-6c8a-4f2f-a76f-bffb2ad7e70d}</Project>
<Name>ReswPlus.SourceGenerator</Name>
</ProjectReference>
<ProjectReference Include="..\ReswPlusWinAppSDKSampleExternalLibrary\ReswPlusWinAppSDKSampleExternalLibrary.csproj" />
</ItemGroup>
<ItemGroup>
Expand Down
31 changes: 22 additions & 9 deletions src/ReswPlus.CommandLine/Converters/AndroidXMLConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ private static IEnumerable<ReswItem> ExtractAndroidItems(string sourcePath)
{
var sourceAndroidXML = XDocument.Load(sourcePath);
var xmlRoot = sourceAndroidXML.Root;
string currentComment = null;
if (xmlRoot is null)
{
yield break;
}

string? currentComment = null;
foreach (var xNode in xmlRoot.Nodes())
{
switch (xNode)
{
case XComment commentNode:
case XComment commentNode when commentNode is { }:
{
currentComment = RemoveAntislash(commentNode.Value.Trim());
}
break;
case XElement elementNode:
case XElement elementNode when elementNode is { }:
{
switch (elementNode.Name.LocalName.ToLower())
{
Expand All @@ -49,13 +54,16 @@ private static IEnumerable<ReswItem> ExtractAndroidItems(string sourcePath)
}
case "plurals":
{
var name = elementNode.Attribute("name").Value;
foreach (var subItem in elementNode.Elements("item"))
var name = elementNode.Attribute("name")?.Value;
if (name is not null)
{
var quantity = subItem.Attribute("quantity")?.Value;
if (quantity != null)
foreach (var subItem in elementNode.Elements("item"))
{
yield return new ReswItem($"{name}_{UpperCaseFirstLetter(quantity)}", RemoveAntislash(subItem.Value), currentComment);
var quantity = subItem.Attribute("quantity")?.Value;
if (quantity != null)
{
yield return new ReswItem($"{name}_{UpperCaseFirstLetter(quantity)}", RemoveAntislash(subItem.Value), currentComment);
}
}
}
break;
Expand All @@ -77,6 +85,11 @@ private static void SaveReswFile(string destinationPath, IEnumerable<ReswItem> k
{
var reswFileXml = XDocument.Parse(_reswTemplate);
var templateRoot = reswFileXml.Root;
if (templateRoot is null)
{
return;
}

foreach (var item in keys)
{
var dataNode = new XElement("data");
Expand Down Expand Up @@ -184,7 +197,7 @@ public static bool AndroidXMLDirectoryToResw(string sourcePath, string destinati
var indexDash = directoryName.IndexOf('-');
var languageId = directoryName.Substring(indexDash + 1);
var generatedFilePath = $"{destinationPath}\\{AndroidToReswLanguageId(languageId)}\\Resources.resw";
_ = Directory.CreateDirectory(Path.GetDirectoryName(generatedFilePath));
_ = Directory.CreateDirectory(Path.GetDirectoryName(generatedFilePath)!);
if (!AndroidXMLFileToResw(xmlFilePath, generatedFilePath))
{
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ namespace ReswPlusCmd.Parameters;
public class AndroidToReswParameters
{
[Value(0, HelpText = "Output Directory Path", MetaName = "output")]
public string OutputPath { get; set; }
public string? OutputPath { get; set; }

[Option('i', "input", HelpText = "A single XML File or a path of a directory containing xml files", Required = true)]
public string Input { get; set; }
public string? Input { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ namespace ReswPlusCmd.Parameters;
public class ReswToAndroidParameters
{
[Value(0, HelpText = "Output File Path", MetaName = "output")]
public string OutputFilePath { get; set; }
public string? OutputFilePath { get; set; }

[Option('i', "input", HelpText = "Resw input file", Required = true)]
public string Input { get; set; }
public string? Input { get; set; }

[Option('p', "pluralization", Default = true, HelpText = "boolean indicating if the resw file supports pluralization", Required = false)]
public bool SupportPluralization { get; set; }
Expand Down
6 changes: 3 additions & 3 deletions src/ReswPlus.CommandLine/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ private static int AndroidToReswCommand(AndroidToReswParameters parameters)
{
if (Directory.Exists(parameters.Input))
{
var success = AndroidXMLConverter.AndroidXMLDirectoryToResw(parameters.Input, parameters.OutputPath);
var success = AndroidXMLConverter.AndroidXMLDirectoryToResw(parameters.Input, parameters.OutputPath!);
if (success)
{
Console.WriteLine($"Directory created: {parameters.OutputPath}");
Expand All @@ -43,7 +43,7 @@ private static int AndroidToReswCommand(AndroidToReswParameters parameters)
}
else if (File.Exists(parameters.Input))
{
var success = AndroidXMLConverter.AndroidXMLFileToResw(parameters.Input, parameters.OutputPath);
var success = AndroidXMLConverter.AndroidXMLFileToResw(parameters.Input, parameters.OutputPath!);
if (success)
{
Console.WriteLine($"File created: {parameters.OutputPath}");
Expand Down Expand Up @@ -83,7 +83,7 @@ private static int ReswToAndroidCommand(ReswToAndroidParameters parameters)
Console.WriteLine($"Error during the conversion of the file: {parameters.Input}");
return -1;
}
androidXML.Save(parameters.OutputFilePath);
androidXML.Save(parameters.OutputFilePath!);
return 0;
}
#endregion
Expand Down
1 change: 1 addition & 0 deletions src/ReswPlus.CommandLine/ReswPlusCmd.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<TargetFramework>net8.0</TargetFramework>
<StartupObject>ReswPlusCmd.Program</StartupObject>
<AssemblyName>ReswPlusCmd</AssemblyName>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/ReswPlus.Shared/Interfaces/IErrorLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ namespace ReswPlus.Core.Interfaces;

public interface IErrorLogger
{
void LogError(string message, string document = null);
void LogWarning(string message, string document = null);
void LogError(string message, string? document = null);
void LogWarning(string message, string? document = null);
}
62 changes: 39 additions & 23 deletions src/ReswPlus.Shared/ResourceParser/FormatTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,54 @@ public interface IFormatTagParameter
}
public sealed class StringRefFormatTagParameter : IFormatTagParameter
{
public string Id { get; set; }
public string Id { get; }

public StringRefFormatTagParameter(string id)
{
Id = id;
}
}
public sealed class LiteralStringFormatTagParameter : IFormatTagParameter
{
public string Value { get; set; }
public string Value { get; }

public LiteralStringFormatTagParameter(string value)
{
Value = value;
}
}

public sealed class MacroFormatTagParameter : IFormatTagParameter
{
public string Id { get; set; }
public string Id { get; }

public MacroFormatTagParameter(string id)
{
Id = id;
}
}

public sealed class FunctionFormatTagParameter : IFormatTagParameter
{
public ParameterType Type { get; set; }
public string Name { get; set; }
public ParameterType? TypeToCast { get; set; }
public bool IsVariantId { get; set; }
public ParameterType Type { get; }
public string Name { get; }
public ParameterType? TypeToCast { get; }
public bool IsVariantId { get; }

public FunctionFormatTagParameter(ParameterType type, string name, ParameterType? typeToCast, bool isVariantId)
{
Type = type;
Name = name;
TypeToCast = typeToCast;
IsVariantId = isVariantId;
}
}

public sealed class FunctionFormatTagParametersInfo
{
public List<IFormatTagParameter> Parameters { get; set; } = [];
public FunctionFormatTagParameter PluralizationParameter { get; set; }
public FunctionFormatTagParameter VariantParameter { get; set; }
public List<IFormatTagParameter> Parameters { get; set; } = new();
public FunctionFormatTagParameter? PluralizationParameter { get; set; }
public FunctionFormatTagParameter? VariantParameter { get; set; }
}

public sealed class FormatTagParameterTypeInfo(ParameterType type, bool canBeQuantifier)
Expand Down Expand Up @@ -95,7 +118,7 @@ public sealed class FormatTag

private static readonly Regex RegexNamedParameters = new("^(?:(?:\"(?<literalString>(?:\\\\.|[^\\\"])*)\")|(?:(?<localizationRef>\\w+)\\(\\))|(?:(?<quantifier>Plural\\s+)?(?<type>\\w+)\\s*(?<name>\\w+)?))$");

public static FunctionFormatTagParametersInfo ParseParameters(string key, IEnumerable<string> types, IEnumerable<ReswItem> basicLocalizedItems, string resourceFilename, IErrorLogger logger)
public static FunctionFormatTagParametersInfo? ParseParameters(string key, IEnumerable<string> types, IEnumerable<ReswItem> basicLocalizedItems, string resourceFilename, IErrorLogger? logger)
{
var result = new FunctionFormatTagParametersInfo();
var paramIndex = 1;
Expand All @@ -109,11 +132,7 @@ public static FunctionFormatTagParametersInfo ParseParameters(string key, IEnume
}
if (matchNamedParameters.Groups["literalString"].Success)
{
var param = new LiteralStringFormatTagParameter()
{
Value = matchNamedParameters.Groups["literalString"].Value
};

var param = new LiteralStringFormatTagParameter(matchNamedParameters.Groups["literalString"].Value);
result.Parameters.Add(param);
}
else
Expand All @@ -128,10 +147,7 @@ public static FunctionFormatTagParametersInfo ParseParameters(string key, IEnume
logger?.LogError($"ReswPlus: Incorrect tag for the key '{key}': '{localizationRef}' doesn't exist in the resw file.", resourceFilename);
return null;
}
var param = new StringRefFormatTagParameter()
{
Id = localizationRef
};
var param = new StringRefFormatTagParameter(localizationRef);

result.Parameters.Add(param);
}
Expand All @@ -156,7 +172,7 @@ public static FunctionFormatTagParametersInfo ParseParameters(string key, IEnume
}
else if (!isQuantifier && MacrosAvailable.TryGetValue(paramTypeId, out var macroID) && string.IsNullOrEmpty(paramName))
{
result.Parameters.Add(new MacroFormatTagParameter() { Id = macroID });
result.Parameters.Add(new MacroFormatTagParameter(macroID));
continue;
}

Expand All @@ -171,8 +187,8 @@ public static FunctionFormatTagParametersInfo ParseParameters(string key, IEnume
paramName = paramTypeId == "Variant" ? "variantId" : isQuantifier ? "pluralCount" : "param" + paramType.type + paramIndex;
}

var functionParam = new FunctionFormatTagParameter { Type = paramType.type.Value, Name = paramName, TypeToCast = paramType.typeToCast, IsVariantId = paramType.isVariantId };
if (isQuantifier && result.PluralizationParameter == null)
var functionParam = new FunctionFormatTagParameter(paramType.type.Value, paramName, paramType.typeToCast, paramType.isVariantId);
if (isQuantifier && result.PluralizationParameter is null)
{
result.PluralizationParameter = functionParam;
}
Expand Down
Loading