Skip to content
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

Allow format strings in ThisAssembly.Strings #313

Merged
merged 1 commit into from
Jun 9, 2024
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
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