Localization of taxonomies: what is possible today? #16124
Replies: 9 comments 3 replies
-
|
@hishamco Saw your https://www.youtube.com/watch?v=WQbpRHV9ugw video. Have you ever tried taxonomy localization? |
Beta Was this translation helpful? Give feedback.
-
|
Unfortunately not much, it seems @urbanit is the guy who is interested in this if I remember correctly. I might look to the issue |
Beta Was this translation helpful? Give feedback.
-
|
Hey @steven-spits-servico ! I hope you are fine! The TaxonomyField only allows one taxonomy, so users will see for example the English terms when creating a French content item. No ideal, but no showstopper. How do you relate 'translated' terms to each other to show the correct translation to users on the frontend? |
Beta Was this translation helpful? Give feedback.
-
|
And apparently sorry for the late response |
Beta Was this translation helpful? Give feedback.
-
|
Hi @urbanit, thanks for your insight on this issue. Am I correct to assume localizing terms is something that will never happen in OC, (at least not anytime soon), as all efforts try to make localized taxonomies work correctly? @sebastienros, care to share your point of view on this? Our particular case: |
Beta Was this translation helpful? Give feedback.
-
|
I don't remember the details. What I know is that each language will usually have it's own taxonomy, meaning different terms. So you can't have a 1-1 mapping of terms between taxonomies, and find the corresponding one from the other taxonomy's localization. I believe the current option is to tag content items with terms coming from multiple taxonomies, each term from each language. Would that work? In your case you will probably say that all these terms would have a matching entry in each language, so you would be fine with having each term be localizable, and a single taxonomy content item. Maybe that could be a fine assumption to allow term to have the localization part. |
Beta Was this translation helpful? Give feedback.
-
|
For inspiration: Orchard 1.x has this figured out, in large part thanks to the hard work of the Laser team who invested heavily into localization. The last half hour of my Harvest 2017 talk covers the then-ongoing developments and here are the mentioned design notes: OrchardCMS/Orchard#7352 |
Beta Was this translation helpful? Give feedback.
-
This is the way I am trying to get my taxonomy localized. Thanks, |
Beta Was this translation helpful? Give feedback.
-
|
Our solution in Lombiq was to create a new content type called LocalizedTaxonomy, which has:
Let's say this LocalizedTaxonomy will contain Categories, so the TermContentType will be the Category content type. This Category content type has the following parts:
Then to sync these taxonomies we introduced a handler that goes through the terms recursively: public class LocalizedTaxonomyHandler : ContentHandlerBase
{
private readonly IHttpContextAccessor _hca;
private readonly IEnumerable<ILocalizedTermUpdateEventHandler> _localizedTermUpdateEventHandlers;
private readonly ISession _session;
public LocalizedTaxonomyHandler(
IHttpContextAccessor httpContextAccessor,
IEnumerable<ILocalizedTermUpdateEventHandler> localizedTermUpdateEventHandlers,
ISession session)
{
_hca = httpContextAccessor;
_localizedTermUpdateEventHandlers = localizedTermUpdateEventHandlers;
_session = session;
}
public override async Task UpdatedAsync(UpdateContentContext context)
{
// Only handle updates for localized taxonomies.
if (context.ContentItem.ContentType != LocalizedTaxonomy) return;
// When a taxonomy is updated search for other localized versions of it and update their terms accordingly.
var localizationPart = context.ContentItem.As<LocalizationPart>();
if (localizationPart == null) return;
var contentLocalizationManager = _hca.HttpContext?.RequestServices.GetRequiredService<IContentLocalizationManager>();
var alreadyTranslated = await contentLocalizationManager!.GetItemsForSetAsync(localizationPart.LocalizationSet);
foreach (var localizedTaxonomy in alreadyTranslated)
{
// No need to update itself.
if (localizedTaxonomy.ContentItemId == context.ContentItem.ContentItemId) continue;
// We need to clone the localized taxonomy because we will overwrite the Terms with the current update.
var localizedOriginal = localizedTaxonomy.Clone();
localizedTaxonomy.Apply(context.ContentItem.As<TaxonomyPart>());
// Now we need to go through the terms and update their TitlePart and trigger event handlers.
// So their original values are restored.
await localizedTaxonomy.AlterAsync<TaxonomyPart>(async part =>
{
var newTerms = await UpdateChildrenTermsRecursivelyAsync(
(JsonArray)part.Content.Terms,
(JsonArray)localizedOriginal.Content.TaxonomyPart.Terms);
part.Terms = newTerms.ToObject<List<ContentItem>>();
});
await _session.SaveAsync(localizedTaxonomy);
}
}
private async Task<JsonArray> UpdateChildrenTermsRecursivelyAsync(JsonArray children, JsonArray localizedTerms)
{
var updatedChildren = new JsonArray();
foreach (var child in children)
{
if (child == null) continue;
var foundContentItem = FindTermRecursively(localizedTerms, child[nameof(CategoryPart)]?[nameof(CategoryPart.CommonId)]?.ToString());
if (foundContentItem != null)
{
await ApplyChangesAsync(child as JsonObject, foundContentItem);
}
if (child[nameof(TaxonomyPart.Terms)] != null && (JsonArray)child[nameof(TaxonomyPart.Terms)]! is { Count: > 0 } terms)
{
var newTerms = await UpdateChildrenTermsRecursivelyAsync(terms, localizedTerms);
child[nameof(TaxonomyPart.Terms)] = newTerms;
}
updatedChildren.Add(child.DeepClone());
}
return updatedChildren;
}
private async Task ApplyChangesAsync(JsonObject? child, ContentItem foundContentItem)
{
if (child == null) return;
child[nameof(ContentItem.DisplayText)] = foundContentItem.DisplayText;
if (child[nameof(TermPart)]?[nameof(TermPart.TaxonomyContentItemId)] != null)
{
child[nameof(TermPart)]![nameof(TermPart.TaxonomyContentItemId)] = foundContentItem.As<TermPart>()?.TaxonomyContentItemId;
}
if (child[nameof(TitlePart)]?[nameof(TitlePart.Title)] != null)
{
child[nameof(TitlePart)]![nameof(TitlePart.Title)] = foundContentItem.As<TitlePart>().Title;
}
// Trigger an event handler, in case there are other parts to update.
await _localizedTermUpdateEventHandlers.AwaitEachAsync(handler => handler.ApplyChangesAsync(child, foundContentItem));
}
private static ContentItem? FindTermRecursively(JsonArray termsArray, string? termCommonId)
{
foreach (var term in termsArray.Cast<JsonObject>())
{
var commonId = term[nameof(CategoryPart)]?[nameof(CategoryPart.CommonId)]?.ToString();
if (commonId == termCommonId)
{
return term.ToObject<ContentItem>();
}
if (term[nameof(TaxonomyPart.Terms)] is JsonArray children)
{
var found = FindTermRecursively(children, termCommonId);
if (found != null)
{
return found;
}
}
}
return null;
}
}The As we added autoroute as well we need to handle it a bit differently, because localization just copies the content without changing the content item id for each category: public class AutorouteLocalizedTaxonomyHandler : IContentLocalizationHandler
{
private readonly IIdGenerator _idGenerator;
private string taxonomyContentItemId = string.Empty;
public AutorouteLocalizedTaxonomyHandler(IIdGenerator idGenerator) => _idGenerator = idGenerator;
public async Task LocalizingAsync(LocalizationContentContext context)
{
if (context.ContentItem.Has<TaxonomyPart>())
{
taxonomyContentItemId = context.ContentItem.ContentItemId;
await context.ContentItem.AlterAsync<TaxonomyPart>(async part =>
{
var newTerms = await UpdateChildrenTermsRecursivelyAsync((JsonArray)part.Content.Terms);
part.Terms = newTerms.ToObject<List<ContentItem>>();
});
}
}
public Task LocalizedAsync(LocalizationContentContext context) => Task.CompletedTask;
private async Task<JsonArray> UpdateChildrenTermsRecursivelyAsync(JsonArray children)
{
var updatedChildren = new JsonArray();
foreach (var child in children)
{
if (child == null) continue;
// Generate a new ContentItemId for the term. We don't want to copy the same content item id, because it can
// cause troubles with autoroute in case it is a contained item. It would override the same item in the other
// localized versions, causing autoroute conflicts.
child[nameof(ContentItem.ContentItemId)] = _idGenerator.GenerateUniqueId();
// Clearing the AutoroutePart path to regenerate the permalink automatically. We don't want to copy the path.
if (child[nameof(AutoroutePart)]?[nameof(AutoroutePart.Path)] != null)
{
child[nameof(AutoroutePart)]![nameof(AutoroutePart.Path)] = null;
}
// Set the TaxonomyContentItemId to the localized taxonomy's ContentItemId.
if (child[nameof(TermPart)]?[nameof(TermPart.TaxonomyContentItemId)] != null)
{
child[nameof(TermPart)]![nameof(TermPart.TaxonomyContentItemId)] = taxonomyContentItemId;
}
if (child[nameof(TaxonomyPart.Terms)] != null && (JsonArray)child[nameof(TaxonomyPart.Terms)]! is { Count: > 0 } terms)
{
var newTerms = await UpdateChildrenTermsRecursivelyAsync(terms);
child[nameof(TaxonomyPart.Terms)] = newTerms;
}
updatedChildren.Add(child.DeepClone());
}
return updatedChildren;
}
}And finally a handler to fill the public class CategoryHandler : ContentHandlerBase
{
private readonly IIdGenerator _idGenerator;
private readonly IHttpContextAccessor _hca;
public CategoryHandler(IIdGenerator idGenerator, IHttpContextAccessor httpContextAccessor)
{
_idGenerator = idGenerator;
_hca = httpContextAccessor;
}
public override async Task UpdatedAsync(UpdateContentContext context)
{
if (!context.ContentItem.Has<CategoryPart>()) return;
var categoryPart = context.ContentItem.As<CategoryPart>();
if (string.IsNullOrEmpty(categoryPart.CommonId))
{
// Assign a new CommonId if it doesn't have one. We use this to link categories across different languages.
context.ContentItem.Alter<CategoryPart>(part => part.CommonId = _idGenerator.GenerateUniqueId());
}
// Ensure the taxonomy is published after updating the category, so routing is updated.
var taxonomyContentItemId = context.ContentItem.As<TermPart>().TaxonomyContentItemId;
var contentManager = _hca.HttpContext!.RequestServices.GetRequiredService<IContentManager>();
var taxonomy = await contentManager.GetAsync(taxonomyContentItemId);
await contentManager.PublishAsync(taxonomy);
}
}Of course there is room for improvement, but this ensures that the following for all localizations:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Yes, another topic on localization of taxonomies. And yes, I have read all previous topics, like #4845 en #11070, so I understand it's a difficult thing to implement.
An ideal solution would be to have term localizations, but all attempts were abandoned. So I don't see this happen in the near future.
So that leaves localization of taxonomies, which is possible today, correct?
I see two problems with this approach:
Any guidance on what works (and what doesn't) is much appreciated. I need to implement this for a client and I'm looking for a viable solution.
Beta Was this translation helpful? Give feedback.
All reactions