Collaborate, Innovate, Automate

Create Multilingual Term Store

This PnP PowerShell script extends the basic term store script with full multilingual support. It reads a JSON configuration file to create term groups, term sets, and terms in SharePoint Online, then applies per-locale translated labels to each term using Set-PnPTerm. Terms and labels are idempotent — the script skips anything that already exists, making it safe to re-run.

The Script

The script handles two layers of configuration. First, the term group name and description can be defined either directly in the script variables or within the JSON file — the JSON takes precedence, so the same script binary can provision different taxonomies by swapping the JSON. Second, terms support both plain string format (name only) and object format (name plus a Translations map), so you can mix translated and non-translated terms within the same term set.

Each translated label is applied via Set-PnPTerm with the target LCID. A short delay is introduced between label writes to avoid throttling. All operations — connections, group/set/term creation, label application, and disconnection — are written to a timestamped CSV log file alongside the console output.

The JSON

The JSON file drives the entire provisioning run. The top-level TermGroupName and TermGroupDescription fields set the term group. Under TermGroups, each entry defines a TermSet name and its Terms array. Each term object has a Name (the default English label) and an optional Translations object where keys are LCID codes and values are the translated labels. The example file uses LCID 1036 for French — replace or extend these with any locale your tenant supports (e.g. 3082 for Spanish, 1031 for German).

Prerequisites

PowerShell Script

# ============================================================
# SharePoint Taxonomy Management Script — with Translations
# Creates term groups, term sets, terms, and per-locale labels
# TermGroupName and TermGroupDescription can be set in the JSON
# or fall back to the variables defined below.
# ============================================================

$AdminCenterURL = "https://tenantName-admin.sharepoint.com"
$ClientId        = ""

# Fallback values — used if not present in JSON
$TermGroupName        = ""
$TermGroupDescription = ""

# Default LCID for term creation (English)
$DefaultLcid = 1033

# ── Logging ──────────────────────────────────────────────────
$ScriptPath = $PSScriptRoot
if (!$ScriptPath) { $ScriptPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition }
$LogFile = Join-Path $ScriptPath "TermStoreCreation_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"

"Timestamp,Action,ItemType,ItemName,Status,Message" | Out-File -FilePath $LogFile -Encoding UTF8

function Write-Log {
    param(
        [string]$Message,
        [string]$Color    = "White",
        [string]$Action   = "",
        [string]$ItemType = "",
        [string]$ItemName = "",
        [string]$Status   = ""
    )
    $TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    Write-Host $Message -ForegroundColor $Color
    [PSCustomObject]@{
        Timestamp = $TimeStamp
        Action    = $Action
        ItemType  = $ItemType
        ItemName  = $ItemName
        Status    = $Status
        Message   = $Message
    } | Export-Csv -Path $LogFile -Append -NoTypeInformation
}

# ── Term translation helper ───────────────────────────────────
function Add-TermTranslations {
    param(
        [object]$Term,
        [string]$TermSetName,
        [string]$TermGroupName,
        [hashtable]$Translations
    )

    foreach ($lcidKey in $Translations.Keys) {
        $lcid       = [int]$lcidKey
        $labelValue = $Translations[$lcidKey]

        try {
            Set-PnPTerm `
                -Identity  $Term.Id `
                -TermSet   $TermSetName `
                -TermGroup $TermGroupName `
                -Name      $labelValue `
                -Lcid      $lcid `
                -ErrorAction Stop

            Write-Log `
                -Message  "  Added label '$labelValue' (LCID $lcid) to '$($Term.Name)'" `
                -Color    "Green" `
                -Action   "AddLabel" `
                -ItemType "TermLabel" `
                -ItemName "$($Term.Name) [$lcid]" `
                -Status   "Success"
        }
        catch {
            Write-Log `
                -Message  "  Failed to add label '$labelValue' (LCID $lcid) to '$($Term.Name)': $($_.Exception.Message)" `
                -Color    "Red" `
                -Action   "AddLabel" `
                -ItemType "TermLabel" `
                -ItemName "$($Term.Name) [$lcid]" `
                -Status   "Error"
        }

        Start-Sleep -Seconds 2
    }
}

# ── Main ─────────────────────────────────────────────────────
try {
    Write-Log -Message "Connecting to SharePoint..." -Color "Cyan" -Action "Connect" -ItemType "SharePoint" -ItemName $AdminCenterURL -Status "Started"
    Connect-PnPOnline -Url $AdminCenterURL -Interactive -ClientId $ClientId
    Write-Log -Message "Connected successfully" -Color "Green" -Action "Connect" -ItemType "SharePoint" -ItemName $AdminCenterURL -Status "Success"

    # ── Read JSON ────────────────────────────────────────────
    $jsonPath     = Read-Host "Enter the path to your JSON file"
    $jsonContent  = Get-Content -Path $jsonPath -Raw
    $taxonomyData = $jsonContent | ConvertFrom-Json

    # ── Override group name/description from JSON if present ─
    if ($taxonomyData.TermGroupName) {
        $TermGroupName = $taxonomyData.TermGroupName
        Write-Log -Message "Using TermGroupName from JSON: '$TermGroupName'" -Color "Cyan" -Action "Config" -ItemType "TermGroup" -ItemName $TermGroupName -Status "FromJSON"
    }
    if ($taxonomyData.TermGroupDescription) {
        $TermGroupDescription = $taxonomyData.TermGroupDescription
    }

    # Guard — fail early if no term group name resolved
    if (-not $TermGroupName) {
        throw "TermGroupName must be provided in the JSON file."
    }

    # ── Term Group ───────────────────────────────────────────
    Write-Log -Message "Checking term group '$TermGroupName'..." -Color "Cyan" -Action "CheckGroup" -ItemType "TermGroup" -ItemName $TermGroupName -Status "Started"
    try {
        $termGroup = Get-PnPTermGroup -Identity $TermGroupName -ErrorAction Stop
        Write-Log -Message "Found existing term group '$TermGroupName'" -Color "Green" -Action "CheckGroup" -ItemType "TermGroup" -ItemName $TermGroupName -Status "Exists"
    }
    catch {
        try {
            $termGroup = New-PnPTermGroup -Name $TermGroupName -Description $TermGroupDescription -ErrorAction Stop
            Write-Log -Message "Created term group '$TermGroupName'" -Color "Green" -Action "CreateGroup" -ItemType "TermGroup" -ItemName $TermGroupName -Status "Created"

            # Retry re-fetch until SharePoint is ready
            $retries = 0
            $termGroup = $null
            while (-not $termGroup -and $retries -lt 5) {
                Start-Sleep -Seconds 10
                try {
                    $termGroup = Get-PnPTermGroup -Identity $TermGroupName -ErrorAction Stop
                }
                catch {
                    $retries++
                    Write-Log -Message "Waiting for term group to provision... attempt $retries" -Color "Yellow" -Action "CheckGroup" -ItemType "TermGroup" -ItemName $TermGroupName -Status "Retrying"
                }
            }

            if (-not $termGroup) {
                throw "Term group '$TermGroupName' could not be retrieved after 5 attempts."
            }
        }
        catch {
            Write-Log -Message "Error creating term group '$TermGroupName': $($_.Exception.Message)" -Color "Red" -Action "CreateGroup" -ItemType "TermGroup" -ItemName $TermGroupName -Status "Error"
            throw
        }
    }

    # ── Term Sets & Terms ────────────────────────────────────
    foreach ($termSetGroup in $taxonomyData.TermGroups) {
        $termSetName = $termSetGroup.TermSet

        Write-Log -Message "Processing term set: '$termSetName'" -Color "Cyan" -Action "ProcessTermSet" -ItemType "TermSet" -ItemName $termSetName -Status "Started"

        # Get or create term set
        try {
            $termSet = Get-PnPTermSet -Identity $termSetName -TermGroup $TermGroupName -ErrorAction Stop
            Write-Log -Message "Found existing term set: '$termSetName'" -Color "Green" -Action "CheckTermSet" -ItemType "TermSet" -ItemName $termSetName -Status "Exists"
        }
        catch {
            try {
                $termSet = New-PnPTermSet -Name $termSetName -TermGroup $TermGroupName -Lcid $DefaultLcid -ErrorAction Stop
                Write-Log -Message "Created term set: '$termSetName'" -Color "Green" -Action "CreateTermSet" -ItemType "TermSet" -ItemName $termSetName -Status "Created"
                Start-Sleep -Seconds 3
            }
            catch {
                Write-Log -Message "Error creating term set '$termSetName': $($_.Exception.Message)" -Color "Red" -Action "CreateTermSet" -ItemType "TermSet" -ItemName $termSetName -Status "Error"
                continue
            }
        }

        # ── Terms ────────────────────────────────────────────
        foreach ($termEntry in $termSetGroup.Terms) {

            # Support both plain strings and objects with Translations
            if ($termEntry -is [string]) {
                $termName     = $termEntry
                $translations = @{}
            }
            else {
                $termName     = $termEntry.Name
                $translations = @{}
                if ($termEntry.Translations) {
                    $termEntry.Translations.PSObject.Properties | ForEach-Object {
                        $translations[$_.Name] = $_.Value
                    }
                }
            }

            Write-Log -Message "Processing term: '$termName'" -Color "Cyan" -Action "ProcessTerm" -ItemType "Term" -ItemName $termName -Status "Started"

            try {
                $existingTerm = Get-PnPTerm -TermSet $termSetName -TermGroup $TermGroupName -Identity $termName -ErrorAction SilentlyContinue

                if ($existingTerm) {
                    Write-Log -Message "Term already exists: '$termName'" -Color "Yellow" -Action "CheckTerm" -ItemType "Term" -ItemName $termName -Status "Exists"
                    $targetTerm = $existingTerm
                }
                else {
                    $null = New-PnPTerm -TermSet $termSetName -TermGroup $TermGroupName -Name $termName -Lcid $DefaultLcid -ErrorAction Stop
                    Write-Log -Message "Created term: '$termName'" -Color "Green" -Action "CreateTerm" -ItemType "Term" -ItemName $termName -Status "Created"
                    Start-Sleep -Seconds 3
                    # Re-fetch to get fully hydrated term object
                    $targetTerm = Get-PnPTerm -TermSet $termSetName -TermGroup $TermGroupName -Identity $termName -ErrorAction Stop
                }

                # Add translations if present
                if ($translations.Count -gt 0) {
                    Add-TermTranslations -Term $targetTerm -TermSetName $termSetName -TermGroupName $TermGroupName -Translations $translations
                }
            }
            catch {
                Write-Log -Message "Error processing term '$termName': $($_.Exception.Message)" -Color "Red" -Action "ProcessTerm" -ItemType "Term" -ItemName $termName -Status "Error"
            }
        }

        Start-Sleep -Seconds 3
    }

    Write-Log -Message "All term sets processed successfully." -Color "Green" -Action "Script" -ItemType "Global" -ItemName "Complete" -Status "Success"
}
catch {
    Write-Log -Message "Critical error: $($_.Exception.Message)" -Color "Red" -Action "Script" -ItemType "Global" -ItemName "Error" -Status "Critical"
}
finally {
    if (Get-PnPConnection -ErrorAction SilentlyContinue) {
        Write-Log -Message "Disconnecting from SharePoint..." -Color "Cyan" -Action "Disconnect" -ItemType "SharePoint" -ItemName $AdminCenterURL -Status "Started"
        Disconnect-PnPOnline
        Write-Log -Message "Disconnected successfully" -Color "Green" -Action "Disconnect" -ItemType "SharePoint" -ItemName $AdminCenterURL -Status "Success"
    }
    Write-Log -Message "Log saved to: $LogFile" -Color "Yellow" -Action "Script" -ItemType "Global" -ItemName "LogFile" -Status "Complete"
}

JSON Configuration File

{
  "TermGroupName": "YOUR GROUP NAME",
  "TermGroupDescription": "Terms used for the new Intranet Taxonomy",
  "TermGroups": [
    {
      "TermSet": "Departments",
      "Terms": [
        {
          "Name": "Information Technology",
          "Translations": {
            "1036": "Technologies de l'Information"
          }
        },
        {
          "Name": "Human Resources",
          "Translations": {
            "1036": "Ressources Humaines"
          }
        },
        {
          "Name": "Finance",
          "Translations": {
            "1036": "Finance"
          }
        },
        {
          "Name": "Sales",
          "Translations": {
            "1036": "Ventes"
          }
        },
        {
          "Name": "Marketing",
          "Translations": {
            "1036": "Marketing"
          }
        },
        {
          "Name": "Operations",
          "Translations": {
            "1036": "Opérations"
          }
        }
      ]
    },
    {
      "TermSet": "Document Types",
      "Terms": [
        {
          "Name": "Policy",
          "Translations": {
            "1036": "Politique"
          }
        },
        {
          "Name": "Procedure",
          "Translations": {
            "1036": "Procédure"
          }
        },
        {
          "Name": "Training Material",
          "Translations": {
            "1036": "Matériel de Formation"
          }
        },
        {
          "Name": "Project Document",
          "Translations": {
            "1036": "Document de Projet"
          }
        },
        {
          "Name": "Meeting Minutes",
          "Translations": {
            "1036": "Compte Rendu de Réunion"
          }
        },
        {
          "Name": "Report",
          "Translations": {
            "1036": "Rapport"
          }
        }
      ]
    },
    {
      "TermSet": "Locations",
      "Terms": [
        {
          "Name": "Valencia",
          "Translations": {
            "1036": "Valence"
          }
        },
        {
          "Name": "Madrid",
          "Translations": {
            "1036": "Madrid"
          }
        },
        {
          "Name": "Barcelona",
          "Translations": {
            "1036": "Barcelone"
          }
        },
        {
          "Name": "Seville",
          "Translations": {
            "1036": "Séville"
          }
        },
        {
          "Name": "Bilbao",
          "Translations": {
            "1036": "Bilbao"
          }
        }
      ]
    },
    {
      "TermSet": "Project Status",
      "Terms": [
        {
          "Name": "Planning",
          "Translations": {
            "1036": "Planification"
          }
        },
        {
          "Name": "In Progress",
          "Translations": {
            "1036": "En Cours"
          }
        },
        {
          "Name": "On Hold",
          "Translations": {
            "1036": "En Attente"
          }
        },
        {
          "Name": "Completed",
          "Translations": {
            "1036": "Terminé"
          }
        },
        {
          "Name": "Cancelled",
          "Translations": {
            "1036": "Annulé"
          }
        }
      ]
    }
  ]
}

Usage Notes