#6793: Adding a content-independent culture selector shape for the front-end (#8784)

* Adds a new CultureSelector shape for front-end

* fixed query string culture change

* Moving NameValueCollectionExtensions from Orchard.DynamicForms and Orchard.Localization to Orchard.Framework

* Code styling

* Simplifying UserCultureSelectorController and removing the addition of the culture to the query string

* EOF empty lines and code styling

* Fixing that the main Orchard.Localization should depend on Orchard.Autoroute

* Code styling in LocalizationService

* Updating LocalizationService to not have to use IEnumerable.Single

* Matching culture name matching in LocalizationService culture- and casing-invariant

---------

Co-authored-by: Sergio Navarro <jersio@hotmail.com>
Co-authored-by: psp589 <pablosanchez589@gmail.com>
This commit is contained in:
Benedek Farkas 2024-04-18 23:35:48 +02:00 committed by GitHub
parent 0b86413e60
commit 15cad85d1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 226 additions and 55 deletions

View File

@ -1,12 +0,0 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Web;
namespace Orchard.DynamicForms.Helpers {
public static class NameValueCollectionExtensions {
public static string ToQueryString(this NameValueCollection nameValues) {
return String.Join("&", (from string name in nameValues select String.Concat(name, "=", HttpUtility.UrlEncode(nameValues[name]))).ToArray());
}
}
}

View File

@ -340,7 +340,6 @@
<Compile Include="Migrations.cs" /> <Compile Include="Migrations.cs" />
<Compile Include="Handlers\ReadFormValuesHandler.cs" /> <Compile Include="Handlers\ReadFormValuesHandler.cs" />
<Compile Include="Services\FormElementEventHandlerBase.cs" /> <Compile Include="Services\FormElementEventHandlerBase.cs" />
<Compile Include="Helpers\NameValueCollectionExtensions.cs" />
<Compile Include="Models\Submission.cs" /> <Compile Include="Models\Submission.cs" />
<Compile Include="Permissions.cs" /> <Compile Include="Permissions.cs" />
<Compile Include="Services\FormService.cs" /> <Compile Include="Services\FormService.cs" />

View File

@ -467,4 +467,4 @@ namespace Orchard.DynamicForms.Services {
return validatorElementType == elementType || validatorElementType.IsAssignableFrom(elementType); return validatorElementType == elementType || validatorElementType.IsAssignableFrom(elementType);
} }
} }
} }

View File

@ -0,0 +1,48 @@
using System;
using System.Web.Mvc;
using Orchard.Autoroute.Models;
using Orchard.CulturePicker.Services;
using Orchard.Environment.Extensions;
using Orchard.Localization.Providers;
using Orchard.Localization.Services;
using Orchard.Mvc.Extensions;
namespace Orchard.Localization.Controllers {
[OrchardFeature("Orchard.Localization.CultureSelector")]
public class UserCultureSelectorController : Controller {
private readonly ILocalizationService _localizationService;
private readonly ICultureStorageProvider _cultureStorageProvider;
public IOrchardServices Services { get; set; }
public UserCultureSelectorController(
IOrchardServices services,
ILocalizationService localizationService,
ICultureStorageProvider cultureStorageProvider) {
Services = services;
_localizationService = localizationService;
_cultureStorageProvider = cultureStorageProvider;
}
public ActionResult ChangeCulture(string culture) {
if (string.IsNullOrEmpty(culture)) {
throw new ArgumentNullException(culture);
}
var returnUrl = Utils.GetReturnUrl(Services.WorkContext.HttpContext.Request);
if (string.IsNullOrEmpty(returnUrl))
returnUrl = "";
if (_localizationService.TryGetRouteForUrl(returnUrl, out AutoroutePart currentRoutePart)
&& _localizationService.TryFindLocalizedRoute(currentRoutePart.ContentItem, culture, out AutoroutePart localizedRoutePart)) {
returnUrl = localizedRoutePart.Path;
}
_cultureStorageProvider.SetCulture(culture);
if (!returnUrl.StartsWith("~/")) {
returnUrl = "~/" + returnUrl;
}
return this.RedirectLocal(returnUrl);
}
}
}

View File

@ -9,7 +9,7 @@ Features:
Orchard.Localization: Orchard.Localization:
Description: Enables localization of content items. Description: Enables localization of content items.
Category: Content Category: Content
Dependencies: Settings Dependencies: Settings, Orchard.Autoroute
Name: Content Localization Name: Content Localization
Orchard.Localization.DateTimeFormat: Orchard.Localization.DateTimeFormat:
Description: Enables PO-based translation of date/time formats and names of days and months. Description: Enables PO-based translation of date/time formats and names of days and months.
@ -30,4 +30,4 @@ Features:
Description: Enables transliteration of the autoroute slug when creating a piece of content. Description: Enables transliteration of the autoroute slug when creating a piece of content.
Category: Content Category: Content
Name: URL Transliteration Name: URL Transliteration
Dependencies: Orchard.Localization.Transliteration, Orchard.Autoroute Dependencies: Orchard.Localization.Transliteration

View File

@ -89,10 +89,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AdminMenu.cs" /> <Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminCultureSelectorController.cs" />
<Compile Include="Extensions\Constants.cs" /> <Compile Include="Extensions\Constants.cs" />
<Compile Include="Controllers\AdminController.cs" /> <Compile Include="Controllers\AdminController.cs" />
<Compile Include="Controllers\TransliterationAdminController.cs" /> <Compile Include="Controllers\TransliterationAdminController.cs" />
<Compile Include="Controllers\AdminCultureSelectorController.cs" /> <Compile Include="Controllers\UserCultureSelectorController.cs" />
<Compile Include="Models\TransliterationSpecificationRecord.cs" /> <Compile Include="Models\TransliterationSpecificationRecord.cs" />
<Compile Include="Providers\ContentLocalizationTokens.cs" /> <Compile Include="Providers\ContentLocalizationTokens.cs" />
<Compile Include="Selectors\ContentCultureSelector.cs" /> <Compile Include="Selectors\ContentCultureSelector.cs" />
@ -118,6 +119,7 @@
<Compile Include="Services\LocalizationService.cs" /> <Compile Include="Services\LocalizationService.cs" />
<Compile Include="Services\TransliterationService.cs" /> <Compile Include="Services\TransliterationService.cs" />
<Compile Include="Events\TransliterationSlugEventHandler.cs" /> <Compile Include="Events\TransliterationSlugEventHandler.cs" />
<Compile Include="Services\Utils.cs" />
<Compile Include="ViewModels\ContentLocalizationsViewModel.cs" /> <Compile Include="ViewModels\ContentLocalizationsViewModel.cs" />
<Compile Include="ViewModels\EditLocalizationViewModel.cs" /> <Compile Include="ViewModels\EditLocalizationViewModel.cs" />
<Compile Include="ViewModels\CreateTransliterationViewModel.cs" /> <Compile Include="ViewModels\CreateTransliterationViewModel.cs" />
@ -196,6 +198,9 @@
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Views\UserCultureSelector.cshtml" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion> <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
@ -229,4 +234,4 @@
</FlavorProperties> </FlavorProperties>
</VisualStudio> </VisualStudio>
</ProjectExtensions> </ProjectExtensions>
</Project> </Project>

View File

@ -1,4 +1,3 @@
using System;
using System.Web; using System.Web;
using Orchard.Environment.Configuration; using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions; using Orchard.Environment.Extensions;
@ -19,7 +18,8 @@ namespace Orchard.Localization.Selectors {
private const string AdminCookieName = "OrchardCurrentCulture-Admin"; private const string AdminCookieName = "OrchardCurrentCulture-Admin";
private const int DefaultExpireTimeYear = 1; private const int DefaultExpireTimeYear = 1;
public CookieCultureSelector(IHttpContextAccessor httpContextAccessor, public CookieCultureSelector(
IHttpContextAccessor httpContextAccessor,
IClock clock, IClock clock,
ShellSettings shellSettings) { ShellSettings shellSettings) {
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
@ -36,11 +36,10 @@ namespace Orchard.Localization.Selectors {
var cookie = new HttpCookie(cookieName, culture) { var cookie = new HttpCookie(cookieName, culture) {
Expires = _clock.UtcNow.AddYears(DefaultExpireTimeYear), Expires = _clock.UtcNow.AddYears(DefaultExpireTimeYear),
Domain = httpContext.Request.IsLocal ? null : httpContext.Request.Url.Host
}; };
cookie.Domain = !httpContext.Request.IsLocal ? httpContext.Request.Url.Host : null; if (!string.IsNullOrEmpty(_shellSettings.RequestUrlPrefix)) {
if (!String.IsNullOrEmpty(_shellSettings.RequestUrlPrefix)) {
cookie.Path = GetCookiePath(httpContext); cookie.Path = GetCookiePath(httpContext);
} }
@ -73,4 +72,4 @@ namespace Orchard.Localization.Selectors {
return cookiePath; return cookiePath;
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Orchard.Autoroute.Models;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.Localization.Models; using Orchard.Localization.Models;
@ -10,5 +11,7 @@ namespace Orchard.Localization.Services {
void SetContentCulture(IContent content, string culture); void SetContentCulture(IContent content, string culture);
IEnumerable<LocalizationPart> GetLocalizations(IContent content); IEnumerable<LocalizationPart> GetLocalizations(IContent content);
IEnumerable<LocalizationPart> GetLocalizations(IContent content, VersionOptions versionOptions); IEnumerable<LocalizationPart> GetLocalizations(IContent content, VersionOptions versionOptions);
bool TryFindLocalizedRoute(ContentItem routableContent, string cultureName, out AutoroutePart localizedRoute);
bool TryGetRouteForUrl(string url, out AutoroutePart route);
} }
} }

View File

@ -1,53 +1,59 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Orchard.Autoroute.Models;
using Orchard.Autoroute.Services;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.ContentManagement.Aspects;
using Orchard.Localization.Models; using Orchard.Localization.Models;
namespace Orchard.Localization.Services { namespace Orchard.Localization.Services {
public class LocalizationService : ILocalizationService { public class LocalizationService : ILocalizationService {
private readonly IContentManager _contentManager; private readonly IContentManager _contentManager;
private readonly ICultureManager _cultureManager; private readonly ICultureManager _cultureManager;
private readonly IHomeAliasService _homeAliasService;
public LocalizationService(IContentManager contentManager, ICultureManager cultureManager, IHomeAliasService homeAliasService) {
public LocalizationService(IContentManager contentManager, ICultureManager cultureManager) {
_contentManager = contentManager; _contentManager = contentManager;
_cultureManager = cultureManager; _cultureManager = cultureManager;
_homeAliasService = homeAliasService;
} }
/// <summary>
/// Warning: Returns only the first item of same culture localizations.
/// </summary>
public LocalizationPart GetLocalizedContentItem(IContent content, string culture) =>
GetLocalizedContentItem(content, culture, null);
public LocalizationPart GetLocalizedContentItem(IContent content, string culture) { /// <summary>
// Warning: Returns only the first of same culture localizations. /// Warning: Returns only the first item of same culture localizations.
return GetLocalizedContentItem(content, culture, null); /// </summary>
}
public LocalizationPart GetLocalizedContentItem(IContent content, string culture, VersionOptions versionOptions) { public LocalizationPart GetLocalizedContentItem(IContent content, string culture, VersionOptions versionOptions) {
var cultureRecord = _cultureManager.GetCultureByName(culture); var cultureRecord = _cultureManager.GetCultureByName(culture);
if (cultureRecord == null) return null; if (cultureRecord == null) {
return null;
}
var localized = content.As<LocalizationPart>(); var localized = content.As<LocalizationPart>();
if (localized == null) return null; if (localized == null) {
return null;
}
var masterContentItemId = localized.HasTranslationGroup ? localized.Record.MasterContentItemId : localized.Id; var masterContentItemId = localized.HasTranslationGroup ? localized.Record.MasterContentItemId : localized.Id;
// Warning: Returns only the first of same culture localizations.
return _contentManager return _contentManager
.Query<LocalizationPart>(versionOptions, content.ContentItem.ContentType) .Query<LocalizationPart>(versionOptions, content.ContentItem.ContentType)
.Where<LocalizationPartRecord>(l => .Where<LocalizationPartRecord>(localization =>
(l.Id == masterContentItemId || l.MasterContentItemId == masterContentItemId) && (localization.Id == masterContentItemId || localization.MasterContentItemId == masterContentItemId)
l.CultureId == cultureRecord.Id) && localization.CultureId == cultureRecord.Id)
.Slice(1) .Slice(1)
.FirstOrDefault(); .FirstOrDefault();
} }
public string GetContentCulture(IContent content) { public string GetContentCulture(IContent content) =>
var localized = content.As<LocalizationPart>(); content.As<LocalizationPart>()?.Culture?.Culture ?? _cultureManager.GetSiteCulture();
return localized?.Culture == null ?
_cultureManager.GetSiteCulture() :
localized.Culture.Culture;
}
public void SetContentCulture(IContent content, string culture) { public void SetContentCulture(IContent content, string culture) {
var localized = content.As<LocalizationPart>(); var localized = content.As<LocalizationPart>();
@ -57,11 +63,14 @@ namespace Orchard.Localization.Services {
localized.Culture = _cultureManager.GetCultureByName(culture); localized.Culture = _cultureManager.GetCultureByName(culture);
} }
public IEnumerable<LocalizationPart> GetLocalizations(IContent content) { /// <summary>
// Warning: May contain more than one localization of the same culture. /// Warning: May contain more than one localization of the same culture.
return GetLocalizations(content, null); /// </summary>
} public IEnumerable<LocalizationPart> GetLocalizations(IContent content) => GetLocalizations(content, null);
/// <summary>
/// Warning: May contain more than one localization of the same culture.
/// </summary>
public IEnumerable<LocalizationPart> GetLocalizations(IContent content, VersionOptions versionOptions) { public IEnumerable<LocalizationPart> GetLocalizations(IContent content, VersionOptions versionOptions) {
if (content.ContentItem.Id == 0) return Enumerable.Empty<LocalizationPart>(); if (content.ContentItem.Id == 0) return Enumerable.Empty<LocalizationPart>();
@ -76,16 +85,58 @@ namespace Orchard.Localization.Services {
if (localized.HasTranslationGroup) { if (localized.HasTranslationGroup) {
int masterContentItemId = localized.MasterContentItem.ContentItem.Id; int masterContentItemId = localized.MasterContentItem.ContentItem.Id;
query = query.Where<LocalizationPartRecord>(l => query = query.Where<LocalizationPartRecord>(localization =>
l.Id != contentItemId && // Exclude the content localization.Id != contentItemId && // Exclude the content
(l.Id == masterContentItemId || l.MasterContentItemId == masterContentItemId)); (localization.Id == masterContentItemId || localization.MasterContentItemId == masterContentItemId));
} }
else { else {
query = query.Where<LocalizationPartRecord>(l => l.MasterContentItemId == contentItemId); query = query.Where<LocalizationPartRecord>(localization => localization.MasterContentItemId == contentItemId);
} }
// Warning: May contain more than one localization of the same culture.
return query.List().ToList(); return query.List().ToList();
} }
public bool TryGetRouteForUrl(string url, out AutoroutePart route) {
route = _contentManager.Query<AutoroutePart, AutoroutePartRecord>()
.ForVersion(VersionOptions.Published)
.Where(r => r.DisplayAlias == url)
.List()
.FirstOrDefault();
route = route ?? _homeAliasService.GetHomePage(VersionOptions.Latest).As<AutoroutePart>();
return route != null;
}
public bool TryFindLocalizedRoute(ContentItem routableContent, string cultureName, out AutoroutePart localizedRoute) {
if (!routableContent.Parts.Any(p => p.Is<ILocalizableAspect>())) {
localizedRoute = null;
return false;
}
IEnumerable<LocalizationPart> localizations = GetLocalizations(routableContent, VersionOptions.Published);
ILocalizableAspect localizationPart = null, siteCultureLocalizationPart = null;
foreach (var localization in localizations) {
if (localization.Culture.Culture.Equals(cultureName, StringComparison.InvariantCultureIgnoreCase)) {
localizationPart = localization;
break;
}
if (localization.Culture == null && siteCultureLocalizationPart == null) {
siteCultureLocalizationPart = localization;
}
}
if (localizationPart == null) {
localizationPart = siteCultureLocalizationPart;
}
localizedRoute = localizationPart?.As<AutoroutePart>();
return localizedRoute != null;
}
} }
} }

View File

@ -0,0 +1,31 @@
using System.Web;
namespace Orchard.CulturePicker.Services {
public static class Utils {
public static string GetReturnUrl(HttpRequestBase request) {
if (request.UrlReferrer == null) {
return "";
}
string localUrl = GetAppRelativePath(request.UrlReferrer.AbsolutePath, request);
return HttpUtility.UrlDecode(localUrl);
}
public static string GetAppRelativePath(string logicalPath, HttpRequestBase request) {
if (request.ApplicationPath == null) {
return "";
}
logicalPath = logicalPath.ToLower();
string appPath = request.ApplicationPath.ToLower();
if (appPath != "/") {
appPath += "/";
}
else {
return logicalPath.Substring(1);
}
return logicalPath.Replace(appPath, "");
}
}
}

View File

@ -0,0 +1,34 @@
@using Orchard.Localization.Services
@{
var currentCulture = WorkContext.CurrentCulture;
var supportedCultures = WorkContext.Resolve<ICultureManager>().ListCultures().ToList();
}
<div id="culture-selection">
<ul>
@foreach (var supportedCulture in supportedCultures)
{
var url = Url.Action(
"ChangeCulture",
"UserCultureSelector",
new
{
area = "Orchard.Localization",
culture = supportedCulture,
returnUrl = Html.ViewContext.HttpContext.Request.RawUrl
});
<li>
@if (supportedCulture.Equals(currentCulture))
{
<a href="@url">@T("{0} (current)", supportedCulture)</a>
}
else
{
<a href="@url">@supportedCulture</a>
}
</li>
}
</ul>
</div>

View File

@ -685,6 +685,7 @@
<Compile Include="Messaging\Services\IMessageManager.cs" /> <Compile Include="Messaging\Services\IMessageManager.cs" />
<Compile Include="Messaging\Services\IMessagingChannel.cs" /> <Compile Include="Messaging\Services\IMessagingChannel.cs" />
<Compile Include="IWorkContextAccessor.cs" /> <Compile Include="IWorkContextAccessor.cs" />
<Compile Include="Utility\Extensions\NameValueCollectionExtensions.cs" />
<Compile Include="Utility\Extensions\VirtualPathProviderExtensions.cs" /> <Compile Include="Utility\Extensions\VirtualPathProviderExtensions.cs" />
<Compile Include="Utility\NamedReaderWriterLock.cs" /> <Compile Include="Utility\NamedReaderWriterLock.cs" />
<Compile Include="Utility\ReflectionHelper.cs" /> <Compile Include="Utility\ReflectionHelper.cs" />

View File

@ -0,0 +1,12 @@
using System.Collections.Specialized;
using System.Linq;
using System.Web;
namespace Orchard.Utility.Extensions {
public static class NameValueCollectionExtensions {
public static string ToQueryString(this NameValueCollection nameValues) =>
string.Join(
"&",
(from string name in nameValues select string.Concat(name, "=", HttpUtility.UrlEncode(nameValues[name]))).ToArray());
}
}