Set language, culture and timezone using PowerShell
As much as I’d like to say this is a perfect one-liner solution, it isn’t. It doesn’t actually count as PowerShell either, technically. This article is written for people deploying from Marketplace Azure images that always get the US locale, even though you need it to be GB – clearly if the image you deploy from has been customised or is already based on the en-GB image of your OS, then you probably won’t have these problems but when you deploy from the Marketplace image, you get en-US settings.
I’ve seen the cmdlets introduced in Windows 8 and Windows 2012 that allow you to set the language, culture and timezone but these are lacking. The primary issue is that they do not contain any method to affect the Default user account, so any time a new user logs on, they get en-US language settings, which for anyone in the UK is a huge pain. The other issue is compatibility. Not everyone is ready to deploy Windows Server 2016 or even Windows Server 2012 and so are stuck on Windows Server 2008 R2, for better or worse.
1 2 3 4 |
# These would be nice but they don't affect the default user account and aren't backwards compatible with Windows 7/2008R2, so are pretty useless.. Set-WinSystemLocale -SystemLocale en-GB Set-WinHomeLocation -GeoId 242 Set-WinUserLanguageList -LanguageList (New-WinUserLanguageList -Language en-GB) -Force |
If you’re deploying in Azure, Microsoft provide a market place image that makes life very easy, instead of rolling your own image and deploying from that. While Managed Disks are the first step to removing the frustration with handling your own custom images, they’re brand new and with anything new, it takes time to bed in and become part of a well-honed process.
With all this background covered, there are, undoubtedly full PowerShell solutions that will achieve the same as I’m about to give but they are also unquestionably more complicated to deploy and maintain.
To resolve the issue of deploying from Azure Marketplace images that have en-US set as default across the board, I implement a CustomScriptExtension for every VM build. I won’t cover the detail of the approach here, but I will provide a link to an Azure article that I contributed to that covers using Custom Script Extensions, specifically with private Azure Blob Storage. – I sent the pull request to update the document about 5 days ago but it’s not been accepted yet. The PR is here if you cannot see any mention of my request in the article yet.
The solution I implement requires two files to be downloaded from private Blob Storage, one is a PowerShell script, the other, an XML file which is where the language settings are. The CustomScriptExtension then runs a command line which invokes the Powershell script and changes the language settings and timezone, among other things.
I’ve deployed thousands of Azure VMs and this CustomScriptExtension has been instrumental in saving days of manually changing settings. The following code is the PowerShell (well, OK, there’s some PowerShell in there) to initialise the VM with language, locale, culture and timezone as well as format/prepare any RAW data disks – which you might not want to do based on the requirement or not for Storage Spaces/striping.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# Set Locale, language etc. & $env:SystemRoot\System32\control.exe "intl.cpl,,/f:`"UKRegion.xml`"" # These would be nice but they don't affect the default user account, so are pretty pointless. #Set-WinSystemLocale -SystemLocale en-GB #Set-WinHomeLocation -GeoId 242 #Set-WinUserLanguageList -LanguageList (New-WinUserLanguageList -Language en-GB) -Force # Set Timezone & tzutil /s "GMT Standard Time" # Set languages/culture Set-Culture en-GB # This one works! # Add/format any raw data disks and name them as per their LUN ID. $CandidateRawDisks = Get-Disk | Where {$_.PartitionStyle -eq 'raw'} | Sort -Property Number Foreach ($RawDisk in $CandidateRawDisks) { $LUN = (Get-WmiObject Win32_DiskDrive | Where index -eq $RawDisk.Number | Select SCSILogicalUnit -ExpandProperty SCSILogicalUnit) $Disk = Initialize-Disk -PartitionStyle MBR -Number $RawDisk.Number $Partition = New-Partition -DiskNumber $RawDisk.Number -UseMaximumSize -AssignDriveLetter $Volume = Format-Volume -Partition $Partition -FileSystem NTFS -NewFileSystemLabel "DATA-$LUN" -Confirm:$false } |
You’ll notice that line 2 actually calls control.exe and imports an XML file called UKRegion.xml. Here’s the contents of that file – this is specifically coded for UK (en-GB) language, home location etc. so if you’re elsewhere in the world, you’ll need your own location’s codes and what not. Check out this MSDN article for more information. https://msdn.microsoft.com/en-us/goglobal/bb964650
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<gs:GlobalizationServices xmlns:gs="urn:longhornGlobalizationUnattend"> <!--User List--> <gs:UserList> <gs:User UserID="Current" CopySettingsToDefaultUserAcct="true" CopySettingsToSystemAcct="true"/> </gs:UserList> <!-- user locale --> <gs:UserLocale> <gs:Locale Name="en-GB" SetAsCurrent="true"/> </gs:UserLocale> <!-- system locale --> <gs:SystemLocale Name="en-GB"/> <!-- GeoID --> <gs:LocationPreferences> <gs:GeoID Value="242"/> </gs:LocationPreferences> <gs:MUILanguagePreferences> <gs:MUILanguage Value="en-GB"/> <gs:MUIFallback Value="en-US"/> </gs:MUILanguagePreferences> <!-- input preferences --> <gs:InputPreferences> <!--en-GB--> <gs:InputLanguageID Action="add" ID="0809:00000809" Default="true"/> </gs:InputPreferences> </gs:GlobalizationServices> |
If you’re not trying to automate Azure builds and tinkering with JSON ARM templates isn’t required, then you need only the PowerShell and XML as shown above. Simply copy the PowerShell code and XML code in to two separate files (named appropriately), drop them on to the system you want to update and run the PowerShell. If you don’t want the disk format section, remove it. I should warn you that you DO need a reboot to fully apply the settings to the whole system.
For those of you doing Azure based deployments and want to take advantage of the above during deployment using a CustomScriptExtension, the resource I use in my JSON Azure Resource Manager template looks similar to the following – I actually have all of my inputs parameterised but you’ll want to see the information I’m actually passing in so I’ve manually edited the resource below, substituting in the values that would be passed from the parameters section (which isn’t shown below!). There is also the Set-AzureRmCustomScriptExtension cmdlet if you’re building with Azure PowerShell.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{ "apiVersion": "2015-05-01-preview", "type": "Microsoft.Compute/virtualMachines/extensions", "name": "[concat(parameters('VirtualMachineNames')[copyIndex()],'/LanguageSettings')]", "location": "[resourceGroup().location]", "copy": { "name": "VirtualMachineCSELoop", "count": "[length(parameters('VirtualMachineNames'))]" }, "dependsOn": [ "[concat('Microsoft.Compute/virtualMachines/', parameters('VirtualMachineNames')[copyIndex()])]" ], "properties": { "publisher": "Microsoft.Compute", "type": "CustomScriptExtension", "typeHandlerVersion": "1.7", "autoUpgradeMinorVersion": "true", "settings": { "fileUris": ["https://StorageAccountWithFiles.blob.core.windows.net/customscripts/Initialise-VM.ps1", "https://StorageAccountWithFiles.blob.core.windows.net/customscripts/UKRegion.xml"], "commandToExecute": "powershell.exe -ExecutionPolicy Unrestricted -File Initialise-VM.ps1" }, "protectedSettings": { "storageAccountName": "StorageAccountWithFiles", "storageAccountKey": "[listKeys(resourceId('ResourceGroupOfStorageAccountWithFiles','Microsoft.Storage/storageAccounts', 'StorageAccountWithFiles')), '2015-06-15').key1]" } } } |
“Why not use DSC!” I hear you scream. Absolutely, yes, you can definitely do that but I can tell you for free that you will reach a fully configured solution that you can log on to and start using far quicker with this method than you will if you use DSC which might have to install WMF, reboot, compile the MOF, configure LCM and then apply the config. It’s all about the right tool at the right time and for simple[?!] language settings and formatting disks, a CSE is the way to go as far as I’m concerned.
-Lewis
Thanks – this really helped me a lot when trying to localize vm in Azure Dev Test labs ! 🙂
/Bo
Hey Lewis, thanks for this.
Are there any compatibility issues between Windows 2008/2012/2016?
It’s not clear from your initial para, but it sounds like you may be using this as a workaround for 2008 servers… is there an easier/better way to do it on later Windows versions?
Cheers,
Ferg.
the xml in the example is for “en-GB”, if I need to change in another language what should I change in xml ?
tnx
Thank you for all this information.
When I was trying this with a Windows Server 2019 oddly I had to run the script twice in order for it to take the settings.