Originally posted in 2018, I’ve decided to update this post as the script has evolved a bit over the years. 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, we 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=$destinationOptions; 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 }
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 if ($includeDatasources) { Foreach($item in $items) { $items += GetItemDatasources($item) } } # add optional datasource subitems if ($includeSnippets) { $items += $items | Where-Object { $_.TemplateName -eq 'MySite 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.
$items | ForEach-Object { Add-ItemLanguage $_ -Language $originLanguage -TargetLanguage $destinationLanguages -IfExist $ifExists }
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 $user = Get-User -Current $languages = Get-ChildItem "master:\sitecore\system\Languages" $currentLanguage = [Sitecore.Context]::Language.Name $langOptions = @{}; $destinationOptions = @{}; foreach ($lang in $languages) { $langOptions[$lang.Name] = $lang.Name if (Test-ItemAcl -Identity $user -Path $lang.Paths.Path -AccessRight language:write) { $destinationOptions[$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=$destinationOptions; 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 'MySite 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…