diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ExpressiveAnnotations.MvcUnobtrusive.Tests.csproj b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ExpressiveAnnotations.MvcUnobtrusive.Tests.csproj index ae50311..7b17c93 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ExpressiveAnnotations.MvcUnobtrusive.Tests.csproj +++ b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ExpressiveAnnotations.MvcUnobtrusive.Tests.csproj @@ -68,6 +68,11 @@ + + True + True + Resources.resx + @@ -86,6 +91,13 @@ Designer + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.Designer.cs b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.Designer.cs new file mode 100644 index 0000000..3f709c7 --- /dev/null +++ b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ExpressiveAnnotations.MvcUnobtrusive.Tests { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ExpressiveAnnotations.MvcUnobtrusive.Tests.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to default. + /// + public static string Lang { + get { + return ResourceManager.GetString("Lang", resourceCulture); + } + } + } +} diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.pl.resx b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.pl.resx new file mode 100644 index 0000000..d114000 --- /dev/null +++ b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.pl.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + polski + + \ No newline at end of file diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.resx b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.resx new file mode 100644 index 0000000..3e13e89 --- /dev/null +++ b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + default + + \ No newline at end of file diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ValidatorsTest.cs b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ValidatorsTest.cs index 84634cc..62f3e43 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ValidatorsTest.cs +++ b/src/ExpressiveAnnotations.MvcUnobtrusive.Tests/ValidatorsTest.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Threading; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -148,21 +149,47 @@ public void empty_client_validation_rules_are_not_created() [TestMethod] public void verify_formatted_message_sent_to_client() { - var model = new Model(); + var model = new MsgModel(); var metadata = GetModelMetadata(model, m => m.Value); var controllerContext = GetControllerContext(); var assert = new AssertThatValidator(metadata, controllerContext, new AssertThatAttribute("1 > 2") { - ErrorMessage = "{0}{1}{Value:n}{Value}{{Value}}" + ErrorMessage = "_{0}{1}{Value:n}{Value:N}{Value}{Value}_{{Value}}{{{Value}}}{{{{Value}}}}_" }); var assertRule = assert.GetClientValidationRules().Single(); var map = JsonConvert.DeserializeObject((string) assertRule.ValidationParameters["errfieldsmap"]); - var expected = "Value1 > 2_{Value}_" + map.Value + "{Value}"; + var expected = "_Value1 > 2_{Value}__{Value}_" + map.Value + map.Value + "_{Value}" + "{" + map.Value + "}" + "{{Value}}_"; Assert.AreEqual(expected, assertRule.ErrorMessage); } + [TestMethod] + public void verify_that_culture_change_affects_message_sent_to_client() + { + var model = new MsgModel(); + var metadata = GetModelMetadata(model, m => m.Lang); + var controllerContext = GetControllerContext(); + + var assert = new AssertThatValidator(metadata, controllerContext, + new AssertThatAttribute("1 > 2") {ErrorMessage = "{Lang:n}"}); + var assertRule = assert.GetClientValidationRules().Single(); + Assert.AreEqual("default", assertRule.ErrorMessage); + + // change culture + var culture = Thread.CurrentThread.CurrentUICulture; + Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("pl"); + + // simulate next request - create new validator + assert = new AssertThatValidator(metadata, controllerContext, + new AssertThatAttribute("1 > 2") {ErrorMessage = "{Lang:n}"}); + assertRule = assert.GetClientValidationRules().Single(); + Assert.AreEqual("polski", assertRule.ErrorMessage); + + // restore culture + Thread.CurrentThread.CurrentUICulture = culture; + } + [TestMethod] public void possible_naming_colission_at_client_side_are_detected() { @@ -257,11 +284,19 @@ public enum State public class Model { - [Display(Name="_{Value}_")] public int Value { get; set; } [ValueParser("arrayparser")] public int[] Array { get; set; } public State Status { get; set; } } + + public class MsgModel + { + [Display(Name = "_{Value}_")] + public int Value { get; set; } + + [Display(ResourceType = typeof (Resources), Name = "Lang")] + public string Lang { get; set; } + } } } diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive/MapCache.cs b/src/ExpressiveAnnotations.MvcUnobtrusive/MapCache.cs index b169bd8..d1c67a9 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive/MapCache.cs +++ b/src/ExpressiveAnnotations.MvcUnobtrusive/MapCache.cs @@ -39,6 +39,5 @@ internal class CacheItem public IDictionary ConstsMap { get; set; } public IDictionary ParsersMap { get; set; } public IDictionary ErrFieldsMap { get; set; } - public string FormattedErrorMessage { get; set; } } } diff --git a/src/ExpressiveAnnotations.MvcUnobtrusive/Validators/ExpressiveValidator.cs b/src/ExpressiveAnnotations.MvcUnobtrusive/Validators/ExpressiveValidator.cs index 125d547..be6390f 100644 --- a/src/ExpressiveAnnotations.MvcUnobtrusive/Validators/ExpressiveValidator.cs +++ b/src/ExpressiveAnnotations.MvcUnobtrusive/Validators/ExpressiveValidator.cs @@ -54,10 +54,6 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, }).Where(x => x.ParserAttribute != null) .ToDictionary(x => x.PropertyName, x => x.ParserAttribute.ParserName); - IDictionary errFieldsMap; - FormattedErrorMessage = attribute.FormatErrorMessage(metadata.GetDisplayName(), attribute.Expression, metadata.ContainerType, out errFieldsMap); // fields names, in contrast to values, do not change in runtime, so will be provided in message (less code in js) - ErrFieldsMap = errFieldsMap; - AssertNoNamingCollisionsAtCorrespondingSegments(); attribute.Compile(metadata.ContainerType); @@ -67,7 +63,6 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, ConstsMap = ConstsMap, ParsersMap = ParsersMap, ErrFieldsMap = ErrFieldsMap, - FormattedErrorMessage = FormattedErrorMessage }; }); @@ -75,9 +70,12 @@ protected ExpressiveValidator(ModelMetadata metadata, ControllerContext context, ConstsMap = item.ConstsMap; ParsersMap = item.ParsersMap; ErrFieldsMap = item.ErrFieldsMap; - FormattedErrorMessage = item.FormattedErrorMessage; Expression = attribute.Expression; + + IDictionary errFieldsMap; + FormattedErrorMessage = attribute.FormatErrorMessage(metadata.GetDisplayName(), attribute.Expression, metadata.ContainerType, out errFieldsMap); // fields names, in contrast to values, do not change in runtime, so will be provided in message (less code in js) + ErrFieldsMap = errFieldsMap; } catch (Exception e) { diff --git a/src/ExpressiveAnnotations.MvcWebSample/Resources.Designer.cs b/src/ExpressiveAnnotations.MvcWebSample/Resources.Designer.cs index 783e562..92e2223 100644 --- a/src/ExpressiveAnnotations.MvcWebSample/Resources.Designer.cs +++ b/src/ExpressiveAnnotations.MvcWebSample/Resources.Designer.cs @@ -502,7 +502,7 @@ public static string ReasonForTravelRequired { } /// - /// Looks up a localized string similar to If you are under 18 (indicated {Age} in {Age:N} field, yes {Age}), give us a reason of your travel no matter where you go (BTW. {Country}... nice choice).. + /// Looks up a localized string similar to If you are under 18 (indicated {Age} in {Age:N} field, yes - {Age:N} {Age}), give us a reason of your travel no matter where you go (BTW. {Country}... nice choice).. /// public static string ReasonForTravelRequiredForYouth { get { diff --git a/src/ExpressiveAnnotations.MvcWebSample/Resources.pl.resx b/src/ExpressiveAnnotations.MvcWebSample/Resources.pl.resx index 821937e..aed180e 100644 --- a/src/ExpressiveAnnotations.MvcWebSample/Resources.pl.resx +++ b/src/ExpressiveAnnotations.MvcWebSample/Resources.pl.resx @@ -265,7 +265,7 @@ Jeśli planujesz wyjechać za granicę i jesteś w wieku 25-55 lat lub planujesz odwiedzić dwukrotnie ten sam obcy kraj, podaj powody. - Jeśli planujesz jakąkolwiek podróż i nie masz ukończonych 18 lat (wskazałeś w polu {Age:N} wartość {Age}, tak {Age}), podaj powody (PS. {Country}... dobry wybór). + Jeśli planujesz jakąkolwiek podróż i nie masz ukończonych 18 lat (wskazałeś w polu {Age:N} wartość {Age}, tak - {Age:N} {Age}), podaj powody (PS. {Country}... dobry wybór). Data powrotu diff --git a/src/ExpressiveAnnotations.MvcWebSample/Resources.resx b/src/ExpressiveAnnotations.MvcWebSample/Resources.resx index 191b6e4..140dc57 100644 --- a/src/ExpressiveAnnotations.MvcWebSample/Resources.resx +++ b/src/ExpressiveAnnotations.MvcWebSample/Resources.resx @@ -265,7 +265,7 @@ If you plan to go abroad and you are between 25 and 55 or plan to visit the same foreign country twice, write down your reasons. - If you are under 18 (indicated {Age} in {Age:N} field, yes {Age}), give us a reason of your travel no matter where you go (BTW. {Country}... nice choice). + If you are under 18 (indicated {Age} in {Age:N} field, yes - {Age:N} {Age}), give us a reason of your travel no matter where you go (BTW. {Country}... nice choice). Return date diff --git a/src/ExpressiveAnnotations.Tests/AttribsTest.cs b/src/ExpressiveAnnotations.Tests/AttribsTest.cs index 3666cd9..8be4044 100644 --- a/src/ExpressiveAnnotations.Tests/AttribsTest.cs +++ b/src/ExpressiveAnnotations.Tests/AttribsTest.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Reflection; +using System.Threading; using ExpressiveAnnotations.Attributes; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -168,6 +170,21 @@ public void verify_custom_error_message_after_validation() }); } + [TestMethod] + public void verify_that_culture_change_affects_validation_message() + { + AssertErrorMessage("{Lang:n}", "default", "default"); + + // change culture + var culture = Thread.CurrentThread.CurrentUICulture; + Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("pl"); + + AssertErrorMessage("{Lang:n}", "polski", "polski"); + + // restore culture + Thread.CurrentThread.CurrentUICulture = culture; + } + private static void AssertErrorMessage(string input, string assertThatOutput, string requiredIfOutput) { var assertThat = new AssertThatAttribute("1!=1"); @@ -237,10 +254,14 @@ private class MsgModel { [DisplayAttribute(Name = "_{Value1}_")] public int Value1 { get; set; } - [DisplayAttribute(ResourceType = typeof(Resources), Name = "Value2")] + + [DisplayAttribute(ResourceType = typeof (Resources), Name = "Value2")] public int Value2 { get; set; } public MsgModel Internal { get; set; } + + [Display(ResourceType = typeof (Resources), Name = "Lang")] + public string Lang { get; set; } } } diff --git a/src/ExpressiveAnnotations.Tests/ExpressiveAnnotations.Tests.csproj b/src/ExpressiveAnnotations.Tests/ExpressiveAnnotations.Tests.csproj index 245aef9..44cfe07 100644 --- a/src/ExpressiveAnnotations.Tests/ExpressiveAnnotations.Tests.csproj +++ b/src/ExpressiveAnnotations.Tests/ExpressiveAnnotations.Tests.csproj @@ -78,6 +78,7 @@ + PublicResXFileCodeGenerator Resources.Designer.cs diff --git a/src/ExpressiveAnnotations.Tests/Resources.Designer.cs b/src/ExpressiveAnnotations.Tests/Resources.Designer.cs index 77ded0c..71d93d4 100644 --- a/src/ExpressiveAnnotations.Tests/Resources.Designer.cs +++ b/src/ExpressiveAnnotations.Tests/Resources.Designer.cs @@ -60,6 +60,15 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to default. + /// + public static string Lang { + get { + return ResourceManager.GetString("Lang", resourceCulture); + } + } + /// /// Looks up a localized string similar to _{Value2}_. /// diff --git a/src/ExpressiveAnnotations.Tests/Resources.pl.resx b/src/ExpressiveAnnotations.Tests/Resources.pl.resx new file mode 100644 index 0000000..d114000 --- /dev/null +++ b/src/ExpressiveAnnotations.Tests/Resources.pl.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + polski + + \ No newline at end of file diff --git a/src/ExpressiveAnnotations.Tests/Resources.resx b/src/ExpressiveAnnotations.Tests/Resources.resx index 195b3bc..0b35630 100644 --- a/src/ExpressiveAnnotations.Tests/Resources.resx +++ b/src/ExpressiveAnnotations.Tests/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + default + _{Value2}_ diff --git a/src/ExpressiveAnnotations/Attributes/ExpressiveAttribute.cs b/src/ExpressiveAnnotations/Attributes/ExpressiveAttribute.cs index 600cc39..82dad3a 100644 --- a/src/ExpressiveAnnotations/Attributes/ExpressiveAttribute.cs +++ b/src/ExpressiveAnnotations/Attributes/ExpressiveAttribute.cs @@ -177,8 +177,8 @@ public string FormatErrorMessage(string displayName, string expression, object o IList items; var message = PreformatMessage(displayName, expression, out items); - message = items.Aggregate(message, (cargo, current) => current.Indicator != null && !current.Constant ? cargo.Replace(current.Id.ToString(), Helper.ExtractDisplayName(objectInstance.GetType(), current.FieldPath)) : cargo); - message = items.Aggregate(message, (cargo, current) => current.Indicator == null && !current.Constant ? cargo.Replace(current.Id.ToString(), (Helper.ExtractValue(objectInstance, current.FieldPath) ?? string.Empty).ToString()) : cargo); + message = items.Aggregate(message, (cargo, current) => current.Indicator != null && !current.Constant ? cargo.Replace(current.Uuid.ToString(), Helper.ExtractDisplayName(objectInstance.GetType(), current.FieldPath)) : cargo); + message = items.Aggregate(message, (cargo, current) => current.Indicator == null && !current.Constant ? cargo.Replace(current.Uuid.ToString(), (Helper.ExtractValue(objectInstance, current.FieldPath) ?? string.Empty).ToString()) : cargo); return message; } @@ -201,8 +201,10 @@ public string FormatErrorMessage(string displayName, string expression, Type obj IList items; var message = PreformatMessage(displayName, expression, out items); - fieldsMap = items.Where(x => x.Indicator == null && !x.Constant).ToDictionary(x => x.FieldPath, x => x.Id); - message = items.Aggregate(message, (cargo, current) => current.Indicator != null && !current.Constant ? cargo.Replace(current.Id.ToString(), Helper.ExtractDisplayName(objectType, current.FieldPath)) : cargo); + var map = items.Where(x => x.Indicator == null && !x.Constant).Select(x => x.FieldPath).Distinct().ToDictionary(x => x, x => Guid.NewGuid()); // sanitize + message = items.Aggregate(message, (cargo, current) => current.Indicator != null && !current.Constant ? cargo.Replace(current.Uuid.ToString(), Helper.ExtractDisplayName(objectType, current.FieldPath)) : cargo); + message = items.Aggregate(message, (cargo, current) => current.Indicator == null && !current.Constant ? cargo.Replace(current.Uuid.ToString(), map[current.FieldPath].ToString()) : cargo); + fieldsMap = map; return message; } @@ -264,7 +266,7 @@ private string PreformatMessage(string displayName, string expression, out IList { var message = MessageFormatter.FormatString(ErrorMessageString, out items); // process custom format items: {fieldPath[:indicator]}, and substitute them entirely with guids, not to interfere with standard string.Format() invoked below message = string.Format(message, displayName, expression); // process standard format items: {index[,alignment][:formatString]}, https://msdn.microsoft.com/en-us/library/txafckwd(v=vs.110).aspx - message = items.Aggregate(message, (cargo, current) => cargo.Replace(current.Id.ToString(), current.Substitute)); // give back, initially preprocessed, custom format items + message = items.Aggregate(message, (cargo, current) => cargo.Replace(current.Uuid.ToString(), current.Substitute)); // give back, initially preprocessed, custom format items return message; } } diff --git a/src/ExpressiveAnnotations/MessageFormatter.cs b/src/ExpressiveAnnotations/MessageFormatter.cs index ca07524..60823cf 100644 --- a/src/ExpressiveAnnotations/MessageFormatter.cs +++ b/src/ExpressiveAnnotations/MessageFormatter.cs @@ -24,7 +24,7 @@ public static string FormatString(string input, out IList items) for (var i = 0; i < matches.Count; i++) { var match = matches[i]; - var arg = match.Value; + var item = match.Value; var leftBraces = match.Groups[1]; var rightBraces = match.Groups[2]; @@ -36,8 +36,12 @@ public static string FormatString(string input, out IList items) message.Append(input.Substring(start, chars)); prev = match; - if (items.Any(x => x.Body == arg)) + var added = items.SingleOrDefault(x => x.Body == item); + if (added != null) + { + message.Append(added.Uuid); continue; + } var length = leftBraces.Length; @@ -45,28 +49,31 @@ public static string FormatString(string input, out IList items) var leftBracesFlattened = new string('{', length / 2); var rightBracesFlattened = new string('}', length / 2); - var guid = Guid.NewGuid(); - - var param = arg.Substring(length, arg.Length - 2 * length); - items.Add(new FormatItem + var uuid = Guid.NewGuid(); + var param = item.Substring(length, item.Length - 2 * length); + var current = new FormatItem { - Id = guid, - Body = arg, + Uuid = uuid, + Body = item, Constant = length % 2 == 0, FieldPath = param.Contains(":") ? param.Substring(0, param.IndexOf(":", StringComparison.Ordinal)) : param, Indicator = param.Contains(":") ? param.Substring(param.IndexOf(":", StringComparison.Ordinal) + 1) : null, - Substitute = string.Format("{0}{1}{2}", leftBracesFlattened, length % 2 != 0 ? guid.ToString() : param, rightBracesFlattened) // for odd number of braces, substitute param with respective value (just like string.Format() does) - }); - message.Append(guid); + Substitute = string.Format("{0}{1}{2}", leftBracesFlattened, length % 2 != 0 ? uuid.ToString() : param, rightBracesFlattened) // for odd number of braces, substitute param with respective value (just like string.Format() does) + }; + items.Add(current); + message.Append(current.Uuid); } + if(prev != null) + message.Append(input.Substring(prev.Index + prev.Length)); + return message.Length > 0 ? message.ToString() : input; } } internal class FormatItem { - public Guid Id { get; set; } + public Guid Uuid { get; set; } public string Body { get; set; } public bool Constant { get; set; } public string FieldPath { get; set; }