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

Apply culture in casing formatters #743

Merged
merged 6 commits into from
Jan 7, 2025
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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 26 additions & 6 deletions Fluid.Tests/MiscFiltersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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]
Expand Down
32 changes: 16 additions & 16 deletions Fluid/Filters/MiscFilters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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':
Expand All @@ -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':
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -585,15 +585,15 @@ 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':
{
// 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':
Expand All @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions Fluid/Filters/StringFilters.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Fluid.Values;
using Fluid.Values;

namespace Fluid.Filters
{
Expand Down Expand Up @@ -48,7 +48,7 @@ public static ValueTask<FluidValue> 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);
}
}

Expand All @@ -57,7 +57,7 @@ public static ValueTask<FluidValue> Capitalize(FluidValue input, FilterArguments

public static ValueTask<FluidValue> Downcase(FluidValue input, FilterArguments arguments, TemplateContext context)
{
return new StringValue(input.ToStringValue().ToLowerInvariant());
return new StringValue(input.ToStringValue().ToLower(context.CultureInfo));
}

public static ValueTask<FluidValue> LStrip(FluidValue input, FilterArguments arguments, TemplateContext context)
Expand Down Expand Up @@ -360,7 +360,7 @@ public static ValueTask<FluidValue> TruncateWords(FluidValue input, FilterArgume

public static ValueTask<FluidValue> Upcase(FluidValue input, FilterArguments arguments, TemplateContext context)
{
return new StringValue(input.ToStringValue().ToUpperInvariant());
return new StringValue(input.ToStringValue().ToUpper(context.CultureInfo));
}
}
}
4 changes: 2 additions & 2 deletions Versions.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project>
<!-- This file define constants that can be changed per TFM -->
<PropertyGroup>
<MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>9.0.0</MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>
<SystemTextJsonPackageVersion>9.0.0</SystemTextJsonPackageVersion>
<MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>8.0.0</MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>
<SystemTextJsonPackageVersion>8.0.5</SystemTextJsonPackageVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
Expand Down
Loading