diff --git a/README.md b/README.md index 1306c92..f116d67 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Why MultiLanguages is better: | Memory Heavy, Hard to Maintain RESX Resource Files | :heavy_check_mark: | | | Less Memory, Easy to Maintain YAML Resource Files | | :heavy_check_mark: | | Generate English Resource YAML File from Localizable Strings from your UI Code | | :heavy_check_mark: | -| Replace Localizable Strings with Variables | | :heavy_check_mark: | +| Automatically Replace Localizable Strings with Variables | | :heavy_check_mark: | | Data Attribute Localization | :heavy_check_mark: | :heavy_check_mark: | | Hierarchal Language Key Support | | :heavy_check_mark: | | Translate Resource Files into 69 Different Languages | | :heavy_check_mark: | @@ -72,7 +72,7 @@ https://youtu.be/Xz68c8GBYz4 # Why YAML -Most common solution for multilanguage in .NET is the .resx resource files. .resx files are XML based so they are not too friendly to deal with and most likely a GUI tool is needed for keys management. XML is also huge and slower to parse. On the other hand, YAML is new, very fast to parse, and file structure is very simple and doesn't contain any single not-needed character which make the file size too small comparing to the XML one. +Most common solution for multilanguage in .NET are .resx resource files. .resx files are XML based so they are not too friendly to deal with and most likely a GUI tool is needed for keys management. XML is also huge and slower to parse. On the other hand, YAML is new, very fast to parse, and the file structure is very simple and doesn't contain any unneeded characters which make the file size smaller compared to XML. For modern SPA apps with Blazor WebAssembly for example, large language files with .resx might slow down the load time for the download. YAML file structure allows for nested objects which a lovely feature you can take advantage of to build an organized language key-values files without long concatenated names. Finally, due to the simplicity of YAML, it's makes it very easy to build automation on top of it like source generator and static classes creation. @@ -80,8 +80,8 @@ Finally, due to the simplicity of YAML, it's makes it very easy to build automat # Features AKSoftware.Localization.Multilanguage prvoides all the feature set needed for any multilanguage support like: - Easy to get started. -- Online traslator tool to translate your files in one click for more 65 languages https://akmultilanguages.azurewebsites.net -- Light and high-performant +- Online translator tool to translate your files in one click for more 65 languages https://akmultilanguages.azurewebsites.net +- Light and high-performance - Blazor Server & WebAssembly support - Out of the box state management for **Blazor** components - Multiple language file sources (Files in folder or embedded files) @@ -89,7 +89,7 @@ AKSoftware.Localization.Multilanguage prvoides all the feature set needed for an - Dynamically list all language keys - Dynamically list all available langauges - Dependency injection support -- Hierarcy language keys in YAML +- Hierarchical language keys in YAML - Code generators to generate full keys accessor service, static class with const strings, enums, and more.. - v6.1 will bring the localization assistant to localize existing apps with minimal effort. - Full UWP support @@ -187,7 +187,7 @@ Welcome: Welcome -Select the file in the Solution Explorer window and from the properties window set the build action property to "Embeded Resources" +Select the file in the Solution Explorer window and from the properties window set the build action property to "Embedded Resource" >**Note** >In case of using the Source Generator package, that will be taken care of automatically. @@ -412,6 +412,7 @@ We are currently working on version 6. Here are the upcoming features. Verify All Keys Can Be Found](#verify-all-keys-can-be-found) * [Verify No Unused Keys](#verify-no-unused-keys) * [Verify No Duplicate Keys](#verify-no-duplicate-keys) +* [Data Attribute Localization Validation](#data-attribute-localization-validation) ## Specify the assembly by name If you have multiple projects in your Visual Studio Solution that depend upon language translation, as of version 6.0 and higher you can specify the assembly by name. Place your resources in a project that can be used by the other projects in your Solution. @@ -817,6 +818,53 @@ public void ReplaceLocalizableStringsWithVariablesExample() } ``` +## Data Attribute Localization Validation +In order to localize validation messages, use the Create or Update [Resource File from Localizable Strings](#create-or-update-resource-file-from-localizable-strings) feature. Below is a full Blazor example using a Contact Us Page. + +```C# +public partial class ContactUsPage : ComponentBase +{ + [Inject] private ILanguageContainerService Language { get; set; } + private ContactUs _contactUs; + private ValidationMessageStore _validationMessageStore; + private EditContext? EC { get; set; } + + protected override void OnInitialized() + { + Language.InitLocalizedComponent(this); + _contactUs = new Common.Models.ContactUs(); + EC = new EditContext(_contactUs); + _validationMessageStore = new ValidationMessageStore(EC); + } + + private ClearValidationMessages() + { + _validationMessageStore.Clear(); + } + + private bool IsValid() + { + ClearValidationMessages(); + bool isValid = ValidationLocalization.ValidateModel(_contactUs, _validationMessageStore, Language); + if (!isValid) + { + EC.NotifyValidationStateChanged(); + return false; + } + + return true; + } + + private void HandleSubmitClick() + { + if (!IsValid()) + return; + + //Handle Form Submission + } +} +``` + # Thanks for the awesome contributors diff --git a/src/AKSoftware.Localization.MultiLanguages.Tests/TestClasses/Customer.cs b/src/AKSoftware.Localization.MultiLanguages.Tests/TestClasses/Customer.cs new file mode 100644 index 0000000..b0c988c --- /dev/null +++ b/src/AKSoftware.Localization.MultiLanguages.Tests/TestClasses/Customer.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace AKSoftware.Localization.MultiLanguages.Tests.TestClasses +{ + public class Customer + { + [Required] + public string Name { get; set; } + } +} diff --git a/src/AKSoftware.Localization.MultiLanguages.Tests/ValidationLocalizationTests.cs b/src/AKSoftware.Localization.MultiLanguages.Tests/ValidationLocalizationTests.cs new file mode 100644 index 0000000..8ec36b0 --- /dev/null +++ b/src/AKSoftware.Localization.MultiLanguages.Tests/ValidationLocalizationTests.cs @@ -0,0 +1,44 @@ +using AKSoftware.Localization.MultiLanguages.Providers; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components.Forms; +using Xunit; +using Xunit.Abstractions; + +namespace AKSoftware.Localization.MultiLanguages.Tests +{ + public class ValidationLocalizationTests + { + private readonly ITestOutputHelper _output; + + public ValidationLocalizationTests(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void ValidateModelTest() + { + //Arrange + var customer = new TestClasses.Customer(); + var keysProvider = new EmbeddedResourceKeysProvider(Assembly.GetExecutingAssembly()); + ILanguageContainerService language = new LanguageContainer(CultureInfo.GetCultureInfo("en-US"), keysProvider); + EditContext editContext = new EditContext(customer); + var validationMessageStore = new ValidationMessageStore(editContext); + + //Act + ValidationLocalization.ValidateModel(customer, validationMessageStore, language); + + //Assert + List messages = validationMessageStore[editContext.Field("Name")].ToList(); + Assert.NotEmpty(messages); + Assert.Equal("Name is required", messages[0]); + + } + } +} diff --git a/src/AKSoftware.Localization.MultiLanguages.Tests/VerifyLocalization.cs b/src/AKSoftware.Localization.MultiLanguages.Tests/VerifyLocalization.cs index e705957..ba0fc3b 100644 --- a/src/AKSoftware.Localization.MultiLanguages.Tests/VerifyLocalization.cs +++ b/src/AKSoftware.Localization.MultiLanguages.Tests/VerifyLocalization.cs @@ -28,6 +28,7 @@ public void VerifyAllSourceCodeFilesAreLocalized() parms.ResourceFilePath = Path.Combine(solutionPath, "BlazorServerLocalizationSample", "Resources", "en-US.yml"); parms.KeyReference = "Language"; + parms.RemoveLocalizedKeys = true; //Act ParseCodeLogic logic = new ParseCodeLogic(); diff --git a/src/AKSoftware.Localization.MultiLanguages/AKSoftware.Localization.MultiLanguages.csproj b/src/AKSoftware.Localization.MultiLanguages/AKSoftware.Localization.MultiLanguages.csproj index 87d000b..e88fa66 100644 --- a/src/AKSoftware.Localization.MultiLanguages/AKSoftware.Localization.MultiLanguages.csproj +++ b/src/AKSoftware.Localization.MultiLanguages/AKSoftware.Localization.MultiLanguages.csproj @@ -22,6 +22,7 @@ + diff --git a/src/AKSoftware.Localization.MultiLanguages/DataAttributeParsing.cs b/src/AKSoftware.Localization.MultiLanguages/DataAttributeParsing.cs new file mode 100644 index 0000000..24d6341 --- /dev/null +++ b/src/AKSoftware.Localization.MultiLanguages/DataAttributeParsing.cs @@ -0,0 +1,443 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace AKSoftware.Localization.MultiLanguages +{ + internal class DataAttributeParsing + { + public const string RequiredPrefix = "Required"; + public const string MaxLengthPrefix = "MaxLength"; + public const string StringLengthPrefix = "StringLength"; + public const string RangePrefix = "Range"; + public const string RegularExpressionPrefix = "RegularExpression"; + public const string ComparePrefix = "Compare"; + public const string CreditCardPrefix = "CreditCard"; + public const string EmailAddressPrefix = "EmailAddress"; + public const string PhonePrefix = "Phone"; + public const string UrlPrefix = "Url"; + + private static Regex _propertyRegex = new Regex(@"public\s+(?[^\s\?]+\??)\s+(?\w+)\s*{\s*get;\s*set;\s*}", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _requiredAttributeWithMessageRegex = new Regex( + @"\[Required\(ErrorMessage\s*=\s*""(?.*?)""\)\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _requiredAttributeRegex = new Regex( + @"\[Required\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _emailAddressAttributeWithMessageRegex = new Regex( + @"\[EmailAddress\(ErrorMessage\s*=\s*""(?.*?)""\)\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _emailAddressAttributeRegex = new Regex( + @"\[EmailAddress\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _phoneAttributeWithMessageRegex = new Regex( + @"\[Phone\(ErrorMessage\s*=\s*""(?.*?)""\)\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _phoneAttributeRegex = new Regex( + @"\[Phone\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _creditCardAttributeWithMessageRegex = new Regex( + @"\[CreditCard\(ErrorMessage\s*=\s*""(?.*?)""\)\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _creditCardAttributeRegex = new Regex( + @"\[CreditCard\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _urlAttributeWithMessageRegex = new Regex( + @"\[Url\(ErrorMessage\s*=\s*""(?.*?)""\)\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _urlAttributeRegex = new Regex( + @"\[Url\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _maxLengthAttributeWithMessageRegex = new Regex(@"\[MaxLength\((?\d+),\s*ErrorMessage\s*=\s*""(?.*?)""\)\]", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _maxLengthAttributeRegex = new Regex(@"\[MaxLength\((?\d+)\]", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _rangeAttributeRegex = + new Regex(@"\[Range\((?\d+),\s*(?\d+)\)\]", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _rangeAttributeWithMessageRegex = + new Regex(@"\[Range\((?\d+),\s*(?\d+),\s*ErrorMessage\s*=\s*""(?.*?)""\)\]", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _compareAttributeRegex = + new Regex(@"\[Compare\(\s*""(?.*?)""\s*\)\]", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _compareAttributeWithMessageRegex = + new Regex(@"\[Compare\(\s*""(?.*?)""\s*,\s*ErrorMessage\s*=\s*""(?.*?)""\s*\)\]", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _regularExpressionAttributeRegex = + new Regex(@"\[RegularExpression\(\s*@?""(?.*?)""\s*\)\]", + RegexOptions.Compiled | RegexOptions.Multiline); + + private static Regex _regularExpressionWithMessageRegex = + new Regex(@"\[RegularExpression\(\s*@?""(?.*?)""\s*,\s*ErrorMessage\s*=\s*""(?.*?)""\s*\)\]", + RegexOptions.Compiled | RegexOptions.Multiline); + + private const string StringType = "string"; + private const string PropertyTypeGroup = "propertyType"; + private const string ContentGroup = "content"; + + public List GetDataAttributePrefixes() + { + return new List() + { + RequiredPrefix, + MaxLengthPrefix, + StringLengthPrefix, + RangePrefix, + RegularExpressionPrefix, + ComparePrefix, + CreditCardPrefix, + EmailAddressPrefix, + PhonePrefix, + UrlPrefix + }; + } + + public List ParseDataAttributes(List parseResults, string text) + { + parseResults.AddRange(GetRequiredAttributeInText(text)); + parseResults.AddRange(GetRequiredWithErrorMessageInText(text)); + parseResults.AddRange(GetEmailAddressAttributeInText(text)); + parseResults.AddRange(GetEmailAddressWithErrorMessageInText(text)); + parseResults.AddRange(GetPhoneAttributeInText(text)); + parseResults.AddRange(GetPhoneWithErrorMessageInText(text)); + parseResults.AddRange(GetCreditCardAttributeInText(text)); + parseResults.AddRange(GetCreditCardWithErrorMessageInText(text)); + parseResults.AddRange(GetUrlAttributeInText(text)); + parseResults.AddRange(GetUrlWithErrorMessageInText(text)); + parseResults.AddRange(GetMaxLengthAttributesInText(text)); + parseResults.AddRange(GetMaxLengthAttributesWithErrorMessageInText(text)); + parseResults.AddRange(GetRangeAttributesInText(text)); + parseResults.AddRange(GetRangeAttributesWithMessageInText(text)); + parseResults.AddRange(GetCompareAttributesInText(text)); + parseResults.AddRange(GetCompareAttributesWithMessageInText(text)); + parseResults.AddRange(GetRegularExpressionAttributeInText(text)); + parseResults.AddRange(GetRegularExpressionWithErrorMessageInText(text)); + + + return parseResults; + } + + private List GetRequiredWithErrorMessageInText(string text) + { + return NoParmAttributeWithErrorMessageInText(_requiredAttributeWithMessageRegex, text, RequiredPrefix); + } + + private List GetRequiredAttributeInText(string text) + { + List result = new List(); + + MatchCollection matches = _requiredAttributeRegex.Matches(text); + + foreach (Match match in matches) + { + int index = text.IndexOf(match.Value); + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + + string errorMessage = $"{StringUtil.InsertSpaces(propertyName)} is required"; + + result.Add(new ParseResult + { + LocalizableString = errorMessage, + MatchingExpression = _requiredAttributeRegex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{RequiredPrefix}{propertyName}" + }); + } + + return result; + } + + private List GetEmailAddressWithErrorMessageInText(string text) + { + return NoParmAttributeWithErrorMessageInText(_emailAddressAttributeWithMessageRegex, text, EmailAddressPrefix); + } + + private List GetEmailAddressAttributeInText(string text) + { + return NoParmAttributeInText(_emailAddressAttributeRegex, text, EmailAddressPrefix); + } + + private List GetPhoneWithErrorMessageInText(string text) + { + return NoParmAttributeWithErrorMessageInText(_phoneAttributeWithMessageRegex, text, PhonePrefix); + } + + private List GetPhoneAttributeInText(string text) + { + return NoParmAttributeInText(_phoneAttributeRegex, text, PhonePrefix); + } + private List GetCreditCardWithErrorMessageInText(string text) + { + return NoParmAttributeWithErrorMessageInText(_creditCardAttributeWithMessageRegex, text, CreditCardPrefix); + } + + private List GetCreditCardAttributeInText(string text) + { + return NoParmAttributeInText(_creditCardAttributeRegex, text, CreditCardPrefix); + } + + private List GetUrlWithErrorMessageInText(string text) + { + return NoParmAttributeWithErrorMessageInText(_urlAttributeWithMessageRegex, text, UrlPrefix); + } + + private List GetUrlAttributeInText(string text) + { + return NoParmAttributeInText(_urlAttributeRegex, text, UrlPrefix); + } + + private List GetRegularExpressionWithErrorMessageInText(string text) + { + return NoParmAttributeWithErrorMessageInText(_regularExpressionWithMessageRegex, text, RegularExpressionPrefix); + } + + private List GetRegularExpressionAttributeInText(string text) + { + return NoParmAttributeInText(_urlAttributeRegex, text, RegularExpressionPrefix); + } + + private List NoParmAttributeWithErrorMessageInText(Regex regex, string text, string prefix) + { + List result = new List(); + + MatchCollection matches = regex.Matches(text); + + foreach (Match match in matches) + { + string errorMessage = match.Groups[ContentGroup].Value; + int index = text.IndexOf(match.Value); + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + + result.Add(new ParseResult + { + LocalizableString = errorMessage, + MatchingExpression = regex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{prefix}{propertyName}" + }); + } + + return result; + } + + + private List NoParmAttributeInText(Regex regex, string text, string prefix) + { + List result = new List(); + + MatchCollection matches = regex.Matches(text); + + foreach (Match match in matches) + { + int index = text.IndexOf(match.Value); + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + + string errorMessage = $"{StringUtil.InsertSpaces(propertyName)} is an invalid format"; + + result.Add(new ParseResult + { + LocalizableString = errorMessage, + MatchingExpression = regex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{prefix}{propertyName}" + }); + } + + return result; + } + + private List GetMaxLengthAttributesWithErrorMessageInText(string text) + { + List result = new List(); + + MatchCollection matches = _maxLengthAttributeWithMessageRegex.Matches(text); + + foreach (Match match in matches) + { + string errorMessage = match.Groups[ContentGroup].Value; + string maxLength = match.Groups["maxLength"].Value; + int index = text.IndexOf(match.Value); + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + string propertyType = propertyStringMatch.Groups[PropertyTypeGroup].Value; + + //TODO: We currently only support string properties for required + if (!propertyType.ToLower().StartsWith(StringType)) + continue; + + result.Add(new ParseResult + { + LocalizableString = errorMessage, + MatchingExpression = _maxLengthAttributeWithMessageRegex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{MaxLengthPrefix}{propertyName}{maxLength}" + }); + } + + return result; + } + + private List GetMaxLengthAttributesInText(string text) + { + List result = new List(); + + MatchCollection matches = _maxLengthAttributeRegex.Matches(text); + + foreach (Match match in matches) + { + string maxLength = match.Groups["maxLength"].Value; + int index = text.IndexOf(match.Value); + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + + string errorMessage = $"{StringUtil.InsertSpaces(propertyName)} has a maximum length of {maxLength} characters"; + result.Add(new ParseResult + { + LocalizableString = errorMessage, + MatchingExpression = _maxLengthAttributeRegex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{MaxLengthPrefix}{propertyName}{maxLength}" + }); + } + + return result; + } + + private List GetRangeAttributesInText(string text) + { + List result = new List(); + + MatchCollection matches = _rangeAttributeRegex.Matches(text); + + foreach (Match match in matches) + { + string min = match.Groups["min"].Value; + string max = match.Groups["max"].Value; + int index = text.IndexOf(match.Value); + + // Find the associated property name + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + + string errorMessage = $"{StringUtil.InsertSpaces(propertyName)} must be between {min} and {max}"; + result.Add(new ParseResult + { + LocalizableString = errorMessage, + MatchingExpression = _rangeAttributeRegex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{RangePrefix}{propertyName}{min}To{max}" + }); + } + + return result; + } + + private List GetRangeAttributesWithMessageInText(string text) + { + List result = new List(); + + MatchCollection matchesWithMessage = _rangeAttributeWithMessageRegex.Matches(text); + + foreach (Match match in matchesWithMessage) + { + string min = match.Groups["min"].Value; + string max = match.Groups["max"].Value; + string content = match.Groups["content"].Value; + int index = text.IndexOf(match.Value); + + // Find the associated property name + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + + result.Add(new ParseResult + { + LocalizableString = content, + MatchingExpression = _rangeAttributeWithMessageRegex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{RangePrefix}{propertyName}{min}{max}" + }); + } + + return result; + } + + private List GetCompareAttributesInText(string text) + { + List result = new List(); + + MatchCollection matches = _compareAttributeRegex.Matches(text); + + foreach (Match match in matches) + { + string compare = match.Groups["compare"].Value; + int index = text.IndexOf(match.Value); + + // Find the associated property name + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + + string errorMessage = $"{StringUtil.InsertSpaces(propertyName)} must match {StringUtil.InsertSpaces(compare)}"; + result.Add(new ParseResult + { + LocalizableString = errorMessage, + MatchingExpression = _compareAttributeRegex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{ComparePrefix}{propertyName}To{compare}" + }); + } + + return result; + } + + private List GetCompareAttributesWithMessageInText(string text) + { + List result = new List(); + + MatchCollection matchesWithMessage = _compareAttributeWithMessageRegex.Matches(text); + + foreach (Match match in matchesWithMessage) + { + string compare = match.Groups["compare"].Value; + string content = match.Groups["content"].Value; + int index = text.IndexOf(match.Value); + + // Find the associated property name + Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); + string propertyName = propertyStringMatch.Groups[ContentGroup].Value; + + result.Add(new ParseResult + { + LocalizableString = content, + MatchingExpression = _compareAttributeWithMessageRegex, + FilePath = string.Empty, + MatchValue = match.Value, + Key = $"{ComparePrefix}{propertyName}To{compare}" + }); + } + + return result; + } + + } +} diff --git a/src/AKSoftware.Localization.MultiLanguages/ParseCodeLogic.cs b/src/AKSoftware.Localization.MultiLanguages/ParseCodeLogic.cs index dfae75f..d0ade7c 100644 --- a/src/AKSoftware.Localization.MultiLanguages/ParseCodeLogic.cs +++ b/src/AKSoftware.Localization.MultiLanguages/ParseCodeLogic.cs @@ -11,41 +11,23 @@ namespace AKSoftware.Localization.MultiLanguages { public class ParseCodeLogic { + private DataAttributeParsing _dataAttributeParsing = new DataAttributeParsing(); + private static List _ignoreStartsWith = new List() { - "http://", "https://", "class=", "style=", "src=", "alt=", "width=", "height=", "id=", "if (", "var ", "%", - "display:", "}", "else", "@*", "[" + "http://", "https://", "class=", "style=", "src=", "alt=", "width=", "height=", "id=", "if (", "var ", "%", "display:", "else", "@", "<", "[" }; private static List _ignoreContains = new List() { - "@(", "@_", "[@_", "=\"" + "@(", "@_", "[@_", "=\"", "{", "}", "(\"" }; private const string ReplacementMarker = "~~~"; - private const string StringType = "string"; - private const string PropertyTypeGroup = "propertyType"; private const string ContentGroup = "content"; - private Regex _keyReferenceRegex; - private static Regex _propertyRegex = - new Regex(@"public\s+(?[^\s\?]+\??)\s+(?\w+)\s*{\s*get;\s*set;\s*}", - RegexOptions.Compiled | RegexOptions.Multiline); - - private static Regex _requiredAttributeWithMessageRegex = new Regex( - @"\[Required\(ErrorMessage\s*=\s*""(?.*?)""\)\]", RegexOptions.Compiled | RegexOptions.Multiline); - private static Regex _requiredAttributeRegex = new Regex( - @"\[Required\]", RegexOptions.Compiled | RegexOptions.Multiline); - - private static Regex _maxLengthAttributeWithMessageRegex = new Regex( - @"\[MaxLength\((?\d+),\s*ErrorMessage\s*=\s*""(?.*?)""\)\]", - RegexOptions.Compiled | RegexOptions.Multiline); - - private static Regex _maxLengthAttributeRegex = new Regex( - @"\[MaxLength\((?\d+)\]", - RegexOptions.Compiled | RegexOptions.Multiline); private static Regex _styleTag = new Regex(@"]*>[^<]*<\/style>", RegexOptions.Compiled | RegexOptions.Multiline); private static Regex _scriptTag = new Regex(@"]*>[^<]*<\/script>", RegexOptions.Compiled | RegexOptions.Multiline); @@ -149,10 +131,7 @@ public List GetLocalizableStrings(ParseParms parms) if (Path.GetExtension(file) == ".cs") { - fileResults.AddRange(GetRequiredWithErrorMessageInText(content)); - fileResults.AddRange(GetRequiredAttributeInText(content)); - fileResults.AddRange(GetMaxLengthAttributesWithErrorMessageInText(content)); - fileResults.AddRange(GetMaxLengthAttributesInText(content)); + fileResults = _dataAttributeParsing.ParseDataAttributes(fileResults, content); } else { @@ -169,7 +148,7 @@ public List GetLocalizableStrings(ParseParms parms) if (parms.RemoveLocalizedKeys) { var existingKeyValues = ReadYamlFile(parms.ResourceFilePath); - result = RemoveLocalizedKeys(parms, result, existingKeyValues); + result = RemoveLocalizedKeys(result, existingKeyValues); } return result @@ -181,148 +160,36 @@ public List GetLocalizableStrings(ParseParms parms) - private List RemoveLocalizedKeys(ParseParms parms, List parseResults, Dictionary existingKeyValues) + private List RemoveLocalizedKeys(List parseResults, Dictionary existingKeyValues) { List result = new List(); + var attributePrefixes = _dataAttributeParsing.GetDataAttributePrefixes(); foreach (var parseResult in parseResults) { - if (parms.Prefixes.Any(o => parseResult.Key.StartsWith(o)) - && !existingKeyValues.ContainsKey(parseResult.Key)) + //Special logic for data attributes + if (attributePrefixes.Any(o => parseResult.Key.StartsWith(o) && parseResult.Key.Length > o.Length)) { - result.Add(parseResult); + if (!existingKeyValues.ContainsKey(parseResult.Key)) + { + result.Add(parseResult); + } } - } - - return result; - } - - private List GetMaxLengthAttributesWithErrorMessageInText(string text) - { - List result = new List(); - - MatchCollection matches = _maxLengthAttributeWithMessageRegex.Matches(text); - - foreach (Match match in matches) - { - string errorMessage = match.Groups[ContentGroup].Value; - string maxLength = match.Groups["maxLength"].Value; - int index = text.IndexOf(match.Value); - Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); - string propertyName = propertyStringMatch.Groups[ContentGroup].Value; - string propertyType = propertyStringMatch.Groups[PropertyTypeGroup].Value; - - //TODO: We currently only support string properties for required - if (!propertyType.ToLower().StartsWith(StringType)) - continue; - - result.Add(new ParseResult - { - LocalizableString = errorMessage, - MatchingExpression = _maxLengthAttributeWithMessageRegex, - FilePath = string.Empty, - MatchValue = match.Value, - Key = $"MaxLength{propertyName}{maxLength}" - }); - } - - return result; - } - - private List GetMaxLengthAttributesInText(string text) - { - List result = new List(); - - MatchCollection matches = _maxLengthAttributeRegex.Matches(text); - - foreach (Match match in matches) - { - string maxLength = match.Groups["maxLength"].Value; - int index = text.IndexOf(match.Value); - Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); - string propertyName = propertyStringMatch.Groups[ContentGroup].Value; - string propertyType = propertyStringMatch.Groups[PropertyTypeGroup].Value; - - //TODO: We currently only support string properties for required - if (!propertyType.ToLower().StartsWith(StringType)) - continue; - - string errorMessage = $"{StringUtil.InsertSpaces(propertyName)} has a maximum length of {maxLength} characters"; - result.Add(new ParseResult + else { - LocalizableString = errorMessage, - MatchingExpression = _maxLengthAttributeRegex, - FilePath = string.Empty, - MatchValue = match.Value, - Key = $"MaxLength{propertyName}{maxLength}" - }); - } - - return result; - } - - private List GetRequiredWithErrorMessageInText(string text) - { - List result = new List(); - - MatchCollection matches = _requiredAttributeWithMessageRegex.Matches(text); - - foreach (Match match in matches) - { - string errorMessage = match.Groups[ContentGroup].Value; - int index = text.IndexOf(match.Value); - Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); - string propertyName = propertyStringMatch.Groups[ContentGroup].Value; - string propertyType = propertyStringMatch.Groups[PropertyTypeGroup].Value; - - //TODO: We currently only support string properties for required - if (!propertyType.ToLower().StartsWith(StringType)) - continue; + result.Add(parseResult); + } - result.Add(new ParseResult - { - LocalizableString = errorMessage, - MatchingExpression = _requiredAttributeWithMessageRegex, - FilePath = string.Empty, - MatchValue = match.Value, - Key = $"Required{propertyName}" - }); } return result; } - private List GetRequiredAttributeInText(string text) - { - List result = new List(); - MatchCollection matches = _requiredAttributeRegex.Matches(text); - foreach (Match match in matches) - { - int index = text.IndexOf(match.Value); - Match propertyStringMatch = _propertyRegex.Match(text.Substring(index)); - string propertyName = propertyStringMatch.Groups[ContentGroup].Value; - string propertyType = propertyStringMatch.Groups[PropertyTypeGroup].Value; - //TODO: We currently only support string properties for required - if (!propertyType.ToLower().StartsWith(StringType)) - continue; - string errorMessage = $"{StringUtil.InsertSpaces(propertyName)} is required"; - result.Add(new ParseResult - { - LocalizableString = errorMessage, - MatchingExpression = _requiredAttributeWithMessageRegex, - FilePath = string.Empty, - MatchValue = match.Value, - Key = $"Required{propertyName}" - }); - } - - return result; - } /// @@ -374,7 +241,7 @@ public List GetLocalizableStringsInText(string text) return result; } - + private List GetExistingLocalizedStringsInText(string html, Dictionary existingKeyValues) { @@ -475,8 +342,10 @@ public List GetUnusedKeys(ParseParms parms) // Read existing keys from the YAML file var existingKeys = ReadYamlFile(parms.ResourceFilePath).Keys.ToList(); - //Exclude Required, MaxLength, Dynamic because those are used dynamically in ValidationLocalization - existingKeys = existingKeys.Where(k => !parms.Prefixes.Any(p => k.StartsWith(p))).ToList(); + var attributePrefixes = _dataAttributeParsing.GetDataAttributePrefixes(); + //Exclude Data Attributes and Dynamic because those are used dynamically in ValidationLocalization + existingKeys = existingKeys.Where(k => !parms.Prefixes.Any(p => k.StartsWith(p)) + && !attributePrefixes.Any(ap => k.StartsWith(ap))).ToList(); // Get localizable strings to find keys in use var parseResults = GetExistingLocalizedStrings(parms); @@ -498,12 +367,13 @@ public List GetUnusedKeys(ParseParms parms) public List GetExistingLocalizedStrings(ParseParms parms) { ValidateParseParameters(parms); + string escapedKeyReference = Regex.Escape(parms.KeyReference); string pattern = $@"({escapedKeyReference}\.Keys\[""?(?[^,""\]]+)?)|({escapedKeyReference}\[""?(?[^,""\]]+)?)"; - _keyReferenceRegex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.Multiline); + var existingKeyValues = ReadYamlFile(parms.ResourceFilePath); - List files = GetAllFilesForSourceDirectoriesAndWildcards(parms); + List files = GetAllFilesForSourceDirectoriesAndWildcards(parms); List filesToProcess = files .Where(o => parms.ExcludeFiles.All(a => a != Path.GetFileName(o))) @@ -541,7 +411,7 @@ private List GetAllFilesForSourceDirectoriesAndWildcards(ParseParms parm } } - return files.Distinct().OrderBy(o => o) .ToList(); + return files.Distinct().OrderBy(o => o).ToList(); } internal Dictionary ReadYamlFile(string filePath) @@ -677,5 +547,13 @@ private void ValidateParseParameters(ParseParms parms) } } + + + + + + + + } } diff --git a/src/AKSoftware.Localization.MultiLanguages/ParseParms.cs b/src/AKSoftware.Localization.MultiLanguages/ParseParms.cs index 3946698..5119c38 100644 --- a/src/AKSoftware.Localization.MultiLanguages/ParseParms.cs +++ b/src/AKSoftware.Localization.MultiLanguages/ParseParms.cs @@ -4,7 +4,7 @@ namespace AKSoftware.Localization.MultiLanguages { public class ParseParms { - public List Prefixes { get; set; } = new List() {"Required","MaxLength", "Dynamic"}; + public List Prefixes { get; set; } = new List() { "Dynamic"}; public List SourceDirectories { get; set; } = new List(); public List WildcardPatterns { get; set; } = new List(); public List ExcludeDirectories { get; set; } = new List(); diff --git a/src/AKSoftware.Localization.MultiLanguages/ValidationLocalization.cs b/src/AKSoftware.Localization.MultiLanguages/ValidationLocalization.cs new file mode 100644 index 0000000..e559d63 --- /dev/null +++ b/src/AKSoftware.Localization.MultiLanguages/ValidationLocalization.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Components.Forms; + +namespace AKSoftware.Localization.MultiLanguages +{ + public static class ValidationLocalization + { + public static bool ValidateModel(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc) where T : class, new() + { + bool valid = true; + var type = model.GetType(); + var properties = type.GetProperties(); + + foreach (var property in properties) + { + var attributes = property.GetCustomAttributes(true); + valid = ValidateAttributes(model, validationMessageStore, lc, attributes, property, valid); + } + + return valid; + } + + private static bool ValidateAttributes(T model, ValidationMessageStore validationMessageStore, + ILanguageContainerService lc, object[] attributes, PropertyInfo property, bool valid) where T : class, new() + { + foreach (var attribute in attributes) + { + if (attribute is RequiredAttribute requiredAttribute) + { + if (!ValidateRequired(model, validationMessageStore, lc, property, requiredAttribute)) + valid = false; + } + else if (attribute is MaxLengthAttribute maxLengthAttribute) + { + if (!ValidateMaxLength(model, validationMessageStore, lc, property, maxLengthAttribute)) + valid = false; + } + else if (attribute is StringLengthAttribute stringLengthAttribute) + { + if (!ValidateStringLength(model, validationMessageStore, lc, property, stringLengthAttribute)) + valid = false; + } + else if (attribute is RangeAttribute rangeAttribute) + { + if (!ValidateRange(model, validationMessageStore, lc, property, rangeAttribute)) + valid = false; + } + else if (attribute is RegularExpressionAttribute regularExpressionAttribute) + { + if (!ValidateRegularExpression(model, validationMessageStore, lc, property, regularExpressionAttribute)) + valid = false; + } + else if (attribute is CompareAttribute compareAttribute) + { + if (!ValidateCompare(model, validationMessageStore, lc, property, compareAttribute)) + valid = false; + } + else if (attribute is EmailAddressAttribute emailAddressAttribute) + { + if (!ValidateEmail(model, validationMessageStore, lc, property, emailAddressAttribute)) + valid = false; + } + else if (attribute is PhoneAttribute phoneAttribute) + { + if (!ValidatePhone(model, validationMessageStore, lc, property, phoneAttribute)) + valid = false; + } + else if (attribute is CreditCardAttribute creditCardAttribute) + { + if (!ValidateCreditCard(model, validationMessageStore, lc, property, creditCardAttribute)) + valid = false; + } + else if (attribute is UrlAttribute urlAttribute) + { + if (!ValidateUrl(model, validationMessageStore, lc, property, urlAttribute)) + valid = false; + } + + if (!valid) + break; + } + + return valid; + } + + private static bool ValidateMaxLength(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, MaxLengthAttribute maxLengthAttribute) where T : class, new() + { + if (property.PropertyType != typeof(string)) + return true; + + if (property.GetValue(model) != null && property.GetValue(model).ToString().Length > maxLengthAttribute.Length) + { + string languageKey = $"{DataAttributeParsing.MaxLengthPrefix}{property.Name}{maxLengthAttribute.Length}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (maxLengthAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), maxLengthAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} should not exceed {maxLengthAttribute.Length} characters"); + } + + return false; + } + + return true; + } + + private static bool ValidateStringLength(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, StringLengthAttribute stringLengthAttribute) where T : class, new() + { + if (property.PropertyType != typeof(string)) + return true; + + var value = property.GetValue(model)?.ToString(); + if (value != null && (value.Length < stringLengthAttribute.MinimumLength || value.Length > stringLengthAttribute.MaximumLength)) + { + string languageKey = $"{DataAttributeParsing.StringLengthPrefix}{property.Name}{stringLengthAttribute.MinimumLength}{stringLengthAttribute.MaximumLength}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (stringLengthAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), stringLengthAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} should be between {stringLengthAttribute.MinimumLength} and {stringLengthAttribute.MaximumLength} characters"); + } + + return false; + } + + return true; + } + + private static bool ValidateRange(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, RangeAttribute rangeAttribute) where T : class, new() + { + var value = property.GetValue(model); + if (value == null) + return true; + + double doubleValue; + try + { + doubleValue = Convert.ToDouble(value); + } + catch (Exception) + { + return true; + } + + if (doubleValue < Convert.ToDouble(rangeAttribute.Minimum) || doubleValue > Convert.ToDouble(rangeAttribute.Maximum)) + { + string languageKey = $"{DataAttributeParsing.RangePrefix}{property.Name}{rangeAttribute.Minimum}{rangeAttribute.Maximum}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (rangeAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), rangeAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} should be between {rangeAttribute.Minimum} and {rangeAttribute.Maximum}"); + } + + return false; + } + + return true; + } + + private static bool ValidateRegularExpression(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, RegularExpressionAttribute regularExpressionAttribute) where T : class, new() + { + if (property.PropertyType != typeof(string)) + return true; + + var value = property.GetValue(model)?.ToString(); + if (value != null && !Regex.IsMatch(value, regularExpressionAttribute.Pattern)) + { + string languageKey = $"{DataAttributeParsing.RegularExpressionPrefix}{property.Name}{regularExpressionAttribute.Pattern}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (regularExpressionAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), regularExpressionAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} is not in the correct format"); + } + + return false; + } + + return true; + } + + private static bool ValidateCompare(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, CompareAttribute compareAttribute) where T : class, new() + { + var currentValue = property.GetValue(model)?.ToString(); + var compareProperty = model.GetType().GetProperty(compareAttribute.OtherProperty); + var compareValue = compareProperty?.GetValue(model)?.ToString(); + + if (currentValue != compareValue) + { + string languageKey = $"{DataAttributeParsing.ComparePrefix}{property.Name}{compareAttribute.OtherProperty}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (compareAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), compareAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} must be equal to {compareAttribute.OtherProperty}"); + } + + return false; + } + + return true; + } + + private static bool ValidateEmail(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, EmailAddressAttribute emailAddressAttribute) where T : class, new() + { + if (property.PropertyType != typeof(string)) + return true; + + var value = property.GetValue(model)?.ToString(); + if (value != null && !new EmailAddressAttribute().IsValid(value)) + { + string languageKey = $"{DataAttributeParsing.EmailAddressPrefix}{property.Name}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (emailAddressAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), emailAddressAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} is not a valid email address"); + } + + return false; + } + + return true; + } + + private static bool ValidatePhone(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, PhoneAttribute phoneAttribute) where T : class, new() + { + if (property.PropertyType != typeof(string)) + return true; + + var value = property.GetValue(model)?.ToString(); + if (value != null && !new PhoneAttribute().IsValid(value)) + { + string languageKey = $"{DataAttributeParsing.PhonePrefix}{property.Name}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (phoneAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), phoneAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} is not a valid phone number"); + } + + return false; + } + + return true; + } + + + private static bool ValidateCreditCard(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, CreditCardAttribute creditCardAttribute) where T : class, new() + { + if (property.PropertyType != typeof(string)) + return true; + + var value = property.GetValue(model)?.ToString(); + if (value != null && !new CreditCardAttribute().IsValid(value)) + { + string languageKey = $"{DataAttributeParsing.CreditCardPrefix}{property.Name}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (creditCardAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), creditCardAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} is not a valid credit card number"); + } + + return false; + } + + return true; + } + + private static bool ValidateUrl(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, UrlAttribute urlAttribute) where T : class, new() + { + if (property.PropertyType != typeof(string)) + return true; + + var value = property.GetValue(model)?.ToString(); + if (value != null && !new UrlAttribute().IsValid(value)) + { + string languageKey = $"{DataAttributeParsing.UrlPrefix}{property.Name}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (urlAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), urlAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} is not a valid URL"); + } + + return false; + } + + return true; + } + private static bool ValidateRequired(T model, ValidationMessageStore validationMessageStore, ILanguageContainerService lc, PropertyInfo property, RequiredAttribute requiredAttribute) where T : class, new() + { + if (property.PropertyType != typeof(string)) + return true; + + if (property.GetValue(model) == null || string.IsNullOrWhiteSpace(property.GetValue(model).ToString())) + { + string languageKey = $"{DataAttributeParsing.RequiredPrefix}{property.Name}"; + string translation = lc.Keys[languageKey]; + + if (!string.IsNullOrEmpty(translation) && translation != languageKey) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), lc.Keys[languageKey]); + } + else if (requiredAttribute.ErrorMessage != null) + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), requiredAttribute.ErrorMessage); + } + else + { + validationMessageStore.Add(new FieldIdentifier(model, property.Name), $"{property.Name} is required"); + } + + return false; + } + + return true; + } + } +}