From 6285b6672455db8dc22ff25a924b1cfe6e218555 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 22 Feb 2021 16:55:21 +0100 Subject: [PATCH 1/4] Fix bug where Functions dict on MessageContext gets shared. --- Fluent.Net/MessageContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Fluent.Net/MessageContext.cs b/Fluent.Net/MessageContext.cs index c85a360..888f9b7 100644 --- a/Fluent.Net/MessageContext.cs +++ b/Fluent.Net/MessageContext.cs @@ -439,7 +439,7 @@ public MessageContext( UseIsolating = options.UseIsolating; } Transform = options?.Transform ?? NoOpTransform; - Functions = options?.Functions ?? s_emptyFunctions; + Functions = new Dictionary(options?.Functions ?? s_emptyFunctions); } public MessageContext( From 7c4d8359d18a2017a7c5cfb1914d864a945a9958 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 22 Feb 2021 17:01:06 +0100 Subject: [PATCH 2/4] Expose Messages property. 1. It wasn't public which seemed like a mistake. 2. It previously returned IEnumerator> which makes no sense. I upgraded it to IReadOnlyDictionary since there's no reason not to and this is more useful. --- Fluent.Net/MessageContext.cs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Fluent.Net/MessageContext.cs b/Fluent.Net/MessageContext.cs index 888f9b7..2ad9f44 100644 --- a/Fluent.Net/MessageContext.cs +++ b/Fluent.Net/MessageContext.cs @@ -386,8 +386,8 @@ public class MessageContext readonly static IDictionary s_emptyFunctions = new Dictionary(); public IEnumerable Locales { get; private set; } - internal IDictionary _messages = new Dictionary(); - internal IDictionary _terms = new Dictionary(); + internal Dictionary _messages = new Dictionary(); + internal Dictionary _terms = new Dictionary(); public Func Transform { get; private set; } public bool UseIsolating { get; private set; } = true; public IDictionary Functions { get; private set; } @@ -448,18 +448,11 @@ public MessageContext( ) : this(new string[] { locale }, options) { } - - - + /// - /// Return an iterator over public `[id, message]` pairs. - /// - /// @returns {Iterator} - /// - IEnumerator> Messages - { - get { return _messages.GetEnumerator(); } - } + /// All available messages in the context. + /// + private IReadOnlyDictionary Messages => _messages; /// /// Check if a message is present in the context. @@ -578,7 +571,7 @@ public IList AddResource(FluentResource resource) $"Attempt to override an existing term: \"{entry.Key}\"")); continue; } - _terms.Add(entry); + _terms.Add(entry.Key, entry.Value); } else { @@ -588,7 +581,7 @@ public IList AddResource(FluentResource resource) $"Attempt to override an existing message: \"{entry.Key}\"")); continue; } - _messages.Add(entry); + _messages.Add(entry.Key, entry.Value); } } return errors; From 3e8bcb16f62ae86606093d593aaa44112a1dd543 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 22 Feb 2021 17:23:00 +0100 Subject: [PATCH 3/4] Move things out of MessageContext.cs This file had no business containing all these random types. --- Fluent.Net/BuiltIns.cs | 35 ++++ Fluent.Net/FluentError.cs | 36 ++++ Fluent.Net/FluentResource.cs | 30 +++ Fluent.Net/MessageContext.cs | 354 ----------------------------------- Fluent.Net/ParseException.cs | 8 + Fluent.Net/Resolver.cs | 62 ++++-- Fluent.Net/Types.cs | 272 +++++++++++++++++++++++++++ 7 files changed, 424 insertions(+), 373 deletions(-) create mode 100644 Fluent.Net/BuiltIns.cs create mode 100644 Fluent.Net/FluentError.cs create mode 100644 Fluent.Net/FluentResource.cs create mode 100644 Fluent.Net/Types.cs diff --git a/Fluent.Net/BuiltIns.cs b/Fluent.Net/BuiltIns.cs new file mode 100644 index 0000000..84659b5 --- /dev/null +++ b/Fluent.Net/BuiltIns.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace Fluent.Net +{ + internal static class BuiltIns + { + public static FluentType Number(IList args, IDictionary options) + { + // TODO: add to errors? what we doin here? + if (args.Count != 1) + { + throw new Exception("Too many arguments to NUMBER() function"); + } + if (args[0].GetType() != typeof(FluentNumber)) + { + throw new Exception("NUMBER() expected an argument of type FluentNumber"); + } + return (FluentNumber)args[0]; + } + + public static FluentType DateTime(IList args, IDictionary options) + { + if (args.Count != 1) + { + throw new Exception("Too many arguments to DATETIME() function"); + } + if (args[0].GetType() != typeof(FluentDateTime)) + { + throw new Exception("DATETIME() expected an argument of type FluentDateTime"); + } + return (FluentDateTime)args[0]; + } + } +} \ No newline at end of file diff --git a/Fluent.Net/FluentError.cs b/Fluent.Net/FluentError.cs new file mode 100644 index 0000000..16d9a07 --- /dev/null +++ b/Fluent.Net/FluentError.cs @@ -0,0 +1,36 @@ +namespace Fluent.Net +{ + public abstract class FluentError + { + public string Message { get; set; } + + public FluentError(string message) + { + Message = message; + } + } + + class RangeError : FluentError + { + public RangeError(string message) : + base(message) + { + } + } + + class TypeError : FluentError + { + public TypeError(string message) : + base(message) + { + } + } + + class ReferenceError : FluentError + { + public ReferenceError(string message) : + base(message) + { + } + } +} \ No newline at end of file diff --git a/Fluent.Net/FluentResource.cs b/Fluent.Net/FluentResource.cs new file mode 100644 index 0000000..0410614 --- /dev/null +++ b/Fluent.Net/FluentResource.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.IO; +using Fluent.Net.RuntimeAst; + +namespace Fluent.Net +{ + /// + /// Fluent Resource is a structure storing a map + /// of localization entries. + /// + public class FluentResource + { + public IDictionary Entries { get; } + public IList Errors { get; } + + public FluentResource(IDictionary entries, + IList errors) + { + Entries = entries; + Errors = errors; + } + + public static FluentResource FromReader(TextReader reader) + { + var parser = new RuntimeParser(); + var resource = parser.GetResource(reader); + return new FluentResource(resource.Entries, resource.Errors); + } + } +} \ No newline at end of file diff --git a/Fluent.Net/MessageContext.cs b/Fluent.Net/MessageContext.cs index 2ad9f44..2c9274e 100644 --- a/Fluent.Net/MessageContext.cs +++ b/Fluent.Net/MessageContext.cs @@ -14,360 +14,6 @@ public class MessageContextOptions public IDictionary Functions { get; set; } } - static class BuiltIns - { - public static FluentType Number(IList args, IDictionary options) - { - // TODO: add to errors? what we doin here? - if (args.Count != 1) - { - throw new Exception("Too many arguments to NUMBER() function"); - } - if (args[0].GetType() != typeof(FluentNumber)) - { - throw new Exception("NUMBER() expected an argument of type FluentNumber"); - } - return (FluentNumber)args[0]; - } - - public static FluentType DateTime(IList args, IDictionary options) - { - if (args.Count != 1) - { - throw new Exception("Too many arguments to DATETIME() function"); - } - if (args[0].GetType() != typeof(FluentDateTime)) - { - throw new Exception("DATETIME() expected an argument of type FluentDateTime"); - } - return (FluentDateTime)args[0]; - } - } - - public class ResolverEnvironment - { - public ICollection Errors { get; set; } - public IDictionary Arguments { get; set; } - public MessageContext Context { get; set; } - public HashSet Dirty { get; set; } = new HashSet(); - } - - public abstract class FluentError - { - public string Message { get; set; } - - public FluentError(string message) - { - Message = message; - } - } - - class RangeError : FluentError - { - public RangeError(string message) : - base(message) - { - } - } - - class TypeError : FluentError - { - public TypeError(string message) : - base(message) - { - } - } - - class ReferenceError : FluentError - { - public ReferenceError(string message) : - base(message) - { - } - } - - class OverrideError : ParseException - { - public OverrideError(string message) : - base(message) - { - } - } - - public interface IFluentType - { - string Value { get; set; } - string Format(MessageContext ctx); - bool Match(MessageContext ctx, object obj); - } - - /** - * The `FluentType` class is the base of Fluent's type system. - * - * Fluent types wrap JavaScript values and store additional configuration for - * them, which can then be used in the `toString` method together with a proper - * `Intl` formatter. - */ - public abstract class FluentType : IFluentType - { - public string Value { get; set; } - - /** - * Create an `FluentType` instance. - * - * @param {Any} value - JavaScript value to wrap. - * @param {Object} opts - Configuration. - * @returns {FluentType} - */ - public FluentType(string value = null) - { - Value = value; - } - - /** - * Unwrap the raw value stored by this `FluentType`. - * - * @returns {Any} - public string ValueOf() - { - return Value; - } - */ - - /** - * Format this instance of `FluentType` to a string. - * - * Formatted values are suitable for use outside of the `MessageContext`. - * This method can use `Intl` formatters memoized by the `MessageContext` - * instance passed as an argument. - * - * @param {MessageContext} [ctx] - * @returns {string} - */ - public abstract string Format(MessageContext ctx); - public abstract bool Match(MessageContext ctx, object obj); - } - - public class FluentNone : Node, IFluentType - { - public string Value { get; set; } - - public FluentNone(string value = null) - { - Value = value; - } - - public string Format(MessageContext ctx) - { - return !String.IsNullOrEmpty(Value) ? Value : "???"; - } - - public bool Match(MessageContext ctx, object other) - { - return other is FluentNone; - } - } - - public class FluentString : FluentType - { - public FluentString(string value) : - base(value) - { - } - - public override string Format(MessageContext ctx) - { - return Value; - } - - public override bool Match(MessageContext ctx, object other) - { - if (other is FluentString str) - { - return str.Value == Value; - } - if (other is string s) - { - return s == Value; - } - return false; - } - } - - public class FluentNumber : FluentType - { - double _numberValue; - - public FluentNumber(string value) : - base(value) - { - _numberValue = Double.Parse(value); - } - - public override string Format(MessageContext ctx) - { - // TODO: match js number formattiing here - // System.Globalization.CultureInfo culture = new System.Globalization.CultureInfo( - return String.Format(ctx.Culture, "{0}", _numberValue); - } - - /** - * Compare the object with another instance of a FluentType. - * - * @param {MessageContext} ctx - * @param {FluentType} other - * @returns {bool} - */ - public override bool Match(MessageContext ctx, object other) - { - if (other is FluentNumber n) - { - return _numberValue == n._numberValue; - } - if (other is double d) - { - return _numberValue == d; - } - if (other is float f) - { - return (float)_numberValue == f; - } - if (other is decimal dec) - { - return _numberValue == (double)dec; - } - if (other is sbyte sb) - { - return _numberValue == sb; - } - if (other is short s) - { - return _numberValue == s; - } - if (other is int i) - { - return _numberValue == i; - } - if (other is long l) - { - return _numberValue == l; - } - if (other is byte b) - { - return _numberValue == b; - } - if (other is ushort us) - { - return _numberValue == us; - } - if (other is uint ui) - { - return _numberValue == ui; - } - if (other is ulong ul) - { - return _numberValue == ul; - } - return false; - } - } - - public class FluentDateTime : FluentType - { - DateTime _dateValue; - - public FluentDateTime(DateTime value) : - base(value.ToString("o")) - { - _dateValue = value; - } - - public override string Format(MessageContext ctx) - { - // TODO: match js number formattiig here? - // System.Globalization.CultureInfo culture = new System.Globalization.CultureInfo( - return String.Format(ctx.Culture, "{0}", _dateValue); - } - - public override bool Match(MessageContext ctx, object other) - { - if (other is FluentDateTime d) - { - return _dateValue == d._dateValue; - } - if (other is DateTime dt) - { - return _dateValue == dt; - } - return false; - } - } - - public class FluentSymbol : FluentType - { - public FluentSymbol(string value) : - base(value) - { - } - - public override string Format(MessageContext ctx) - { - return Value; - } - - /** - * Compare the object with another instance of a FluentType. - * - * @param {MessageContext} ctx - * @param {FluentType} other - * @returns {bool} - */ - public override bool Match(MessageContext ctx, object other) - { - if (other is FluentSymbol symbol) - { - return Value == symbol.Value; - } - else if (other is string str) - { - return Value == str; - } - else if (other is FluentString fstr) - { - return Value == fstr.Value; - } - else if (other is FluentNumber fnum) - { - return Value == Plural.LocaleRules.Select(ctx.Locales, fnum.Value); - } - return false; - } - } - - /// - /// Fluent Resource is a structure storing a map - /// of localization entries. - /// - public class FluentResource - { - public IDictionary Entries { get; } - public IList Errors { get; } - - public FluentResource(IDictionary entries, - IList errors) - { - Entries = entries; - Errors = errors; - } - - public static FluentResource FromReader(TextReader reader) - { - var parser = new RuntimeParser(); - var resource = parser.GetResource(reader); - return new FluentResource(resource.Entries, resource.Errors); - } - } - /// /// Message contexts are single-language stores of translations. They are /// responsible for parsing translation resources in the Fluent syntax and can diff --git a/Fluent.Net/ParseException.cs b/Fluent.Net/ParseException.cs index 35d3f5e..de7aafc 100644 --- a/Fluent.Net/ParseException.cs +++ b/Fluent.Net/ParseException.cs @@ -80,4 +80,12 @@ static string GetErrorMessage(string code, string[] args) } } } + + class OverrideError : ParseException + { + public OverrideError(string message) : + base(message) + { + } + } } diff --git a/Fluent.Net/Resolver.cs b/Fluent.Net/Resolver.cs index f4a96f7..60c619b 100644 --- a/Fluent.Net/Resolver.cs +++ b/Fluent.Net/Resolver.cs @@ -6,7 +6,7 @@ namespace Fluent.Net { -/** + /** * @overview * * The role of the Fluent resolver is to format a translation object to an @@ -59,10 +59,11 @@ public delegate FluentType ExternalFunction(IList args, IDictionary options); static public IDictionary BuiltInFunctions { get; } = - new Dictionary() { - { "NUMBER", BuiltIns.Number }, - { "DATETIME", BuiltIns.DateTime } - }; + new Dictionary() + { + {"NUMBER", BuiltIns.Number}, + {"DATETIME", BuiltIns.DateTime} + }; // Prevent expansion of too long placeables. const int MAX_PLACEABLE_LENGTH = 2500; @@ -93,6 +94,7 @@ static IFluentType ResolveNode(ResolverEnvironment env, Node expr) { return new FluentString(env.Context.Transform(se.Value)); } + if (expr is FluentNone none) { return none; @@ -102,48 +104,58 @@ static IFluentType ResolveNode(ResolverEnvironment env, Node expr) { return ResolveNode(env, msg.Value); } + if (expr is Pattern p) { return Pattern(env, p); } + if (expr is VariantName varName) { return new FluentSymbol(varName.Name); } + if (expr is NumberLiteral num) { return new FluentNumber(num.Value); } + if (expr is VariableReference arg) { return VariableReference(env, arg); } + // case "fun": // return FunctionReference(env, expr); if (expr is CallExpression call) { return CallExpression(env, call); } + if (expr is MessageReference ref_) { var msgRef = MessageReference(env, ref_); return ResolveNode(env, msgRef); } + if (expr is GetAttribute attrExpr) { var attr = AttributeExpression(env, attrExpr); return ResolveNode(env, attr); } + if (expr is GetVariant varExpr) { var variant = VariantExpression(env, varExpr); return ResolveNode(env, variant); } + if (expr is SelectExpression sel) { var member = SelectExpression(env, sel); return ResolveNode(env, member); } + env.Errors?.Add(new RangeError("No value")); return new FluentNone(); } @@ -186,13 +198,13 @@ static Node DefaultMember(ResolverEnvironment env, IList members, int? * @returns {FluentType} * @private */ - static Node MessageReference(ResolverEnvironment env, MessageReference ref_) { bool isTerm = ref_.Name.StartsWith("-"); Message message; - (isTerm ? env.Context._terms - : env.Context._messages).TryGetValue(ref_.Name, out message); + (isTerm + ? env.Context._terms + : env.Context._messages).TryGetValue(ref_.Name, out message); if (message == null) { @@ -229,6 +241,7 @@ static Node VariantExpression(ResolverEnvironment env, GetVariant expr) { return message; } + if (message is Message actualMessage) { message = actualMessage.Value; @@ -241,7 +254,7 @@ static Node VariantExpression(ResolverEnvironment env, GetVariant expr) } } - var keyword = (IFluentType)ResolveNode(env, expr.Key); + var keyword = (IFluentType) ResolveNode(env, expr.Key); if (message is SelectExpression sexp) { @@ -283,7 +296,7 @@ static Node AttributeExpression(ResolverEnvironment env, GetAttribute expr) return message; } - var messageNode = (Message)message; + var messageNode = (Message) message; if (messageNode.Attributes != null) { // Match the specified name against keys of each attribute. @@ -332,14 +345,14 @@ static Node SelectExpression(ResolverEnvironment env, SelectExpression sexp) { var key = ResolveNode(env, variant.Key); bool keyCanMatch = - key is FluentNumber || key is FluentSymbol; + key is FluentNumber || key is FluentSymbol; if (!keyCanMatch) { continue; } - if (((IFluentType)key).Match(env.Context, selector)) + if (((IFluentType) key).Match(env.Context, selector)) { return variant.Value; } @@ -381,12 +394,14 @@ static IFluentType VariableReference(ResolverEnvironment env, VariableReference { return new FluentString(str); } + if (arg is sbyte || arg is short || arg is int || arg is long || arg is byte || arg is ushort || arg is uint || arg is ulong || arg is float || arg is double || arg is decimal) { return new FluentNumber(arg.ToString()); } + if (arg is DateTime dt) { return new FluentDateTime(dt); @@ -438,6 +453,7 @@ static IFluentType CallExpression(ResolverEnvironment env, CallExpression expr) posArgs.Add(ResolveNode(env, arg)); } } + return fn(posArgs, keyArgs); // try { // return callee(posargs, keyargs); @@ -473,7 +489,7 @@ static IFluentType Pattern(ResolverEnvironment env, Pattern pattern) // Wrap interpolations with Directional Isolate Formatting characters // only when the pattern has more than one element. var useIsolating = env.Context.UseIsolating && - pattern.Elements.Count > 1; + pattern.Elements.Count > 1; foreach (var elem in pattern.Elements) { @@ -484,7 +500,7 @@ static IFluentType Pattern(ResolverEnvironment env, Pattern pattern) } // var part = ((IFluentType)ResolveNode(env, elem)).Format(env.Context); - var resolved = ((IFluentType)ResolveNode(env, elem)); + var resolved = ((IFluentType) ResolveNode(env, elem)); var part = resolved.Format(env.Context); if (useIsolating) @@ -495,9 +511,9 @@ static IFluentType Pattern(ResolverEnvironment env, Pattern pattern) if (part.Length > MAX_PLACEABLE_LENGTH) { env.Errors?.Add( - new RangeError( - "Too many characters in placeable " + - $"({part.Length}, max allowed is {MAX_PLACEABLE_LENGTH})")); + new RangeError( + "Too many characters in placeable " + + $"({part.Length}, max allowed is {MAX_PLACEABLE_LENGTH})")); result.Append(part.Substring(0, MAX_PLACEABLE_LENGTH)); } else @@ -539,7 +555,15 @@ static public string Resolve(MessageContext ctx, Node message, Context = ctx, Errors = errors }; - return ((IFluentType)ResolveNode(env, message)).Format(ctx); + return ((IFluentType) ResolveNode(env, message)).Format(ctx); } } -} + + public class ResolverEnvironment + { + public ICollection Errors { get; set; } + public IDictionary Arguments { get; set; } + public MessageContext Context { get; set; } + public HashSet Dirty { get; set; } = new HashSet(); + } +} \ No newline at end of file diff --git a/Fluent.Net/Types.cs b/Fluent.Net/Types.cs new file mode 100644 index 0000000..b6f0904 --- /dev/null +++ b/Fluent.Net/Types.cs @@ -0,0 +1,272 @@ +using System; +using Fluent.Net.RuntimeAst; + +namespace Fluent.Net +{ + public interface IFluentType + { + string Value { get; set; } + string Format(MessageContext ctx); + bool Match(MessageContext ctx, object obj); + } + + /** + * The `FluentType` class is the base of Fluent's type system. + * + * Fluent types wrap JavaScript values and store additional configuration for + * them, which can then be used in the `toString` method together with a proper + * `Intl` formatter. + */ + public abstract class FluentType : IFluentType + { + public string Value { get; set; } + + /** + * Create an `FluentType` instance. + * + * @param {Any} value - JavaScript value to wrap. + * @param {Object} opts - Configuration. + * @returns {FluentType} + */ + public FluentType(string value = null) + { + Value = value; + } + + /** + * Unwrap the raw value stored by this `FluentType`. + * + * @returns {Any} + public string ValueOf() + { + return Value; + } + */ + /** + * Format this instance of `FluentType` to a string. + * + * Formatted values are suitable for use outside of the `MessageContext`. + * This method can use `Intl` formatters memoized by the `MessageContext` + * instance passed as an argument. + * + * @param {MessageContext} [ctx] + * @returns {string} + */ + public abstract string Format(MessageContext ctx); + + public abstract bool Match(MessageContext ctx, object obj); + } + + public class FluentNone : Node, IFluentType + { + public string Value { get; set; } + + public FluentNone(string value = null) + { + Value = value; + } + + public string Format(MessageContext ctx) + { + return !String.IsNullOrEmpty(Value) ? Value : "???"; + } + + public bool Match(MessageContext ctx, object other) + { + return other is FluentNone; + } + } + + public class FluentString : FluentType + { + public FluentString(string value) : + base(value) + { + } + + public override string Format(MessageContext ctx) + { + return Value; + } + + public override bool Match(MessageContext ctx, object other) + { + if (other is FluentString str) + { + return str.Value == Value; + } + + if (other is string s) + { + return s == Value; + } + + return false; + } + } + + public class FluentNumber : FluentType + { + double _numberValue; + + public FluentNumber(string value) : + base(value) + { + _numberValue = Double.Parse(value); + } + + public override string Format(MessageContext ctx) + { + // TODO: match js number formattiing here + // System.Globalization.CultureInfo culture = new System.Globalization.CultureInfo( + return String.Format(ctx.Culture, "{0}", _numberValue); + } + + /** + * Compare the object with another instance of a FluentType. + * + * @param {MessageContext} ctx + * @param {FluentType} other + * @returns {bool} + */ + public override bool Match(MessageContext ctx, object other) + { + if (other is FluentNumber n) + { + return _numberValue == n._numberValue; + } + + if (other is double d) + { + return _numberValue == d; + } + + if (other is float f) + { + return (float) _numberValue == f; + } + + if (other is decimal dec) + { + return _numberValue == (double) dec; + } + + if (other is sbyte sb) + { + return _numberValue == sb; + } + + if (other is short s) + { + return _numberValue == s; + } + + if (other is int i) + { + return _numberValue == i; + } + + if (other is long l) + { + return _numberValue == l; + } + + if (other is byte b) + { + return _numberValue == b; + } + + if (other is ushort us) + { + return _numberValue == us; + } + + if (other is uint ui) + { + return _numberValue == ui; + } + + if (other is ulong ul) + { + return _numberValue == ul; + } + + return false; + } + } + + public class FluentDateTime : FluentType + { + DateTime _dateValue; + + public FluentDateTime(DateTime value) : + base(value.ToString("o")) + { + _dateValue = value; + } + + public override string Format(MessageContext ctx) + { + // TODO: match js number formattiig here? + // System.Globalization.CultureInfo culture = new System.Globalization.CultureInfo( + return String.Format(ctx.Culture, "{0}", _dateValue); + } + + public override bool Match(MessageContext ctx, object other) + { + if (other is FluentDateTime d) + { + return _dateValue == d._dateValue; + } + + if (other is DateTime dt) + { + return _dateValue == dt; + } + + return false; + } + } + + public class FluentSymbol : FluentType + { + public FluentSymbol(string value) : + base(value) + { + } + + public override string Format(MessageContext ctx) + { + return Value; + } + + /** + * Compare the object with another instance of a FluentType. + * + * @param {MessageContext} ctx + * @param {FluentType} other + * @returns {bool} + */ + public override bool Match(MessageContext ctx, object other) + { + if (other is FluentSymbol symbol) + { + return Value == symbol.Value; + } + else if (other is string str) + { + return Value == str; + } + else if (other is FluentString fstr) + { + return Value == fstr.Value; + } + else if (other is FluentNumber fnum) + { + return Value == Plural.LocaleRules.Select(ctx.Locales, fnum.Value); + } + + return false; + } + } +} \ No newline at end of file From b790b8635569d4d688a230570d34697682016422 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Mon, 22 Feb 2021 18:16:53 +0100 Subject: [PATCH 4/4] Pipe cultures around instead of locale strings. This means that if the user passes in a single locale like `en-US` plural handling won't break. Also allows users to pass custom CultureInfo objects that can have custom formatting parameters (or, for example, disable user system overrides) Yes I did this because the tests failed on my system due to that last point. --- .../TranslationService.cs | 15 +---- Fluent.Net.Test/IsolatingTest.cs | 2 +- Fluent.Net.Test/MessageContextTest.cs | 3 +- Fluent.Net.Test/MessageContextTestBase.cs | 4 +- Fluent.Net.Test/Plural/LocaleRulesTest.cs | 62 +++++++++---------- Fluent.Net/MessageContext.cs | 28 +++++++-- Fluent.Net/Plural/LocaleRules.cs | 29 ++++++--- Fluent.Net/Types.cs | 2 +- 8 files changed, 79 insertions(+), 66 deletions(-) diff --git a/Fluent.Net.SimpleExample/TranslationService.cs b/Fluent.Net.SimpleExample/TranslationService.cs index 0d87d99..aa0f493 100644 --- a/Fluent.Net.SimpleExample/TranslationService.cs +++ b/Fluent.Net.SimpleExample/TranslationService.cs @@ -72,19 +72,6 @@ public string GetString(string id, IDictionary args = null, return ""; } - public string PreferredLocale => _contexts.First().Locales.First(); - - CultureInfo _culture; - public CultureInfo Culture - { - get - { - if (_culture == null) - { - _culture = new CultureInfo(PreferredLocale); - } - return _culture; - } - } + public CultureInfo Culture => _contexts.First().Culture; } } diff --git a/Fluent.Net.Test/IsolatingTest.cs b/Fluent.Net.Test/IsolatingTest.cs index 40868ee..5284559 100644 --- a/Fluent.Net.Test/IsolatingTest.cs +++ b/Fluent.Net.Test/IsolatingTest.cs @@ -67,7 +67,7 @@ public void IsolatesInterpolatedDateTypedVariables() var args = new Dictionary { { "arg", dt } }; var msg = ctx.GetMessage("baz"); var val = ctx.Format(msg, args, errors); - var dtf = dt.ToString(new CultureInfo(ctx.Locales.First())); + var dtf = dt.ToString(ctx.Culture); val.Should().Be($"{FSI}{dtf}{PDI} Baz"); errors.Count.Should().Be(0); } diff --git a/Fluent.Net.Test/MessageContextTest.cs b/Fluent.Net.Test/MessageContextTest.cs index b542a1a..b138c5d 100644 --- a/Fluent.Net.Test/MessageContextTest.cs +++ b/Fluent.Net.Test/MessageContextTest.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using System; using System.Collections.Generic; +using System.Globalization; namespace Fluent.Net.Test { @@ -203,7 +204,7 @@ public void TestFluentNumber() public void TestFluentDateTime() { var dt = new FluentDateTime(new DateTime(2009, 01, 02)); - var ctx = new MessageContext("en-US"); + var ctx = new MessageContext(new CultureInfo("en-US", useUserOverride: false)); dt.Format(ctx).Should().Be("1/2/2009 12:00:00 AM"); dt.Match(ctx, new FluentDateTime(new DateTime(2009, 01, 02))).Should().BeTrue(); dt.Match(ctx, new FluentDateTime(new DateTime(2009, 01, 03))).Should().BeFalse(); diff --git a/Fluent.Net.Test/MessageContextTestBase.cs b/Fluent.Net.Test/MessageContextTestBase.cs index 2f9f107..6dae5f7 100644 --- a/Fluent.Net.Test/MessageContextTestBase.cs +++ b/Fluent.Net.Test/MessageContextTestBase.cs @@ -1,5 +1,6 @@ using FluentAssertions; using System.Collections.Generic; +using System.Globalization; namespace Fluent.Net.Test { @@ -7,8 +8,7 @@ public class MessageContextTestBase : FtlTestBase { protected static MessageContext CreateContext(string ftl, bool useIsolating = false) { - var locales = new string[] { "en-US", "en" }; - var ctx = new MessageContext(locales, new MessageContextOptions() + var ctx = new MessageContext(new CultureInfo("en-US", useUserOverride: false), new MessageContextOptions() { UseIsolating = useIsolating }); var errors = ctx.AddMessages(Ftl(ftl)); errors.Should().BeEquivalentTo(new List()); diff --git a/Fluent.Net.Test/Plural/LocaleRulesTest.cs b/Fluent.Net.Test/Plural/LocaleRulesTest.cs index 296b881..a2fd600 100644 --- a/Fluent.Net.Test/Plural/LocaleRulesTest.cs +++ b/Fluent.Net.Test/Plural/LocaleRulesTest.cs @@ -7,41 +7,35 @@ namespace Fluent.Net.Test.Plural public class LocaleRulesTest { [Test] - public void CheckEnRules() + [TestCase("en", "1", ExpectedResult = "one")] + [TestCase("en-US", "1", ExpectedResult = "one")] + [TestCase("en", "1.6", ExpectedResult = "other")] + [TestCase("en", "1.0", ExpectedResult = "other")] + [TestCase("en", "9", ExpectedResult = "other")] + // for lt: + // n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, … + // n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, … + // f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … + // @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … + [TestCase("lt", "1", ExpectedResult = "one")] + [TestCase("lt", "11", ExpectedResult = "other")] + [TestCase("lt", "21", ExpectedResult = "one")] + [TestCase("lt", "91", ExpectedResult = "one")] + [TestCase("lt", "101", ExpectedResult = "one")] + [TestCase("lt", "2", ExpectedResult = "few")] + [TestCase("lt", "37", ExpectedResult = "few")] + [TestCase("lt", "9", ExpectedResult = "few")] + [TestCase("lt", "10", ExpectedResult = "other")] + [TestCase("lt", "23", ExpectedResult = "few")] + [TestCase("lt", "0.6", ExpectedResult = "many")] + [TestCase("lt", "1.1", ExpectedResult = "many")] + [TestCase("lt", "2.0", ExpectedResult = "few")] + [TestCase("lt", "1234.456", ExpectedResult = "many")] + [TestCase("lt", "14", ExpectedResult = "other")] + [TestCase("lt", "40", ExpectedResult = "other")] + public string TestPluralSelect(string locale, string num) { - LocaleRules.Select("en", "1").Should().Be("one"); - LocaleRules.Select("en", "1.6").Should().Be("other"); - LocaleRules.Select("en", "1.0").Should().Be("other"); - LocaleRules.Select("en", "9").Should().Be("other"); - } - - [Test] - public void CheckLtRules() - { - // n % 10 = 1 and n % 100 != 11..19 @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, … @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 71.0, 81.0, 101.0, 1001.0, … - // n % 10 = 2..9 and n % 100 != 11..19 @integer 2~9, 22~29, 102, 1002, … @decimal 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22.0, 102.0, 1002.0, … - // f != 0 @decimal 0.1~0.9, 1.1~1.7, 10.1, 100.1, 1000.1, … - // @integer 0, 10~20, 30, 40, 50, 60, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, … - - LocaleRules.Select("lt", "1").Should().Be("one"); - LocaleRules.Select("lt", "11").Should().Be("other"); - LocaleRules.Select("lt", "21").Should().Be("one"); - LocaleRules.Select("lt", "91").Should().Be("one"); - LocaleRules.Select("lt", "101").Should().Be("one"); - - LocaleRules.Select("lt", "2").Should().Be("few"); - LocaleRules.Select("lt", "37").Should().Be("few"); - LocaleRules.Select("lt", "9").Should().Be("few"); - LocaleRules.Select("lt", "10").Should().Be("other"); - LocaleRules.Select("lt", "23").Should().Be("few"); - - LocaleRules.Select("lt", "0.6").Should().Be("many"); - LocaleRules.Select("lt", "1.1").Should().Be("many"); - LocaleRules.Select("lt", "2.0").Should().Be("few"); - LocaleRules.Select("lt", "1234.456").Should().Be("many"); - - LocaleRules.Select("lt", "14").Should().Be("other"); - LocaleRules.Select("lt", "40").Should().Be("other"); + return LocaleRules.Select(locale, num); } } } diff --git a/Fluent.Net/MessageContext.cs b/Fluent.Net/MessageContext.cs index 2c9274e..48b6737 100644 --- a/Fluent.Net/MessageContext.cs +++ b/Fluent.Net/MessageContext.cs @@ -31,7 +31,12 @@ public class MessageContext { readonly static IDictionary s_emptyFunctions = new Dictionary(); - public IEnumerable Locales { get; private set; } + + public IReadOnlyList Cultures { get; } + + [Obsolete("Access Cultures instead")] + public IEnumerable Locales => Cultures.Select(c => c.IetfLanguageTag); + internal Dictionary _messages = new Dictionary(); internal Dictionary _terms = new Dictionary(); public Func Transform { get; private set; } @@ -76,10 +81,14 @@ public class MessageContext public MessageContext( IEnumerable locales, MessageContextOptions options = null - ) + ) : this(locales.Select(l => new CultureInfo(l)), options) + { + } + + public MessageContext(IEnumerable cultures, MessageContextOptions options = null) { - Locales = locales; - Culture = new CultureInfo(Locales.First()); + Cultures = cultures.ToArray(); + Culture = Cultures[0]; if (options != null) { UseIsolating = options.UseIsolating; @@ -87,11 +96,18 @@ public MessageContext( Transform = options?.Transform ?? NoOpTransform; Functions = new Dictionary(options?.Functions ?? s_emptyFunctions); } - + + public MessageContext( + CultureInfo culture, + MessageContextOptions options = null + ) : this(new [] { culture }, options) + { + } + public MessageContext( string locale, MessageContextOptions options = null - ) : this(new string[] { locale }, options) + ) : this(new CultureInfo(locale), options) { } diff --git a/Fluent.Net/Plural/LocaleRules.cs b/Fluent.Net/Plural/LocaleRules.cs index 2fefd16..0115833 100644 --- a/Fluent.Net/Plural/LocaleRules.cs +++ b/Fluent.Net/Plural/LocaleRules.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Xml; @@ -27,7 +28,7 @@ public static IDictionary ParseRules(XmlDocument doc) var result = new Dictionary(); foreach (XmlElement localeRules in doc.SelectNodes("supplementalData/plurals/pluralRules")) { - var rules = new Rules() { CountRules = new List() }; + var rules = new Rules() {CountRules = new List()}; foreach (XmlElement countRule in localeRules.SelectNodes("pluralRule")) { var count = countRule.GetAttribute("count"); @@ -38,35 +39,49 @@ public static IDictionary ParseRules(XmlDocument doc) } string[] locales = localeRules.GetAttribute("locales").Split( - new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + new char[] {' '}, StringSplitOptions.RemoveEmptyEntries); foreach (var locale in locales) { result.Add(locale, rules); } } + return result; } - public static string Select(IEnumerable locales, string num) + public static string Select(IEnumerable cultures, string num) { Rules rules = null; - foreach (var locale in locales) + foreach (var culture in cultures) { - if (s_localeRules.TryGetValue(locale, out rules)) + if (s_localeRules.TryGetValue(culture.TwoLetterISOLanguageName, out rules) || + s_localeRules.TryGetValue(culture.ThreeLetterISOLanguageName, out rules)) { break; } } + if (rules != null) { return rules.Select(num); } + return "other"; } + public static string Select(IEnumerable locales, string num) + { + return Select(locales.Select(l => new CultureInfo(l)), num); + } + + public static string Select(CultureInfo culture, string num) + { + return Select(Enumerable.Repeat(culture, 1), num); + } + public static string Select(string locale, string num) { - return Select(Enumerable.Repeat(locale, 1), num); + return Select(new CultureInfo(locale), num); } } -} +} \ No newline at end of file diff --git a/Fluent.Net/Types.cs b/Fluent.Net/Types.cs index b6f0904..6e98290 100644 --- a/Fluent.Net/Types.cs +++ b/Fluent.Net/Types.cs @@ -263,7 +263,7 @@ public override bool Match(MessageContext ctx, object other) } else if (other is FluentNumber fnum) { - return Value == Plural.LocaleRules.Select(ctx.Locales, fnum.Value); + return Value == Plural.LocaleRules.Select(ctx.Cultures, fnum.Value); } return false;