Skip to content
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ jobs:
fetch-depth: 100

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5.1.0
with:
dotnet-version: 8.0.x
dotnet-version: 10.0.x

- name: Install dependencies
working-directory: ./src
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,11 @@ var custom = new CustomValueFormatters
};

// Create a MessageFormatter with the custom value formatter.
var formatter = new MessageFormatter(locale: "en-US", customValueFormatter: custom);
var formatter = new MessageFormatter(customValueFormatter: custom);

// Format a message.
var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 });
// Format a message, passing the culture to FormatMessage.
var message = formatter.FormatMessage("{value, number, $0.0}", new { value = 23 },
CultureInfo.GetCultureInfo("en-US"));
// "$23.0"
```

Expand Down Expand Up @@ -158,11 +159,11 @@ mf.CardinalPluralizers.Add("<locale>", n => {
});
````

There's no restrictions on what strings you may return, nor what strings
There are no restrictions on what strings you may return, nor what strings
you may use in your pluralization block.

````csharp
var mf = new MessageFormatter(true, "en"); // true = use cache
var mf = new MessageFormatter(); // uses cache by default
mf.CardinalPluralizers["en"] = n =>
{
// ´n´ is the number being pluralized.
Expand All @@ -175,7 +176,7 @@ mf.CardinalPluralizers["en"] = n =>

mf.FormatMessage("You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", new Dictionary<string, object>{
{"number", 1001}
});
}, CultureInfo.GetCultureInfo("en"));
````

## Escaping literals
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageReference Include="PolySharp" Version="1.15.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing;
using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing;
using Jeffijoe.MessageFormat.MetadataGenerator.Plural.SourceGeneration;

using Microsoft.CodeAnalysis;

using System;
using System.IO;
using System.Xml;

namespace Jeffijoe.MessageFormat.MetadataGenerator.Plural;

[Generator]
public class PluralLanguagesGenerator : ISourceGenerator
public class PluralLanguagesGenerator : IIncrementalGenerator
{
public void Execute(GeneratorExecutionContext context)
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var excludeLocales = ReadExcludeLocales(context);
var rules = GetRules(excludeLocales);
var generator = new PluralRulesMetadataGenerator(rules);
var sourceCode = generator.GenerateClass();

context.AddSource("PluralRulesMetadata.Generated.cs", sourceCode);
}

private string[] ReadExcludeLocales(GeneratorExecutionContext context)
{
if(context.AnalyzerConfigOptions.GlobalOptions.TryGetValue($"build_property.PluralLanguagesMetadataExcludeLocales", out var value))
context.RegisterPostInitializationOutput(static spc =>
{
var locales = value.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
return locales;
}
// Not currently excluding any locales.
var rules = GetRules(excludedLocales: []);
var generator = new PluralRulesMetadataGenerator(rules);
var sourceCode = generator.GenerateClass();

return Array.Empty<string>();
spc.AddSource("PluralRulesMetadata.Generated.cs", sourceCode);
});
}

private PluralRuleSet GetRules(string[] excludedLocales)
private static PluralRuleSet GetRules(string[] excludedLocales)
{
PluralRuleSet ruleIndex = new();
foreach (var ruleset in new[] { "plurals.xml", "ordinals.xml" })
Expand All @@ -49,14 +40,9 @@ private PluralRuleSet GetRules(string[] excludedLocales)
return ruleIndex;
}


private Stream GetRulesContentStream(string cldrFileName)
{
return typeof(PluralLanguagesGenerator).Assembly.GetManifestResourceStream($"Jeffijoe.MessageFormat.MetadataGenerator.data.{cldrFileName}")!;
}

public void Initialize(GeneratorInitializationContext context)
private static Stream GetRulesContentStream(string cldrFileName)
{

return typeof(PluralLanguagesGenerator).Assembly
.GetManifestResourceStream($"Jeffijoe.MessageFormat.MetadataGenerator.data.{cldrFileName}")!;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ public string GenerateClass()
// Export a constant for the normalized root locale to match the logic we're using internally.
// This way the rest of the lib's locale chaining can continue to work if we swap out
// normalization internally.
var rootRules = _rules.RuleIndicesByLocale[PluralRuleSet.RootLocale];
WriteLine($"public static readonly string RootLocale = \"{PluralRuleSet.RootLocale}\";");

// Generate a method for each unique rule, by index, that chooses the plural form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters;

public class DateFormatterTests
{
private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en");

[Theory]
[InlineData("en-US", "1994-09-06T15:00:00Z", "9/6/1994")]
[InlineData("da-DK", "1994-09-06T15:00:00Z", "06.09.1994")]
public void DateFormatter_Short(string locale, string dateStr, string expected)
{
var mf = new MessageFormatter(locale: locale);
var mf = new MessageFormatter();
var actual = mf.FormatMessage("{value, date}", new
{
value = DateTimeOffset.Parse(dateStr)
});
}, CultureInfo.GetCultureInfo(locale));

Assert.Equal(expected, actual);
}
Expand All @@ -26,11 +28,11 @@ public void DateFormatter_Short(string locale, string dateStr, string expected)
[InlineData("da-DK", "1994-09-06T15:00:00Z", "tirsdag den 6. september 1994")]
public void DateFormatter_Full(string locale, string dateStr, string expected)
{
var mf = new MessageFormatter(locale: locale);
var mf = new MessageFormatter();
var actual = mf.FormatMessage("{value, date, full}", new
{
value = DateTimeOffset.Parse(dateStr)
});
}, CultureInfo.GetCultureInfo(locale));

Assert.Equal(expected, actual);
}
Expand All @@ -45,25 +47,25 @@ public void DateFormatter_UnsupportedStyle()
value = DateTimeOffset.UtcNow
}));
}

[Fact]
public void DateFormatter_Custom()
{
var formatter = new CustomValueFormatters
{
Date = (CultureInfo culture, object? value, string? _, out string? formatted) =>
Date = (culture, value, _, out formatted) =>
{
// This is just a test, you probably shouldn't be doing this in real workloads.
formatted = ((FormattableString)$"{value:MMMM d 'in the year' yyyy}").ToString(culture);
return true;
}
};
var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter);
var mf = new MessageFormatter(customValueFormatter: formatter);
var actual = mf.FormatMessage("{value, date, long}", new
{
value = DateTimeOffset.Parse("1994-09-06T15:00:00Z")
});
}, En);

Assert.Equal("September 6 in the year 1994", actual);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters;

public class NumberFormatterTests
{
private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en");

[Theory]
[InlineData(69, "69")]
[InlineData(69.420, "69.42")]
[InlineData(123_456.789, "123,456.789")]
[InlineData(1234567.1234567, "1,234,567.123")]
public void NumberFormatter_Default(decimal number, string expected)
{
var mf = new MessageFormatter(locale: "en-US");
var mf = new MessageFormatter();
// NOTE: The whitespace at the end is on purpose to cover whitespace tolerance in parsing.
var actual = mf.FormatMessage("{value, number }", new
{
value = number
});
}, En);

Assert.Equal(expected, actual);
}
Expand All @@ -30,18 +32,18 @@ public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected
{
var formatters = new CustomValueFormatters
{
Number = (CultureInfo culture, object? value, string? style, out string? formatted) =>
Number = (culture, value, style, out formatted) =>
{
formatted = string.Format(culture, $"{{0:{style}}}", value);
return true;
}
};
var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatters);
var mf = new MessageFormatter(customValueFormatter: formatters);

var actual = mf.FormatMessage("{value, number, 0.0000}", new
{
value = number
});
}, En);

Assert.Equal(expected, actual);
}
Expand All @@ -52,13 +54,13 @@ public void NumberFormatter_Decimal_CustomFormat(decimal number, string expected
[InlineData(1234567.1234567, "123,456,712%")]
public void NumberFormatter_Percent(decimal number, string expected)
{
var mf = new MessageFormatter(locale: "en-US");
var mf = new MessageFormatter();

// NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing.
var actual = mf.FormatMessage("{value, number,percent}", new
{
value = number
});
}, En);

Assert.Equal(expected, actual);
}
Expand All @@ -72,11 +74,11 @@ public void NumberFormatter_Percent(decimal number, string expected)
[InlineData(true, "True")]
public void NumberFormatter_Integer(object? value, string expected)
{
var mf = new MessageFormatter(locale: "en-US");
var mf = new MessageFormatter();
var actual = mf.FormatMessage("{value, number, integer}", new
{
value
});
}, En);

Assert.Equal(expected, actual);
}
Expand All @@ -87,13 +89,13 @@ public void NumberFormatter_Integer(object? value, string expected)
[InlineData("da-DK", 99.99, "99,99 kr.")]
public void NumberFormatter_Currency(string locale, decimal number, string expected)
{
var mf = new MessageFormatter(locale: locale);
var mf = new MessageFormatter();

// NOTE: The inconsistent whitespace in the pattern is to cover whitespace tolerance in parsing.
var actual = mf.FormatMessage("{value, number, currency }", new
{
value = number
});
}, CultureInfo.GetCultureInfo(locale));

Assert.Equal(expected, actual);
}
Expand All @@ -102,13 +104,13 @@ public void NumberFormatter_Currency(string locale, decimal number, string expec
public void NumberFormatter_ThrowsIfStyleIsNotSupported()
{
const decimal Number = 12.34m;
var mf = new MessageFormatter(locale: "en-US");
var mf = new MessageFormatter();
var ex = Assert.Throws<UnsupportedFormatStyleException>(() =>
mf.FormatMessage($"{{value, number, wow}}",
new
{
value = Number
}));
}, En));
Assert.Equal("value", ex.Variable);
Assert.Equal("number", ex.Format);
Assert.Equal("wow", ex.Style);
Expand All @@ -117,13 +119,13 @@ public void NumberFormatter_ThrowsIfStyleIsNotSupported()
[Fact]
public void NumberFormatter_BadInput_FallsBackToRegularFormat()
{
var mf = new MessageFormatter(locale: "en-US");
var mf = new MessageFormatter();

{
var actual = mf.FormatMessage($"{{value, number, currency}}", new
{
value = "a lot of money"
});
}, En);

Assert.Equal("a lot of money", actual);
}
Expand All @@ -132,7 +134,7 @@ public void NumberFormatter_BadInput_FallsBackToRegularFormat()
var actual = mf.FormatMessage($"{{value, number, integer}}", new
{
value = "a lot of money"
});
}, En);

Assert.Equal("a lot of money", actual);
}
Expand All @@ -141,9 +143,9 @@ public void NumberFormatter_BadInput_FallsBackToRegularFormat()
var actual = mf.FormatMessage($"{{value, number, integer}}", new
{
value = true
});
}, En);

Assert.Equal("True", actual);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Copyright (C) Jeff Hansen 2015. All rights reserved.

using System.Collections.Generic;
using System.Globalization;
using Jeffijoe.MessageFormat.Formatting;
using Jeffijoe.MessageFormat.Formatting.Formatters;
using Jeffijoe.MessageFormat.Parsing;
Expand Down Expand Up @@ -62,7 +63,7 @@ public void Format(string formatterArgs, string gender, string expectedBlock)
"select",
formatterArgs);
var args = new Dictionary<string, object?> { { "gender", gender } };
var result = subject.Format("en", req, args, gender, messageFormatter);
var result = subject.Format(CultureInfo.GetCultureInfo("en"), req, args, gender, messageFormatter);
Assert.Equal(expectedBlock, result);
}

Expand All @@ -83,7 +84,7 @@ public void VerifyFormatThrowsWhenNoOtherOptionIsGiven()

Assert.Throws<MessageFormatterException>(() =>
{
subject.Format("en", req, args, "non-binary", messageFormatter);
subject.Format(CultureInfo.GetCultureInfo("en"), req, args, "non-binary", messageFormatter);
});
}

Expand Down
Loading