Multilingual SharePoint intranets require more than translated page content. For search filters, taxonomy-driven metadata, and managed metadata columns to display correctly in each language, the terms themselves need translated labels in the SharePoint term store. This post covers how to provision a multilingual taxonomy programmatically using PnP PowerShell — including the parameter issues you will hit along the way and how to work around them.
This post accompanies the Create Multilingual Term Store script on the scripts page.
Why Scripted Taxonomy Provisioning Matters
If you are building a multilingual intranet — particularly one using PnP Modern Search with taxonomy-based filters — you need your term store to carry translated labels for each term. Without them, filter labels and search result metadata display in the default language regardless of the user's language preference.
Doing this manually through the SharePoint term store UI is feasible for small taxonomies. For anything beyond a handful of terms across multiple term sets and languages, manual entry becomes error-prone, time-consuming, and impossible to reproduce consistently across environments. A scripted approach driven by a JSON configuration file gives you repeatability, auditability, and the ability to deploy the same taxonomy to dev, test, and production with a single command.
What the Script Does
The script provisions three layers of taxonomy structure:
- Term group — the top-level container, equivalent to a department or project namespace in the term store
- Term sets — logical groupings of related terms within a group (Departments, Document Types, Locations, and so on)
- Terms — the individual taxonomy values, each with a default English label and optional translated labels per locale
Everything is driven by a JSON configuration file. The term group name and description live at the top level of the JSON, which means the same script binary can provision entirely different taxonomies by swapping the JSON — useful when you are managing multiple environments or client deployments.
The script checks for existing term groups, term sets, and terms before attempting to create them, and skips anything that already exists. Re-running it against a partially provisioned taxonomy picks up where it left off without duplicating content.
All operations are logged to a timestamped CSV file alongside the console output, giving you a full audit trail of every action taken.
The PnP PowerShell Gotchas
This is where most documentation falls short. Getting translations applied via PnP PowerShell is not as straightforward as it should be, and the issues vary by module version.
The -Label Parameter Does Not Exist in PnP.PowerShell 3.x
The first instinct when applying a translated label to a term is to reach for something like:
Set-PnPTerm -Identity $term.Id -Label "Ressources Humaines" -Lcid 1036
In PnP.PowerShell 3.x, -Label is not a valid parameter. Running it throws:
A parameter cannot be found that matches parameter name 'Label'.
You can verify what parameters are actually available in your version:
(Get-Command Set-PnPTerm).Parameters.Keys
In 3.x the correct parameter is -Name combined with -Lcid. This sets the term's display name for the specified locale:
Set-PnPTerm `
-Identity $term.Id `
-TermSet "Departments" `
-TermGroup "Intranet_Taxonomy" `
-Name "Ressources Humaines" `
-Lcid 1036
Term Store Timing Issues
SharePoint's term store API is asynchronous in ways that are not always obvious. Two timing issues caused consistent failures during development.
New term groups need time to provision. After New-PnPTermGroup returns successfully, the group is not immediately available for Get-PnPTermGroup. Attempting to fetch it too quickly throws an index error. The fix is a retry loop that re-fetches the group with a delay between attempts rather than a fixed sleep:
$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-Host "Waiting for term group to provision... attempt $retries" -ForegroundColor Yellow
}
}
New terms need time before labels can be applied. After New-PnPTerm creates a term, calling Set-PnPTerm immediately to apply a translation returns "The specified term does not exist" — even though the term was just created successfully. The fix is to re-fetch the term after creation with a short sleep, rather than using the object returned by New-PnPTerm:
$null = New-PnPTerm -TermSet $termSetName -TermGroup $TermGroupName -Name $termName -Lcid 1033
Start-Sleep -Seconds 3
$targetTerm = Get-PnPTerm -TermSet $termSetName -TermGroup $TermGroupName -Identity $termName
The re-fetched object has a fully hydrated Id property that Set-PnPTerm -Identity can reliably use.
Save Conflicts on Translation Writes
Writing multiple translated labels in quick succession can trigger save conflicts in the term store:
TermStoreErrorCodeEx:Term update failed because of save conflict.
A two-second sleep between each label write resolves this reliably for taxonomies of typical size.
The JSON Configuration
The JSON file drives the entire provisioning run. The structure is straightforward:
{
"TermGroupName": "Intranet_Taxonomy",
"TermGroupDescription": "Terms used for the Intranet Taxonomy",
"TermGroups": [
{
"TermSet": "Departments",
"Terms": [
{
"Name": "Human Resources",
"Translations": {
"1036": "Ressources Humaines",
"3082": "Recursos Humanos"
}
},
{
"Name": "Finance",
"Translations": {
"1036": "Finance"
}
}
]
}
]
}
A few things worth noting about the schema:
TermGroupName in the JSON takes precedence over the script variable. If TermGroupName is absent from the JSON, the script falls back to the variable defined at the top of the script file. If neither is set, the script throws a clear error before attempting any provisioning.
Terms support both plain strings and objects. If a term needs no translations, you can include it as a plain string rather than an object:
"Terms": [
"Finance",
{
"Name": "Human Resources",
"Translations": { "1036": "Ressources Humaines" }
}
]
This keeps the JSON clean for taxonomies where only some terms need translation.
LCID codes map to specific locales. Common ones for European deployments:
| LCID | Language |
|---|---|
| 1033 | English (default) |
| 1036 | French |
| 3082 | Spanish (Spain) |
| 1031 | German |
| 1040 | Italian |
| 1043 | Dutch |
Prerequisites
Before running the script:
Target languages must be enabled in the term store. In SharePoint Admin Center → More features → Term store → Language Settings, add each language you intend to use. If the language is not enabled at the term store level, label writes for that LCID will fail silently or throw errors.
SharePoint Administrator or Term Store Administrator permissions. Standard site owner permissions are not sufficient for term store write operations at the tenant level.
Running the Script
The script prompts for the JSON file path at runtime. You will see console output for each operation and a CSV log file written to the same directory as the script.
A successful run looks like:
Connecting to SharePoint...
Connected successfully
Using TermGroupName from JSON: 'Intranet_Taxonomy'
Checking term group 'Intranet_Taxonomy'...
Found existing term group 'Intranet_Taxonomy'
Processing term set: 'Departments'
Found existing term set: 'Departments'
Processing term: 'Human Resources'
Term already exists: 'Human Resources'
Added label 'Ressources Humaines' (LCID 1036) to 'Human Resources'
Verifying the Results
After the script completes, verify translations in the SharePoint Admin Center → More features → Term store. Navigate to your term group → term set → select a term → the General tab shows the Translations and synonyms table with each language and its label.
You should see your default English label and each translated label listed against the correct language row.
Using the Taxonomy in PnP Modern Search
Provisioning the taxonomy is the first step. Before PnP Modern Search can use translated labels for filter display, several more steps are required:
- Create a site column of type Managed Metadata and bind it to your term set
- Apply the site column to the relevant content type or document library
- Tag content — pages or items must have terms applied to the column
- Wait for a crawl — once the crawler visits the site and indexes the tagged content, it automatically creates a crawled property named
ows_taxId_YourColumn(where YourColumn matches your site column's internal name) - Map the crawled property — in SharePoint Admin Center → Search → Manage Search Schema, map
ows_taxId_YourColumnto a clean RefinableString managed property with no other mappings - Trigger a reindex — so the mapping is applied to already-indexed content
- Configure PnP Modern Search — set the RefinableString as the Tags slot in the data source with localization enabled, and use the corresponding
{{slot item @root.slots.Tags}}reference in your card template
The ows_taxId_ crawled property is what carries the term GUID and locale label information that PnP Modern Search needs for multilingual filter display. The plain ows_YourColumn crawled property carries the default label only and should be mapped to a separate RefinableString if needed for non-localised display.
The full configuration for multilingual PnP Modern Search — managed property mapping, result type card templates, and filter localization — is covered in the PnP Modern Search multilingual series on the technical pages.