Language Copy Tool with Sitecore Powershell Extensions

I was chatting with Michael West (@michaelwest101) the other day and came to the conclusion that we have a bunch of Powershell scripts at Brainjocks that have revolutionized the way we approach Sitecore. If you are unfamiliar with Powershell in the Sitecore context, then I highly recommend you check out Sitecore Powershell Extensions. This script, in particular, we have used on multiple projects – and it enables the Content Authoring team to quickly copy languages across multiple content items in bulk. Every new project I start up, the team asks me to include this feature.

Use Case

From anywhere within the Content Tree, I want the ability to right click an item and choose to copy its language. I also want the ability to copy datasources and subitems. Here’s how you should access it, and what the modal will look like:

 

Explanation

To create this functionality, I have added a context menu script to my tenant’s Powershell script library. This could also be a Feature in your Helix setup. Mine looks like this:

First of all, we need a function that lets us pull the datasources of an item. I’m going to implement it so that it skips Above Page Content and Below Page Content placeholders, as I don’t want to run this copying command on my header and footer snippets. You may want to change this depending on your assembly practices.

function GetItemDatasources {
    [CmdletBinding()]
    param([Item]$Item)
 
    # grab all datasources that are not header and footer elements
    return Get-Rendering -Item $item -FinalLayout -Device (Get-LayoutDevice -Default) |
        Where-Object { -not [string]::IsNullOrEmpty($_.Datasource)} |
        Where-Object { $_.Placeholder -ne 'Above Page Content' } |
        Where-Object { $_.Placeholder -ne 'Below Page Content' } |
        ForEach-Object { Get-Item "$($item.Database):" -ID $_.Datasource }
}

Next, I need the main entry point into the program. Since this is a context menu script, the user invokes it by right-clicking an item within the tree. The get-location cmdlet will return the item that was clicked on.

 

$location = get-location

Now, I need to build up some options to display to the user. I want them to pick languages, a copy mode, and some additional options. That’s what all of this logic does.

 

$languages = Get-ChildItem "master:\sitecore\system\Languages"
$currentLanguage = [Sitecore.Context]::Language.Name
 
$langOptions = @{};
foreach ($lang in $languages) {
    $langOptions[$lang.Name] = $lang.Name
}
 
$ifExists = @{};
$ifExists["Append"] = "Append";
$ifExists["Skip"] = "Skip";
$ifExists["Overwrite Latest"] = "OverwriteLatest";

 

Once I’ve got my arguments ready, I can go ahead and prompt the user with a dialog. Within this dialog, I’ve added some columns, help text, etc. At the very end, I want to make sure that they hit the OK button. You can see that if the $result is not set to “ok”, then we exit the script.

 

$result = Read-Variable -Parameters `
    @{ Name = "originLanguage"; Value=$currentLanguage; Title="Origin Language"; Options=$langOptions; },
    @{ Name = "destinationLanguages"; Title="Destination Language(s)"; Options=$langOptions; Editor="checklist"; },
    @{ Name = "includeSubitems"; Value=$false; Title="Include Subitems"; Columns = 4;},
    @{ Name = "includeDatasources"; Value=$false; Title="Include Datasources"; Columns = 4 },
    @{ Name = "includeSnippets"; Value=$false; Title="Include Snippet Datasources"; Columns = 4 },
    @{ Name = "ifExists"; Value="Skip"; Title="If Exists"; Options=$ifExists; Tooltip="Append: Create new language version with copied content.
Skip: do nothing if destination has language version.
Overwrite Latest: overwrite latest language version with copied content."; } `
    -Description "Select an origin and destination language, with options on how to perform the copy" `
    -Title "Copy Language" -Width 650 -Height 660 -OkButtonName "Proceed" -CancelButtonName "Cancel" -ShowHints
 
if($result -ne "ok") {
    Exit
}

Now, we need to calculate which items the user selected based upon the parameters they’ve chosen (include subitems, include datasources, include snippet datasources, etc). We’ll store the list of items in an object called $items, and we’ll remove duplicates at the end.

 

$items = @()
 
$items += Get-Item $location
 
# add optional subitems
if ($includeSubitems) {
    $items += Get-ChildItem $location -Recurse
}
 
# add optional datasources of the page
if ($includeDatasources) {
    Foreach($item in $items) {
        $items += GetItemDatasources($item)
    }
}
 
# add optional datasources of snippets
if ($includeSnippets) {
    $items += $items | Where-Object { $_.TemplateName -eq 'Snippet' } | ForEach-Object { GetItemDatasources($_) }
}
 
# Remove any duplicates, based on ID
$items = $items | Sort-Object -Property 'ID' -Unique

At this point, I want the user to confirm that I’ve pulled the necessary items. They can’t really see a list of items, but they should have an idea of roughly how many they’re about to translate. For instance, if they think they’re translating 5 items, but the list comes back with 1200 items, then this is a chance for them to cancel the execution and try again.

 

$message = "You are about to update <span style='font-weight: bold'>$($items.Count) item(s)</span> with the following options:<br>"
$message += "<br><table>"
$message += "<tr><td style='width: auto'>Origin Language:</td><td>$originLanguage</td></tr>"
$message += "<tr><td style='width: auto'>Destination Languages:</td><td>$destinationLanguages</td></tr>"
$message += "<tr><td style='width: auto'>Include Subitems:</td><td>$includeSubitems</td></tr>"
$message += "<tr><td style='width: auto'>Include Datasources:</td><td>$includeDatasources</td></tr>"
$message += "<tr><td style='width: auto'>Include Snippet Datasources:</td><td>$includeSnippets</td></tr>"
$message += "<tr><td style='width: auto'>Copy Method:</td><td>$ifExists</td></tr>"
$message += "</table>"
$message += "<br><p style='font-weight: bold'>Are you sure?</p>"
 
$proceed = Show-Confirm -Title $message
 
if ($proceed -ne 'yes') {
    Write-Host "Canceling"
    Exit
}

 

At the end, the algorithm is pretty simple. All we need to do is take each item and run it through the Add-ItemLanguage command, passing in the different options that the user elected.

 

$total = $items.Count
$count = 1
$items | ForEach-Object {
    Write-Progress "$count / $total : $($_.Paths.FullPath)"
    Add-ItemLanguage $_ -Language $originLanguage -TargetLanguage $destinationLanguages -IfExist $ifExists
    $count++
}

The full script

Here’s the full script I ended up with, in all of its glory.

 


function GetItemDatasources {
    [CmdletBinding()]
    param([Item]$Item)
 
    # grab all datasources that are not header and footer elements
    return Get-Rendering -Item $item -FinalLayout -Device (Get-LayoutDevice -Default) |
        Where-Object { -not [string]::IsNullOrEmpty($_.Datasource)} |
        Where-Object { $_.Placeholder -ne 'Above Page Content' } |
        Where-Object { $_.Placeholder -ne 'Below Page Content' } |
        ForEach-Object { Get-Item "$($item.Database):" -ID $_.Datasource }
        # ForEach-Object { Write-Host ($_ | Format-List | Out-String) }
}
 
$location = get-location
 
$languages = Get-ChildItem "master:\sitecore\system\Languages"
$currentLanguage = [Sitecore.Context]::Language.Name
 
$langOptions = @{};
 
foreach ($lang in $languages) {
    $langOptions[$lang.Name] = $lang.Name
}
 
$ifExists = @{};
$ifExists["Append"] = "Append";
$ifExists["Skip"] = "Skip";
$ifExists["Overwrite Latest"] = "OverwriteLatest";
 
$result = Read-Variable -Parameters `
    @{ Name = "originLanguage"; Value=$currentLanguage; Title="Origin Language"; Options=$langOptions; },
    @{ Name = "destinationLanguages"; Title="Destination Language(s)"; Options=$langOptions; Editor="checklist"; },
    @{ Name = "includeSubitems"; Value=$false; Title="Include Subitems"; Columns = 4;},
    @{ Name = "includeDatasources"; Value=$false; Title="Include Datasources"; Columns = 4 },
    @{ Name = "includeSnippets"; Value=$false; Title="Include Snippet Datasources"; Columns = 4 },
    @{ Name = "ifExists"; Value="Skip"; Title="If Exists"; Options=$ifExists; Tooltip="Append: Create new language version with copied content.<br>Skip: do nothing if destination has language version.<br>Overwrite Latest: overwrite latest language version with copied content."; } `
    -Description "Select an origin and destination language, with options on how to perform the copy" `
    -Title "Copy Language" -Width 650 -Height 660 -OkButtonName "Proceed" -CancelButtonName "Cancel" -ShowHints
 
if($result -ne "ok") {
    Exit
}
 
Write-Host "originLanguage = $originLanguage"
Write-Host "destinationLanguages = $destinationLanguages"
 
$items = @()
 
$items += Get-Item $location
 
# add optional subitems
if ($includeSubitems) {
    $items += Get-ChildItem $location -Recurse
}
 
# add optional datasources
if ($includeDatasources) {
    Foreach($item in $items) {
        $items += GetItemDatasources($item)
    }
}
 
# add optional datasource subitems
if ($includeSnippets) {
    $items += $items | Where-Object { $_.TemplateName -eq 'Snippet' } | ForEach-Object { GetItemDatasources($_) }
}
 
# Remove any duplicates, based on ID
$items = $items | Sort-Object -Property 'ID' -Unique
 
$items | ForEach-Object { Write-Host ($_.ItemPath | Sort-Object | Format-List | Out-String) }
 
$message = "You are about to update <span style='font-weight: bold'>$($items.Count) item(s)</span> with the following options:<br>"
$message += "<br><table>"
$message += "<tr><td style='width: auto'>Origin Language:</td><td>$originLanguage</td></tr>"
$message += "<tr><td style='width: auto'>Destination Languages:</td><td>$destinationLanguages</td></tr>"
$message += "<tr><td style='width: auto'>Include Subitems:</td><td>$includeSubitems</td></tr>"
$message += "<tr><td style='width: auto'>Include Datasources:</td><td>$includeDatasources</td></tr>"
$message += "<tr><td style='width: auto'>Include Snippet Datasources:</td><td>$includeSnippets</td></tr>"
$message += "<tr><td style='width: auto'>Copy Method:</td><td>$ifExists</td></tr>"
$message += "</table>"
$message += "<br><p style='font-weight: bold'>Are you sure?</p>"
 
 
$proceed = Show-Confirm -Title $message
 
if ($proceed -ne 'yes') {
    Write-Host "Canceling"
    Exit
}
 
Write-Host "Proceeding with execution"
 
$items | ForEach-Object { Add-ItemLanguage $_ -Language $originLanguage -TargetLanguage $destinationLanguages -IfExist $ifExists }

A few extra pointers…

Always tests your scripts before putting them into production. This also assumes you have a feature similar to Snippets from SCORE or SXA. You can easily remove that portion if you need to. I hope this helps you in your Sitecore journey…

Dylan McCurry

I am a certified Sitecore developer with a passion for the web. I hopped into the .NET space 5 years ago to work on enterprise-class applications and never looked back. I love building things—everything from from Legos to software that solves real problems. I have a strong foundation of backend skills, with sweet spots like security, portal solutions and APIs. Early on, before I had the benefit of SCORE, I made a lot of mistakes with Sitecore but learned a lot in the course of the struggle. I would like to support other developers by contributing my perspective on doing things “the Sitecore way,” rather than fighting the framework. Did I mention I love video games?

More posts from Dylan McCurry >

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back