How to completely change Windows 10 language with Intune

In this article we dive into a way to completely switch the language of Windows 10 in a scripted way with the help of Intune and without the need for explicit language cab files. The new language setting will include the Welcome screen and New user defaults as well. This approach is beneficial for further maintenance as we do not need to update language files for newer Windows 10 versions like 20H1.

If you ever installed a new language in Windows 10 you probably have seen the following. You went to the Windows Settings > Language section and clicked on the Add a Windows display language in Microsoft Store:

Then you will see the Store with a lot of available languages:

after clicking on the desired one, it well get downloaded and you see the following message if you would like to active the new language now:

Finally after a logoff and logon you will have a new language for your user.

Seems to be easy and straight forward and the job is done, right? Not in my opinion. We have configured the UI language for the current user but not the UI language of the logon screen for example. I do get the concept of switching the user and not the logon screen to maintain base language and individual user languages, but if I look at my customer base they are not depending on this, they rather depend on a device delivered in english and then they want to switch this completely to german for example. Most devices are used by one user, the primary user. So, the first question I always get with this approach is why is my logon screen still english?

Additionally I find this solution not complete as we have a few more settings to reconfigure a device from e.g. english to german. In my opinion a complete switch should include the Regional Settings, Speech, and Input etc.

What can we do to achieve a comfortable language switch for the user?

A general approach I see is to use the Intune and Microsoft Store for Business (MSfB) integration. This way you add the language pack (online) version to the MSfB and assign it in Intune to the user as available or to a device as required. Assigned to a user would look like this:

If we assign it to the device as a required install, it will get provisioned but never activated (entry Deutsch is added in this example from the required install):

Typically people start to target a PowerShell user script (running in user context) which includes the following code:

Set-WinUILanguageOverride -Language 'de-DE'

This switches the user session to the newly provisioned language pack.

It looks like it accomplishes the goal, but I’ve seen this approach failing when the language pack was not properly registered and the language entry was missing. In that situation the PowerShell command failed also during my tests. Additionally the fact that we belong for the language switch on two independent components (LXP as device assigned package and user PowerShell script which don’t know each other) looked to me that it may break quite easily. I looked for a solution to switch the language and all other necessary settings in one sequence.

The approach from Michael Niehaus used in the Autopilot Branding uses language cab files and the intl.cpl with the xml GlobalizationUnattend file. This would achieve my goal to switch completely as the input xml file for the intl.cpl does have all necessary settings and can be set during system context. Meaning we can install the language cab files at the device setup phase and switch the language. A new user logging on after that will have the correct language. Downside of this approach, I don’t like to maintain the language cab files in the package for every new Windows 10 version.

What is the solution to not rely on language cab files?

The idea is to download the language experience pack (LXP) just in time and reconfigure the device. Microsoft provides a MDM Bridge WMI Provider to execute MDM functions. As Intune is able to trigger an install of an online language experience pack we should be able to call the same MDM install function from the MDM Bridge WMI Provider. There are two great articles to find all necessary information to get going:

The first article describes the usage of the MDM Bridge WMI Provider and the second one describes the actual Configuration Service Provider (CSP) to install Store applications. As the Language Experience Packs are Store applications, this is all we need and this is the same CSP Intune is using to install the LXP. The EnterpriseModernAppManagement CSP has some requirements to “Deploy apps to user from the Store“:

  • The app is assigned to a user Azure Active Directory (AAD) identity in the Store for Business. You can do this directly in the Store for Business or through a management server.
  • The device requires connectivity to the Microsoft Store.
  • Microsoft Store services must be enabled on the device. Note that the UI for the Microsoft Store can be disabled by the enterprise admin.
  • The user must be signed in with their AAD identity.

The is a SyncML example where we can find necessary information how to use the CSP:

<Exec>
   <CmdID>1</CmdID>
          <Item>
            <Target>
              <LocURI>./User/Vendor/MSFT/EnterpriseModernAppManagement/AppInstallation/{PackageFamilyName}/StoreInstall</LocURI>
            </Target>
            <Meta>
                <Format xmlns="syncml:metinf">xml</Format>
            </Meta>
            <Data><Application id="{ProductID}" flags="0" skuid=" "/></Data>
          </Item>
</Exec>

From interest is the part {PackageFamilyName} and StoreInstall. StoreInstall describes our function we have to look for with the MDM Bridge WMI Provider and the {PackageFamilyName} is the Store app we want to install. In the <Data> tag we find the parameter for the StoreInstall function. There we have to lookup the individual parts:

  1. {ProductID}, this value is acquired as a part of the Store for Business management tool.
  2. The value for flags can be “0” or “1”. When using “0” the management tool calls back to the Store for Business sync to assign a user a seat of an application. When using “1” the management tool does not call back in to the Store for Business sync to assign a user a seat of an application. The CSP will claim a seat if one is available.
  3. The skuid is a new parameter that is required. This value is acquired as a part of the Store for Business to management tool sync.

Let’s have a look into the CSP in WMI:

There it is, the WMI class and a StoreInstallMethod to initiate an install process. If we follow the guide Using PowerShell scripting with the WMI Bridge Provider we can build a PowerShell script to install the store app via PowerShell. Keep in mind that we have to lookup all the values like PackageFamilyName (PFN), ProductID, SKUID.

A good way to find the PFN is documented her: Find a package family name (PFN) for per-app VPN. The basic steps are:

  1. Go to https://www.microsoft.com/store/apps
  2. Enter the name of the app in the search bar. In our example, search for OneNote.
  3. Click the link to the app. Note that the URL that you access has a series of letters at the end. In our example, the URL looks like this: https://www.microsoft.com/store/apps/onenote/9wzdncrfhvjl
  4. In a different tab, paste the following URL, https://bspmts.mp.microsoft.com/v1/public/catalog/Retail/Products/<app id>/applockerdata, replacing <app id> with the app id you obtained from https://www.microsoft.com/store/apps – that series of letters at the end of the URL in step 3. In our example, example of OneNote, you’d paste: https://bspmts.mp.microsoft.com/v1/public/catalog/Retail/Products/9wzdncrfhvjl/applockerdata.

If we do this for the german LXP we will find the App ID and PFN:

Application ID: 
9p6ct0slw589

PackageFamilyName: 
Microsoft.LanguageExperiencePackde-DE_8wekyb3d8bbwe

SKU ID: 0016

PackageFamilyName is easy to construct for other packages as 8wekyb3d8bbwe is the PublisherId for Microsoft and therefore the en-US PackageFamilyName is constructed by replacing the de-DE with en-US -> Microsoft.LanguageExperiencePacken-US_8wekyb3d8bbwe. For the SKU ID just open the Microsoft Store for Business and have a look at the URL:

https://businessstore.microsoft.com/en-us/manage/inventory/apps/9P6CT0SLW589/0016/00000000000000000000000000000000

If have seen only 16 until now. So the main part for other languages will be to get the Application ID then.

The gathered information is enough information to create a short PowerShell script to validate the MDM Bridge WMI Provider if we can successfully install the german language experience pack:

<#
Author: Oliver Kieselbach (oliverkieselbach.com)
Script: Install-LanguageExperiencePack-de-DE.ps1
Description:
run in SYSTEM context, usage of MDM Bridge WMI Provider to install german language experience pack
Release notes:
Version 1.0: 2020-04-21 – Original published version.
The script is provided "AS IS" with no warranties.
#>
$namespaceName = "root\cimv2\mdm\dmmap"
$session = New-CimSession
$packageFamilyName = 'Microsoft.LanguageExperiencePackde-DE_8wekyb3d8bbwe'
$applicationId = "9p6ct0slw589"
$skuId = 0016
$omaUri = "./Vendor/MSFT/EnterpriseModernAppManagement/AppInstallation"
$newInstance = New-Object Microsoft.Management.Infrastructure.CimInstance "MDM_EnterpriseModernAppManagement_AppInstallation01_01", $namespaceName
$property = [Microsoft.Management.Infrastructure.CimProperty]::Create("ParentID", $omaUri, "string", "Key")
$newInstance.CimInstanceProperties.Add($property)
$property = [Microsoft.Management.Infrastructure.CimProperty]::Create("InstanceID", $packageFamilyName, "String", "Key")
$newInstance.CimInstanceProperties.Add($property)
$flags = 0
$paramValue = [Security.SecurityElement]::Escape($('<Application id="{0}" flags="{1}" skuid="{2}"/>' -f $applicationId, $flags, $skuId))
$params = New-Object Microsoft.Management.Infrastructure.CimMethodParametersCollection
$param = [Microsoft.Management.Infrastructure.CimMethodParameter]::Create("param", $paramValue, "String", "In")
$params.Add($param)
try {
# we create the MDM instance and trigger the StoreInstallMethod
$instance = $session.CreateInstance($namespaceName, $newInstance)
$result = $session.InvokeMethod($namespaceName, $instance, "StoreInstallMethod", $params)
}
catch [Exception] {
write-host $_ | out-string
}
#$session.DeleteInstance($namespaceName, $instance) | Out-Null
Remove-CimSession -CimSession $session

After execution in system context and a quick check we can see the Language Experience Pack for german was downloaded and installed:

This builds the basis for our script to completely switch the language without language cab files.

What do we need to do now in total for a scripted solution?

We run a PowerShell script in system context (system context is needed for the MDM Bride WMI Provider) and executing the following steps:

  • Usage of the MDM Bridge WMI Provider and StoreInstallMethod to install LXP from the Store
  • Call dism /online /Add-Capability /CapabilityName: to install Feature on Demand (FOD) packages from Windows Update
    • Language.Basic~~~de-DE~0.0.1.0,
    • Language.Handwriting~~~de-DE~0.0.1.0,
    • Language.OCR~~~de-DE~0.0.1.0,
    • Language.Speech~~~de-DE~0.0.1.0,
    • Language.TextToSpeech~~~de-DE~0.0.1.0

If FODs are not installed, it may happen that you will see a UAC dialog right after restart, if the user is a standard user and doesn’t have administrative permissions:

  • Schedule and trigger a user script (which runs in user context) to switch the current user session to the new language defaults. Also make sure the language pack is registered in the current user session. This was during testing often an issue for me as the language was simply not switched from time to time if I did not explicitly register it in user context (Add-AppxPackage -Register …).
$appxLxpPath = (Get-AppxPackage | Where-Object Name -Like *LanguageExperiencePackde-DE).InstallLocation
Add-AppxPackage -Register -Path "$appxLxpPath\AppxManifest.xml" -DisableDevelopmentMode
Set-WinUILanguageOverride -Language de-DE
Set-WinUserLanguageList de-DE -Force
Set-WinSystemLocale -SystemLocale de-DE
Set-Culture -CultureInfo de-DE
Set-WinHomeLocation -GeoId 94
  • Enforce the language change to be reflected by the OS more quickly by calling the scheduled task “\Microsoft\Windows\LanguageComponentsInstaller\ReconcileLanguageResources“, otherwise Windows Settings for example takes some fairly long time to fully switch to the new the language
  • Call the well known intl.cpl,,/f:<path-to-language-config-xml> to switch the “Welcome screen” and “New user” language defaults
<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="94"/>
    </gs:LocationPreferences>

    <!-- UI Language Preferences -->
    <gs:MUILanguagePreferences>
        <gs:MUILanguage Value="de-DE"/>
    </gs:MUILanguagePreferences>

    <!-- system locale -->
    <gs:SystemLocale Name="de-DE"/>

    <!-- input preferences -->
    <gs:InputPreferences>
        <gs:InputLanguageID Action="add" ID="0407:00000407" Default="true"/>
    </gs:InputPreferences>

    <!-- user locale -->
    <gs:UserLocale>
        <gs:Locale Name="de-DE" SetAsCurrent="true" ResetAllSettings="false"/>
    </gs:UserLocale>

</gs:GlobalizationServices>
  • Finally trigger the MS Store to get Updates for the apps due to the language change. This is done also by using the MDM Bridge WMI Provider
Get-CimInstance -Namespace "root\cimv2\mdm\dmmap" -ClassName "MDM_EnterpriseModernAppManagement_AppManagement01" | Invoke-CimMethod -MethodName "UpdateScanMethod"

As we have the restriction for the StoreInstallMethod to have an AAD user logged on, we can’t use the script to run in ESP and device preparation phase. I ended up creating a Win32 app package in Intune to give users the ability to run it by them self. To make it pretty I used the same language image from the Store:

This way we can also request the mandatory reboot:

On the assignment we define the restart grace period and snooze duration:

For the detection of the Intune Win32 app I’m using a custom PowerShell detection script to check for a custom registry key which is written by the language script. I’m using a custom registry key and do not check for the actual LXP as I want to prevent a flip-flop situation when you have e.g. de-DE and en-US assigned to a user and he is able to choose by his own. After install of the second LXP and a detection of actual LXP or UI language, it would result in IME trying constantly to apply the apps, as one detection would always fail and the app would be re-applied.

$language = "de-DE"

$app = "SetLanguage-$language"
$property = Get-ItemProperty -Path HKLM:\Software\MyIntuneApps -Name $app -ErrorAction SilentlyContinue
if ($property.$app -eq 1) {
    Write-Output "$app detected."
}

The final script logs all activities to the corresponding session context temporary folders which are the following. The filename includes the language tag like de-DE or en-US at the end:

  • C:\Windows\Temp\LXP-SystemContext-Installer-de-DE.log for system context execution
  • C:\Users\<username>\AppData\Local\Temp\LXP-UserSession-Config-de-DE.log for user context execution

How does the language switch experience look like for the user?

If a user installed the published app to switch to ‘de-DE’ from an ‘en-US’ Windows 10 it looks like this:

followed the mandatory reboot with grace period and snooze capability:

he will see the new language default in ‘de-DE’ even on the welcome and logon screen:

How are the results and conclusions?

I tested the script in virtual machines on Hyper-V and did the following language switches to verify correct behavior:

en-US‘ base installation -> switch to ‘de-DE‘ -> switch back to ‘en-US

de-DE‘ base installation -> switch to ‘en-US‘ -> switch back to ‘de-DE

All the language switches were done with Pro and Enterprise Editions, Windows 10 versions 1809, 1903 and 1909, and in all situations it went well. I did no test with production devices or devices running for a longer time (greater device lifetime). I did more or less lab environment tests.

Keep in mind if the environment has Enterprise State Roaming (ESR) enabled some settings might get roamed and overwritten and may cause confusion during testing with multiple devices and the same user. ESR roams the following settings:

Language preferences, which include settings for keyboard layouts, system language, date and time, and more.

Reference: https://docs.microsoft.com/en-us/azure/active-directory/devices/enterprise-state-roaming-faqs

Modern Apps like the MS Store app itself took always some time after it finally switched the language to german. As soon as the Updates for the Modern apps are finished the Store was displayed also in german.

Another issue seems to be the Company Portal. It does not reliably switch the language. Maybe it is affected by ESR and roaming setting or something else. At the time of writing I couldn’t finally clarify this.

UPDATE: I’m now aware of the language handling and store apps, see here the quick fix:
Company Portal stuck in a different language?

The final solution and possible enhancements

The final script itself brings a variety of options. As we have control over every language aspect and running everything in one sequence we can enhance the script to our needs. For example regions which are using special keyboards, different from the UI language can easily be handled in the script. This makes it possible to build a de-CH language pack which installs german UI and ‘de-CH’ input language for example. A sample Win32 app package for de-DE is available on my GitHub, it can be easily modified for other languages, see txt files:

SetLanguage-de-DE – Intune Win32 app files
https://github.com/okieselbach/Intune/tree/master/Win32/SetLanguage-de-DE

For further automation we could play around with some Requirement rules on the Intune Win32 app package. One idea is to assign the package as required and the requirement rule checks if the Enrollment Status Page (ESP) has finished. This way we could allow the app to run only if the ESP has finished. It would make sure that the app is executed when the AAD user is logged on and the InstallStoreMethod can successfully install the language experience pack. The problem is that I’ve seen during my tests, that the ESP indicator for this HasProvisioningCompleted in the WMI class MDM_EnrollmentStatusTracking_Setup01 is not reliable. I’ve seen several installations where the HasProvisioningCompleted status was False but I was in the Windows user session working already… This left an impression that I can’t rely on this WMI class. Which is strange as I think this is used for the Intune Management Extension as well. Here is the Requirement PowerShell snippet for this:

$namespace = "root\cimv2\mdm\dmmap"
$className = "MDM_EnrollmentStatusTracking_Setup01"

if ($(Get-WmiObject -Class $className -Namespace $namespace).HasProvisioningCompleted -eq "True") {
    "Provisioning finished."
}

Anyway, during the tests I found it very comfortable to have the language switch app available in the Company Portal. For now it seems a good way to provide a complete language switch option for the users.

For me it’s a quite useful script and I’m in evaluation with real hardware.

I wish happy language configuration.

Further reading

Windows 10 Multilanguage Deployment with MEMCM
…a very good article with a similar approach by using LXP packages and ConfigMgr (without online LXP install by leveraging MDM Bridge WMI Provider)