diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c07a19..0099f60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 52ad3dd..d9ce36f 100644 --- a/README.md +++ b/README.md @@ -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" ``` @@ -158,11 +159,11 @@ mf.CardinalPluralizers.Add("", 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. @@ -175,7 +176,7 @@ mf.CardinalPluralizers["en"] = n => mf.FormatMessage("You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", new Dictionary{ {"number", 1001} -}); +}, CultureInfo.GetCultureInfo("en")); ```` ## Escaping literals diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj index 4a55968..241670f 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj @@ -15,11 +15,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs index d47015c..f12b098 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/PluralLanguagesGenerator.cs @@ -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(); + 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" }) @@ -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}")!; } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs index caf0dec..bc3a132 100644 --- a/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs +++ b/src/Jeffijoe.MessageFormat.MetadataGenerator/Plural/SourceGeneration/PluralRulesMetadataGenerator.cs @@ -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 diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs index c36168f..5c33a00 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/DateFormatterTests.cs @@ -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); } @@ -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); } @@ -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); } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs index 648cfab..d52ccff 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/NumberFormatterTests.cs @@ -6,6 +6,8 @@ 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")] @@ -13,12 +15,12 @@ public class NumberFormatterTests [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); } @@ -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); } @@ -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); } @@ -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); } @@ -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); } @@ -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(() => mf.FormatMessage($"{{value, number, wow}}", new { value = Number - })); + }, En)); Assert.Equal("value", ex.Variable); Assert.Equal("number", ex.Format); Assert.Equal("wow", ex.Style); @@ -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); } @@ -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); } @@ -141,9 +143,9 @@ public void NumberFormatter_BadInput_FallsBackToRegularFormat() var actual = mf.FormatMessage($"{{value, number, integer}}", new { value = true - }); + }, En); Assert.Equal("True", actual); } } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs index 4e2ce37..b5ef8fb 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/SelectFormatterTests.cs @@ -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; @@ -62,7 +63,7 @@ public void Format(string formatterArgs, string gender, string expectedBlock) "select", formatterArgs); var args = new Dictionary { { "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); } @@ -83,7 +84,7 @@ public void VerifyFormatThrowsWhenNoOtherOptionIsGiven() Assert.Throws(() => { - subject.Format("en", req, args, "non-binary", messageFormatter); + subject.Format(CultureInfo.GetCultureInfo("en"), req, args, "non-binary", messageFormatter); }); } diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs index ca22c7f..04f5715 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/TimeFormatterTests.cs @@ -8,17 +8,18 @@ namespace Jeffijoe.MessageFormat.Tests.Formatting.Formatters; public partial class TimeFormatterTests { + private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en"); [Theory] [InlineData("en-US", "1994-09-06T15:01:23Z", "3:01 PM")] [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01")] public void TimeFormatter_Short(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(locale: locale); + var mf = new MessageFormatter(); var actual = mf.FormatMessage("{value, time, short}", new { value = DateTimeOffset.Parse(dateStr) - }); + }, CultureInfo.GetCultureInfo(locale)); // Replacing all whitespace due to a difference in formatting on macOS vs Linux. expected = Normalize(expected); @@ -31,11 +32,11 @@ public void TimeFormatter_Short(string locale, string dateStr, string expected) [InlineData("da-DK", "1994-09-06T15:01:23Z", "15.01.23")] public void TimeFormatter_Default(string locale, string dateStr, string expected) { - var mf = new MessageFormatter(locale: locale); + var mf = new MessageFormatter(); var actual = mf.FormatMessage("{value, time}", new { value = DateTimeOffset.Parse(dateStr) - }); + }, CultureInfo.GetCultureInfo(locale)); // Replacing all whitespace due to a difference in formatting on macOS vs Linux. expected = Normalize(expected); @@ -53,23 +54,23 @@ public void TimeFormatter_UnsupportedStyle() value = DateTimeOffset.UtcNow })); } - + [Fact] public void TimeFormatter_Custom() { var formatter = new CustomValueFormatters { - Time = (CultureInfo _, object? value, string? _, out string? formatted) => + Time = (_, value, _, out formatted) => { formatted = $"{value:hmm} nice"; return true; } }; - var mf = new MessageFormatter(locale: "en-US", customValueFormatter: formatter); + var mf = new MessageFormatter(customValueFormatter: formatter); var actual = mf.FormatMessage("{value, time, long}", new { value = DateTimeOffset.Parse("1994-09-06T16:20:09Z") - }); + }, En); Assert.Equal("420 nice", actual); } @@ -81,4 +82,4 @@ private static string Normalize(string input) { return WhitespaceRegex().Replace(input, string.Empty); } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs index 9cd26c7..fbd8f44 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Formatting/Formatters/VariableFormatterTests.cs @@ -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; @@ -57,7 +58,7 @@ public void VerifyAnEmptyStringIsReturnedWhenTheArgumentIsNull() var req = CreateRequest(); var args = new Dictionary(); - Assert.Equal(string.Empty, this.subject.Format("en", req, args, null, this.formatter)); + Assert.Equal(string.Empty, this.subject.Format(CultureInfo.GetCultureInfo("en"), req, args, null, this.formatter)); } /// @@ -69,7 +70,7 @@ public void VerifyTheValueFromTheGivenArgumentsIsReturnedAsAString() var req = CreateRequest(); var args = new Dictionary(); - Assert.Equal("is good", this.subject.Format("en", req, args, "is good", this.formatter)); + Assert.Equal("is good", this.subject.Format(CultureInfo.GetCultureInfo("en"), req, args, "is good", this.formatter)); } #endregion diff --git a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj index a24f302..31baa32 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj +++ b/src/Jeffijoe.MessageFormat.Tests/Jeffijoe.MessageFormat.Tests.csproj @@ -4,20 +4,19 @@ True MessageFormat.snk False - 12 + default enable - net8.0 + net10.0 - - - + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs index ddb6497..c0b28ff 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterFullIntegrationTests.cs @@ -1,12 +1,12 @@ -// MessageFormat for .NET +// MessageFormat for .NET // - MessageFormatter_full_integration_tests.cs // // Author: Jeff Hansen // 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.Tests.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -20,6 +20,10 @@ public class MessageFormatterFullIntegrationTests { #region Fields + private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en"); + private static readonly CultureInfo EnUs = CultureInfo.GetCultureInfo("en-US"); + private static readonly CultureInfo DaDk = CultureInfo.GetCultureInfo("da-DK"); + /// /// The output helper. /// @@ -142,32 +146,32 @@ public static IEnumerable Tests { get { - const string Case1 = @"{gender, select, + const string Case1 = @"{gender, select, male {He - '{'{name}'}' -} female {She - '{'{name}'}' -} other {They} } said: You're pretty cool!"; - const string Case2 = @"{gender, select, + const string Case2 = @"{gender, select, male {He - '{'{name}'}' -} female {She - '{'{name}'}' -} other {They} - } said: You have {count, plural, + } said: You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} other {# notifications} }. Have a nice day!"; - const string Case3 = @"You have {count, plural, + const string Case3 = @"You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} other {# notifications} }. Have a nice day!"; - const string Case4 = @"{gender, select, + const string Case4 = @"{gender, select, male {He} female {She} other {They} - } said: You have {count, plural, + } said: You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} @@ -175,21 +179,21 @@ public static IEnumerable Tests }. Have a nice day!"; // Please take the following sample in the spirit it was intended. :) - const string Case5 = @"{gender, select, - male {He (who has {genitals, plural, + const string Case5 = @"{gender, select, + male {He (who has {genitals, plural, zero {no testicles} one {just one testicle} =2 {a normal amount of testicles} other {the insane amount of # testicles} })} - female {She (who has {genitals, plural, + female {She (who has {genitals, plural, zero {no boobies} one {just one boob} =2 {a pair of lovelies} other {the freakish amount of # boobies} })} other {They} - } said: You have {count, plural, + } said: You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} @@ -401,30 +405,22 @@ public static IEnumerable Tests [MemberData(nameof(Tests))] public void FormatMessage(string source, Dictionary args, string expected) { - var subject = new MessageFormatter(false); + var subject = new MessageFormatter(useCache: false); // Historically these tests relied on a default English pluralizer that mapped // 0 to "zero"; adding that back in manually to ensure we maintain test coverage // for multiple forms. - subject.CardinalPluralizers!.Add("en", (number) => + subject.CardinalPluralizers!.Add("en", number => number switch { - if (number == 0) - { - return "zero"; - } else if (number == 1) - { - return "one"; - } - else - { - return "other"; - } + 0 => "zero", + 1 => "one", + _ => "other" }); // Warmup - subject.FormatMessage(source, args); + subject.FormatMessage(source, args, En); Benchmark.Start("Formatting", this.outputHelper); - string result = subject.FormatMessage(source, args); + string result = subject.FormatMessage(source, args, En); Benchmark.End(this.outputHelper); Assert.Equal(expected, result); this.outputHelper.WriteLine(result); @@ -449,11 +445,11 @@ public void FormatMessage_escaping(string source, Dictionary ar [Fact] public void FormatMessage_debug() { - const string Source = @"{gender, select, + const string Source = @"{gender, select, male {He} female {She} other {They} - } said: You have {count, plural, + } said: You have {count, plural, zero {no notifications} one {just one notification} =42 {a universal amount of notifications} @@ -486,8 +482,8 @@ public void FormatMessage_lets_non_ascii_characters_right_through() public void FormatMessage_nesting_with_brace_escaping() { var subject = new MessageFormatter(false); - const string Pattern = @"{s1, select, - 1 {{s2, select, + const string Pattern = @"{s1, select, + 1 {{s2, select, 2 {'{'} }} }"; @@ -502,7 +498,7 @@ public void FormatMessage_nesting_with_brace_escaping() [Fact] public void FormatMessage_with_reflection_overload() { - var subject = new MessageFormatter(false); + var subject = new MessageFormatter(false, culture: EnUs); const string Pattern = "You have {UnreadCount, plural, " + "=0 {no unread messages}" + "one {just one unread message}" + "other {# unread messages}" + "} today."; @@ -525,7 +521,7 @@ public void FormatMessage_with_reflection_overload() /// /// The read me_test_to_make_sure_ i_dont_look_like_a_fool. /// - [Fact] + [Fact, UseCulture("en")] public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { { @@ -545,7 +541,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() { var mf = new MessageFormatter(false); const string Str = @"You {NUM_ADDS, plural, offset:1 - =0{didnt add this to your profile} + =0{didnt add this to your profile} =1{added this to your profile} one{and one other person added this to their profile} other{and # others added this to their profiles} @@ -653,7 +649,7 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() } { - var mf = new MessageFormatter(useCache: true, locale: "en"); + var mf = new MessageFormatter(useCache: true); mf.CardinalPluralizers!["en"] = n => { // ´n´ is the number being pluralized. @@ -680,10 +676,51 @@ public void ReadMe_test_to_make_sure_I_dont_look_like_a_fool() var actual = mf.FormatMessage( "You have {number, plural, thatsalot {a shitload of notifications} other {# notifications}}", - new Dictionary { { "number", 1001 } }); + new Dictionary { { "number", 1001 } }, + En); Assert.Equal("You have a shitload of notifications", actual); } } + [Fact] + public void FormatMessage_uses_constructor_culture_as_default() + { + var mf = new MessageFormatter(culture: DaDk); + + // Should use da-DK formatting without specifying culture on FormatMessage. + var result = mf.FormatMessage("{value, number}", new Dictionary { { "value", 1234.5m } }); + Assert.Equal("1.234,5", result); + } + + [Fact] + public void FormatMessage_with_culture_override() + { + var mf = new MessageFormatter(culture: EnUs); + + // Without override, uses the constructor culture (en-US). + var resultUs = mf.FormatMessage("{value, number}", new Dictionary { { "value", 1234.5m } }); + Assert.Equal("1,234.5", resultUs); + + // With override, uses da-DK formatting (period as thousands separator, comma as decimal). + var resultDk = mf.FormatMessage( + "{value, number}", + new Dictionary { { "value", 1234.5m } }, + DaDk); + Assert.Equal("1.234,5", resultDk); + } + + [Fact] + public void FormatMessage_culture_override_propagates_to_nested_formatting() + { + var mf = new MessageFormatter(); + + // The culture override should propagate through nested formatting (e.g. select -> number). + var result = mf.FormatMessage( + "{gender, select, male {He earned {amount, number}} other {They earned {amount, number}}}", + new Dictionary { { "gender", "male" }, { "amount", 1234.5m } }, + DaDk); + Assert.Equal("He earned 1.234,5", result); + } + #endregion -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs index 87aeca9..acb2cd1 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterIssues.cs @@ -1,10 +1,11 @@ -// MessageFormat for .NET +// MessageFormat for .NET // - MessageFormatter_full_integration_tests.cs -// +// // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; +using System.Globalization; using Xunit; namespace Jeffijoe.MessageFormat.Tests; @@ -14,6 +15,8 @@ namespace Jeffijoe.MessageFormat.Tests; /// public class MessageFormatterIssues { + private static readonly CultureInfo En = CultureInfo.GetCultureInfo("en"); + [Fact] public void Issue13_Bad_escaping_on_pound_symbol() { @@ -41,7 +44,7 @@ public void Issue27_WhiteSpace_in_identifiers_is_ignored() [Fact] public void Issue31_IDictionary_interface_support() { - var subject = new MessageFormatter(locale: "en-US"); + var subject = new MessageFormatter(); IDictionary idict = new Dictionary { @@ -53,14 +56,14 @@ public void Issue31_IDictionary_interface_support() ["string"] = "value" }; - Assert.Equal("value", subject.FormatMessage("{string}", idict)); - Assert.Equal("value", subject.FormatMessage("{string}", idictNullable!)); + Assert.Equal("value", subject.FormatMessage("{string}", idict, En)); + Assert.Equal("value", subject.FormatMessage("{string}", idictNullable!, En)); } [Fact] public void Issue34_Newlines_are_stripped() { - var subject = new MessageFormatter(locale: "en-US"); + var subject = new MessageFormatter(); const string Expected = "Single text which will not change.\nSummary:\nAccepted\nData:\n-X\n-Y\n-Z"; @@ -69,14 +72,14 @@ public void Issue34_Newlines_are_stripped() new { acceptedData = "\n-X\n-Y\n-Z" - }); + }, En); Assert.Equal(Expected, result); } [Fact] public void Issue45_Url_should_not_be_parsed_as_extension() { - var subject = new MessageFormatter(locale: "en-US"); + var subject = new MessageFormatter(); IDictionary dict = new Dictionary { @@ -85,7 +88,7 @@ public void Issue45_Url_should_not_be_parsed_as_extension() var result = subject.FormatMessage( "{cond, select, foo{https://www.google.com/} other{https://www.bing.com/}}", - dict); + dict, En); Assert.Equal("https://www.google.com/", result); } -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs index 1f55141..9aa57c3 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterStringExtensionTests.cs @@ -4,6 +4,7 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. +using System.Globalization; using System.Threading.Tasks; using Xunit; @@ -26,17 +27,19 @@ public class MessageFormatterStringExtensionTests [Fact] public async Task FormatMessage_with_multiple_tasks() { - var pattern = "Copying {fileCount, plural, one {one file} other{# files}}."; + const string Pattern = "Copying {fileCount, plural, one {one file} other{# files}}."; + + var en = CultureInfo.GetCultureInfo("en"); // 2 with the same message to test there are no issues with caching with multiple threads. - var t1 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); - var t2 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 1 })); - var t3 = Task.Run(() => MessageFormatter.Format(pattern, new { fileCount = 5 })); + var t1 = Task.Run(() => MessageFormatter.Format(Pattern, new { fileCount = 1 }, en)); + var t2 = Task.Run(() => MessageFormatter.Format(Pattern, new { fileCount = 1 }, en)); + var t3 = Task.Run(() => MessageFormatter.Format(Pattern, new { fileCount = 5 }, en)); await Task.WhenAll(t1, t2, t3); - Assert.Equal("Copying one file.", t1.Result); - Assert.Equal("Copying one file.", t2.Result); - Assert.Equal("Copying 5 files.", t3.Result); + Assert.Equal("Copying one file.", await t1); + Assert.Equal("Copying one file.", await t2); + Assert.Equal("Copying 5 files.", await t3); } #endregion diff --git a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs index 62677c0..270fa97 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MessageFormatterTests.cs @@ -5,6 +5,7 @@ // Copyright (C) Jeff Hansen 2015. All rights reserved. using System.Collections.Generic; +using System.Globalization; using System.Text; using Jeffijoe.MessageFormat.Formatting; @@ -110,7 +111,7 @@ public TestFormatter(bool variableMustExist, string formatterName) public bool CanFormat(FormatterRequest request) => request.FormatterName == this.formatterName; - public string Format(string locale, FormatterRequest request, IReadOnlyDictionary args, object? value, + public string Format(CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter) { return "formatted"; diff --git a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs index a17f8a5..08bc95a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/MetadataGenerator/ParserTests.cs @@ -2,7 +2,6 @@ using Jeffijoe.MessageFormat.MetadataGenerator.Plural.Parsing.AST; using System; -using System.Collections.Generic; using System.Xml; using Xunit; diff --git a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs index de7237a..1b04c6a 100644 --- a/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs +++ b/src/Jeffijoe.MessageFormat.Tests/Parsing/LiteralParserTests.cs @@ -4,7 +4,6 @@ // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2015. All rights reserved. -using System; using System.Linq; using System.Text; diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs index 0b97189..ef6fae7 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeFormatter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using Jeffijoe.MessageFormat.Formatting; namespace Jeffijoe.MessageFormat.Tests.TestHelpers; @@ -43,7 +44,7 @@ public FakeFormatter(bool canFormat = false, string formatResult = "formatted") /// public string Format( - string locale, + CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs index 59264a3..bc3819d 100644 --- a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/FakeMessageFormatter.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; namespace Jeffijoe.MessageFormat.Tests.TestHelpers; @@ -9,7 +10,7 @@ internal class FakeMessageFormatter : IMessageFormatter { public CustomValueFormatter? CustomValueFormatter { get; set; } - public string FormatMessage(string pattern, IReadOnlyDictionary argsMap) => pattern; + public string FormatMessage(string pattern, IReadOnlyDictionary argsMap, CultureInfo? culture = null) => pattern; public string FormatMessage(string pattern, object args) => pattern; } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat.Tests/TestHelpers/UseCultureAttribute.cs b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/UseCultureAttribute.cs new file mode 100644 index 0000000..f3bdf33 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/TestHelpers/UseCultureAttribute.cs @@ -0,0 +1,73 @@ +using System; +using System.Globalization; +using System.Reflection; +using System.Threading; +using Xunit.Sdk; + +namespace Jeffijoe.MessageFormat.Tests.TestHelpers; + +/// +/// Apply this attribute to your test method to replace the +/// and +/// with another culture. +/// +/// +/// Replaces the culture and UI culture of the current thread with +/// and +/// +/// The name of the culture. +/// The name of the UI culture. +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public class UseCultureAttribute(string culture, string uiCulture) : BeforeAfterTestAttribute +{ + private readonly Lazy culture = new(() => new CultureInfo(culture, false)); + private readonly Lazy uiCulture = new(() => new CultureInfo(uiCulture, false)); + + private CultureInfo? originalCulture; + private CultureInfo? originalUiCulture; + + /// + /// Replaces the culture and UI culture of the current thread with + /// + /// + /// The name of the culture. + /// + /// This constructor overload uses for both Culture and UICulture. + /// + public UseCultureAttribute(string culture) + : this(culture, culture) { } + + /// + /// Stores the current + /// and + /// and replaces them with the new cultures defined in the constructor. + /// + /// The method under test + public override void Before(MethodInfo methodUnderTest) + { + originalCulture = Thread.CurrentThread.CurrentCulture; + originalUiCulture = Thread.CurrentThread.CurrentUICulture; + + Thread.CurrentThread.CurrentCulture = culture.Value; + Thread.CurrentThread.CurrentUICulture = uiCulture.Value; + + CultureInfo.CurrentCulture.ClearCachedData(); + CultureInfo.CurrentUICulture.ClearCachedData(); + } + + /// + /// Restores the original and + /// to + /// + /// The method under test + public override void After(MethodInfo methodUnderTest) + { + if (originalCulture is not null) + Thread.CurrentThread.CurrentCulture = originalCulture; + if (originalUiCulture is not null) + Thread.CurrentThread.CurrentUICulture = originalUiCulture; + + CultureInfo.CurrentCulture.ClearCachedData(); + CultureInfo.CurrentUICulture.ClearCachedData(); + } +} \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs index 221e223..950202b 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/BaseValueFormatter.cs @@ -37,14 +37,13 @@ protected abstract string FormatValue(CultureInfo culture, CustomValueFormatter? /// public string Format( - string locale, + CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, IMessageFormatter messageFormatter) { var formatterArgs = request.FormatterArguments!; - var culture = CultureInfo.GetCultureInfo(locale); return FormatValue( culture: culture, customValueFormatter: messageFormatter.CustomValueFormatter, diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs index 3d005ff..a2bf1b5 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralFormatter.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; namespace Jeffijoe.MessageFormat.Formatting.Formatters; @@ -96,8 +97,8 @@ public bool CanFormat(FormatterRequest request) /// nested formatting. This is only called if returns true. /// The args will always contain the . /// - /// - /// The locale being used. It is up to the formatter what they do with this information. + /// + /// The culture being used. It is up to the formatter what they do with this information. /// /// /// The parameters. @@ -115,7 +116,7 @@ public bool CanFormat(FormatterRequest request) /// /// If does not specify a formatter name supported by . /// - public string Format(string locale, + public string Format(CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, @@ -148,6 +149,7 @@ public string Format(string locale, throw new MessageFormatterException($"Unsupported plural formatter name: {request.FormatterName}"); } + var locale = culture.Name; var ctx = CreatePluralContext(value, offset); var pluralized = this.Pluralize( locale, @@ -157,7 +159,7 @@ public string Format(string locale, ctx, offset); var result = this.ReplaceNumberLiterals(pluralized, ctx.Number); - var formatted = messageFormatter.FormatMessage(result, args); + var formatted = messageFormatter.FormatMessage(result, args, culture); return formatted; } diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs index 3c18382..ee9cab0 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/PluralRulesMetadata.cs @@ -1,6 +1,4 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Jeffijoe.MessageFormat.Formatting.Formatters; +namespace Jeffijoe.MessageFormat.Formatting.Formatters; [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] internal static partial class PluralRulesMetadata diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs index f90cace..0a5492c 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/SelectFormatter.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; namespace Jeffijoe.MessageFormat.Formatting.Formatters; @@ -48,7 +49,7 @@ public bool CanFormat(FormatterRequest request) /// nested formatting. This is only called if returns true. /// The args will always contain the . /// - /// The locale being used. It is up to the formatter what they do with this information. + /// The culture being used. It is up to the formatter what they do with this information. /// The parameters. /// The arguments. /// The value of from the given args dictionary. Can be null. @@ -59,7 +60,7 @@ public bool CanFormat(FormatterRequest request) /// 'other' option not found in pattern, and variable was not present in collection. [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1126:PrefixCallsCorrectly", Justification = "Reviewed. Suppression is OK here.")] - public string Format(string locale, + public string Format(CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, @@ -72,7 +73,7 @@ public string Format(string locale, { if (str == keyedBlock.Key) { - return messageFormatter.FormatMessage(keyedBlock.BlockText, args); + return messageFormatter.FormatMessage(keyedBlock.BlockText, args, culture); } if (keyedBlock.Key == OtherKey) @@ -87,7 +88,7 @@ public string Format(string locale, "'other' option not found in pattern, and variable was not present in collection."); } - return messageFormatter.FormatMessage(other.BlockText, args); + return messageFormatter.FormatMessage(other.BlockText, args, culture); } #endregion diff --git a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs index ea58b26..fbf2d21 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/Formatters/VariableFormatter.cs @@ -1,10 +1,9 @@ -// MessageFormat for .NET +// MessageFormat for .NET // - VariableFormatter.cs // Author: Jeff Hansen // Copyright (C) Jeff Hansen 2014. All rights reserved. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; @@ -15,21 +14,15 @@ namespace Jeffijoe.MessageFormat.Formatting.Formatters; /// public class VariableFormatter : IFormatter { - #region Fields - - private readonly ConcurrentDictionary cultures = new ConcurrentDictionary(); - - #endregion - #region Public Properties /// /// This formatter requires the input variable to exist. /// public bool VariableMustExist => true; - + #endregion - + #region Public Methods and Operators /// @@ -47,12 +40,12 @@ public bool CanFormat(FormatterRequest request) } /// - /// Using the specified parameters and arguments, a formatted string shall be returned. - /// The is being provided as well, to enable - /// nested formatting. This is only called if returns true. - /// The args will always contain the . + /// Using the specified parameters and arguments, a formatted string shall be returned. + /// The is being provided as well, to enable + /// nested formatting. This is only called if returns true. + /// The args will always contain the . /// - /// The locale being used. It is up to the formatter what they do with this information. + /// The culture being used. It is up to the formatter what they do with this information. /// The parameters. /// The arguments. /// The value of from the given args dictionary. Can be null. @@ -60,7 +53,7 @@ public bool CanFormat(FormatterRequest request) /// /// The . /// - public string Format(string locale, + public string Format(CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, @@ -69,27 +62,11 @@ public string Format(string locale, switch (value) { case IFormattable formattable: - return formattable.ToString(null, GetCultureInfo(locale)); + return formattable.ToString(null, culture); default: return value?.ToString() ?? string.Empty; } } - /// - /// Get and cache the culture for a locale. - /// - /// Locale for which to get the culture. - /// - /// Culture of locale. - /// - private CultureInfo GetCultureInfo(string locale) - { - if (!this.cultures.ContainsKey(locale)) - { - this.cultures[locale] = new CultureInfo(locale); - } - return this.cultures[locale]; - } - #endregion -} \ No newline at end of file +} diff --git a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs index abc8f2b..88e0327 100644 --- a/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs +++ b/src/Jeffijoe.MessageFormat/Formatting/IFormatter.cs @@ -4,6 +4,7 @@ // Copyright (C) Jeff Hansen 2014. All rights reserved. using System.Collections.Generic; +using System.Globalization; namespace Jeffijoe.MessageFormat.Formatting; @@ -41,7 +42,7 @@ public interface IFormatter /// nested formatting. This is only called if returns true. /// The args will always contain the . /// - /// The locale being used. It is up to the formatter what they do with this information. + /// The culture being used. It is up to the formatter what they do with this information. /// The parameters. /// The arguments. /// The value of from the given args dictionary. Can be null. @@ -50,7 +51,7 @@ public interface IFormatter /// The . /// string Format( - string locale, + CultureInfo culture, FormatterRequest request, IReadOnlyDictionary args, object? value, diff --git a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs index 41d6bbc..2029b4a 100644 --- a/src/Jeffijoe.MessageFormat/IMessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/IMessageFormatter.cs @@ -4,6 +4,7 @@ // Copyright (C) Jeff Hansen 2014. All rights reserved. using System.Collections.Generic; +using System.Globalization; namespace Jeffijoe.MessageFormat; @@ -32,10 +33,13 @@ public interface IMessageFormatter /// /// The arguments. /// + /// + /// The culture to use, or null to use . + /// /// /// The . /// - string FormatMessage(string pattern, IReadOnlyDictionary argsMap); + string FormatMessage(string pattern, IReadOnlyDictionary argsMap, CultureInfo? culture = null); #endregion } \ No newline at end of file diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index fd00743..e17ea51 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -10,7 +10,7 @@ https://github.com/jeffijoe/messageformat.net latest enable - net6.0;net8.0;netstandard2.0;netstandard2.1 + net8.0;netstandard2.0;netstandard2.1;net10.0 true true @@ -20,8 +20,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,8 +35,5 @@ - - - diff --git a/src/Jeffijoe.MessageFormat/MessageFormatter.cs b/src/Jeffijoe.MessageFormat/MessageFormatter.cs index 3e90160..b92fdc2 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatter.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatter.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -64,19 +65,20 @@ public class MessageFormatter : IMessageFormatter /// /// The use Cache. /// - /// - /// The locale. + /// + /// The default culture to use, or null to use . /// /// /// The custom value formatter to use. Can be null. /// - public MessageFormatter(bool useCache = true, string locale = "en", + public MessageFormatter(bool useCache = true, + CultureInfo? culture = null, CustomValueFormatter? customValueFormatter = null) : this( patternParser: new PatternParser(new LiteralParser()), library: new FormatterLibrary(), useCache: useCache, - locale: locale, + culture: culture, customValueFormatter: customValueFormatter) { } @@ -93,8 +95,8 @@ public MessageFormatter(bool useCache = true, string locale = "en", /// /// if set to true uses the cache. /// - /// - /// The locale to use. Formatters may need this. + /// + /// The default culture to use, or null to use . /// /// /// The custom value formatter to use. Can be null. @@ -103,13 +105,13 @@ internal MessageFormatter( IPatternParser patternParser, IFormatterLibrary library, bool useCache, - string locale = "en", + CultureInfo? culture = null, CustomValueFormatter? customValueFormatter = null) { this.patternParser = patternParser ?? throw new ArgumentNullException("patternParser"); this.library = library ?? throw new ArgumentNullException("library"); + this.Culture = culture; this.CustomValueFormatter = customValueFormatter; - this.Locale = locale; if (useCache) { this.cache = new ConcurrentDictionary(); @@ -120,6 +122,11 @@ internal MessageFormatter( #region Public Properties + /// + /// The default culture to use for formatting, or null to use . + /// + public CultureInfo? Culture { get; } + /// /// The custom value formatter to use for formats like `number`, `date`, `time` etc. /// @@ -136,14 +143,6 @@ public IFormatterLibrary Formatters get { return this.library; } } - /// - /// Gets or sets the locale. - /// - /// - /// The locale. - /// - public string Locale { get; set; } - /// /// Gets the custom cardinal pluralizers dictionary from the , if set. Key is the locale. /// These are the pluralizers used to translate e.g., {count, plural, one {1 book} other {# books}} @@ -192,7 +191,7 @@ public IDictionary? OrdinalPluralizers /// Formats the specified pattern with the specified data. /// /// - /// This method calls + /// This method calls /// on a singleton instance using a lock. /// Do not use in a tight loop, as a lock is being used to ensure thread safety. /// @@ -202,14 +201,17 @@ public IDictionary? OrdinalPluralizers /// /// The data. /// + /// + /// The culture to use, or null to use . + /// /// /// The formatted message. /// - public static string Format(string pattern, IReadOnlyDictionary data) + public static string Format(string pattern, IReadOnlyDictionary data, CultureInfo? culture = null) { lock (Lock) { - return Instance.FormatMessage(pattern, data); + return Instance.FormatMessage(pattern, data, culture); } } @@ -217,7 +219,7 @@ public static string Format(string pattern, IReadOnlyDictionary /// Formats the specified pattern with the specified data. /// /// This method calls - /// + /// /// on a singleton instance using a lock. /// Do not use in a tight loop, as a lock is being used to ensure thread safety. /// @@ -226,16 +228,19 @@ public static string Format(string pattern, IReadOnlyDictionary /// /// The data. /// + /// + /// The culture to use, or null to use . + /// /// /// The formatted message. /// [OverloadResolutionPriority(-1)] [RequiresUnreferencedCode("This method uses the FormatMessage extension which uses reflection to convert object into dictionary")] - public static string Format(string pattern, object data) + public static string Format(string pattern, object data, CultureInfo? culture = null) { lock (Lock) { - return Instance.FormatMessage(pattern, data); + return Instance.FormatMessage(pattern, data, culture); } } @@ -248,15 +253,19 @@ public static string Format(string pattern, object data) /// /// The arguments. /// + /// + /// The culture to use, or null to use . + /// /// /// The . /// - public string FormatMessage(string pattern, IReadOnlyDictionary args) + public string FormatMessage(string pattern, IReadOnlyDictionary args, CultureInfo? culture = null) { /* * We are assuming the formatters are ordered correctly * - that is, from left to right, string-wise. */ + var activeCulture = culture ?? this.Culture ?? CultureInfo.CurrentCulture; var sourceBuilder = StringBuilderPool.Get(); try @@ -276,7 +285,7 @@ public string FormatMessage(string pattern, IReadOnlyDictionary } // Double dispatch, yeah! - var result = formatter.Format(this.Locale, request, args, value, this); + var result = formatter.Format(activeCulture, request, args, value, this); // First, we remove the literal from the source. var sourceLiteral = request.SourceLiteral; diff --git a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs index 52787e8..8ec9d7c 100644 --- a/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs +++ b/src/Jeffijoe.MessageFormat/MessageFormatterExtensions.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Runtime.CompilerServices; using Jeffijoe.MessageFormat.Helpers; @@ -22,15 +23,19 @@ public static class MessageFormatterExtensions /// /// The arguments. /// + /// + /// The culture to use, or null to use . + /// /// /// The . /// public static string FormatMessage( this IMessageFormatter formatter, string pattern, - IDictionary args) + IDictionary args, + CultureInfo? culture = null) { - return formatter.FormatMessage(pattern, (IReadOnlyDictionary)args); + return formatter.FormatMessage(pattern, (IReadOnlyDictionary)args, culture); } /// @@ -45,13 +50,16 @@ public static string FormatMessage( /// /// The arguments. /// + /// + /// The culture to use, or null to use . + /// /// /// The . /// [OverloadResolutionPriority(-1)] [RequiresUnreferencedCode("This method uses the ToDictionary extension which uses reflection to convert object into dictionary")] - public static string FormatMessage(this IMessageFormatter formatter, string pattern, object args) + public static string FormatMessage(this IMessageFormatter formatter, string pattern, object args, CultureInfo? culture = null) { - return formatter.FormatMessage(pattern, args.ToDictionary()); + return formatter.FormatMessage(pattern, args.ToDictionary(), culture); } }