diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88638e18..60d922f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,9 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: + dotnet-version: | + 6.0.x + 8.0.x global-json-file: global.json - name: Test diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cb8a4a4a..c9318305 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -20,6 +20,9 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: + dotnet-version: | + 6.0.x + 8.0.x global-json-file: global.json - name: Version diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4c701860..f8962902 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,9 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: + dotnet-version: | + 6.0.x + 8.0.x global-json-file: global.json - name: Install dependencies diff --git a/Fluid.Tests/MiscFiltersTests.cs b/Fluid.Tests/MiscFiltersTests.cs index 97d3aaed..65a43ac7 100644 --- a/Fluid.Tests/MiscFiltersTests.cs +++ b/Fluid.Tests/MiscFiltersTests.cs @@ -287,8 +287,13 @@ public async Task EscapeOnce() [InlineData("%Y-%m-%dT%H:%M:%S.%L", "2017-08-01T17:04:36.123")] public async Task Date(string format, string expected, string dateTime = "2017-08-01T17:04:36.123456789+08:00") { + // This test sets the CultureInfo.DateTimeFormat so it's not impacted by changes in ICU + // see https://github.com/dotnet/runtime/issues/95620 + var enUsCultureInfo = new CultureInfo("en-US", useUserOverride: false); + enUsCultureInfo.DateTimeFormat.FullDateTimePattern = "dddd, MMMM d, yyyy h:mm:ss tt"; + var arguments = new FilterArguments(new StringValue(format)); - var options = new TemplateOptions() { CultureInfo = new CultureInfo("en-US", useUserOverride: false), TimeZone = TimeZoneInfo.Utc }; + var options = new TemplateOptions() { CultureInfo = enUsCultureInfo, TimeZone = TimeZoneInfo.Utc }; var context = new TemplateContext(options); new StringValue(dateTime).TryGetDateTimeInput(new TemplateContext(), out var customDateTime); @@ -610,19 +615,34 @@ public async Task DateIsParsedWithCulture() [Fact] public async Task DateIsRenderedWithCulture() { - var input = new StringValue("08/01/2017"); - var format = "%c"; + // This tests 4 things: + // - The date is parsed with the specified culture (July vs February) + // - The date is rendered with the specified culture (French vs English) + // - The date is rendered with the specified timezone (UTC) + // - The uppercase modifier is applied with the culture (Turkish i) + + var input = new StringValue("07/02/2017"); + var format = "%^c"; var arguments = new FilterArguments(new StringValue(format)); var context = new TemplateContext { CultureInfo = new CultureInfo("fr-FR", useUserOverride: false), TimeZone = TimeZoneInfo.Utc }; var resultFR = await MiscFilters.Date(input, arguments, context); - context = new TemplateContext { CultureInfo = new CultureInfo("en-US", useUserOverride: false), TimeZone = TimeZoneInfo.Utc }; + context = new TemplateContext { CultureInfo = new CultureInfo("tr-TR", useUserOverride: false), TimeZone = TimeZoneInfo.Utc }; + var resultTR = await MiscFilters.Date(input, arguments, context); + + // This test sets the CultureInfo.DateTimeFormat so it's not impacted by changes in ICU + // see https://github.com/dotnet/runtime/issues/95620 + var enUsCultureInfo = new CultureInfo("en-US", useUserOverride: false); + enUsCultureInfo.DateTimeFormat.FullDateTimePattern = "dddd, MMMM d, yyyy h:mm:ss tt"; + + context = new TemplateContext { CultureInfo = enUsCultureInfo, TimeZone = TimeZoneInfo.Utc }; var resultUS = await MiscFilters.Date(input, arguments, context); - Assert.Equal("dimanche 8 janvier 2017 00:00:00", resultFR.ToStringValue()); - Assert.Equal("Tuesday, August 1, 2017 12:00:00 AM", resultUS.ToStringValue()); + Assert.Equal("7 ŞUBAT 2017 SALI 00:00:00", resultTR.ToStringValue()); + Assert.Equal("MARDI 7 FÉVRIER 2017 00:00:00", resultFR.ToStringValue()); + Assert.Equal("SUNDAY, JULY 2, 2017 12:00:00 AM", resultUS.ToStringValue()); } [Theory] diff --git a/Fluid/Filters/MiscFilters.cs b/Fluid/Filters/MiscFilters.cs index 4c248feb..064c819e 100644 --- a/Fluid/Filters/MiscFilters.cs +++ b/Fluid/Filters/MiscFilters.cs @@ -420,29 +420,29 @@ string AbbreviatedDayName() } var abbreviatedDayName = AbbreviatedDayName(); - result.Append(upperCaseFlag ? abbreviatedDayName.ToUpperInvariant() : abbreviatedDayName); + result.Append(upperCaseFlag ? abbreviatedDayName.ToUpper(context.CultureInfo) : abbreviatedDayName); break; case 'A': { var dayName = context.CultureInfo.DateTimeFormat.DayNames[(int)value.DayOfWeek]; - result.Append(upperCaseFlag ? dayName.ToUpperInvariant() : dayName); + result.Append(upperCaseFlag ? dayName.ToUpper(context.CultureInfo) : dayName); break; } case 'b': var abbreviatedMonthName = context.CultureInfo.DateTimeFormat.AbbreviatedMonthNames[value.Month - 1]; - result.Append(upperCaseFlag ? abbreviatedMonthName.ToUpperInvariant() : abbreviatedMonthName); + result.Append(upperCaseFlag ? abbreviatedMonthName.ToUpper(context.CultureInfo) : abbreviatedMonthName); break; case 'B': { var monthName = context.CultureInfo.DateTimeFormat.MonthNames[value.Month - 1]; - result.Append(upperCaseFlag ? monthName.ToUpperInvariant() : monthName); + result.Append(upperCaseFlag ? monthName.ToUpper(context.CultureInfo) : monthName); break; } case 'c': { // c is defined as "%a %b %e %T %Y" but it's also supposed to be locale aware, so we are using the // C# standard format instead - result.Append(upperCaseFlag ? value.ToString("F", context.CultureInfo).ToUpperInvariant() : value.ToString("F", context.CultureInfo)); + result.Append(upperCaseFlag ? value.ToString("F", context.CultureInfo).ToUpper(context.CultureInfo) : value.ToString("F", context.CultureInfo)); break; } case 'C': result.Append(Format(value.Year / 100, 2)); break; @@ -451,7 +451,7 @@ string AbbreviatedDayName() { var sb = new StringBuilder(); ForStrf(value, "%m/%d/%y", sb); - result.Append(upperCaseFlag ? sb.ToString().ToUpperInvariant() : sb.ToString()); + result.Append(upperCaseFlag ? sb.ToString().ToUpper(context.CultureInfo) : sb.ToString()); break; } case 'e': @@ -461,7 +461,7 @@ string AbbreviatedDayName() { var sb = new StringBuilder(); ForStrf(value, "%Y-%m-%d", sb); - result.Append(upperCaseFlag ? sb.ToString().ToUpperInvariant() : sb.ToString()); + result.Append(upperCaseFlag ? sb.ToString().ToUpper(context.CultureInfo) : sb.ToString()); break; } case 'g': @@ -522,20 +522,20 @@ string AbbreviatedDayName() var v = (value.Ticks % 10000000).ToString(context.CultureInfo); result.Append(v.Length >= width ? v.Substring(0, width.Value) : v.PadRight(width.Value, '0')); break; - case 'p': result.Append(value.ToString("tt", context.CultureInfo).ToUpperInvariant()); break; - case 'P': result.Append(value.ToString("tt", context.CultureInfo).ToLowerInvariant()); break; + case 'p': result.Append(value.ToString("tt", context.CultureInfo).ToUpper(context.CultureInfo)); break; + case 'P': result.Append(value.ToString("tt", context.CultureInfo).ToLower(context.CultureInfo)); break; case 'r': { var sb = new StringBuilder(); ForStrf(value, "%I:%M:%S %p", sb); - result.Append(upperCaseFlag ? sb.ToString().ToUpperInvariant() : sb.ToString()); + result.Append(upperCaseFlag ? sb.ToString().ToUpper(context.CultureInfo) : sb.ToString()); break; } case 'R': { var sb = new StringBuilder(); ForStrf(value, "%H:%M", sb); - result.Append(upperCaseFlag ? sb.ToString().ToUpperInvariant() : sb.ToString()); + result.Append(upperCaseFlag ? sb.ToString().ToUpper(context.CultureInfo) : sb.ToString()); break; } case 's': result.Append(Format(value.ToUnixTimeSeconds())); break; @@ -547,7 +547,7 @@ string AbbreviatedDayName() { var sb = new StringBuilder(); ForStrf(value, "%H:%M:%S", sb); - result.Append(upperCaseFlag ? sb.ToString().ToUpperInvariant() : sb.ToString()); + result.Append(upperCaseFlag ? sb.ToString().ToUpper(context.CultureInfo) : sb.ToString()); break; } case 'u': result.Append(value.DayOfWeek switch { DayOfWeek.Sunday => 7, _ => (int)value.DayOfWeek }); break; @@ -565,7 +565,7 @@ string AbbreviatedDayName() { var sb = new StringBuilder(); ForStrf(value, "%e-%b-%Y", sb); - result.Append(upperCaseFlag ? sb.ToString().ToUpperInvariant() : sb.ToString()); + result.Append(upperCaseFlag ? sb.ToString().ToUpper(context.CultureInfo) : sb.ToString()); break; } case 'V': result.Append(Format(value.DayOfYear / 7 + 1, 2)); break; @@ -585,7 +585,7 @@ string AbbreviatedDayName() // x is defined as "%m/%d/%y" but it's also supposed to be locale aware, so we are using the // C# short date pattern standard format instead - result.Append(upperCaseFlag ? value.ToString("d", context.CultureInfo).ToUpperInvariant() : value.ToString("d", context.CultureInfo)); + result.Append(upperCaseFlag ? value.ToString("d", context.CultureInfo).ToUpper(context.CultureInfo) : value.ToString("d", context.CultureInfo)); break; } case 'X': @@ -593,7 +593,7 @@ string AbbreviatedDayName() // X is defined as "%T" but it's also supposed to be locale aware, so we are using the // C# short time pattern standard format instead - result.Append(upperCaseFlag ? value.ToString("t", context.CultureInfo).ToUpperInvariant() : value.ToString("t", context.CultureInfo)); + result.Append(upperCaseFlag ? value.ToString("t", context.CultureInfo).ToUpper(context.CultureInfo) : value.ToString("t", context.CultureInfo)); break; } case 'y': @@ -616,7 +616,7 @@ string AbbreviatedDayName() { var sb = new StringBuilder(); ForStrf(value, "%a %b %e %H:%M:%S %Z %Y", sb); - result.Append(upperCaseFlag ? sb.ToString().ToUpperInvariant() : sb.ToString()); + result.Append(upperCaseFlag ? sb.ToString().ToUpper(context.CultureInfo) : sb.ToString()); break; } default: result.Append('%').Append(c); break; diff --git a/Fluid/Filters/StringFilters.cs b/Fluid/Filters/StringFilters.cs index 3d11f4a0..5fc72d68 100644 --- a/Fluid/Filters/StringFilters.cs +++ b/Fluid/Filters/StringFilters.cs @@ -1,4 +1,4 @@ -using Fluid.Values; +using Fluid.Values; namespace Fluid.Filters { @@ -48,7 +48,7 @@ public static ValueTask Capitalize(FluidValue input, FilterArguments char c; if (i == 0 || char.IsWhiteSpace(c = source[i - 1]) || c == '-' || c == '.') { - source[i] = char.ToUpper(source[i]); + source[i] = char.ToUpper(source[i], context.CultureInfo); } } @@ -57,7 +57,7 @@ public static ValueTask Capitalize(FluidValue input, FilterArguments public static ValueTask Downcase(FluidValue input, FilterArguments arguments, TemplateContext context) { - return new StringValue(input.ToStringValue().ToLowerInvariant()); + return new StringValue(input.ToStringValue().ToLower(context.CultureInfo)); } public static ValueTask LStrip(FluidValue input, FilterArguments arguments, TemplateContext context) @@ -360,7 +360,7 @@ public static ValueTask TruncateWords(FluidValue input, FilterArgume public static ValueTask Upcase(FluidValue input, FilterArguments arguments, TemplateContext context) { - return new StringValue(input.ToStringValue().ToUpperInvariant()); + return new StringValue(input.ToStringValue().ToUpper(context.CultureInfo)); } } } diff --git a/Versions.props b/Versions.props index 39dcb372..988d3576 100644 --- a/Versions.props +++ b/Versions.props @@ -1,8 +1,8 @@ - 9.0.0 - 9.0.0 + 8.0.0 + 8.0.5