diff --git a/src/ThisAssembly.Strings/CSharp.sbntxt b/src/ThisAssembly.Strings/CSharp.sbntxt index 10921ee3..df7ba476 100644 --- a/src/ThisAssembly.Strings/CSharp.sbntxt +++ b/src/ThisAssembly.Strings/CSharp.sbntxt @@ -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( @@ -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 ~}} diff --git a/src/ThisAssembly.Strings/Model.cs b/src/ThisAssembly.Strings/Model.cs index f34f306f..561c74ba 100644 --- a/src/ThisAssembly.Strings/Model.cs +++ b/src/ThisAssembly.Strings/Model.cs @@ -14,7 +14,7 @@ record Model(ResourceArea RootArea, string ResourceName) static class ResourceFile { - static readonly Regex FormatExpression = new("{(?[^{}]+)}", RegexOptions.Compiled); + static readonly Regex FormatExpression = new("{(?[^:{}]+)(?::(?[^{}]+))?}", RegexOptions.Compiled); internal static readonly Regex NameReplaceExpression = new(@"\||:|;|\>|\<", RegexOptions.Compiled); public static ResourceArea Load(string fileName, string rootArea) @@ -115,7 +115,12 @@ static ResourceValue GetValue(string resourceId, string resourceName, string res value.Format.AddRange(FormatExpression .Matches(resourceValue) .OfType() - .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()); } @@ -135,10 +140,14 @@ record ResourceValue(string Id, string Name, string? Raw) { public string? Value => Raw?.Replace(Environment.NewLine, "")?.Replace("<", "<")?.Replace(">", ">"); 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 Format { get; } = new List(); -} \ No newline at end of file + 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 Format { get; } = []; + public HashSet Args => new(Format.Select(x => x.Arg)); +} + +record ArgFormat(string Value, string Arg, string? Format); \ No newline at end of file diff --git a/src/ThisAssembly.Strings/readme.md b/src/ThisAssembly.Strings/readme.md index 4966e030..04163a30 100644 --- a/src/ThisAssembly.Strings/readme.md +++ b/src/ThisAssembly.Strings/readme.md @@ -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: @@ -53,6 +54,14 @@ partial class ThisAssembly /// public static string OutOfStock => Strings.GetResourceManager("ThisStore.Properties.Resources").GetString("OutOfStock"); + + /// + /// Product available on {date:yyyy-MM}. + /// + 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)); } } } diff --git a/src/ThisAssembly.Tests/Resources.resx b/src/ThisAssembly.Tests/Resources.resx index 0c455cce..b1f6b08c 100644 --- a/src/ThisAssembly.Tests/Resources.resx +++ b/src/ThisAssembly.Tests/Resources.resx @@ -132,8 +132,14 @@ Hello {first}, {last}. Should we call you {first}? + + Year {0:yyyy}, Month {0:MM} + + + Year {date:yyyy}, Month {date:MM} + Hello, World! - + \ No newline at end of file diff --git a/src/ThisAssembly.Tests/Tests.cs b/src/ThisAssembly.Tests/Tests.cs index fe2f6b7f..91c31dc9 100644 --- a/src/ThisAssembly.Tests/Tests.cs +++ b/src/ThisAssembly.Tests/Tests.cs @@ -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")); @@ -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);