From 15cad85d1e0008d01bea84c68736a4e0324c7627 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Thu, 18 Apr 2024 23:35:48 +0200 Subject: [PATCH] #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 Co-authored-by: psp589 --- .../Helpers/NameValueCollectionExtensions.cs | 12 -- .../Orchard.DynamicForms.csproj | 1 - .../Services/FormService.cs | 2 +- .../UserCultureSelectorController.cs | 48 ++++++++ .../Modules/Orchard.Localization/Module.txt | 4 +- .../Orchard.Localization.csproj | 9 +- .../Selectors/CookieCultureSelector.cs | 11 +- .../Services/ILocalizationService.cs | 3 + .../Services/LocalizationService.cs | 113 +++++++++++++----- .../Orchard.Localization/Services/Utils.cs | 31 +++++ .../Views/UserCultureSelector.cshtml | 34 ++++++ src/Orchard/Orchard.Framework.csproj | 1 + .../NameValueCollectionExtensions.cs | 12 ++ 13 files changed, 226 insertions(+), 55 deletions(-) delete mode 100644 src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Localization/Controllers/UserCultureSelectorController.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Localization/Services/Utils.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Localization/Views/UserCultureSelector.cshtml create mode 100644 src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs diff --git a/src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs b/src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs deleted file mode 100644 index cdd14b43d54..00000000000 --- a/src/Orchard.Web/Modules/Orchard.DynamicForms/Helpers/NameValueCollectionExtensions.cs +++ /dev/null @@ -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()); - } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj b/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj index 3954c9a070b..1b17c9c0718 100644 --- a/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj +++ b/src/Orchard.Web/Modules/Orchard.DynamicForms/Orchard.DynamicForms.csproj @@ -340,7 +340,6 @@ - diff --git a/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs b/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs index 7c602486905..603f5099d7e 100644 --- a/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs +++ b/src/Orchard.Web/Modules/Orchard.DynamicForms/Services/FormService.cs @@ -467,4 +467,4 @@ private static bool IsFormElementType(IElementValidator validator, Type elementT return validatorElementType == elementType || validatorElementType.IsAssignableFrom(elementType); } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Controllers/UserCultureSelectorController.cs b/src/Orchard.Web/Modules/Orchard.Localization/Controllers/UserCultureSelectorController.cs new file mode 100644 index 00000000000..2a4543c289b --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Localization/Controllers/UserCultureSelectorController.cs @@ -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); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Module.txt b/src/Orchard.Web/Modules/Orchard.Localization/Module.txt index f9fd36172ca..26cf3fe4285 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Localization/Module.txt @@ -9,7 +9,7 @@ Features: Orchard.Localization: Description: Enables localization of content items. Category: Content - Dependencies: Settings + Dependencies: Settings, Orchard.Autoroute Name: Content Localization Orchard.Localization.DateTimeFormat: 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. Category: Content Name: URL Transliteration - Dependencies: Orchard.Localization.Transliteration, Orchard.Autoroute + Dependencies: Orchard.Localization.Transliteration diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj b/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj index 3718e30d9ed..771a933d610 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj +++ b/src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj @@ -89,10 +89,11 @@ + - + @@ -118,6 +119,7 @@ + @@ -196,6 +198,9 @@ + + + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) @@ -229,4 +234,4 @@ - + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Selectors/CookieCultureSelector.cs b/src/Orchard.Web/Modules/Orchard.Localization/Selectors/CookieCultureSelector.cs index d39abbb5b67..00c82464f87 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Selectors/CookieCultureSelector.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Selectors/CookieCultureSelector.cs @@ -1,4 +1,3 @@ -using System; using System.Web; using Orchard.Environment.Configuration; using Orchard.Environment.Extensions; @@ -19,7 +18,8 @@ public class CookieCultureSelector : ICultureSelector, ICultureStorageProvider { private const string AdminCookieName = "OrchardCurrentCulture-Admin"; private const int DefaultExpireTimeYear = 1; - public CookieCultureSelector(IHttpContextAccessor httpContextAccessor, + public CookieCultureSelector( + IHttpContextAccessor httpContextAccessor, IClock clock, ShellSettings shellSettings) { _httpContextAccessor = httpContextAccessor; @@ -36,11 +36,10 @@ public void SetCulture(string culture) { var cookie = new HttpCookie(cookieName, culture) { 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); } @@ -73,4 +72,4 @@ private string GetCookiePath(HttpContextBase httpContext) { return cookiePath; } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/ILocalizationService.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/ILocalizationService.cs index f2ace3a9cb5..04cda8d60b6 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Services/ILocalizationService.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/ILocalizationService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Orchard.Autoroute.Models; using Orchard.ContentManagement; using Orchard.Localization.Models; @@ -10,5 +11,7 @@ public interface ILocalizationService : IDependency { void SetContentCulture(IContent content, string culture); IEnumerable GetLocalizations(IContent content); IEnumerable GetLocalizations(IContent content, VersionOptions versionOptions); + bool TryFindLocalizedRoute(ContentItem routableContent, string cultureName, out AutoroutePart localizedRoute); + bool TryGetRouteForUrl(string url, out AutoroutePart route); } } diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs index e8baceda04d..97bcb0ae80b 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs @@ -1,53 +1,59 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using Orchard.Autoroute.Models; +using Orchard.Autoroute.Services; using Orchard.ContentManagement; +using Orchard.ContentManagement.Aspects; using Orchard.Localization.Models; namespace Orchard.Localization.Services { public class LocalizationService : ILocalizationService { private readonly IContentManager _contentManager; private readonly ICultureManager _cultureManager; + private readonly IHomeAliasService _homeAliasService; - - public LocalizationService(IContentManager contentManager, ICultureManager cultureManager) { + public LocalizationService(IContentManager contentManager, ICultureManager cultureManager, IHomeAliasService homeAliasService) { _contentManager = contentManager; _cultureManager = cultureManager; + _homeAliasService = homeAliasService; } + /// + /// Warning: Returns only the first item of same culture localizations. + /// + public LocalizationPart GetLocalizedContentItem(IContent content, string culture) => + GetLocalizedContentItem(content, culture, null); - public LocalizationPart GetLocalizedContentItem(IContent content, string culture) { - // Warning: Returns only the first of same culture localizations. - return GetLocalizedContentItem(content, culture, null); - } - + /// + /// Warning: Returns only the first item of same culture localizations. + /// public LocalizationPart GetLocalizedContentItem(IContent content, string culture, VersionOptions versionOptions) { var cultureRecord = _cultureManager.GetCultureByName(culture); - if (cultureRecord == null) return null; + if (cultureRecord == null) { + return null; + } var localized = content.As(); - if (localized == null) return null; + if (localized == null) { + return null; + } var masterContentItemId = localized.HasTranslationGroup ? localized.Record.MasterContentItemId : localized.Id; - // Warning: Returns only the first of same culture localizations. return _contentManager .Query(versionOptions, content.ContentItem.ContentType) - .Where(l => - (l.Id == masterContentItemId || l.MasterContentItemId == masterContentItemId) && - l.CultureId == cultureRecord.Id) + .Where(localization => + (localization.Id == masterContentItemId || localization.MasterContentItemId == masterContentItemId) + && localization.CultureId == cultureRecord.Id) .Slice(1) .FirstOrDefault(); } - public string GetContentCulture(IContent content) { - var localized = content.As(); - - return localized?.Culture == null ? - _cultureManager.GetSiteCulture() : - localized.Culture.Culture; - } + public string GetContentCulture(IContent content) => + content.As()?.Culture?.Culture ?? _cultureManager.GetSiteCulture(); public void SetContentCulture(IContent content, string culture) { var localized = content.As(); @@ -57,11 +63,14 @@ public void SetContentCulture(IContent content, string culture) { localized.Culture = _cultureManager.GetCultureByName(culture); } - public IEnumerable GetLocalizations(IContent content) { - // Warning: May contain more than one localization of the same culture. - return GetLocalizations(content, null); - } + /// + /// Warning: May contain more than one localization of the same culture. + /// + public IEnumerable GetLocalizations(IContent content) => GetLocalizations(content, null); + /// + /// Warning: May contain more than one localization of the same culture. + /// public IEnumerable GetLocalizations(IContent content, VersionOptions versionOptions) { if (content.ContentItem.Id == 0) return Enumerable.Empty(); @@ -76,16 +85,58 @@ public IEnumerable GetLocalizations(IContent content, VersionO if (localized.HasTranslationGroup) { int masterContentItemId = localized.MasterContentItem.ContentItem.Id; - query = query.Where(l => - l.Id != contentItemId && // Exclude the content - (l.Id == masterContentItemId || l.MasterContentItemId == masterContentItemId)); + query = query.Where(localization => + localization.Id != contentItemId && // Exclude the content + (localization.Id == masterContentItemId || localization.MasterContentItemId == masterContentItemId)); } else { - query = query.Where(l => l.MasterContentItemId == contentItemId); + query = query.Where(localization => localization.MasterContentItemId == contentItemId); } - // Warning: May contain more than one localization of the same culture. return query.List().ToList(); } + + public bool TryGetRouteForUrl(string url, out AutoroutePart route) { + route = _contentManager.Query() + .ForVersion(VersionOptions.Published) + .Where(r => r.DisplayAlias == url) + .List() + .FirstOrDefault(); + + route = route ?? _homeAliasService.GetHomePage(VersionOptions.Latest).As(); + + return route != null; + } + + public bool TryFindLocalizedRoute(ContentItem routableContent, string cultureName, out AutoroutePart localizedRoute) { + if (!routableContent.Parts.Any(p => p.Is())) { + localizedRoute = null; + + return false; + } + + IEnumerable 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(); + + return localizedRoute != null; + } } -} \ No newline at end of file +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/Utils.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/Utils.cs new file mode 100644 index 00000000000..c69c4e5df33 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/Utils.cs @@ -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, ""); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Views/UserCultureSelector.cshtml b/src/Orchard.Web/Modules/Orchard.Localization/Views/UserCultureSelector.cshtml new file mode 100644 index 00000000000..71f4f80844e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Localization/Views/UserCultureSelector.cshtml @@ -0,0 +1,34 @@ +@using Orchard.Localization.Services + +@{ + var currentCulture = WorkContext.CurrentCulture; + var supportedCultures = WorkContext.Resolve().ListCultures().ToList(); +} + +
+
    + @foreach (var supportedCulture in supportedCultures) + { + var url = Url.Action( + "ChangeCulture", + "UserCultureSelector", + new + { + area = "Orchard.Localization", + culture = supportedCulture, + returnUrl = Html.ViewContext.HttpContext.Request.RawUrl + }); + +
  • + @if (supportedCulture.Equals(currentCulture)) + { + @T("{0} (current)", supportedCulture) + } + else + { + @supportedCulture + } +
  • + } +
+
diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 57312d77f90..56e9fbc43bf 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -685,6 +685,7 @@ + diff --git a/src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs b/src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs new file mode 100644 index 00000000000..f4606979042 --- /dev/null +++ b/src/Orchard/Utility/Extensions/NameValueCollectionExtensions.cs @@ -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()); + } +}