Installing and Configuring Additional Languages during Windows Autopilot

I was experimenting with different ways to get additional languages installed and configured during Windows Autopilot and it proved to be an interesting challenge. The following is what I settled on in the end and what produced the results that I wanted.

Here were my particular requirements, but you can customize this per your own need:

  • The primary language should be English (United Kingdom)
  • An additional secondary language of English (United States)
  • Display language should be English (United Kingdom)
  • Default input override should be English (United Kingdom)
  • System locale should be English (United Kingdom)
  • The administrative defaults for the Welcome screen and New user accounts must have a display language, input language, format and location matching the primary language (UK / UK English)
  • All optional features for the primary language should be installed (handwriting, optical character recognition, etc)

To achieve this, I basically created three elements:

  1. Installed the Local Experience Pack for English (United Kingdom)
  2. Deployed a powershell script running in administrative context that sets the administrative language defaults and system locale
  3. Deployed a powershell script running in user context that sets the correct order in the user preferred languages list

This was deployed during Autopilot to a Windows 10 1909 (United States) base image.

Local Experience Packs

Local Experience Packs (LXPs) are the modern way to go for installing additional languages since Windows 10 1803. These are published to the Microsoft Store and are automatically updated. They also install more quickly that the traditional cab language packs that you would install with DISM.

LXPs are available in the Microsoft Store for Business, so they can be synced with Intune and deployed as apps. However, the problem with using LXPs as apps during Autopilot is the order of things. The LXP needs to be installed before the PowerShell script that configures the language defaults runs, and since PowerShell scripts are not currently tracked in the ESP, and apps are the last thing to install in the device setup phase, the scripts will very likely run before the app is installed.

To get around that, I decided to get the LXP from the Volume Licensing Center instead. Then I uploaded this to a storage account in Azure, where it gets downloaded and installed by the PowerShell script. This way I can control the order and be sure the LXP is installed before making configuration changes.

When downloading from the VLC, be sure to select the Multilanguage option:

Then get the highlighted ISO. The 1903 LXPs work for 1909 also.

Get the applicable appx file and the license file from the ISO, zip them, and upload the zip file into an Azure Storage account.

When uploading the zip file, be sure to choose the Account Key authentication type:

Once uploaded, click on the blob and go to the Generate SAS page. Choose Read permissions, set an appropriate expiry date, then copy the Blob SAS URL. You will need this to download the file with PowerShell.

Administrative PowerShell Script

Now lets create a PowerShell script that will:

  • Download and install the Local Experience Pack
  • Install any optional features for the language
  • Configure language and regional settings and defaults

Here’s the script I’m using for that.

# Admin-context script to set the administrative language defaults, system locale and install optional features for the primary language
# Language codes
$PrimaryLanguage = "en-GB"
$SecondaryLanguage = "en-US"
$PrimaryInputCode = "0809:00000809"
$SecondaryInputCode = "0409:00000409"
$PrimaryGeoID = "242"
# Enable side-loading
# Required for appx/msix prior to build 18956 (1909 insider)
New-ItemProperty Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock Name AllowAllTrustedApps Value 1 PropertyType DWORD Force
# Provision Local Experience Pack
$BlobURL = "https://mystorageaccount.blob.core.windows.net/mycontainer/en-gb.zip?sp=r&st=2020-03-26T18:02:28Z&se=2050-03-27T00:02:28Z&spr=https&sv=2019-02-02&sr=b&sig=91234567890OYr%2BI0RcryhGFy1DNMlzhfIWbQ%3D"
$DownloadedFile = "$env:LOCALAPPDATA\en-GB.zip"
Try
{
$WebClient = New-Object System.Net.WebClient
$WebClient.DownloadFile($BlobURL, $DownloadedFile)
Unblock-File Path $DownloadedFile ErrorAction SilentlyContinue
Expand-Archive Path $DownloadedFile DestinationPath $env:LOCALAPPDATA Force ErrorAction Stop
Add-AppxProvisionedPackage Online PackagePath "$env:LOCALAPPDATA\en-gb\LanguageExperiencePack.en-gb.Neutral.appx" LicensePath "$env:LOCALAPPDATA\en-gb\License.xml" ErrorAction Stop
Remove-Item Path $DownloadedFile Force ErrorAction SilentlyContinue
}
Catch
{
Write-Host "Failed to install Local Experience Pack: $_"
}
# Install optional features for primary language
$UKCapabilities = Get-WindowsCapability Online | Where {$_.Name -match "$PrimaryLanguage" -and $_.State -ne "Installed"}
$UKCapabilities | foreach {
Add-WindowsCapability Online Name $_.Name
}
# Apply custom XML to set administrative language defaults
$XML = @"
<gs:GlobalizationServices xmlns:gs="urn:longhornGlobalizationUnattend">
<!– user list –>
<gs:UserList>
<gs:User UserID="Current" CopySettingsToDefaultUserAcct="true" CopySettingsToSystemAcct="true"/>
</gs:UserList>
<!– GeoID –>
<gs:LocationPreferences>
<gs:GeoID Value="$PrimaryGeoID"/>
</gs:LocationPreferences>
<gs:MUILanguagePreferences>
<gs:MUILanguage Value="$PrimaryLanguage"/>
<gs:MUIFallback Value="$SecondaryLanguage"/>
</gs:MUILanguagePreferences>
<!– system locale –>
<gs:SystemLocale Name="$PrimaryLanguage"/>
<!– input preferences –>
<gs:InputPreferences>
<gs:InputLanguageID Action="add" ID="$PrimaryInputCode" Default="true"/>
<gs:InputLanguageID Action="add" ID="$SecondaryInputCode"/>
</gs:InputPreferences>
<!– user locale –>
<gs:UserLocale>
<gs:Locale Name="$PrimaryLanguage" SetAsCurrent="true" ResetAllSettings="false"/>
</gs:UserLocale>
</gs:GlobalizationServices>
"@
New-Item Path $env:TEMP Name "en-GB.xml" ItemType File Value $XML Force
$Process = Start-Process FilePath Control.exe ArgumentList "intl.cpl,,/f:""$env:Temp\en-GB.xml""" NoNewWindow PassThru Wait
$Process.ExitCode

A quick walkthrough:

First, I’ve entered the locale IDs for the primary and secondary languages, as well as the keyboard layout hex codes, and finally the Geo location ID for the primary language as variables.

Then we set a registry key to allow side-loading (required for older W10 versions for the install of appx/msix).

Next we download and install the LXP. You’ll need to enter the URL you copied earlier for the Azure blob, and update the zip filename as required, as well as the LXP filename.

Then we install any optional features for the primary language that aren’t already installed.

Then we define the content of an XML file that will be used to set the language and locale preferences. Obviously customize that per your requirement.

Then we save that content to a file and apply it.

Create the PowerShell script in Intune, make sure you don’t run it using the logged on credentials, and deploy it to your Autopilot AAD group.

User PowerShell Script

Now we need to create a very simple script that will run in the user context. This script simply makes sure that the list of preferred languages is in the correct order, as by default it will look like this:

This script will run for each user that logs in. It won’t run immediately so the order may be wrong when you first log in, but it doesn’t take long before it runs. Create the script in Intune, remember to run it using the logged on credentials, and deploy it to your Autopilot AAD group.

# User-context script to set the Language List
# Language codes
$PrimaryLanguage = "en-GB"
$SecondaryLanguage = "en-US"
$PrimaryInputCode = "0809:00000809"
$SecondaryInputCode = "0409:00000409"
# Set preferred languages
$NewLanguageList = New-WinUserLanguageList Language $PrimaryLanguage
$NewLanguageList.Add([Microsoft.InternationalSettings.Commands.WinUserLanguage]::new($SecondaryLanguage))
$NewLanguageList[1].InputMethodTips.Clear()
$NewLanguageList[1].InputMethodTips.Add($PrimaryInputCode)
$NewLanguageList[1].InputMethodTips.Add($SecondaryInputCode)
Set-WinUserLanguageList $NewLanguageList Force

The Result

After running the Autopilot deployment and logging in, everything checks out 🙂