
Find more on AEM Content Fragments
Find more on links localization
In our previous articles we have prepared a custom CF model, created a CF structure, and configured its translation. In this article we’ll take a look at CF content localization.
A New AEM Content Fragment Translation Project
A translation project helps organize and manage AEM Content Fragment translation within Adobe Experience Manager and acts as a container for similar translation jobs. We can use a project to create translation jobs for an entire site, an entire directory, or a single page, depending on your needs.
Let’s create a translation project for our site:
- Go to /projects.html/content/projects
- Click Create → Project
- Select “Translation Project” template
Basic Tab:
Advanced tab:
- You can also select a thumbnail
- Click Create
This project will be used later when creating/updating language copies.
AEM Content Fragment Localization Workflows
In the CF console, we can create a language copy, update a language copy (all variations), and update a particular AEM Content Fragment variation. A corresponding workflow in AEM is being triggered with each action.
We will create a Workflow step process to extend built-in AEM workflows. It will have a param: SELECTED_REGIONS_ARG_NAME = “regions” – an array of regions where you can perform link localization. The workflow will search for site and XF references within the CF and try to localize them. For this WF, we will use code from this https://exadel.com/news/aem-experience-fragments-rollout-configuration/ article: XFReferencesUpdateActionFactory and SiteReferencesUpdateActionFactory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
package com.wcm.site.workflow; import com.adobe.acs.commons.util.WorkflowHelper; import com.adobe.granite.workflow.WorkflowSession; import com.adobe.granite.workflow.exec.WorkItem; import com.adobe.granite.workflow.exec.WorkflowProcess; import com.adobe.granite.workflow.metadata.MetaDataMap; import com.day.cq.dam.api.DamConstants; import com.day.cq.wcm.msm.api.LiveRelationshipManager; import com.google.common.collect.ImmutableSet; import com.wcm.site.localization.Locale; import com.wcm.site.localization.PathContext; import com.wcm.site.xf.SiteReferencesUpdateActionFactory; import com.wcm.site.xf.ReferencesUpdateAction; import com.wcm.site.xf.XFReferencesUpdateActionFactory; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.RepositoryException; import java.util.Arrays; import java.util.Collections; import static com.wcm.site.servlets.RolloutWorkflowStarter.SELECTED_REGIONS_ARG_NAME; import static com.wcm.site.util.AssetUtils.getTargetLanguageCopyPath; import static com.wcm.site.util.CFMUtils.isContentFragment; import static com.wcm.site.util.LinkUtils.joinAsPath; import static com.wcm.site.workflow.WorkflowProcessBase.WORKFLOW_PROCESS_LABEL; @Component(immediate = true, service = WorkflowProcess.class, property = { WORKFLOW_PROCESS_LABEL + "=Content Fragment Links Localisation" }) public class ContentFragmentLinkLocalisationProcess extends WorkflowProcessBase implements WorkflowProcess { private static final Logger LOG = LoggerFactory.getLogger(ContentFragmentLinkLocalisationProcess.class); @Reference private WorkflowHelper workflowHelper; @Reference private LiveRelationshipManager liveRelationshipManager; @Override public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaData) { ResourceResolver resourceResolver = getResourceResolver(workflowHelper, workflowSession); String sourcePath = getTargetPath(workItem, true); String selectedRegions = getPersistedData(workItem, SELECTED_REGIONS_ARG_NAME, StringUtils.EMPTY); if (StringUtils.isEmpty(selectedRegions)) { processRegion(sourcePath, resourceResolver); return; } Arrays.stream(StringUtils.split(selectedRegions, ";")).forEach(language -> processRegion(getTargetLanguageCopyPath(sourcePath, language, resourceResolver, null), resourceResolver) ); } public void processRegion(String sourcePath, ResourceResolver resourceResolver) { Resource source = resourceResolver.getResource(sourcePath); PathContext sourcePathContext = new PathContext(sourcePath); if (source == null || !isContentFragment(source)) { LOG.error("Resource not found or not content fragment {}", sourcePath); return; } SiteReferencesUpdateActionFactory.SiteReferencesUpdateAction siteReferencesUpdateAction = new SiteReferencesUpdateActionFactory.SiteReferencesUpdateAction(liveRelationshipManager, Collections.emptyList()); XFReferencesUpdateActionFactory.XFReferencesUpdateAction xfReferencesUpdateAction = new XFReferencesUpdateActionFactory.XFReferencesUpdateAction(); Arrays.stream(PathContext.Site.values()).forEach(site -> { adjustReferences(siteReferencesUpdateAction, site.getBlueprintPath(), source, Locale.lookupByIsoLocale(sourcePathContext.getLocaleSegment(), null)); adjustReferences(xfReferencesUpdateAction, joinAsPath(site.getCfPath(), java.util.Locale.ENGLISH.getLanguage()), source, Locale.lookupByIsoLocale(sourcePathContext.getLocaleSegment(), null)); } ); } private void adjustReferences(ReferencesUpdateAction action, String source, Resource target, Locale language) { if (target == null || language == null) { LOG.error("target or language is null, exiting ..."); return; } try { ResourceResolver resourceResolver = target.getResourceResolver(); action.adjustReferences( resourceResolver.getResource(source), resourceResolver.getResource(target.getPath()), language.name(), false, ImmutableSet.of(DamConstants.PN_PARENT_PATH, "cq:translationSourcePath")); } catch (RepositoryException e) { LOG.error("Exception", e); } } } |
A starter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
package com.wcm.site.workflow; import com.adobe.acs.commons.util.WorkflowHelper; import com.adobe.acs.commons.util.visitors.TraversalException; import com.adobe.acs.commons.util.visitors.TreeFilteringResourceVisitor; import com.adobe.granite.workflow.WorkflowSession; import com.adobe.granite.workflow.exec.WorkItem; import com.adobe.granite.workflow.exec.WorkflowData; import com.adobe.granite.workflow.exec.WorkflowProcess; import com.adobe.granite.workflow.metadata.MetaDataMap; import com.wcm.site.util.AssetUtils; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; import static com.wcm.site.util.AssetUtils.TRANSLATE_LANGUAGES_KEY_NAME; import static com.wcm.site.util.AssetUtils.getTargetLanguageCopyPath; import static com.wcm.site.util.CFMUtils.isContentFragment; import static com.wcm.site.workflow.WorkflowProcessBase.WORKFLOW_PROCESS_LABEL; @Component(immediate = true, service = WorkflowProcess.class, property = { WORKFLOW_PROCESS_LABEL + "=Content Fragment Localisation Workflow Starter" }) public class ContentFragmentLocalisationWFStarterProcess extends WorkflowProcessBase implements WorkflowProcess { public static final String WF_MODEL_ID_ARG_NAME = "modelId"; private static final Logger LOG = LoggerFactory.getLogger(ContentFragmentLocalisationWFStarterProcess.class); @Reference private WorkflowHelper workflowHelper; @Override public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaData) { ResourceResolver resourceResolver = getResourceResolver(workflowHelper, workflowSession); String sourcePath = getTargetPath(workItem, true); Resource source = resourceResolver.getResource(sourcePath); if (AssetUtils.isFolder(source)) { processFolder(workItem, workflowSession, metaData, source); return; } processCF(workItem, workflowSession, metaData, source); } private void processCF(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaData, Resource source) { if (source == null || !isContentFragment(source)) { return; } ResourceResolver resourceResolver = getResourceResolver(workflowHelper, workflowSession); WorkflowData data = workItem.getWorkflowData(); MetaDataMap metaDataMap = data.getMetaDataMap(); String updateLanguages = metaDataMap.get(TRANSLATE_LANGUAGES_KEY_NAME, StringUtils.EMPTY); String[] updateLanguagesArr = StringUtils.split(updateLanguages, ";"); String[] args = buildArguments(metaData); String modelId = getArgValueByName(workflowHelper, args, WF_MODEL_ID_ARG_NAME); if (StringUtils.isEmpty(modelId)) { LOG.error("Workflow model id is not found"); return; } Arrays.stream(updateLanguagesArr).forEach(language -> startWorkflow(workflowSession, modelId, getTargetLanguageCopyPath(source.getPath(), language, resourceResolver, null)) ); } private void processFolder(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaData, Resource source) { try { TreeFilteringResourceVisitor cfVisitor = new TreeFilteringResourceVisitor(); cfVisitor.setTraversalFilter(resource -> cfVisitor.isFolder(resource) || isContentFragment(resource)); cfVisitor.setResourceVisitor((resource, level) -> processCF(workItem, workflowSession, metaData, resource)); cfVisitor.accept(source); } catch (TraversalException e) { LOG.error("Exception", e); } } } |
And the workflow itself /var/workflow/models/site/cf-localisation-workflow.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="cq:WorkflowModel" sling:resourceType="cq/workflow/components/model" description="No Description" title="Site Content Fragment Localisation Workflow"> <metaData cq:generatingPage="/conf/global/settings/workflow/models/site/cf-localisation-workflow/jcr:content" cq:lastModified="{Long}1611255928586" cq:lastModifiedBy="admin" jcr:primaryType="nt:unstructured"/> <nodes jcr:primaryType="nt:unstructured"> <node0 jcr:primaryType="cq:WorkflowNode" title="Start" type="START"> <metaData jcr:primaryType="nt:unstructured"/> </node0> <node1 jcr:primaryType="cq:WorkflowNode" title="Content Fragment Links Localisation Step" type="PROCESS"> <metaData jcr:primaryType="nt:unstructured" PROCESS="com.wcm.site.workflow.ContentFragmentLinkLocalisationProcess" PROCESS_AUTO_ADVANCE="true"/> </node1> <node2 jcr:primaryType="cq:WorkflowNode" title="End" type="END"> <metaData jcr:primaryType="nt:unstructured"/> </node2> </nodes> <transitions jcr:primaryType="nt:unstructured"> <node0_x0023_node1 jcr:primaryType="cq:WorkflowTransition" from="node0" rule="" to="node1"> <metaData jcr:primaryType="nt:unstructured"/> </node0_x0023_node1> <node1_x0023_node2 jcr:primaryType="cq:WorkflowTransition" from="node1" rule="" to="node2"> <metaData jcr:primaryType="nt:unstructured"/> </node1_x0023_node2> </transitions> </jcr:root> |
Creating a New Language Copy in AEM
To create a new language copy:
- Select a CF in a blueprint folder
- Click References → Language Copies
- Select Create and Translate
- Select a language to translate to
- Select create structure only → Create
After this, ‘/libs/settings/workflow/models/dam/dam-create-language-copy’ workflow will be triggered in AEM. We need to extend this WF by adding an extra step, which will localize all links within a CF.
Adding a new step to the “DAM Create Language Copy” workflow
/var/workflow/models/dam/dam-create-language-copy.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="cq:WorkflowModel" sling:resourceType="cq/workflow/components/model" description="This workflow creates language copies for assets" title="DAM Create Language Copy"> <metaData cq:generatingPage="/conf/global/settings/workflow/models/dam/dam-create-language-copy/jcr:content" cq:lastModified="{Long}1610816350558" cq:lastModifiedBy="admin" jcr:primaryType="nt:unstructured"/> <nodes jcr:primaryType="nt:unstructured"> <node0 jcr:primaryType="cq:WorkflowNode" title="Start" type="START"> <metaData jcr:primaryType="nt:unstructured"/> </node0> <node1 jcr:primaryType="cq:WorkflowNode" description="Creates language copy of assets" title="Create Language Copy" type="PROCESS"> <metaData jcr:primaryType="nt:unstructured" PROCESS="com.day.cq.dam.core.impl.process.CreateAssetLanguageCopyProcess" PROCESS_AUTO_ADVANCE="true"/> </node1> <node2 jcr:primaryType="cq:WorkflowNode" title="Content Fragment Localisation WFs Starter" type="PROCESS"> <metaData jcr:primaryType="nt:unstructured" PROCESS="com.wcm.site.workflow.ContentFragmentLocalisationWFStarterProcess" PROCESS_ARGS="modelId:/var/workflow/models/site/cf-localisation-workflow" PROCESS_AUTO_ADVANCE="true"/> </node2> <node3 jcr:primaryType="cq:WorkflowNode" title="End" type="END"> <metaData jcr:primaryType="nt:unstructured"/> </node3> </nodes> <transitions jcr:primaryType="nt:unstructured"> <node0_x0023_node1 jcr:primaryType="cq:WorkflowTransition" from="node0" rule="" to="node1"> <metaData jcr:primaryType="nt:unstructured"/> </node0_x0023_node1> <node1_x0023_node2 jcr:primaryType="cq:WorkflowTransition" from="node1" rule="" to="node2"> <metaData jcr:primaryType="nt:unstructured"/> </node1_x0023_node2> <node2_x0023_node3 jcr:primaryType="cq:WorkflowTransition" from="node2" to="node3"> <metaData jcr:primaryType="nt:unstructured"/> </node2_x0023_node3> </transitions> </jcr:root> |
This should be enough to get you started, but it’s only part one of our series. In part two, we will take a look at how to configure links by rewriting a CF Language Copy update and CF Single Variation update.
Author: Iryna Ason