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
- PnP PowerShell module installed
- SharePoint administrator or Term Store administrator permissions
- Target languages enabled in the SharePoint term store language settings
- Planned taxonomy structure in a JSON file (see example below)
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
- Target languages must be enabled in the SharePoint term store language settings before running
- LCID
1036= French — replace or add entries for other locales (e.g.3082= Spanish,1031= German) - The script is idempotent — re-running it will skip existing terms and labels without error
- A small delay is added between label writes to avoid SharePoint throttling on large taxonomies
- Check the generated CSV log file for a full audit trail of every action taken