Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Localization): Allow setting a Language resource to an empty string value #6954 #7026

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 16 additions & 13 deletions src/Libraries/Nop.Services/Localization/LocalizationService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq.Expressions;
#nullable enable
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Xml;
Expand Down Expand Up @@ -225,7 +226,7 @@ public virtual async Task<LocaleStringResource> GetLocaleStringResourceByIdAsync
/// A task that represents the asynchronous operation
/// The task result contains the locale string resource
/// </returns>
public virtual async Task<LocaleStringResource> GetLocaleStringResourceByNameAsync(string resourceName, int languageId,
public virtual async Task<LocaleStringResource?> GetLocaleStringResourceByNameAsync(string resourceName, int languageId,
bool logIfNotFound = true)
{
var query = from lsr in _lsrRepository.Table
Expand All @@ -250,7 +251,7 @@ orderby lsr.ResourceName
/// <returns>
/// The locale string resource
/// </returns>
public virtual LocaleStringResource GetLocaleStringResourceByName(string resourceName, int languageId,
public virtual LocaleStringResource? GetLocaleStringResourceByName(string resourceName, int languageId,
bool logIfNotFound = true)
{
var query = from lsr in _lsrRepository.Table
Expand All @@ -273,9 +274,11 @@ orderby lsr.ResourceName
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task InsertLocaleStringResourceAsync(LocaleStringResource localeStringResource)
{
if (!string.IsNullOrEmpty(localeStringResource?.ResourceName))
localeStringResource.ResourceName = localeStringResource.ResourceName.Trim().ToLowerInvariant();
ArgumentNullException.ThrowIfNull(localeStringResource);

if (!string.IsNullOrEmpty(localeStringResource.ResourceName))
localeStringResource.ResourceName = localeStringResource.ResourceName.Trim().ToLowerInvariant();

await _lsrRepository.InsertAsync(localeStringResource);
}

Expand Down Expand Up @@ -364,7 +367,7 @@ orderby l.ResourceName
/// A task that represents the asynchronous operation
/// The task result contains a string representing the requested resource string.
/// </returns>
public virtual async Task<string> GetResourceAsync(string resourceKey)
public virtual async Task<string?> GetResourceAsync(string resourceKey)
{
var workingLanguage = await _workContext.GetWorkingLanguageAsync();

Expand All @@ -386,10 +389,10 @@ public virtual async Task<string> GetResourceAsync(string resourceKey)
/// A task that represents the asynchronous operation
/// The task result contains a string representing the requested resource string.
/// </returns>
public virtual async Task<string> GetResourceAsync(string resourceKey, int languageId,
public virtual async Task<string?> GetResourceAsync(string? resourceKey, int languageId,
bool logIfNotFound = true, string defaultValue = "", bool returnEmptyIfNotFound = false)
{
var result = string.Empty;
string? result = null;
resourceKey ??= string.Empty;
resourceKey = resourceKey.Trim().ToLowerInvariant();
if (_localizationSettings.LoadAllLocaleRecordsOnStartup)
Expand Down Expand Up @@ -418,7 +421,7 @@ public virtual async Task<string> GetResourceAsync(string resourceKey, int langu
result = lsr;
}

if (!string.IsNullOrEmpty(result))
if (result != null)
return result;

if (logIfNotFound)
Expand Down Expand Up @@ -542,7 +545,7 @@ public virtual async Task ImportResourcesFromXmlAsync(Language language, StreamR
/// A task that represents the asynchronous operation
/// The task result contains the localized property
/// </returns>
public virtual async Task<TPropType> GetLocalizedAsync<TEntity, TPropType>(TEntity entity, Expression<Func<TEntity, TPropType>> keySelector,
public virtual async Task<TPropType?> GetLocalizedAsync<TEntity, TPropType>(TEntity entity, Expression<Func<TEntity, TPropType>> keySelector,
int? languageId = null, bool returnDefaultValue = true, bool ensureTwoPublishedLanguages = true)
where TEntity : BaseEntity, ILocalizedEntity
{
Expand Down Expand Up @@ -608,7 +611,7 @@ public virtual async Task<TPropType> GetLocalizedAsync<TEntity, TPropType>(TEnti
/// A task that represents the asynchronous operation
/// The task result contains the localized property
/// </returns>
public virtual async Task<string> GetLocalizedSettingAsync<TSettings>(TSettings settings, Expression<Func<TSettings, string>> keySelector,
public virtual async Task<string?> GetLocalizedSettingAsync<TSettings>(TSettings settings, Expression<Func<TSettings, string>> keySelector,
int languageId, int storeId, bool returnDefaultValue = true, bool ensureTwoPublishedLanguages = true)
where TSettings : ISettings, new()
{
Expand Down Expand Up @@ -758,7 +761,7 @@ public virtual async Task DeleteLocalizedPermissionNameAsync(PermissionRecord pe
/// <param name="resourceValue">Resource value</param>
/// <param name="languageCulture">Language culture code. If null or empty, then a resource will be added for all languages</param>
/// <returns>A task that represents the asynchronous operation</returns>
public virtual async Task AddOrUpdateLocaleResourceAsync(string resourceName, string resourceValue, string languageCulture = null)
public virtual async Task AddOrUpdateLocaleResourceAsync(string resourceName, string resourceValue, string? languageCulture = null)
{
foreach (var lang in await _languageService.GetAllLanguagesAsync(true))
{
Expand Down Expand Up @@ -934,7 +937,7 @@ public virtual async Task<string> GetLocalizedFriendlyNameAsync<TPlugin>(TPlugin
if (string.IsNullOrEmpty(result) && returnDefaultValue)
result = plugin.PluginDescriptor.FriendlyName;

return result;
return result!;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public Localizer T
_localizer ??= (format, args) =>
{
var resFormat = _localizationService.GetResourceAsync(format).Result;
if (string.IsNullOrEmpty(resFormat))
if (resFormat == null)
{
return new LocalizedString(format);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Nop.Web.Framework.Models;
using Nop.Web.Framework.Mvc;
using Nop.Web.Framework.Mvc.ModelBinding;

namespace Nop.Web.Areas.Admin.Models.Localization;
Expand All @@ -14,7 +15,8 @@ public partial record LocaleResourceModel : BaseNopEntityModel
public string ResourceName { get; set; }

[NopResourceDisplayName("Admin.Configuration.Languages.Resources.Fields.Value")]
public string ResourceValue { get; set; }
[NoTrim]
public string ResourceValue { get; set; } = string.Empty;

public int LanguageId { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public LanguageResourceValidator(ILocalizationService localizationService)
.WithMessageAwait(localizationService.GetResourceAsync("Admin.Configuration.Languages.Resources.Fields.Name.Required"));

RuleFor(model => model.ResourceValue)
.NotEmpty()
.NotNull()
.WithMessageAwait(localizationService.GetResourceAsync("Admin.Configuration.Languages.Resources.Fields.Value.Required"));

SetDatabaseValidationRules<LocaleStringResource>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public async Task AddOrUpdateLocaleResourceShouldSkipAlreadyExistsResources()
}

[Test]
public async Task AddOrUpdateLocaleResourceShouldApdateAllResorcesIfLangIdIsNull()
public async Task AddOrUpdateLocaleResourceShouldUpdateAllResourcesIfLangIdIsNull()
{
var languageService = GetService<ILanguageService>();
var language = new Language
Expand Down Expand Up @@ -152,7 +152,7 @@ public async Task CanDeleteLocaleResources()
}

[Test]
public async Task DeleteLocaleResourcesShuoldIgnoreCase()
public async Task DeleteLocaleResourcesShouldIgnoreCase()
{
await _localizationService.AddOrUpdateLocaleResourceAsync(_resources);

Expand All @@ -169,6 +169,16 @@ public async Task DeleteLocaleResourcesShuoldIgnoreCase()
rez.Count.Should().Be(0);
}

[TestCase("A Value")]
[TestCase("")] //empty string is valid value
public async Task CanGetResourceAsync(string val)
{
var resKey = $"{PREFIX}.CanGetResourceAsync";
await _localizationService.AddOrUpdateLocaleResourceAsync(resKey,val);
var resource = await _localizationService.GetResourceAsync(resKey);
resource.Should().Be(val);
}

public class LocaleResourceConsumer : IConsumer<EntityUpdatedEvent<LocaleStringResource>>
{
public static int UpdateCount { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using FluentValidation.Internal;
using FluentValidation.TestHelper;
using Moq;
using Nop.Services.Localization;
using Nop.Web.Areas.Admin.Models.Localization;
using Nop.Web.Areas.Admin.Validators.Localization;
using NUnit.Framework;

namespace Nop.Tests.Nop.Web.Tests.Public.Validators.Localization;

[TestFixture]
public class LanguageResourceValidatorTests : BaseNopTest
{
private LanguageResourceValidator _validator;

[SetUp]
public void SetUp()
{
var localizationServiceMock = new Mock<ILocalizationService>();
localizationServiceMock.Setup(x => x.GetResourceAsync(It.IsAny<string>())).ReturnsAsync("Mocked message");
_validator = new LanguageResourceValidator(localizationServiceMock.Object);
}

[Test]
public void ShouldHaveErrorWhenResourceNameIsEmpty()
{
var model = new LocaleResourceModel { ResourceName = string.Empty, ResourceValue = "Value" };
var result = _validator.TestValidate(model, DefaultValidationOptions());
result.ShouldHaveValidationErrorFor(x => x.ResourceName);
}

[Test]
public void ShouldNotHaveErrorWhenResourceNameIsSpecified()
{
var model = new LocaleResourceModel { ResourceName = "Name", ResourceValue = "Value" };
var result = _validator.TestValidate(model, DefaultValidationOptions());
result.ShouldNotHaveValidationErrorFor(x => x.ResourceName);
}

[Test]
public void ShouldHaveErrorWhenResourceValueIsNull()
{
var model = new LocaleResourceModel { ResourceName = "Name", ResourceValue = null };
var result = _validator.TestValidate(model, DefaultValidationOptions());
result.ShouldHaveValidationErrorFor(x => x.ResourceValue);
}

[Test]
public void ShouldNotHaveErrorWhenResourceValueIsSpecified()
{
var model = new LocaleResourceModel { ResourceName = "Name", ResourceValue = "Value" };
var result = _validator.TestValidate(model, DefaultValidationOptions());
result.ShouldNotHaveValidationErrorFor(x => x.ResourceValue);
}

private static Action<ValidationStrategy<LocaleResourceModel>> DefaultValidationOptions()
{
return options=>options.IncludeAllRuleSets();
}
}