Skip to main content

Localized values

Overview

The LocalizedValues and handled by ASOL.Core.Localization package. The implementation of localized domain entity is easy and seamless for MongoDB platform, but requires some fix-ups for EFCore platform. See examples and helpers for implementation, validation, comparison and conversion of localized values.

The core package:

  • ASOL.Core.Localization

Usage

Localized domain entity for MongoDB

The implementation of localized domain entity compatible with MongoDB platform is simple. See Profession example.

using ASOL.Core.Domain;
using System.ComponentModel.DataAnnotations;

namespace My.App.Domain
{
public class Profession : BaseEntityOrganization<string>, ICodeIdentifier, ILocalizable<ProfessionLocale>
{
/// <inheritdoc/>
public Profession()
{
}

/// <inheritdoc/>
public Profession(string id, ClaimsPrincipal user, string organizationId = null, string organizationUnitId = null)
: base(id, user, organizationId, organizationUnitId)
{
}

/// <summary>
/// Profession code identifier
/// </summary>
[Required]
public string Code { get; set; }

/// <summary>
/// Localized Profession name
/// </summary>
public LocalizedValue<string> Name { get; set; }

/// <summary>
/// Localized profession description
/// </summary>
public LocalizedValue<string> Description { get; set; }
}
}

Localized domain entity for EFCore

The implementation of localized domain entity compatible with EFCore platform consists of two entities, the master entity implementing ILocalizable interface and slave entity implementing Locale base class. See Profession and ProfessionLocale examples.

using ASOL.Core.Domain;
using System.ComponentModel.DataAnnotations;

namespace My.App.Domain
{
public class Profession : BaseEntityOrganization<string>, ICodeIdentifier, ILocalizable<ProfessionLocale>
{
/// <inheritdoc/>
public Profession()
{
}

/// <inheritdoc/>
public Profession(string id, ClaimsPrincipal user, string organizationId = null, string organizationUnitId = null)
: base(id, user, organizationId, organizationUnitId)
{
}

/// <summary>
/// Profession code identifier
/// </summary>
[Required]
public string Code { get; set; }

public virtual ICollection<ProfessionLocale> LocalizedValues { get; set; } = new List<ProfessionLocale>();
}

public class ProfessionLocale : Locale<ProfessionLocale>, IEntity<string>
{
[Required]
public string ProfessionId { get; set; }

public virtual Profession Profession { get; set; }

/// <summary>
/// Localized Profession name
/// </summary>
public string Name { get; set; }

/// <summary>
/// Localized profession description
/// </summary>
public string Description { get; set; }
}
}

Validation of localized value

Localized value can be validated using LocalizedValueHelper, see example.

public Task<ValidationResult> ValidateAsync(CreateProfessionCommand command, CancellationToken ct = default)
{
var errors = new List<ValidationError>();

if (string.IsNullOrEmpty(command.Code))
{
errors.Add(new ValidationError { Message = ValidationResultMessages.InvalidOrMissingCode, MemberNames = new[] { nameof(command.Code) } });
}

if (!LocalizedValueHelper.Any(command.Name))
{
errors.Add(new ValidationError { Message = ValidationResultMessages.InvalidOrMissingName, MemberNames = new[] { nameof(command.Name) } });
}

return errors.Any()
? new ValidationResult(errors).AsTask()
: ValidationResult.SuccessfulResultTask;
}

Upsert of localized value

Localized value can be updated/inserted in localized domain entity using LocalizedValueHelper, see example.

public async Task<ExecutionResult<string>> HandleAsync(CreateProfessionCommand command, CancellationToken ct = default)
{
var document = new Profession(Guid.NewGuid().ToString(), RuntimeContext.Security.User, command.OrganizationId)
{
Code = command.Code,
};
LocalizedValueHelper.UpsertLocales(document.LocalizedValues, command.Name, (item, value) => item.Name = value);
LocalizedValueHelper.UpsertLocales(document.LocalizedValues, command.Description, (item, value) => item.Description = value);

document.MarkAsReleased(RuntimeContext.Security.User);

Repository.Add(document);
Repository.UnitOfWork?.Complete();

return new ExecutionResult<string>(document.Id);
}

Domain helpers

Domain helper classes for manipulation with localized values and localized domain entities extending ASOL.Core.Localization featues.

ILocalizable<TLocale> interface

using ASOL.Core.Domain;

namespace My.App.Domain
{
/// <summary>
/// Markup interface for localizable object.
/// </summary>
/// <typeparam name="TLocale"></typeparam>
public interface ILocalizable<TLocale>
{
/// <summary>
/// Collection of localized values.
/// </summary>
ICollection<TLocale> LocalizedValues { get; set; }
}
}

Locale<T> abstract class

using System.ComponentModel.DataAnnotations;

namespace My.App.Domain
{
/// <summary>
/// Represents the locale ancestor class.
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class Locale<T> where T : Locale<T>, new()
{
[Key]
[Required]
[MaxLength(36)]
public string Id { get; protected set; }

[Required]
[MaxLength(10)]
public string LocaleCode { get; set; }
}
}

LocalizedValueHelper class

using System.Linq;
using ASOL.Core.Localization;

namespace My.App.Domain.Helpers
{
public static class LocalizedValueHelper
{
public static bool Any(LocalizedValue<string> value)
{
if (value?.Values == null) return false;

return value.Values.Any();
}

public static bool IsDistinct(LocalizedValue<string> value)
{
if (value?.Values == null) return true;

var values = value.Values.ToLookup(i => i.Locale, i => i.Value, StringComparer.OrdinalIgnoreCase);
var anyDuplicity = values.Any(i => values[i.Key].Take(2).Count() > 1);

return !anyDuplicity;
}

public static bool Equals(LocalizedValue<string> value1, LocalizedValue<string> value2)
{
//note: case-insensitive comparison of un-ordered items
var comparer = new LocalizedValueStringComparer(StringComparison.OrdinalIgnoreCase, false);
return comparer.Equals(value1, value2);
}

public static bool Equals<T>(ICollection<T> localizedValues, LocalizedValue<string> localizedValue, Func<T, string> getValue)
where T : Locale<T>, new()
{
if (getValue is null) throw new ArgumentNullException(nameof(getValue));

if (localizedValues == null && localizedValue?.Values == null)
{
return true;
}

if (localizedValues == null || localizedValue?.Values == null)
{
return false;
}

if (localizedValues.Count != localizedValue.Values.Count)
{
return false;
}

var values2 = localizedValue.Values.ToDictionary(i => i.Locale, i => i.Value, StringComparer.Ordinal);
foreach (var item in localizedValues)
{
var equals = values2.TryGetValue(item.LocaleCode, out var lval) && lval == getValue(item);
if (!equals) return false;
}

var values1 = localizedValues.ToDictionary(i => i.LocaleCode, i => getValue(i), StringComparer.Ordinal);
foreach (var item in localizedValue.Values)
{
var equals = values1.TryGetValue(item.Locale, out var lval) && lval == item.Value;
if (!equals) return false;
}

return true;
}

/// <summary>
/// Upsert localized value of specific property in localized collection.
/// </summary>
public static void UpsertLocales<T>(ICollection<T> localizedValues, LocalizedValue<string> localizedValue, Action<T, string> setValue)
where T : Locale<T>, new()
{
if (localizedValues is null) throw new ArgumentNullException(nameof(localizedValues));
if (localizedValue is null) throw new ArgumentNullException(nameof(localizedValue));
if (setValue is null) throw new ArgumentNullException(nameof(setValue));

foreach (var localizedItem in localizedValue.Values)
{
UpsertLocale(localizedValues, localizedItem.Locale, item => setValue(item, localizedItem.Value));
}
}

public static void UpsertLocale<T>(ICollection<T> localizedValues, string localeCode, Action<T> configureValues)
where T : Locale<T>, new()
{
var localeItem = localizedValues.FirstOrDefault(i => i.LocaleCode == localeCode);
if (localeItem != null)
{
configureValues(localeItem);
}
else
{
localeItem = new T() { LocaleCode = localeCode };
configureValues(localeItem);
localizedValues.Add(localeItem);
}
}

/// <summary>
/// Gets localized value of specific property from localized collection.
/// </summary>
public static LocalizedValue<TResult> GetLocalizedValue<T, TResult>(ICollection<T> localizedValues, Func<T, TResult> getValue)
where T : Locale<T>, new()
{
if (getValue is null) throw new ArgumentNullException(nameof(getValue));
if (localizedValues == null || !localizedValues.Any())
{
return new LocalizedValue<TResult>();
}

var result = new LocalizedValue<TResult>();
result.Values = new List<LocalizedValueItem<TResult>>();

foreach (var item in localizedValues)
{
result.Values.Add(new LocalizedValueItem<TResult>
{
Locale = item.LocaleCode,
Value = getValue(item)
});
}
return result;
}

/// <summary>
/// Gets localized value for given, default or any language.
/// </summary>
public static string Translate<T>(ICollection<T> localizedValues, string localeCode, Func<T, string> getValue)
where T : Locale<T>, new()
{
if (getValue is null) throw new ArgumentNullException(nameof(getValue));

if (localizedValues == null || !localizedValues.Any())
{
return null;
}

//get localized value for given language
var localizedValue = localizedValues?.FirstOrDefault(i => i.LocaleCode == localeCode);
if (localizedValue == null)
{
//get localized value for default language
const string defaultLocale = "en-US";
localizedValue = localizedValues?.FirstOrDefault(i => i.LocaleCode == defaultLocale);
if (localizedValue == null)
{
//get localized value for any language
localizedValue = localizedValues?.FirstOrDefault();
}
}

return getValue(localizedValue);
}
}
}