Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.Models;
Expand All @@ -23,8 +23,7 @@ public async Task<DictionaryItemResponseModel> CreateDictionaryItemViewModelAsyn
{
DictionaryItemResponseModel dictionaryResponseModel = _umbracoMapper.Map<DictionaryItemResponseModel>(dictionaryItem)!;

var validLanguageIsoCodes = (await _languageService.GetAllAsync())
.Select(language => language.IsoCode)
var validLanguageIsoCodes = (await _languageService.GetAllIsoCodesAsync())
.ToArray();
IDictionaryTranslation[] validTranslations = dictionaryItem.Translations
.Where(t => validLanguageIsoCodes.Contains(t.LanguageIsoCode))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,7 @@ private static string CleanUserGroupNameOrAliasForXss(string input)

private async Task<Attempt<IEnumerable<string>, UserGroupOperationStatus>> MapLanguageIdsToIsoCodeAsync(IEnumerable<int> ids)
{
IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
string[] isoCodes = languages
.Where(x => ids.Contains(x.Id))
.Select(x => x.IsoCode)
.ToArray();
string[] isoCodes = await _languageService.GetIsoCodesByIdsAsync(ids.ToArray());

// if a language id does not exist, it simply not returned.
// We do this so we don't have to clean up user group data when deleting languages and to make it easier to restore accidentally removed languages
Expand Down
11 changes: 11 additions & 0 deletions src/Umbraco.Core/Persistence/Repositories/ILanguageRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,16 @@ public interface ILanguageRepository : IReadWriteQueryRepository<int, ILanguage>
/// </remarks>
int? GetDefaultId();

/// <summary>
/// Gets multiple language ISO codes from the provided Ids.
/// </summary>
/// <param name="ids">The language Ids.</param>
/// <param name="throwOnNotFound">Indicates whether to throw an exception if the provided Id is not found as a language.</param>
/// <returns></returns>
string[] GetIsoCodesByIds(ICollection<int> ids, bool throwOnNotFound = true);

/// <summary>
/// Gets all available language ISO culture codes.
/// </summary>
IEnumerable<string> GetAllIsoCodes() => GetMany().Select(x => x.IsoCode);
}
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public PublishedUrlInfoProvider(
public async Task<ISet<UrlInfo>> GetAllAsync(IContent content)
{
HashSet<UrlInfo> urlInfos = [];
var cultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray();
IEnumerable<string> cultures = await _languageService.GetAllIsoCodesAsync();

// First we get the urls of all cultures, using the published router, meaning we respect any extensions.
foreach (var culture in cultures)
Expand Down
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Routing/UrlProviderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public static async Task<IEnumerable<UrlInfo>> GetContentUrlsAsync(
// and, not only for those assigned to domains in the branch, because we want
// to show what GetUrl() would return, for every culture.
var urls = new HashSet<UrlInfo>();
var cultures = (await languageService.GetAllAsync()).Select(x => x.IsoCode).ToList();
IEnumerable<string> cultures = await languageService.GetAllIsoCodesAsync();

// get all URLs for all cultures
// in a HashSet, so de-duplicates too
Expand Down
4 changes: 2 additions & 2 deletions src/Umbraco.Core/Services/ContentEditingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus
{
// If no cultures are provided, we are asking to validate all cultures. But if the user doesn't have access to all, we
// should only validate the ones they do.
var allCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToList();
return allowedCultures.Count == allCultures.Count ? null : allowedCultures;
IEnumerable<string> allCultures = await _languageService.GetAllIsoCodesAsync();
return allowedCultures.Count == allCultures.Count() ? null : allowedCultures;
}

// If explicit cultures are provided, we should only validate the ones the user has access to.
Expand Down
4 changes: 2 additions & 2 deletions src/Umbraco.Core/Services/ContentPublishingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ public async Task<Attempt<ContentPublishingResult, ContentPublishingOperationSta
return Attempt.FailWithStatus(ContentPublishingOperationStatus.CannotPublishInvariantWhenVariant, new ContentPublishingResult());
}

IEnumerable<string> validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode);
IEnumerable<string> validCultures = await _languageService.GetAllIsoCodesAsync();
if (validCultures.ContainsAll(cultures) is false)
{
scope.Complete();
Expand Down Expand Up @@ -488,7 +488,7 @@ private async Task<Attempt<ContentPublishingOperationStatus>> UnpublishMultipleC
return Attempt.Fail(ContentPublishingOperationStatus.CannotPublishVariantWhenNotVariant);
}

var validCultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray();
var validCultures = (await _languageService.GetAllIsoCodesAsync()).ToArray();

foreach (var culture in cultures)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Umbraco.Core/Services/ContentValidationServiceBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentEditing.Validation;
Expand Down Expand Up @@ -137,7 +137,7 @@ public async Task<bool> ValidateCulturesAsync(ContentEditingModelBase contentEdi
return invalidCultures.IsCollectionEmpty();
}

private async Task<string[]> GetCultureCodes() => (await _languageService.GetAllAsync()).Select(language => language.IsoCode).ToArray();
private async Task<string[]> GetCultureCodes() => (await _languageService.GetAllIsoCodesAsync()).ToArray();

private IEnumerable<PropertyValidationError> ValidateProperty(IPropertyType propertyType, PropertyValueModel? propertyValueModel, PropertyValidationContext validationContext)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Services/DocumentUrlService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ public async Task<IEnumerable<UrlInfo>> ListUrlsAsync(Guid contentKey)
.Concat(_contentService.GetAncestors(documentIdAttempt.Result).Select(x => x.Key).Reverse());

IEnumerable<ILanguage> languages = await _languageService.GetAllAsync();
var cultures = languages.ToDictionary(x=>x.IsoCode);
var cultures = languages.ToDictionary(x => x.IsoCode);

Guid[] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid[] ?? ancestorsOrSelfKeys.ToArray();
Dictionary<Guid, Task<ILookup<string, Domain>>> ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray
Expand Down
30 changes: 18 additions & 12 deletions src/Umbraco.Core/Services/ILanguageService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Core.Services;
Expand Down Expand Up @@ -32,41 +32,47 @@ public interface ILanguageService
Task<string> GetDefaultIsoCodeAsync();

/// <summary>
/// Gets all available languages
/// Gets all available languages.
/// </summary>
/// <returns>An enumerable list of <see cref="ILanguage" /> objects</returns>
/// <returns>An enumerable list of <see cref="ILanguage" /> objects.</returns>
Task<IEnumerable<ILanguage>> GetAllAsync();

/// <summary>
/// Gets all languages with the given iso codes
/// Gets all available language ISO culture codes.
/// </summary>
/// <returns>An enumerable list of <see cref="ILanguage" /> objects.</returns>
async Task<IEnumerable<string>> GetAllIsoCodesAsync() => (await GetAllAsync()).Select(x => x.IsoCode);

/// <summary>
/// Gets all languages with the given ISO codes.
/// </summary>
/// <returns>An enumerable list of <see cref="ILanguage" /> objects</returns>
Task<IEnumerable<ILanguage>> GetMultipleAsync(IEnumerable<string> isoCodes);

/// <summary>
/// Updates an existing <see cref="ILanguage" /> object
/// Updates an existing <see cref="ILanguage" /> object.
/// </summary>
/// <param name="language"><see cref="ILanguage" /> to update</param>
/// <param name="userKey">Key of the user saving the language</param>
Task<Attempt<ILanguage, LanguageOperationStatus>> UpdateAsync(ILanguage language, Guid userKey);

/// <summary>
/// Creates a new <see cref="ILanguage" /> object
/// Creates a new <see cref="ILanguage" /> object.
/// </summary>
/// <param name="language"><see cref="ILanguage" /> to create</param>
/// <param name="userKey">Key of the user creating the language</param>
/// <param name="language"><see cref="ILanguage" /> to create.</param>
/// <param name="userKey">Key of the user creating the language.</param>
Task<Attempt<ILanguage, LanguageOperationStatus>> CreateAsync(ILanguage language, Guid userKey);

/// <summary>
/// Deletes a <see cref="ILanguage" /> by removing it and its usages from the db
/// Deletes a <see cref="ILanguage" /> by removing it and its usages from the database.
/// </summary>
/// <param name="isoCode">The ISO code of the <see cref="ILanguage" /> to delete</param>
/// <param name="userKey">Key of the user deleting the language</param>
/// <param name="isoCode">The ISO code of the <see cref="ILanguage" /> to delete.</param>
/// <param name="userKey">Key of the user deleting the language.</param>
Task<Attempt<ILanguage?, LanguageOperationStatus>> DeleteAsync(string isoCode, Guid userKey);


/// <summary>
/// Retrieves the isoCodes of configured languages by their Ids
/// Retrieves the ISO codes of configured languages by their Ids.
/// </summary>
/// <param name="ids">The ids of the configured <see cref="ILanguage" />s</param>
/// <returns>The ISO codes of the <see cref="ILanguage" />s</returns>
Expand Down
9 changes: 9 additions & 0 deletions src/Umbraco.Core/Services/LanguageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ public Task<IEnumerable<ILanguage>> GetAllAsync()
}
}

/// <inheritdoc />
public Task<IEnumerable<string>> GetAllIsoCodesAsync()
{
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
return Task.FromResult(_languageRepository.GetAllIsoCodes());
}
}

public Task<string[]> GetIsoCodesByIdsAsync(ICollection<int> ids)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete:true);
Expand Down
4 changes: 1 addition & 3 deletions src/Umbraco.Examine.Lucene/BackOfficeExamineSearcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,7 @@ private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List
// then nodeName will be matched normally with wildcards
// the rest will be normal without wildcards

var allLangs = _languageService.GetAllAsync().GetAwaiter().GetResult()
.Select(x => x.IsoCode.ToLowerInvariant())
.ToList();
var allLangs = _languageService.GetAllIsoCodesAsync().GetAwaiter().GetResult().Select(x => x.ToLowerInvariant()).ToList();

// the chars [*-_] in the query will mess everything up so let's remove those
// However we cannot just remove - and _ since these signify a space, so we instead replace them with that.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
Expand Down Expand Up @@ -90,6 +91,17 @@ public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger
return null;
}

/// <inheritdoc/>
public IEnumerable<string> GetAllIsoCodes()
{
var fields = new Expression<Func<LanguageDto, object?>>[]
{
x => x.IsoCode,
};
Sql<ISqlContext> sql = GetBaseQuery(false, fields);
return Database.Fetch<string>(sql);
}

// multi implementation of GetIsoCodeById
public string[] GetIsoCodesByIds(ICollection<int> ids, bool throwOnNotFound = true)
{
Expand Down Expand Up @@ -228,13 +240,17 @@ protected override IEnumerable<ILanguage> PerformGetByQuery(IQuery<ILanguage> qu

#region Overrides of EntityRepositoryBase<int,Language>

protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
protected override Sql<ISqlContext> GetBaseQuery(bool isCount) => GetBaseQuery(isCount, []);

private Sql<ISqlContext> GetBaseQuery(bool isCount, params Expression<Func<LanguageDto, object?>>[] fields)
{
Sql<ISqlContext> sql = Sql();

sql = isCount
? sql.SelectCount()
: sql.Select<LanguageDto>();
: fields.Length > 0
? sql.Select<LanguageDto>(fields)
: sql.Select<LanguageDto>();

sql.From<LanguageDto>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ private async Task<bool> BuildQueryAsync(StringBuilder sb, string query, string?
// then nodeName will be matched normally with wildcards
// the rest will be normal without wildcards

var allLanguages = (await _languageService.GetAllAsync()).Select(x => x.IsoCode.ToLowerInvariant()).ToList();
var allLanguages = (await _languageService.GetAllIsoCodesAsync()).Select(x => x.ToLowerInvariant()).ToList();

// the chars [*-_] in the query will mess everything up so let's remove those
// However we cannot just remove - and _ since these signify a space, so we instead replace them with that.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public async Task Can_Get_All_Languages()
Assert.That(languages.Count(), Is.EqualTo(3));
}

[Test]
public async Task Can_Get_All_Language_Iso_Codes()
{
var isoCodes = await LanguageService.GetAllIsoCodesAsync();
Assert.That(isoCodes.Count(), Is.EqualTo(3));
Assert.AreEqual("da-DK,en-GB,en-US", string.Join(",", isoCodes.OrderBy(x => x)));
}

[Test]
public async Task Can_GetLanguageByIsoCode()
{
Expand Down
Loading