Skip to content

Commit 4cb0b3b

Browse files
Matteo Piovanellisebastienros
authored andcommitted
Localization improvements (#7596)
Updates to handling of MasterContentItem (as they are in 1.10.x) Synchronization of CultureNeutral Fields and Parts (based on Cloning) Support for multi-language Blogs (in its own feature in the Blogs module) Support for multi-language Taxonomies (in its own feature in the Taxonomies module)
1 parent a9066c0 commit 4cb0b3b

57 files changed

Lines changed: 1774 additions & 85 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Web.Routing;
4+
5+
using Orchard.Autoroute.Models;
6+
using Orchard.Autoroute.Services;
7+
using Orchard.Blogs.Models;
8+
using Orchard.ContentManagement;
9+
using Orchard.ContentManagement.Aspects;
10+
using Orchard.ContentManagement.Handlers;
11+
using Orchard.Core.Common.Models;
12+
using Orchard.Core.Title.Models;
13+
using Orchard.Environment.Extensions;
14+
using Orchard.Localization;
15+
using Orchard.Localization.Models;
16+
using Orchard.Localization.Services;
17+
using Orchard.UI.Notify;
18+
19+
namespace Orchard.Blogs.BlogsLocalizationExtensions.Handlers {
20+
[OrchardFeature("Orchard.Blogs.LocalizationExtensions")]
21+
public class BlogPostPartHandler : ContentHandler {
22+
private readonly IContentManager _contentManager;
23+
private readonly IAutorouteService _routeService;
24+
private readonly ILocalizationService _localizationService;
25+
26+
public BlogPostPartHandler(RequestContext requestContext, IContentManager contentManager, IAutorouteService routeService, ILocalizationService localizationService, INotifier notifier) {
27+
_contentManager = contentManager;
28+
_routeService = routeService;
29+
_localizationService = localizationService;
30+
Notifier = notifier;
31+
T = NullLocalizer.Instance;
32+
//move posts when created, updated or published
33+
//changed OnCreating and OnUpdating in OnCreated and OnUpdated so LocalizationPart is already populated
34+
OnCreated<BlogPostPart>((context, part) => MigrateBlogPost(context.ContentItem));
35+
OnUpdated<BlogPostPart>((context, part) => MigrateBlogPost(context.ContentItem));
36+
OnPublishing<BlogPostPart>((context, part) => MigrateBlogPost(context.ContentItem));
37+
}
38+
39+
public INotifier Notifier { get; set; }
40+
41+
public Localizer T { get; set; }
42+
43+
//This Method checks the blog post's culture and it's parent blog's culture and moves it to the correct blog if they aren't equal.
44+
private void MigrateBlogPost(ContentItem blogPost) {
45+
if (!blogPost.Has<LocalizationPart>() || !blogPost.Has<BlogPostPart>()) {
46+
return;
47+
}
48+
//bolgPost just cloned for translation, never saved
49+
if (blogPost.As<CommonPart>().Container == null) {
50+
return;
51+
}
52+
var blog = _contentManager.Get(blogPost.As<CommonPart>().Container.Id);
53+
if (!blog.Has<LocalizationPart>() || blog.As<LocalizationPart>().Culture == null) {
54+
return;
55+
}
56+
57+
//get our 2 cultures for comparison
58+
var blogCulture = blog.As<LocalizationPart>().Culture;
59+
var blogPostCulture = blogPost.As<LocalizationPart>().Culture;
60+
61+
//if the post is a different culture than the parent blog change the post's parent blog to the right localization...
62+
if (blogPostCulture != null && (blogPostCulture.Id != blogCulture.Id)) {
63+
//Get the id of the current blog
64+
var blogids = new HashSet<int> { blog.As<BlogPart>().ContentItem.Id };
65+
66+
//seek for same culture blog
67+
var realBlog = _localizationService.GetLocalizations(blog).SingleOrDefault(w => w.As<LocalizationPart>().Culture == blogPostCulture);
68+
if (realBlog.Has<LocalizationPart>() && realBlog.As<LocalizationPart>().Culture.Id == blogPostCulture.Id) {
69+
blogPost.As<ICommonPart>().Container = realBlog;
70+
if (blogPost.Has<AutoroutePart>()) {
71+
_routeService.RemoveAliases(blogPost.As<AutoroutePart>());
72+
blogPost.As<AutoroutePart>().DisplayAlias = _routeService.GenerateAlias(blogPost.As<AutoroutePart>());
73+
_routeService.PublishAlias(blogPost.As<AutoroutePart>());
74+
}
75+
Notifier.Information(T("Your Post has been moved under the \"{0}\" Blog", realBlog.As<TitlePart>().Title));
76+
return;
77+
}
78+
79+
return;
80+
}
81+
}
82+
}
83+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Web;
5+
using Orchard.ContentManagement.MetaData;
6+
using Orchard.Data.Migration;
7+
using Orchard.Environment.Extensions;
8+
9+
namespace Orchard.Blogs.BlogsLocalizationExtensions.Migrations {
10+
[OrchardFeature("Orchard.Blogs.LocalizationExtensions")]
11+
public class Migrations : DataMigrationImpl {
12+
public int Create() {
13+
ContentDefinitionManager.AlterTypeDefinition("Blog",
14+
cfg => cfg
15+
.WithPart("LocalizationPart"));
16+
ContentDefinitionManager.AlterTypeDefinition("BlogPost",
17+
cfg => cfg
18+
.WithPart("LocalizationPart"));
19+
return 1;
20+
}
21+
}
22+
}

src/Orchard.Web/Modules/Orchard.Blogs/Module.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ Features:
1414
Description: Blog easier using a dedicated MetaWeblogAPI-compatible publishing tool.
1515
Dependencies: XmlRpc, Orchard.Autoroute, Orchard.ContentPicker
1616
Category: Content Publishing
17+
Orchard.Blogs.LocalizationExtensions:
18+
Name: Blog multi-language support
19+
Description: Extend Orchard Blogs module with fully integrated multi-language support.
20+
Dependencies: Orchard.Localization
21+
Category: Content

src/Orchard.Web/Modules/Orchard.Blogs/Orchard.Blogs.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@
9393
</ItemGroup>
9494
<ItemGroup>
9595
<Compile Include="AdminMenu.cs" />
96+
<Compile Include="BlogsLocalizationExtensions\Handlers\BlogPostPartHandler.cs" />
97+
<Compile Include="BlogsLocalizationExtensions\Migrations\Migrations.cs" />
9698
<Compile Include="Commands\BlogWidgetCommands.cs" />
9799
<Compile Include="Controllers\RemoteBlogPublishingController.cs" />
98100
<Compile Include="Drivers\BlogArchivesPartDriver.cs" />
@@ -193,6 +195,10 @@
193195
<Project>{f301ef7d-f19c-4d83-aa94-cb64f29c037d}</Project>
194196
<Name>Orchard.ContentPicker</Name>
195197
</ProjectReference>
198+
<ProjectReference Include="..\Orchard.Localization\Orchard.Localization.csproj">
199+
<Project>{fbc8b571-ed50-49d8-8d9d-64ab7454a0d6}</Project>
200+
<Name>Orchard.Localization</Name>
201+
</ProjectReference>
196202
<ProjectReference Include="..\Orchard.Widgets\Orchard.Widgets.csproj">
197203
<Project>{194d3ccc-1153-474d-8176-fde8d7d0d0bd}</Project>
198204
<Name>Orchard.Widgets</Name>

src/Orchard.Web/Modules/Orchard.Localization/Controllers/AdminController.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,21 @@ public ActionResult Translate(int id, string to) {
5858
// pass a dummy content to the authorization check to check for "own" variations
5959
var dummyContent = _contentManager.New(masterContentItem.ContentType);
6060

61-
var contentItemTranslation = _contentManager.Clone(masterContentItem);
62-
63-
if (!Services.Authorizer.Authorize(Permissions.EditContent, contentItemTranslation, T("Couldn't create translated content")))
61+
if (!Services.Authorizer.Authorize(Permissions.EditContent, dummyContent, T("Couldn't create translated content")))
6462
return new HttpUnauthorizedResult();
6563

64+
var contentItemTranslation = _contentManager.Clone(masterContentItem);
65+
6666
var localizationPart = contentItemTranslation.As<LocalizationPart>();
6767
if(localizationPart != null) {
68-
localizationPart.MasterContentItem = masterContentItem;
68+
localizationPart.MasterContentItem = masterLocalizationPart.MasterContentItem == null ? masterContentItem : masterLocalizationPart.MasterContentItem;
6969
localizationPart.Culture = string.IsNullOrWhiteSpace(to) ? null : _cultureManager.GetCultureByName(to);
7070
}
7171

7272
Services.Notifier.Success(T("Successfully cloned. The translated content was saved as a draft."));
7373

74-
var adminRouteValues = _contentManager.GetItemMetadata(contentItemTranslation).AdminRouteValues;
75-
return RedirectToRoute(adminRouteValues);
74+
var editorRouteValues = _contentManager.GetItemMetadata(contentItemTranslation).EditorRouteValues;
75+
return RedirectToRoute(editorRouteValues);
7676
}
7777
}
7878
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Globalization;
2+
using Orchard.ContentManagement.MetaData.Builders;
3+
using Orchard.Environment.Extensions;
4+
5+
namespace Orchard.Localization.Extensions {
6+
[OrchardFeature("Orchard.Localization.CultureNeutralPartsAndFields")]
7+
public static class MetaDataExtensions {
8+
/// <summary>
9+
/// Sets the ContentField being built as CultureNeutral. This field will then be synchronized across elements of a localization set.
10+
/// </summary>
11+
/// <param name="builder"></param>
12+
/// <param name="cultureNeutral"></param>
13+
/// <returns></returns>
14+
public static ContentPartFieldDefinitionBuilder CultureNeutral(this ContentPartFieldDefinitionBuilder builder, bool cultureNeutral = true) {
15+
return builder.WithSetting("LocalizationCultureNeutralitySettings.CultureNeutral", cultureNeutral.ToString(CultureInfo.InvariantCulture));
16+
}
17+
/// <summary>
18+
/// Sets the ContentPart being built as CultureNeutral. This part will then be synchronized across elements of a localization set.
19+
/// </summary>
20+
/// <param name="builder"></param>
21+
/// <param name="cultureNeutral"></param>
22+
/// <returns></returns>
23+
public static ContentTypePartDefinitionBuilder CultureNeutral(this ContentTypePartDefinitionBuilder builder, bool cultureNeutral = true) {
24+
return builder.WithSetting("LocalizationCultureNeutralitySettings.CultureNeutral", cultureNeutral.ToString(CultureInfo.InvariantCulture));
25+
}
26+
}
27+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Orchard.ContentManagement;
4+
using Orchard.ContentManagement.Drivers;
5+
using Orchard.ContentManagement.Handlers;
6+
using Orchard.Environment.Extensions;
7+
using Orchard.Localization.Models;
8+
using Orchard.Localization.Services;
9+
using Orchard.Localization.Settings;
10+
11+
namespace Orchard.Localization.Handlers {
12+
[OrchardFeature("Orchard.Localization.CultureNeutralPartsAndFields")]
13+
public class LocalizationCultureNeutralityHandler : ContentHandler {
14+
private readonly ILocalizationService _localizationService;
15+
private readonly IEnumerable<IContentFieldDriver> _fieldDrivers;
16+
private readonly IEnumerable<IContentPartDriver> _partDrivers;
17+
public LocalizationCultureNeutralityHandler(ILocalizationService localizationService,
18+
IEnumerable<IContentFieldDriver> fieldDrivers,
19+
IEnumerable<IContentPartDriver> partDrivers) {
20+
_localizationService = localizationService;
21+
_fieldDrivers = fieldDrivers;
22+
_partDrivers = partDrivers;
23+
24+
OnPublished<IContent>(SynchronizeOnPublish);
25+
}
26+
27+
protected void SynchronizeOnPublish(PublishContentContext context, IContent part) {
28+
//Conditions to try and start a synchronization:
29+
// && The content item is localizable
30+
// - The content item has a LocalizationPart
31+
// - We can check this on either the type or the item itself
32+
// && The part has the CultureNeutral setting set to true
33+
//After eventually synchronizing the part, we check whether we should be synchronizing any of its fields
34+
// - Go through all the fields and check the CultureNeutral setting.
35+
var locPart = part.ContentItem.As<LocalizationPart>();
36+
if (locPart != null) {
37+
//given the LocalizationPart, get the localization set (all the ContentItems on which we'll try to synchronize)
38+
var lSet = GetSynchronizationSet(locPart);
39+
//cycle through all parts
40+
foreach (var pa in part.ContentItem.Parts) {
41+
if (pa.Settings.GetModel<LocalizationCultureNeutralitySettings>().CultureNeutral) {
42+
Synchronize(pa, locPart, lSet);
43+
}
44+
foreach (var field in pa.Fields.Where(fi => fi.PartFieldDefinition.Settings.GetModel<LocalizationCultureNeutralitySettings>().CultureNeutral)) {
45+
Synchronize(field, locPart, lSet);
46+
}
47+
}
48+
}
49+
}
50+
51+
/// <summary>
52+
/// This method attempts to synchronize a part across the localization set
53+
/// </summary>
54+
/// <param name="part">The part that has just been published and that we wish to use to update all corresponding parts from
55+
/// the other elements of the localization set.</param>
56+
/// <param name="localizationPart">The localization part of the ContentItem that was just published.</param>
57+
/// <param name="lSet">The localization set for the synchronization</param>
58+
private void Synchronize(ContentPart part, LocalizationPart localizationPart, List<LocalizationPart> lSet) {
59+
if (lSet.Count > 0) {
60+
var partDrivers = _partDrivers.Where(cpd => cpd.GetPartInfo().FirstOrDefault().PartName == part.PartDefinition.Name);
61+
//use cloning
62+
foreach (var target in lSet.Select(lp => lp.ContentItem)) {
63+
var context = new CloneContentContext(localizationPart.ContentItem, target);
64+
partDrivers.Invoke(driver => driver.Cloning(context), context.Logger);
65+
partDrivers.Invoke(driver => driver.Cloned(context), context.Logger);
66+
}
67+
}
68+
}
69+
/// <summary>
70+
/// This method attempts to synchronize a field across the localization set
71+
/// </summary>
72+
/// <param name="field">The field that has just been published and that we wish to use to update all corresponding parts from
73+
/// the other elements of the localization set.</param>
74+
/// <param name="localizationPart">The localization part of the ContentItem that was just published.</param>
75+
/// <param name="lSet">The localization set for the synchronization</param>
76+
private void Synchronize(ContentField field, LocalizationPart localizationPart, List<LocalizationPart> lSet) {
77+
if (lSet.Count > 0) {
78+
var fieldDrivers = _fieldDrivers.Where(cfd => cfd.GetFieldInfo().FirstOrDefault().FieldTypeName == field.FieldDefinition.Name);
79+
//use cloning
80+
foreach (var target in lSet.Select(lp => lp.ContentItem)) {
81+
var context = new CloneContentContext(localizationPart.ContentItem, target);
82+
context.FieldName = field.Name;
83+
fieldDrivers.Invoke(driver => driver.Cloning(context), context.Logger);
84+
fieldDrivers.Invoke(driver => driver.Cloned(context), context.Logger);
85+
}
86+
}
87+
}
88+
89+
private List<LocalizationPart> GetSynchronizationSet(LocalizationPart lPart) {
90+
var lSet = _localizationService.GetLocalizations(
91+
content: lPart.ContentItem,
92+
versionOptions: VersionOptions.Published).ToList();
93+
lSet.AddRange(_localizationService.GetLocalizations(
94+
content: lPart.ContentItem,
95+
versionOptions: VersionOptions.Latest));
96+
return lSet.Distinct().Where(lp => lp.Id != lPart.Id).ToList();
97+
}
98+
}
99+
}

src/Orchard.Web/Modules/Orchard.Localization/Module.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ AntiForgery: enabled
33
Author: The Orchard Team
44
Website: http://orchardproject.net
55
Version: 1.10.1
6-
OrchardVersion: 1.9
6+
OrchardVersion: 1.10
77
Description: The localization module enables the localization of content items.
88
Features:
99
Orchard.Localization:
@@ -31,3 +31,8 @@ Features:
3131
Category: Content
3232
Name: URL Transliteration
3333
Dependencies: Orchard.Localization.Transliteration, Orchard.Autoroute
34+
Orchard.Localization.CultureNeutralPartsAndFields:
35+
Description: Enables the synchronization among localizations of parts and fields specifically marked as "Culture Neutral".
36+
Category: Content
37+
Name: Culture Neutral Synchronizations
38+
Dependencies: Orchard.Localization

src/Orchard.Web/Modules/Orchard.Localization/Orchard.Localization.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@
9494
<Compile Include="Controllers\AdminController.cs" />
9595
<Compile Include="Controllers\TransliterationAdminController.cs" />
9696
<Compile Include="Controllers\AdminCultureSelectorController.cs" />
97+
<Compile Include="Extensions\MetaDataExtensions.cs" />
98+
<Compile Include="Handlers\LocalizationCultureNeutralityHandler.cs" />
9799
<Compile Include="Helpers\ContextHelpers.cs" />
98100
<Compile Include="Models\TransliterationSpecificationRecord.cs" />
99101
<Compile Include="Providers\ContentLocalizationTokens.cs" />
@@ -120,6 +122,8 @@
120122
<Compile Include="Services\LocalizationService.cs" />
121123
<Compile Include="Services\TransliterationService.cs" />
122124
<Compile Include="Events\TransliterationSlugEventHandler.cs" />
125+
<Compile Include="Settings\LocalizationCultureNeutralityEditorEvents.cs" />
126+
<Compile Include="Settings\LocalizationCultureNeutralitySettings.cs" />
123127
<Compile Include="ViewModels\ContentLocalizationsViewModel.cs" />
124128
<Compile Include="ViewModels\EditLocalizationViewModel.cs" />
125129
<Compile Include="ViewModels\CreateTransliterationViewModel.cs" />
@@ -195,6 +199,9 @@
195199
<ItemGroup>
196200
<Content Include="packages.config" />
197201
</ItemGroup>
202+
<ItemGroup>
203+
<Content Include="Views\DefinitionTemplates\LocalizationCultureNeutralitySettings.cshtml" />
204+
</ItemGroup>
198205
<PropertyGroup>
199206
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
200207
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationService.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public LocalizationService(IContentManager contentManager, ICultureManager cultu
1515

1616
LocalizationPart ILocalizationService.GetLocalizedContentItem(IContent content, string culture) {
1717
// Warning: Returns only the first of same culture localizations.
18-
return ((ILocalizationService) this).GetLocalizedContentItem(content, culture, null);
18+
return ((ILocalizationService)this).GetLocalizedContentItem(content, culture, null);
1919
}
2020

2121
LocalizationPart ILocalizationService.GetLocalizedContentItem(IContent content, string culture, VersionOptions versionOptions) {
@@ -56,18 +56,24 @@ void ILocalizationService.SetContentCulture(IContent content, string culture) {
5656

5757
IEnumerable<LocalizationPart> ILocalizationService.GetLocalizations(IContent content) {
5858
// Warning: May contain more than one localization of the same culture.
59-
return ((ILocalizationService) this).GetLocalizations(content, null);
59+
return ((ILocalizationService)this).GetLocalizations(content, null);
6060
}
6161

6262
IEnumerable<LocalizationPart> ILocalizationService.GetLocalizations(IContent content, VersionOptions versionOptions) {
6363
if (content.ContentItem.Id == 0)
6464
return Enumerable.Empty<LocalizationPart>();
65-
6665
var localized = content.As<LocalizationPart>();
67-
68-
var query = versionOptions == null
69-
? _contentManager.Query<LocalizationPart>(localized.ContentItem.ContentType)
70-
: _contentManager.Query<LocalizationPart>(versionOptions, localized.ContentItem.ContentType);
66+
IContentQuery<LocalizationPart> query;
67+
if (content.ContentItem.TypeDefinition.Parts.Any(x => x.PartDefinition.Name == "TermPart")) { // terms translations can be contained on different TermContentType linked to taxonomies translations
68+
query = versionOptions == null
69+
? _contentManager.Query<LocalizationPart>()
70+
: _contentManager.Query<LocalizationPart>(versionOptions);
71+
}
72+
else {
73+
query = versionOptions == null
74+
? _contentManager.Query<LocalizationPart>(localized.ContentItem.ContentType)
75+
: _contentManager.Query<LocalizationPart>(versionOptions, localized.ContentItem.ContentType);
76+
}
7177

7278
int contentItemId = localized.ContentItem.Id;
7379

0 commit comments

Comments
 (0)