Skip to content
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ ReswPlus allows multiple variants of a string based on different criteria, such

🗨 [How to Use Variants](https://github.com/reswplus/ReswPlus/wiki/Features:-Variants)

### Resource Diagnostics
ReswPlus checks the content of your `.resw` files while it generates the code, and reports the inconsistencies that would otherwise only show up at runtime, in a language your team may not read.

| Rule | Description |
| --- | --- |
| `RESWP0006` | A translated value drops placeholders its value in the default language uses, silently losing information. |
| `RESWP0007` | A value uses a placeholder that has no matching parameter in its `#Format` tag. |
| `RESWP0008` | A pluralized resource is missing the plural forms its language requires, which silently produces grammatically wrong text. |
| `RESWP0009` | Two resources of the same file conflict with each other, because their names only differ by case or because a plain resource collides with a pluralized one. |
| `RESWP0010` | A value that is used as a composite format string is malformed. |

These are reported as **warnings**, so that updating the package never breaks a build that already has an inconsistency. Escalate the ones you want to be fatal from your `.editorconfig`:

```ini
dotnet_diagnostic.RESWP0006.severity = error
```

## Tools
In addition to features to enrich resw files, ReswPlus also provides some interesting tools to improve your productivity or make it easier to use/support resw files in your workflow and localization process.

Expand Down
7 changes: 4 additions & 3 deletions samples/UWP/ReswPlusUWPSample/ReswPlusUWPSample.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="..\..\..\nuget\ReswPlus.targets" />
Expand All @@ -12,7 +12,7 @@
<AssemblyName>ReswPlusUWPSample</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22000.0</TargetPlatformVersion>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.26100.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
Expand All @@ -21,7 +21,8 @@
<PackageCertificateThumbprint>
</PackageCertificateThumbprint>
<PackageCertificateKeyFile>ReswPlusUWPSample_TemporaryKey.pfx</PackageCertificateKeyFile>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<!-- The temporary certificate of this sample expired, and signing a package is not needed to build it -->
<AppxPackageSigningEnabled>False</AppxPackageSigningEnabled>
<Use64BitCompiler>true</Use64BitCompiler>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
Expand Down
4 changes: 2 additions & 2 deletions samples/UWP/ReswPlusUWPSample/Strings/fr/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@
<value>Contoso for Android</value>
</data>
<data name="AnimalTreat_Variant1_One" xml:space="preserve">
<value>Récompensez votre chiot, donnez {0} biscuit à {1} !</value>
<value>Récompensez votre chiot, donnez {0} biscuit à {2} !</value>
<comment>#Format[Plural Double treatNumber, Variant petType, String petName]</comment>
</data>
<data name="AnimalTreat_Variant1_Other" xml:space="preserve">
<value>Récompensez votre chiot, donnez {0} biscuits à {1} !</value>
<value>Récompensez votre chiot, donnez {0} biscuits à {2} !</value>
</data>
<data name="AnimalTreat_Variant2_One" xml:space="preserve">
<value>Récompensez votre chaton, donnez {0} biscuit à votre chat {2} !</value>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="..\..\..\nuget\ReswPlus.targets" />
Expand All @@ -12,7 +12,7 @@
<AssemblyName>ReswPlusUWPSampleExternalLibrary</AssemblyName>
<DefaultLanguage>en</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22000.0</TargetPlatformVersion>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.26100.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@
<value>Contoso for Android</value>
</data>
<data name="AnimalTreat_Variant1_One" xml:space="preserve">
<value>Récompensez votre chiot, donnez {0} biscuit à {1} !</value>
<value>Récompensez votre chiot, donnez {0} biscuit à {2} !</value>
<comment>#Format[Plural Double treatNumber, Variant petType, String petName]</comment>
</data>
<data name="AnimalTreat_Variant1_Other" xml:space="preserve">
<value>Récompensez votre chiot, donnez {0} biscuits à {1} !</value>
<value>Récompensez votre chiot, donnez {0} biscuits à {2} !</value>
</data>
<data name="AnimalTreat_Variant2_One" xml:space="preserve">
<value>Récompensez votre chaton, donnez {0} biscuit à votre chat {2} !</value>
Expand Down
169 changes: 169 additions & 0 deletions src/ReswPlus.SourceGenerator/Analysis/CompositeFormatString.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using System.Collections.Generic;
using System.Globalization;

namespace ReswPlus.SourceGenerator.Analysis;

/// <summary>
/// Parses the composite format strings used by the values of formatted resources.
/// </summary>
/// <remarks>
/// The generated code passes the value of a formatted resource to <see cref="string.Format(string, object[])"/>,
/// so a value that this parser rejects is guaranteed to throw a <see cref="System.FormatException"/> at runtime.
/// The parser is deliberately permissive where the runtime is: it accepts anything the runtime accepts, so that
/// no valid value is ever reported.
/// </remarks>
internal static class CompositeFormatString
{
/// <summary>
/// Extracts the set of argument indexes referenced by a composite format string.
/// </summary>
/// <param name="value">The composite format string to parse.</param>
/// <param name="indexes">The distinct argument indexes it references, in ascending order.</param>
/// <returns>Whether <paramref name="value"/> is a valid composite format string.</returns>
public static bool TryGetArgumentIndexes(string value, out SortedSet<int> indexes)
{
indexes = new SortedSet<int>();

var position = 0;

while (position < value.Length)
{
var character = value[position];

if (character == '}')
{
// Outside of a format item, a closing brace only stands for itself when it is doubled.
if (position + 1 < value.Length && value[position + 1] == '}')
{
position += 2;
continue;
}

return false;
}

if (character != '{')
{
position++;
continue;
}

// A doubled opening brace stands for a literal brace, it doesn't open a format item.
if (position + 1 < value.Length && value[position + 1] == '{')
{
position += 2;
continue;
}

if (!TryReadFormatItem(value, ref position, out var index))
{
return false;
}

_ = indexes.Add(index);
}

return true;
}

/// <summary>
/// Reads a single <c>{index[,alignment][:format]}</c> item, starting from its opening brace.
/// </summary>
private static bool TryReadFormatItem(string value, ref int position, out int index)
{
index = 0;

// Skip the opening brace.
position++;

if (!TryReadInteger(value, ref position, out index))
{
return false;
}

SkipWhiteSpace(value, ref position);

if (position < value.Length && value[position] == ',')
{
position++;

SkipWhiteSpace(value, ref position);

if (position < value.Length && (value[position] == '-' || value[position] == '+'))
{
position++;
}

if (!TryReadInteger(value, ref position, out _))
{
return false;
}

SkipWhiteSpace(value, ref position);
}

if (position < value.Length && value[position] == ':' && !TrySkipFormatSpecifier(value, ref position))
{
return false;
}

if (position >= value.Length || value[position] != '}')
{
return false;
}

position++;

return true;
}

/// <summary>
/// Skips the <c>:format</c> part of a format item, stopping on the closing brace of the item.
/// </summary>
private static bool TrySkipFormatSpecifier(string value, ref int position)
{
// Skip the colon.
position++;

while (position < value.Length)
{
var character = value[position];

if (character is '{' or '}')
{
// Braces are escaped by doubling them inside a format specifier as well.
if (position + 1 < value.Length && value[position + 1] == character)
{
position += 2;
continue;
}

return character == '}';
}

position++;
}

return false;
}

private static bool TryReadInteger(string value, ref int position, out int result)
{
var start = position;

while (position < value.Length && char.IsDigit(value[position]))
{
position++;
}

return int.TryParse(value.Substring(start, position - start), NumberStyles.None, CultureInfo.InvariantCulture, out result);
}

private static void SkipWhiteSpace(string value, ref int position)
{
while (position < value.Length && value[position] == ' ')
{
position++;
}
}
}
Loading
Loading