Create Translations of Pages in Folder
This PnP PowerShell script automates the creation of translated pages for each page in a specified Site Pages folder. It checks that pages haven't already been translated before creating the translated page, preventing duplicates. The user is still required to update the content after the translated pages are created.
Purpose
Multilingual page translation management helps organizations by:
- Automating the creation of page translation structure
- Preventing duplicate translations through validation checks
- Maintaining consistent page hierarchy across languages
- Streamlining multilingual content workflows
- Preserving page metadata and properties
Prerequisites
- PnP PowerShell module installed
- Site collection administrator permissions
- Connection to your SharePoint Online site
- Multilingual features enabled on the site
- Target languages configured in SharePoint
PowerShell Script
# PnP PowerShell script to create French translation variants for all pages in a folder
param(
[Parameter(Mandatory=$false)]
[string]$SiteUrl = "https://tenantName.sharepoint.com/sites/siteName",
[Parameter(Mandatory=$false)]
[string]$ClientId = "",
[Parameter(Mandatory=$false)]
[string]$FolderName = "", # Define anem of target folder
[Parameter(Mandatory=$false)]
[int]$FrenchLCID = 1036 # Define language
)
# Initialize CSV logging
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$csvLogFile = Join-Path $scriptPath "FrenchTranslation_Guidelines_$timestamp.csv"
# Create CSV log array
$logEntries = @()
Write-Host "=== French Translation for Guidelines Folder ===" -ForegroundColor Cyan
Write-Host "Target Folder: $FolderName" -ForegroundColor Yellow
Write-Host "CSV log will be saved to: $csvLogFile" -ForegroundColor Cyan
# Connect to SharePoint site
try {
Write-Host "`nConnecting to SharePoint site: $SiteUrl" -ForegroundColor Yellow
Write-Host "Using Client ID: $ClientId" -ForegroundColor Yellow
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientId -Interactive
Write-Host "Connected successfully!" -ForegroundColor Green
}
catch {
Write-Error "Failed to connect to SharePoint: $($_.Exception.Message)"
exit 1
}
# Get the Site Pages library
Write-Host "`nGetting Site Pages library..." -ForegroundColor Yellow
$Library = Get-PnPList -Identity "Site Pages"
if ($null -eq $Library) {
Write-Host "Error: Could not find the Site Pages library" -ForegroundColor Red
exit 1
}
Write-Host "Site Pages library found. Server relative URL: $($Library.RootFolder.ServerRelativeUrl)" -ForegroundColor Green
# Get the target folder
Write-Host "`nGetting folder '$FolderName'..." -ForegroundColor Yellow
try {
$FolderServerRelativeUrl = $Library.RootFolder.ServerRelativeUrl + "/" + $FolderName
$Folder = Get-PnPFolder -Url $FolderServerRelativeUrl -ErrorAction Stop
Write-Host "✓ Found folder: $($Folder.ServerRelativeUrl)" -ForegroundColor Green
}
catch {
Write-Host "✗ Could not find folder '$FolderName': $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Expected folder path: $FolderServerRelativeUrl" -ForegroundColor Yellow
exit 1
}
# Get all pages from the specific folder
Write-Host "`nGetting all pages from the '$FolderName' folder..." -ForegroundColor Yellow
$FolderPages = Get-PnPListItem -List $Library -FolderServerRelativeUrl $Folder.ServerRelativeUrl -PageSize 1000
# Filter for .aspx pages only and exclude duplicates but don't exclude French translations upfront
$pagesToTranslate = @()
$processedPages = @{}
foreach ($item in $FolderPages)
if ($item.FieldValues.FileLeafRef -like "*.aspx") {
$fileName = $item.FieldValues.FileLeafRef
$fullPath = $item.FieldValues.FileRef
# Only skip obvious French translation files (with _fr suffix)
# We'll check the translation status individually for each page later
if ($fileName -match "_fr\.aspx$") {
Write-Host "⚠ Skipping obvious French translation file: $fileName" -ForegroundColor Yellow
continue
}
# Skip duplicates (same filename)
if ($processedPages.ContainsKey($fileName)) {
Write-Host "⚠ Skipping duplicate page: $fileName" -ForegroundColor Yellow
continue
}
$pageObj = [PSCustomObject]@{
Name = $fileName
Title = if ($item.FieldValues.Title) { $item.FieldValues.Title } else { $fileName }
ServerRelativeUrl = $fullPath
FolderPath = $FolderName
PageId = $item.Id
}
$pagesToTranslate += $pageObj
$processedPages[$fileName] = $true
}
}
if ($pagesToTranslate.Count -eq 0) {
Write-Host "No .aspx pages found in the '$FolderName' folder (or all pages already have French translations)" -ForegroundColor Yellow
exit 0
}
Write-Host "Found $($pagesToTranslate.Count) pages to translate in folder '$FolderName'" -ForegroundColor Green
# Display pages to be translated
Write-Host "`nPages to translate to French (LCID: $FrenchLCID):" -ForegroundColor Cyan
$pagesToTranslate | ForEach-Object { Write-Host "- $($_.Name)" -ForegroundColor White }
# Confirm before proceeding
$confirm = Read-Host "`nProceed with creating French translations for all pages in '$FolderName'? (y/N)"
if ($confirm.ToLower() -ne 'y') {
Write-Host "Operation cancelled" -ForegroundColor Yellow
exit 0
}
# Create French translations for each page
Write-Host "`nStarting translation process..." -ForegroundColor Yellow
$successCount = 0
$failCount = 0
$skipCount = 0
$counter = 0
foreach ($page in $pagesToTranslate) {
$counter++
$startTime = Get-Date
Write-Progress -Activity "Creating French Translations" -Status "Processing page $counter of $($pagesToTranslate.Count): $($page.Name)" -PercentComplete (($counter / $pagesToTranslate.Count) * 100)
$logEntry = [PSCustomObject]@{
PageName = $page.Name
PageTitle = $page.Title
PageUrl = $page.ServerRelativeUrl
FolderPath = $page.FolderPath
FrenchLCID = $FrenchLCID
Status = ""
ErrorMessage = ""
ProcessedDateTime = $startTime.ToString("yyyy-MM-dd HH:mm:ss")
ProcessingTimeSeconds = 0
}
try {
Write-Host "`n[$counter/$($pagesToTranslate.Count)] Processing: $($page.Name)" -ForegroundColor Yellow
# Extract the relative path without the leading site path for subfolder pages
# Convert from: /sites/devintranet/SitePages/Guidelines/PageName.aspx
# To: Guidelines/PageName (without extension and leading paths)
$pagePath = $page.ServerRelativeUrl
$sitePagesPart = "/SitePages/"
if ($pagePath.Contains($sitePagesPart)) {
# Get everything after /SitePages/
$relativePath = $pagePath.Substring($pagePath.IndexOf($sitePagesPart) + $sitePagesPart.Length)
# Remove .aspx extension
$pageIdentity = $relativePath -replace "\.aspx$", ""
} else {
# Fallback to just the page name
$pageIdentity = $page.Name -replace "\.aspx$", ""
}
Write-Host "Using page identity: '$pageIdentity'" -ForegroundColor Cyan
# Check if French translation already exists by examining the _SPTranslatedLanguages field
$translationExists = $false
$skipReason = ""
try {
# Get the list item for this page to check translation fields
$pageListItem = Get-PnPListItem -List $Library -Id $page.PageId
if ($pageListItem) {
# Check the _SPTranslatedLanguages field
$translatedLanguages = $pageListItem.FieldValues["_SPTranslatedLanguages"]
if ($translatedLanguages) {
Write-Host "Found translated languages data: $translatedLanguages" -ForegroundColor Cyan
# Check if French (fr-fr or fr) is already in the translated languages
if ($translatedLanguages -match "fr-fr" -or $translatedLanguages -match '"fr"') {
$translationExists = $true
$skipReason = "French translation already exists (found in _SPTranslatedLanguages field)"
}
} else {
Write-Host "No translated languages found for this page" -ForegroundColor Cyan
}
# Also check if this page is marked as a translation itself
$isTranslation = $pageListItem.FieldValues["_SPIsTranslation"]
$translationLanguage = $pageListItem.FieldValues["_SPTranslationLanguage"]
if ($isTranslation -eq $true -and $translationLanguage -match "fr") {
$translationExists = $true
$skipReason = "This page is already a French translation of another page"
}
}
}
catch {
Write-Host "Could not check translation status, proceeding with creation attempt..." -ForegroundColor Yellow
# Continue with translation attempt if we can't check the fields
}
if ($translationExists) {
Write-Host "⚠ SKIPPED: $skipReason" -ForegroundColor Yellow
$logEntry.Status = "Skipped"
$logEntry.ErrorMessage = $skipReason
$logEntry.ProcessingTimeSeconds = [math]::Round(((Get-Date) - $startTime).TotalSeconds, 2)
$skipCount++
} else {
# No French translation exists, proceed with creation
Write-Host "Creating French translation..." -ForegroundColor Cyan
Set-PnPPage -Identity $pageIdentity -Translate -TranslationLanguageCodes $FrenchLCID
$endTime = Get-Date
$processingTime = ($endTime - $startTime).TotalSeconds
$logEntry.Status = "Success"
$logEntry.ProcessingTimeSeconds = [math]::Round($processingTime, 2)
Write-Host "✓ French translation variant created for: $($page.Name)" -ForegroundColor Green
$successCount++
}
}
catch {
$endTime = Get-Date
$processingTime = ($endTime - $startTime).TotalSeconds
$logEntry.Status = "Failed"
$logEntry.ErrorMessage = $_.Exception.Message
$logEntry.ProcessingTimeSeconds = [math]::Round($processingTime, 2)
Write-Host "✗ Failed to create French translation for $($page.Name): $($_.Exception.Message)" -ForegroundColor Red
$failCount++
}
# Add log entry to array
$logEntries += $logEntry
}
# Clear progress bar
Write-Progress -Activity "Creating French Translations" -Completed
# Export to CSV
Write-Host "`nExporting results to CSV..." -ForegroundColor Yellow
try {
$logEntries | Export-Csv -Path $csvLogFile -NoTypeInformation -Encoding UTF8
Write-Host "✓ CSV log file created: $csvLogFile" -ForegroundColor Green
}
catch {
Write-Warning "Failed to create CSV log file: $($_.Exception.Message)"
}
# Summary
Write-Host "`n" + "="*70 -ForegroundColor Cyan
Write-Host "FRENCH TRANSLATION SUMMARY - GUIDELINES FOLDER" -ForegroundColor Cyan
Write-Host "="*70 -ForegroundColor Cyan
Write-Host "Folder processed: $FolderName" -ForegroundColor White
Write-Host "Total pages found: $($pagesToTranslate.Count)" -ForegroundColor White
Write-Host "Successfully translated: $successCount pages" -ForegroundColor Green
Write-Host "Skipped (already translated): $skipCount pages" -ForegroundColor Yellow
Write-Host "Failed translations: $failCount pages" -ForegroundColor Red
Write-Host "Processing completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor White
if ($successCount -gt 0) {
Write-Host "`n✓ French page variants have been created with proper translation relationships" -ForegroundColor Green
Write-Host "✓ Language switcher should now be available on translated pages" -ForegroundColor Green
Write-Host "✓ You can now add French content to the translated pages" -ForegroundColor Green
}
# Show CSV log file location
Write-Host "`n📋 Detailed log available at: $csvLogFile" -ForegroundColor Magenta
# Show CSV content summary
Write-Host "`nCSV Log Summary:" -ForegroundColor Cyan
Write-Host "- Total Pages Processed: $($logEntries.Count)" -ForegroundColor White
Write-Host "- Successful: $(($logEntries | Where-Object {$_.Status -eq 'Success'}).Count)" -ForegroundColor Green
Write-Host "- Skipped: $(($logEntries | Where-Object {$_.Status -eq 'Skipped'}).Count)" -ForegroundColor Yellow
Write-Host "- Failed: $(($logEntries | Where-Object {$_.Status -eq 'Failed'}).Count)" -ForegroundColor Red
if ($logEntries | Where-Object {$_.Status -eq 'Failed'}) {
Write-Host "`nFailed Pages:" -ForegroundColor Red
$failedEntries = $logEntries | Where-Object {$_.Status -eq 'Failed'}
$failedEntries | ForEach-Object {
Write-Host " - $($_.PageName): $($_.ErrorMessage)" -ForegroundColor Yellow
}
}
if ($logEntries | Where-Object {$_.Status -eq 'Skipped'}) {
Write-Host "`nSkipped Pages:" -ForegroundColor Yellow
$skippedEntries = $logEntries | Where-Object {$_.Status -eq 'Skipped'}
$skippedEntries | ForEach-Object {
Write-Host " - $($_.PageName): $($_.ErrorMessage)" -ForegroundColor Cyan
}
}
Write-Host "`nScript completed!" -ForegroundColor Cyan
Write-Host "You can now navigate to the folder in SharePoint to see the French variants." -ForegroundColor Green
# Disconnect
Disconnect-PnPOnline
Usage Notes
- Update the site URL to match your SharePoint environment
- Modify the
$folderPathvariable to specify your target folder within Site Pages - Update the
$targetLanguagesarray with your required language codes (e.g., "es" for Spanish, "fr" for French) - Ensure multilingual features are enabled on your SharePoint site
- The script creates translation page structure but requires manual content translation
- Existing translations are detected and skipped to prevent duplicates
- Test in a development environment before production deployment
- Monitor the output for any errors during page creation
Common Language Codes
- es: Spanish
- fr: French
- de: German
- it: Italian
- pt: Portuguese
- nl: Dutch
- ja: Japanese
- zh: Chinese
Important Considerations
- This script creates the translation page structure only
- Content translation must be done manually after page creation
- Ensure proper permissions for multilingual content management
- Review SharePoint's translation workflow requirements
- Consider using professional translation services for content