Skip to content

Commit

Permalink
Allow format strings in ThisAssembly.Strings
Browse files Browse the repository at this point in the history
The following are now valid and produce the expected results, just as an equivalent interpolated string would:

- `expires on {date:yyyy-MM-dd}`
- `expires on year {date:yyyy} (wait until {date:MM}`
- `expires on {0:yyyy-MM}`

Note how the same argument name can be used multiple times with different output formatting for increased flexibility and arg reuse.

Fixes #299
  • Loading branch information
kzu committed Jun 9, 2024
1 parent 2442c61 commit 16098d8
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 14 deletions.
16 changes: 10 additions & 6 deletions src/ThisAssembly.Strings/CSharp.sbntxt
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,22 @@
{{~ if value.IsIndexedFormat ~}}
{{- summary value ~}}
public static string {{ value.Id }}(
{{- for arg in value.Format -}}
{{- for arg in value.Args -}}
object arg{{~ arg ~}}{{ if !for.last }}, {{ end }}
{{- end -}})
=> string.Format(
CultureInfo.CurrentCulture,
Strings.GetResourceManager("{{ $1 }}").GetString("{{ value.Name }}"),
Strings.GetResourceManager("{{ $1 }}").GetString("{{ value.Name }}")
{{~ if value.HasArgFormat ~}}
{{- for arg in value.Format }}
.Replace("{{ arg.Value }}", "{%{{}%}{{ for.index }}{%{}}%}"){{- end -}}
{{~ end ~}},
{{ for arg in value.Format -}}
arg{{- arg -}}{{- if !for.last -}}, {{ end }}{{- end -}});
{{- if arg.Format -}}((IFormattable)arg{{- arg.Arg -}}).ToString("{{ arg.Format }}", CultureInfo.CurrentCulture){{- else -}}arg{{- arg.Arg -}}{{- end -}}{{- if !for.last -}}, {{ end }}{{- end -}});
{{~ else if value.IsNamedFormat ~}}
{{- summary value ~}}
public static string {{ value.Id }}(
{{- for arg in value.Format -}}
{{- for arg in value.Args -}}
object {{ arg ~}}{{ if !for.last }}, {{ end }}
{{- end -}})
=> string.Format(
Expand All @@ -48,9 +52,9 @@
.GetResourceManager("{{ $1 }}")
.GetString("{{ value.Name }}")
{{- for arg in value.Format }}
.Replace("{%{{}%}{{ arg }}{%{}}%}", "{%{{}%}{{ for.index }}{%{}}%}"){{- end -}},
.Replace("{{ arg.Value }}", "{%{{}%}{{ for.index }}{%{}}%}"){{- end -}},
{{ for arg in value.Format -}}
{{- arg -}}{{- if !for.last -}}, {{ end }}{{- end -}});
{{- if arg.Format -}}((IFormattable){{- arg.Arg -}}).ToString("{{ arg.Format }}", CultureInfo.CurrentCulture){{- else -}}{{- arg.Arg -}}{{- end -}}{{- if !for.last -}}, {{ end }}{{- end -}});

{{~ end ~}}
{{~ end ~}}
Expand Down
23 changes: 16 additions & 7 deletions src/ThisAssembly.Strings/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ record Model(ResourceArea RootArea, string ResourceName)

static class ResourceFile
{
static readonly Regex FormatExpression = new("{(?<name>[^{}]+)}", RegexOptions.Compiled);
static readonly Regex FormatExpression = new("{(?<arg>[^:{}]+)(?::(?<format>[^{}]+))?}", RegexOptions.Compiled);
internal static readonly Regex NameReplaceExpression = new(@"\||:|;|\>|\<", RegexOptions.Compiled);

public static ResourceArea Load(string fileName, string rootArea)
Expand Down Expand Up @@ -115,7 +115,12 @@ static ResourceValue GetValue(string resourceId, string resourceName, string res
value.Format.AddRange(FormatExpression
.Matches(resourceValue)
.OfType<Match>()
.Select(match => match.Groups["name"].Value)
.Select(match =>
{
var arg = match.Groups["arg"].Value;
var format = match.Groups["format"].Value;
return new ArgFormat(match.Value, arg, string.IsNullOrWhiteSpace(format) ? null : format);
})
.Distinct());
}

Expand All @@ -135,10 +140,14 @@ record ResourceValue(string Id, string Name, string? Raw)
{
public string? Value => Raw?.Replace(Environment.NewLine, "")?.Replace("<", "&lt;")?.Replace(">", "&gt;");
public string? Comment { get; init; }
public bool HasFormat => Format != null && Format.Count > 0;
public bool HasFormat => Format.Count > 0;
public bool HasArgFormat => Format.Any(x => x.Format != null);
// We either have *all* named or all indexed. Can't mix. We'll skip generating
// methods for mixed ones and report as an analyzer error on the Resx.
public bool IsNamedFormat => HasFormat && Format.All(x => !int.TryParse(x, out _));
public bool IsIndexedFormat => HasFormat && Format.All(x => int.TryParse(x, out _));
public List<string> Format { get; } = new List<string>();
}
public bool IsNamedFormat => HasFormat && Format.All(x => !int.TryParse(x.Arg, out _));
public bool IsIndexedFormat => HasFormat && Format.All(x => int.TryParse(x.Arg, out _));
public List<ArgFormat> Format { get; } = [];
public HashSet<string> Args => new(Format.Select(x => x.Arg));
}

record ArgFormat(string Value, string Arg, string? Format);
9 changes: 9 additions & 0 deletions src/ThisAssembly.Strings/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Given the following Resx file:
| Infrastructure_MissingService | Service {0} is required. | For logging only! |
| Shopping_NoShipping | We cannot ship {0} to {1}. | |
| Shopping_OutOfStock | Product is out of stock at this time. | |
| Shopping_AvailableOn | Product available on {date:yyyy-MM}. | |

The following code would be generated:

Expand Down Expand Up @@ -53,6 +54,14 @@ partial class ThisAssembly
/// </summary>
public static string OutOfStock
=> Strings.GetResourceManager("ThisStore.Properties.Resources").GetString("OutOfStock");

/// <summary>
/// Product available on {date:yyyy-MM}.
/// </summary>
public static string AvailableOn(object date)
=> string.Format(CultureInfo.CurrentCulture,
Strings.GetResourceManager("ThisAssemblyTests.Resources").GetString("WithNamedFormat").Replace("{date:yyyy-MM}", "{0}"),
((IFormattable)date).ToString("yyyy-MM", CultureInfo.CurrentCulture));
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/ThisAssembly.Tests/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,14 @@
<data name="Named" xml:space="preserve">
<value>Hello {first}, {last}. Should we call you {first}?</value>
</data>
<data name="WithIndexedFormat" xml:space="preserve">
<value>Year {0:yyyy}, Month {0:MM}</value>
</data>
<data name="WithNamedFormat" xml:space="preserve">
<value>Year {date:yyyy}, Month {date:MM}</value>
</data>
<data name="WithNewLine" xml:space="preserve">
<value>Hello,
World!</value>
</data>
</data>
</root>
9 changes: 9 additions & 0 deletions src/ThisAssembly.Tests/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace ThisAssemblyTests;

public record class Tests(ITestOutputHelper Output)
{
DateTime dxt = DateTime.Now;
[Fact]
public void CanReadResourceFile()
=> Assert.NotNull(ResourceFile.Load("Resources.resx", "Strings"));
Expand Down Expand Up @@ -63,6 +64,14 @@ public void CanUseStringsNamedArguments()
public void CanUseStringsIndexedArguments()
=> Assert.NotNull(ThisAssembly.Strings.Indexed("hello", "world"));

[Fact]
public void CanUseStringsNamedFormattedArguments()
=> Assert.Equal("Year 2020, Month 03", ThisAssembly.Strings.WithNamedFormat(new DateTime(2020, 3, 20)));

[Fact]
public void CanUseStringsIndexedFormattedArguments()
=> Assert.Equal("Year 2020, Month 03", ThisAssembly.Strings.WithIndexedFormat(new DateTime(2020, 3, 20)));

[Fact]
public void CanUseStringResource()
=> Assert.Equal("Value", ThisAssembly.Strings.Foo.Bar.Baz);
Expand Down

0 comments on commit 16098d8

Please sign in to comment.