Skip to content

Commit

Permalink
Support data annotations PO localization (OrchardCMS#4675)
Browse files Browse the repository at this point in the history
  • Loading branch information
hishamco authored Dec 25, 2021
1 parent 1898095 commit 95021f7
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 20 deletions.
4 changes: 3 additions & 1 deletion src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ public override void ConfigureServices(IServiceCollection services)
services.AddScoped<IPermissionProvider, Permissions>();
services.AddScoped<ILocalizationService, LocalizationService>();

services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");
services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization").
AddDataAnnotationsPortableObjectLocalization();

services.Replace(ServiceDescriptor.Singleton<ILocalizationFileLocationProvider, ModularPoFileLocationProvider>());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,17 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;

namespace OrchardCore.Users.ViewModels
{
public class LoginViewModel : IValidatableObject
public class LoginViewModel
{
[Required(ErrorMessage = "Username is required.")]
[Display(Name = "Username")]
public string UserName { get; set; }

[Required(ErrorMessage = "Password is required.")]
[DataType(DataType.Password)]
public string Password { get; set; }

public bool RememberMe { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var S = validationContext.GetService<IStringLocalizer<LoginViewModel>>();
if (string.IsNullOrWhiteSpace(UserName))
{
yield return new ValidationResult(S["Username is required."], new[] { "UserName" });
}

if (string.IsNullOrWhiteSpace(Password))
{
yield return new ValidationResult(S["Password is required."], new[] { "Password" });
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;

namespace OrchardCore.Localization.DataAnnotations
{
internal class LocalizedDataAnnotationsMvcOptions : IConfigureOptions<MvcOptions>
{
private readonly IStringLocalizerFactory _stringLocalizerFactory;

public LocalizedDataAnnotationsMvcOptions(IStringLocalizerFactory stringLocalizerFactory)
{
_stringLocalizerFactory = stringLocalizerFactory;
}

public void Configure(MvcOptions options)
{
var localizer = _stringLocalizerFactory.Create(typeof(LocalizedDataAnnotationsMvcOptions));

options.ModelMetadataDetailsProviders.Add(new LocalizedValidationMetadataProvider(localizer));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Localization;

namespace OrchardCore.Localization.DataAnnotations
{
/// <summary>
/// Provides a validation for a <see cref="DefaultModelMetadata"/>.
/// </summary>
public class LocalizedValidationMetadataProvider : IValidationMetadataProvider
{
private readonly IStringLocalizer _stringLocalizer;

/// <summary>
/// Initializes a new instance of a <see cref="LocalizedValidationMetadataProvider"/> with string localizer.
/// </summary>
/// <param name="stringLocalizer">The <see cref="IStringLocalizer"/>.</param>
public LocalizedValidationMetadataProvider(IStringLocalizer stringLocalizer)
{
_stringLocalizer = stringLocalizer;
}

/// <remarks>This will localize the default data annotations error message if it is exist, otherwise will try to look for a parameterized version.</remarks>
/// <example>
/// A property named 'UserName' that decorated with <see cref="RequiredAttribute"/> will be localized using
/// "The {0} field is required." and "The UserName field is required." error messages.
/// </example>
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
foreach (var metadata in context.ValidationMetadata.ValidatorMetadata)
{
if (metadata is ValidationAttribute attribute)
{
var displayName = context.Attributes.OfType<DisplayAttribute>().FirstOrDefault()?.Name;
// Use DisplayName if present
var argument = displayName ?? context.Key.Name;
var errorMessageString = attribute.ErrorMessage == null && attribute.ErrorMessageResourceName == null
? attribute.FormatErrorMessage(argument)
: attribute.ErrorMessage;

// Localize the parameterized error message
var localizedString = _stringLocalizer[errorMessageString];

if (localizedString == errorMessageString)
{
// Localize the unparameterized error message
var unparameterizedErrorMessage = errorMessageString.Replace(argument, "{0}");
localizedString = _stringLocalizer[unparameterizedErrorMessage];
}

attribute.ErrorMessage = localizedString;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using OrchardCore.Localization;
using OrchardCore.Localization.DataAnnotations;
using OrchardCore.Localization.PortableObject;

namespace Microsoft.Extensions.DependencyInjection
Expand Down Expand Up @@ -43,5 +46,21 @@ public static IServiceCollection AddPortableObjectLocalization(this IServiceColl

return services;
}

/// <summary>
/// Localize data annotations attributes from portable object files.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
public static IServiceCollection AddDataAnnotationsPortableObjectLocalization(this IServiceCollection services)
{
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}

services.AddSingleton<IConfigureOptions<MvcOptions>, LocalizedDataAnnotationsMvcOptions>();

return services;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using OrchardCore.Localization.DataAnnotations;

namespace OrchardCore.Localization.PortableObject
{
Expand All @@ -12,6 +13,9 @@ namespace OrchardCore.Localization.PortableObject
/// </summary>
public class PortableObjectStringLocalizer : IPluralStringLocalizer
{
private static readonly string DataAnnotationsDefaultErrorMessagesContext = typeof(DataAnnotationsDefaultErrorMessages).FullName;
private static readonly string LocalizedDataAnnotationsMvcOptionsContext = typeof(LocalizedDataAnnotationsMvcOptions).FullName;

private readonly ILocalizationManager _localizationManager;
private readonly bool _fallBackToParentCulture;
private readonly ILogger _logger;
Expand Down Expand Up @@ -199,8 +203,31 @@ string ExtractTranslation()

if (dictionary != null)
{
// Extract translation with context
var key = CultureDictionaryRecord.GetKey(name, context);

// Extract translation from data annotations attributes
if (context == LocalizedDataAnnotationsMvcOptionsContext)
{
// Extract translation with context
key = CultureDictionaryRecord.GetKey(name, DataAnnotationsDefaultErrorMessagesContext);
translation = dictionary[key];

if (translation != null)
{
return translation;
}

// Extract translation without context
key = CultureDictionaryRecord.GetKey(name, null);
translation = dictionary[key];

if (translation != null)
{
return translation;
}
}

// Extract translation with context
translation = dictionary[key, count];

if (context != null && translation == null)
Expand Down

0 comments on commit 95021f7

Please sign in to comment.