Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content Culture Picker #3813

Merged
merged 17 commits into from Jul 4, 2019
@@ -4,11 +4,11 @@
using Fluid;
using OrchardCore.Autoroute.Model;
using OrchardCore.Autoroute.Models;
using OrchardCore.Autoroute.Services;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Handlers;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.ContentManagement.Records;
using OrchardCore.ContentManagement.Routing;
using OrchardCore.Environment.Cache;
using OrchardCore.Liquid;
using OrchardCore.Settings;
@@ -1,8 +1,8 @@
using System.Threading.Tasks;
using Fluid;
using Fluid.Values;
using OrchardCore.Autoroute.Services;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Routing;
using OrchardCore.Liquid;

namespace OrchardCore.Autoroute.Liquid
@@ -5,16 +5,16 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Autoroute.Services;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Routing;

namespace OrchardCore.Autoroute.Routing
{
public class AutorouteRoute : IRouter
{
private readonly IAutorouteEntries _entries;
private readonly IRouter _target;
private static HashSet<string> _keys = new HashSet<string>(new[] { "area", "controller", "action", "contentItemId" }, StringComparer.OrdinalIgnoreCase);
private static HashSet<string> _keys = new HashSet<string>(new[] { "area", "controller", "action", "contentItemId" }, StringComparer.OrdinalIgnoreCase);

public AutorouteRoute(IAutorouteEntries entries, IRouter target)
{
@@ -32,9 +32,9 @@ public VirtualPathData GetVirtualPath(VirtualPathContext context)
}

var displayRouteData = GetContentItemDisplayRoutes(context.HttpContext, contentItemId).Result;
if (string.Equals(context.Values["area"]?.ToString(), displayRouteData?["area"]?.ToString(), StringComparison.OrdinalIgnoreCase)
&& string.Equals(context.Values["controller"]?.ToString(), displayRouteData?["controller"]?.ToString(), StringComparison.OrdinalIgnoreCase)

if (string.Equals(context.Values["area"]?.ToString(), displayRouteData?["area"]?.ToString(), StringComparison.OrdinalIgnoreCase)
&& string.Equals(context.Values["controller"]?.ToString(), displayRouteData?["controller"]?.ToString(), StringComparison.OrdinalIgnoreCase)
&& string.Equals(context.Values["action"]?.ToString(), displayRouteData?["action"]?.ToString(), StringComparison.OrdinalIgnoreCase))
{
if (_entries.TryGetPath(contentItemId, out string path))
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Routing;

namespace OrchardCore.Autoroute.Services
{
@@ -1,6 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using OrchardCore.Autoroute.Model;
using OrchardCore.ContentManagement.Routing;

namespace OrchardCore.Autoroute.Services
{
@@ -31,7 +31,7 @@ public void AddEntries(IEnumerable<AutorouteEntry> entries)
{
foreach (var entry in entries)
{
var requestPath = "/" + entry.Path;
var requestPath = "/" + entry.Path.TrimStart('/');
_paths[entry.ContentItemId] = requestPath;
_contentItemIds[requestPath] = entry.ContentItemId;
}
@@ -15,10 +15,10 @@
using OrchardCore.Autoroute.ViewModels;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentManagement.GraphQL;
using OrchardCore.ContentManagement.GraphQL.Options;
using OrchardCore.ContentManagement.Handlers;
using OrchardCore.ContentManagement.Records;
using OrchardCore.ContentManagement.Routing;
using OrchardCore.ContentTypes.Editors;
using OrchardCore.Data.Migration;
using OrchardCore.Indexing;
@@ -0,0 +1,33 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.ContentLocalization.Services;

namespace OrchardCore.ContentLocalization
{
/// <summary>
/// RequestCultureProvider that automatically sets the Culture of a request from the LocalizationPart.Culture property.
/// </summary>
public class ContentRequestCultureProvider : RequestCultureProvider
{
public override async Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}

var culturePickerService = httpContext.RequestServices.GetService<IContentCulturePickerService>();
var localization = await culturePickerService.GetLocalizationFromRouteAsync(httpContext.Request.Path);

if (localization != null)
{
return new ProviderCultureResult(localization.Culture);
}

return null;
}
}
}
@@ -39,6 +39,9 @@ public class AdminController : Controller, IUpdateModel
[HttpPost]
public async Task<IActionResult> Localize(string contentItemId, string targetCulture)
{
// Invariant culture name is empty so a null value is bound.
targetCulture = targetCulture ?? "";

var contentItem = await _contentManager.GetAsync(contentItemId, VersionOptions.Latest);

if (contentItem == null)
@@ -0,0 +1,77 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using OrchardCore.ContentLocalization.Services;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.Localization;
using OrchardCore.Modules;

namespace OrchardCore.ContentLocalization.Controllers
{
[Feature("OrchardCore.ContentLocalization.ContentCulturePicker")]
public class ContentCulturePickerController : Controller, IUpdateModel
{
private readonly ILocalizationService _locationService;
private readonly IContentCulturePickerService _culturePickerService;

public IHtmlLocalizer T { get; }

public ContentCulturePickerController(
IHtmlLocalizer<AdminController> localizer,
ILocalizationService locationService,
IContentCulturePickerService culturePickerService)
{
T = localizer;
_locationService = locationService;
_culturePickerService = culturePickerService;
}

[HttpGet]
public async Task<IActionResult> RedirectToLocalizedContent(string targetCulture, PathString contentItemUrl)
{
// Invariant culture name is empty so a null value is bound.
targetCulture = targetCulture ?? "";

if (!contentItemUrl.HasValue)
{
contentItemUrl = "/";
}

var supportedCultures = await _locationService.GetSupportedCulturesAsync();

if (!supportedCultures.Any(t => t == targetCulture))
{
return LocalRedirect('~' + contentItemUrl);
}

// Redirect the user to the Content with the same localizationSet as the ContentItem of the current url
var localizations = await _culturePickerService.GetLocalizationsFromRouteAsync(contentItemUrl);
if (localizations.Any())
{
var localization = localizations.SingleOrDefault(l => String.Equals(l.Culture, targetCulture, StringComparison.OrdinalIgnoreCase));

if (localization != null)
{
return LocalRedirect(Url.Action("Display", "Item", new { Area = "OrchardCore.Contents", contentItemId = localization.ContentItemId }));
}
}

// Try to get the Homepage url for the culture
var homeLocalizations = await _culturePickerService.GetLocalizationsFromRouteAsync("/");
if (homeLocalizations.Any())
{
var localization = homeLocalizations.SingleOrDefault(h => String.Equals(h.Culture, targetCulture, StringComparison.OrdinalIgnoreCase));

if (localization != null)
{
return LocalRedirect(Url.Action("Display", "Item", new { Area = "OrchardCore.Contents", contentItemId = localization.ContentItemId }));
}
}

return NotFound();
}
}
}
@@ -60,20 +60,16 @@ public Task<IEnumerable<ContentItem>> GetItemsForSet(string localizationSet)

public async Task<ContentItem> LocalizeAsync(ContentItem content, string targetCulture)
{
var localizationPart = content.As<LocalizationPart>();

var supportedCultures = await _localizationService.GetSupportedCulturesAsync();

// not sure if this is redundant or not. The check is also done in the Admin controller
if (!supportedCultures.Any(c => String.Equals(c, targetCulture, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException("Cannot localize an unsupported culture");
}

var localizationPart = content.As<LocalizationPart>();
if (String.IsNullOrEmpty(localizationPart.LocalizationSet))
{
// If the source content item is not yet localized, define its defaults

localizationPart.LocalizationSet = _iidGenerator.GenerateUniqueId();
localizationPart.Culture = await _localizationService.GetDefaultCultureAsync();
_session.Save(content);
@@ -48,7 +48,10 @@ public override async Task<IDisplayResult> UpdateAsync(LocalizationPart model, I
{
var viewModel = new LocalizationPartViewModel();
await updater.TryUpdateModelAsync(viewModel, Prefix, t => t.Culture);
model.Culture = viewModel.Culture;

// Invariant culture name is empty so a null value is bound.
model.Culture = viewModel.Culture ?? "";

// Need to do this here to support displaying the message to save before localizing when the item has not been saved yet.
if (String.IsNullOrEmpty(model.LocalizationSet))
{
@@ -65,7 +68,8 @@ public async Task BuildViewModelAsync(LocalizationPartViewModel model, Localizat
model.LocalizationSet = localizationPart.LocalizationSet;
model.LocalizationPart = localizationPart;

if (String.IsNullOrEmpty(model.Culture))
// Invariant culture name is empty so we only do a null check.
if (model.Culture == null)
{
model.Culture = await _localizationService.GetDefaultCultureAsync();
}
@@ -0,0 +1,57 @@
using System;
using System.Threading.Tasks;
using OrchardCore.ContentLocalization.Models;
using OrchardCore.ContentLocalization.Services;
using OrchardCore.ContentManagement.Handlers;

namespace OrchardCore.ContentLocalization.Handlers
{
public class LocalizationPartHandler : ContentPartHandler<LocalizationPart>
{
private readonly ILocalizationEntries _entries;

public LocalizationPartHandler(ILocalizationEntries entries)
{
_entries = entries;
}

public override Task PublishedAsync(PublishContentContext context, LocalizationPart part)
{
if (!String.IsNullOrWhiteSpace(part.LocalizationSet))
{
_entries.AddEntry(new LocalizationEntry()
{
ContentItemId = part.ContentItem.ContentItemId,
LocalizationSet = part.LocalizationSet,
Culture = part.Culture.ToLowerInvariant()
});
}

return Task.CompletedTask;
}

public override Task UnpublishedAsync(PublishContentContext context, LocalizationPart part)
{
_entries.RemoveEntry(new LocalizationEntry()
{
ContentItemId = part.ContentItem.ContentItemId,
LocalizationSet = part.LocalizationSet,
Culture = part.Culture.ToLowerInvariant()
});

return Task.CompletedTask;
}

public override Task RemovedAsync(RemoveContentContext context, LocalizationPart part)
{
_entries.RemoveEntry(new LocalizationEntry()
{
ContentItemId = part.ContentItem.ContentItemId,
LocalizationSet = part.LocalizationSet,
Culture = part.Culture.ToLowerInvariant()
});

return Task.CompletedTask;
}
}
}
@@ -13,7 +13,7 @@ public override Task BuildIndexAsync(LocalizationPart part, BuildPartIndexContex
foreach (var key in context.Keys)
{
context.DocumentIndex.Set(key + ".LocalizationSet", part.LocalizationSet, options);
context.DocumentIndex.Set(key + ".Culture", part.Culture.ToLowerInvariant(), options);
context.DocumentIndex.Set(key + ".Culture", part.Culture?.ToLowerInvariant(), options);
}

return Task.CompletedTask;
@@ -6,6 +6,22 @@
Website = "http://orchardproject.net",
Version = "1.0.0",
Description = "Provides a part that allows to localize content items.",
Category = "Internationalization"
)]

[assembly: Feature(
Id = "OrchardCore.ContentLocalization",
Name = "Content Localization",
Description = "Provides a part that allows to localize content items.",
Dependencies = new[] { "OrchardCore.ContentTypes", "OrchardCore.Localization" },
Category = "Internationalization"
)]


[assembly: Feature(
Id = "OrchardCore.ContentLocalization.ContentCulturePicker",
Name = "Content Culture Picker",
Description = "Provides a culture picker shape for the frontend.",
Dependencies = new[] { "OrchardCore.ContentLocalization" },
Category = "Internationalization"
)]
@@ -0,0 +1,9 @@
namespace OrchardCore.ContentLocalization.Models
{
public class LocalizationEntry
{
public string ContentItemId;
public string LocalizationSet;
public string Culture;
}
}
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@@ -7,27 +7,24 @@
<ItemGroup>
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Apis.GraphQL.Abstractions\OrchardCore.Apis.GraphQL.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentLocalization.Abstractions\OrchardCore.ContentLocalization.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentManagement.Abstractions\OrchardCore.ContentManagement.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentManagement.GraphQL\OrchardCore.ContentManagement.GraphQL.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Contents.Core\OrchardCore.Contents.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Infrastructure\OrchardCore.Infrastructure.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Liquid.Abstractions\OrchardCore.Liquid.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentManagement.Abstractions\OrchardCore.ContentManagement.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ContentTypes.Abstractions\OrchardCore.ContentTypes.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Data.Abstractions\OrchardCore.Data.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.DisplayManagement\OrchardCore.DisplayManagement.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Indexing.Abstractions\OrchardCore.Indexing.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Data.Abstractions\OrchardCore.Data.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Infrastructure\OrchardCore.Infrastructure.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Liquid.Abstractions\OrchardCore.Liquid.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Module.Targets\OrchardCore.Module.Targets.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.ResourceManagement\OrchardCore.ResourceManagement.csproj" />

<ProjectReference Include="..\OrchardCore.Localization\OrchardCore.Localization.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>

<ItemGroup>
<Folder Include="Properties\" />
<Folder Include="wwwroot\Styles\" />
</ItemGroup>

</Project>
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.