mirror of https://github.com/Squidex/squidex.git
65 changed files with 1100 additions and 411 deletions
@ -0,0 +1,44 @@ |
|||||
|
// ==========================================================================
|
||||
|
// LanguageConfig.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Infrastructure; |
||||
|
using System.Collections.Immutable; |
||||
|
|
||||
|
namespace Squidex.Core |
||||
|
{ |
||||
|
public sealed class LanguageConfig |
||||
|
{ |
||||
|
public bool IsOptional { get; } |
||||
|
|
||||
|
public Language Language { get; } |
||||
|
|
||||
|
public ImmutableList<Language> Fallback { get; } |
||||
|
|
||||
|
public LanguageConfig(Language language, bool isOptional, params Language[] fallback) |
||||
|
: this(language, isOptional, fallback?.ToImmutableList()) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public LanguageConfig(Language language, bool isOptional, IEnumerable<Language> fallback) |
||||
|
: this(language, isOptional, fallback?.ToImmutableList()) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public LanguageConfig(Language language, bool isOptional = false, ImmutableList<Language> fallback = null) |
||||
|
{ |
||||
|
Guard.NotNull(language, nameof(language)); |
||||
|
|
||||
|
Language = language; |
||||
|
|
||||
|
IsOptional = isOptional; |
||||
|
|
||||
|
Fallback = fallback ?? ImmutableList<Language>.Empty; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,184 @@ |
|||||
|
// ==========================================================================
|
||||
|
// LanguagesConfig.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Collections.Immutable; |
||||
|
using System.Linq; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
// ReSharper disable InvertIf
|
||||
|
|
||||
|
namespace Squidex.Core |
||||
|
{ |
||||
|
public sealed class LanguagesConfig : IEnumerable<LanguageConfig> |
||||
|
{ |
||||
|
private readonly ImmutableDictionary<Language, LanguageConfig> languages; |
||||
|
private readonly LanguageConfig master; |
||||
|
|
||||
|
public static readonly LanguagesConfig Empty = Create(); |
||||
|
public static readonly LanguagesConfig Invariant = Create(Language.Invariant); |
||||
|
|
||||
|
public LanguageConfig Master |
||||
|
{ |
||||
|
get { return master; } |
||||
|
} |
||||
|
|
||||
|
IEnumerator IEnumerable.GetEnumerator() |
||||
|
{ |
||||
|
return languages.Values.GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
IEnumerator<LanguageConfig> IEnumerable<LanguageConfig>.GetEnumerator() |
||||
|
{ |
||||
|
return languages.Values.GetEnumerator(); |
||||
|
} |
||||
|
|
||||
|
private LanguagesConfig(ImmutableDictionary<Language, LanguageConfig> languages, LanguageConfig master) |
||||
|
{ |
||||
|
this.languages = ValidateLanguages(languages); |
||||
|
|
||||
|
this.master = master; |
||||
|
} |
||||
|
|
||||
|
public static LanguagesConfig Create(ICollection<LanguageConfig> languageConfigs) |
||||
|
{ |
||||
|
Guard.NotNull(languageConfigs, nameof(languageConfigs)); |
||||
|
|
||||
|
var validated = ValidateLanguages(languageConfigs.ToImmutableDictionary(c => c.Language)); |
||||
|
|
||||
|
return new LanguagesConfig(validated, languageConfigs.FirstOrDefault()); |
||||
|
} |
||||
|
|
||||
|
public static LanguagesConfig Create(params Language[] languages) |
||||
|
{ |
||||
|
Guard.NotNull(languages, nameof(languages)); |
||||
|
|
||||
|
var languageConfigs = languages.Select(l => new LanguageConfig(l)).ToList(); |
||||
|
|
||||
|
return Create(languageConfigs); |
||||
|
} |
||||
|
|
||||
|
public LanguagesConfig MakeMaster(Language language) |
||||
|
{ |
||||
|
ThrowIfNotFound(language); |
||||
|
|
||||
|
return new LanguagesConfig(languages, languages[language]); |
||||
|
} |
||||
|
|
||||
|
public LanguagesConfig Add(Language language) |
||||
|
{ |
||||
|
ThrowIfFound(language, () => $"Cannot add language '{language.Iso2Code}'."); |
||||
|
|
||||
|
var newLanguages = languages.Add(language, new LanguageConfig(language)); |
||||
|
|
||||
|
return new LanguagesConfig(newLanguages, master ?? newLanguages.Values.First()); |
||||
|
} |
||||
|
|
||||
|
public LanguagesConfig Update(Language language, bool isOptional, IEnumerable<Language> fallback) |
||||
|
{ |
||||
|
ThrowIfNotFound(language); |
||||
|
|
||||
|
if (isOptional) |
||||
|
{ |
||||
|
ThrowIfMaster(language, () => $"Cannot cannot make language '{language.Iso2Code}' optional"); |
||||
|
} |
||||
|
|
||||
|
var newLanguages = ValidateLanguages(languages.SetItem(language, new LanguageConfig(language, isOptional, fallback))); |
||||
|
|
||||
|
return new LanguagesConfig(newLanguages, master); |
||||
|
} |
||||
|
|
||||
|
public LanguagesConfig Remove(Language language) |
||||
|
{ |
||||
|
ThrowIfNotFound(language); |
||||
|
ThrowIfMaster(language, () => $"Cannot remove language '{language.Iso2Code}'"); |
||||
|
|
||||
|
var newLanguages = languages.Remove(language); |
||||
|
|
||||
|
foreach (var languageConfig in newLanguages.Values) |
||||
|
{ |
||||
|
if (languageConfig.Fallback.Contains(language)) |
||||
|
{ |
||||
|
newLanguages = |
||||
|
newLanguages.SetItem(languageConfig.Language, |
||||
|
new LanguageConfig( |
||||
|
languageConfig.Language, |
||||
|
languageConfig.IsOptional, |
||||
|
languageConfig.Fallback.Remove(language))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new LanguagesConfig(newLanguages, master); |
||||
|
} |
||||
|
|
||||
|
public bool TryGetConfig(Language language, out LanguageConfig value) |
||||
|
{ |
||||
|
return languages.TryGetValue(language, out value); |
||||
|
} |
||||
|
|
||||
|
public bool Contains(Language language) |
||||
|
{ |
||||
|
return language != null && languages.ContainsKey(language); |
||||
|
} |
||||
|
|
||||
|
private static ImmutableDictionary<Language, LanguageConfig> ValidateLanguages(ImmutableDictionary<Language, LanguageConfig> languages) |
||||
|
{ |
||||
|
var errors = new List<ValidationError>(); |
||||
|
|
||||
|
foreach (var languageConfig in languages.Values) |
||||
|
{ |
||||
|
foreach (var fallback in languageConfig.Fallback) |
||||
|
{ |
||||
|
if (!languages.ContainsKey(fallback)) |
||||
|
{ |
||||
|
var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'"; |
||||
|
|
||||
|
errors.Add(new ValidationError(message)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (errors.Count > 0) |
||||
|
{ |
||||
|
throw new ValidationException("Cannot configure language", errors); |
||||
|
} |
||||
|
|
||||
|
return languages; |
||||
|
} |
||||
|
|
||||
|
private void ThrowIfNotFound(Language language) |
||||
|
{ |
||||
|
if (!Contains(language)) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(language, "Languages", typeof(LanguagesConfig)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void ThrowIfFound(Language language, Func<string> message) |
||||
|
{ |
||||
|
if (Contains(language)) |
||||
|
{ |
||||
|
var error = new ValidationError("Language is already part of the app", "Language"); |
||||
|
|
||||
|
throw new ValidationException(message(), error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void ThrowIfMaster(Language language, Func<string> message) |
||||
|
{ |
||||
|
if (master?.Language == language) |
||||
|
{ |
||||
|
var error = new ValidationError("Language is the master language", "Language"); |
||||
|
|
||||
|
throw new ValidationException(message(), error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppLanguageUpdated.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Events.Apps |
||||
|
{ |
||||
|
[TypeName("AppLanguageUpdated")] |
||||
|
public sealed class AppLanguageUpdated : AppEvent |
||||
|
{ |
||||
|
public Language Language { get; set; } |
||||
|
|
||||
|
public bool IsOptional { get; set; } |
||||
|
|
||||
|
public List<Language> Fallback { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoAppLanguage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using MongoDB.Bson.Serialization.Attributes; |
||||
|
|
||||
|
namespace Squidex.Read.MongoDb.Apps |
||||
|
{ |
||||
|
public sealed class MongoAppLanguage |
||||
|
{ |
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public string Iso2Code { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public bool IsOptional { get; set; } |
||||
|
|
||||
|
[BsonRequired] |
||||
|
[BsonElement] |
||||
|
public List<string> Fallback { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -1,73 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// AppLanguages.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Squidex.Infrastructure; |
|
||||
|
|
||||
// ReSharper disable InvertIf
|
|
||||
|
|
||||
namespace Squidex.Write.Apps |
|
||||
{ |
|
||||
public class AppLanguages |
|
||||
{ |
|
||||
private readonly HashSet<Language> languages = new HashSet<Language>(); |
|
||||
private Language masterLanguage; |
|
||||
|
|
||||
public void Add(Language language) |
|
||||
{ |
|
||||
ThrowIfFound(language, () => "Cannot add language"); |
|
||||
|
|
||||
languages.Add(language); |
|
||||
} |
|
||||
|
|
||||
public void Remove(Language language) |
|
||||
{ |
|
||||
ThrowIfNotFound(language); |
|
||||
ThrowIfMasterLanguage(language, () => "Cannot remove language"); |
|
||||
|
|
||||
languages.Remove(language); |
|
||||
} |
|
||||
|
|
||||
public void SetMasterLanguage(Language language) |
|
||||
{ |
|
||||
ThrowIfNotFound(language); |
|
||||
ThrowIfMasterLanguage(language, () => "Cannot set master language"); |
|
||||
|
|
||||
masterLanguage = language; |
|
||||
} |
|
||||
|
|
||||
private void ThrowIfNotFound(Language language) |
|
||||
{ |
|
||||
if (!languages.Contains(language)) |
|
||||
{ |
|
||||
throw new DomainObjectNotFoundException(language.Iso2Code, "Languages", typeof(AppDomainObject)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void ThrowIfFound(Language language, Func<string> message) |
|
||||
{ |
|
||||
if (languages.Contains(language)) |
|
||||
{ |
|
||||
var error = new ValidationError("Language is already part of the app", "Language"); |
|
||||
|
|
||||
throw new ValidationException(message(), error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void ThrowIfMasterLanguage(Language language, Func<string> message) |
|
||||
{ |
|
||||
if (masterLanguage != null && masterLanguage.Equals(language)) |
|
||||
{ |
|
||||
var error = new ValidationError("Language is the master language", "Language"); |
|
||||
|
|
||||
throw new ValidationException(message(), error); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,30 @@ |
|||||
|
// ==========================================================================
|
||||
|
// UpdateLanguage.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Write.Apps.Commands |
||||
|
{ |
||||
|
public sealed class UpdateLanguage : AppAggregateCommand, IValidatable |
||||
|
{ |
||||
|
public Language Language { get; set; } |
||||
|
|
||||
|
public bool IsOptional { get; set; } |
||||
|
|
||||
|
public List<Language> Fallback { get; set; } |
||||
|
|
||||
|
public void Validate(IList<ValidationError> errors) |
||||
|
{ |
||||
|
if (Language == null) |
||||
|
{ |
||||
|
errors.Add(new ValidationError("Language cannot be null", nameof(Language))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,213 @@ |
|||||
|
// ==========================================================================
|
||||
|
// LanguagesConfigTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Core |
||||
|
{ |
||||
|
public class LanguagesConfigTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_create_initial_config() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
config.ToList().ShouldBeEquivalentTo( |
||||
|
new List<LanguageConfig> |
||||
|
{ |
||||
|
new LanguageConfig(Language.DE) |
||||
|
}); |
||||
|
|
||||
|
Assert.Equal(Language.DE, config.Master.Language); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_initial_config_with_multiple_languages() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE, Language.EN, Language.IT); |
||||
|
|
||||
|
config.ToList().ShouldBeEquivalentTo( |
||||
|
new List<LanguageConfig> |
||||
|
{ |
||||
|
new LanguageConfig(Language.DE), |
||||
|
new LanguageConfig(Language.EN), |
||||
|
new LanguageConfig(Language.IT) |
||||
|
}); |
||||
|
|
||||
|
Assert.Equal(Language.DE, config.Master.Language); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_create_initial_config_with_configs() |
||||
|
{ |
||||
|
var configs = new[] |
||||
|
{ |
||||
|
new LanguageConfig(Language.DE), |
||||
|
new LanguageConfig(Language.EN), |
||||
|
new LanguageConfig(Language.IT), |
||||
|
}; |
||||
|
var config = LanguagesConfig.Create(configs); |
||||
|
|
||||
|
config.ToList().ShouldBeEquivalentTo(configs); |
||||
|
|
||||
|
Assert.Equal(configs[0], config.Master); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_add_language() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT); |
||||
|
|
||||
|
config.ToList().ShouldBeEquivalentTo( |
||||
|
new List<LanguageConfig> |
||||
|
{ |
||||
|
new LanguageConfig(Language.DE), |
||||
|
new LanguageConfig(Language.IT) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_first_language_to_master() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Empty.Add(Language.IT); |
||||
|
|
||||
|
Assert.Equal(Language.IT, config.Master.Language); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_if_language_to_add_already_exists() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => config.Add(Language.DE)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_make_master_language() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).MakeMaster(Language.IT); |
||||
|
|
||||
|
Assert.Equal(Language.IT, config.Master.Language); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_if_language_to_make_master_is_not_found() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => config.MakeMaster(Language.EN)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_throw_exception_if_language_is_already_master_language() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
config.MakeMaster(Language.DE); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_remove_language() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Add(Language.RU).Remove(Language.IT); |
||||
|
|
||||
|
config.ToList().ShouldBeEquivalentTo( |
||||
|
new List<LanguageConfig> |
||||
|
{ |
||||
|
new LanguageConfig(Language.DE), |
||||
|
new LanguageConfig(Language.RU) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_remove_fallbacks_when_removing_language() |
||||
|
{ |
||||
|
var config = |
||||
|
LanguagesConfig.Create(Language.DE) |
||||
|
.Add(Language.IT) |
||||
|
.Add(Language.RU) |
||||
|
.Update(Language.DE, false, new[] { Language.RU, Language.IT }) |
||||
|
.Update(Language.RU, false, new[] { Language.DE, Language.IT }) |
||||
|
.Remove(Language.IT); |
||||
|
|
||||
|
config.ToList().ShouldBeEquivalentTo( |
||||
|
new List<LanguageConfig> |
||||
|
{ |
||||
|
new LanguageConfig(Language.DE, false, Language.RU), |
||||
|
new LanguageConfig(Language.RU, false, Language.DE) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_if_language_to_remove_is_not_found() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => config.Remove(Language.EN)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_if_language_to_remove_is_master_language() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => config.Remove(Language.DE)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_update_language() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE).Add(Language.IT).Update(Language.IT, true, new[] { Language.DE }); |
||||
|
|
||||
|
config.ToList().ShouldBeEquivalentTo( |
||||
|
new List<LanguageConfig> |
||||
|
{ |
||||
|
new LanguageConfig(Language.DE), |
||||
|
new LanguageConfig(Language.IT, true, Language.DE) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_if_language_to_update_is_not_found() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
Assert.Throws<DomainObjectNotFoundException>(() => config.Update(Language.EN, true, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_if_fallback_language_is_invalid() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, new [] { Language.EN })); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_throw_exception_if_language_to_make_optional_is_master_language() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(Language.DE); |
||||
|
|
||||
|
Assert.Throws<ValidationException>(() => config.Update(Language.DE, true, null)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_provide_enumerators() |
||||
|
{ |
||||
|
var config = LanguagesConfig.Create(); |
||||
|
|
||||
|
Assert.NotNull(((IEnumerable)config).GetEnumerator()); |
||||
|
Assert.NotNull(((IEnumerable<LanguageConfig>)config).GetEnumerator()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue