grzegorzo1972
New Member
Strange thing with these language files, Ntlite shows that everything has integrated and after uploading the system can not change the language as if these files did not integrate
Thanks for the advice, I'll implement itLanguage Packs must always be integrated first, before applying updates or removals. The best way is to do two runs:
1. Load clean image, add LPs. Make new image.
2. Load image, continue with other changes.
I mean Windows 11 22H2. When is it solved?Test-Path is null could be UUP dump acting up (due to recent stability problems). I tried W10 21H2 (same as 22H2) NL, and it was working.
If you have a specific repeat case, let me know.
UUP dump changed its repo data for 22H2, and I had to find a different build ID that worked.I mean Windows 11 22H2. When is it solved?
I've written a PowerShell GUI to download Language Packs for most versions of Windows 10 & 11.
This replaces searching UUP dump or rg-adguard for the correct version, downloading the links and renaming files.
We're still using UUP dump to generate the expiring download links. The script will skip files with identical names in the current folder.
View attachment 6951
Code:Downloading "Microsoft-Windows-Client-LanguagePack-Package_cs-cz-amd64-cs-cz.esd" Downloading "Microsoft-Windows-LanguageFeatures-Basic-cs-cz-Package-amd64.cab" Downloading "Microsoft-Windows-LanguageFeatures-Handwriting-cs-cz-Package-amd64.cab" Downloading "Microsoft-Windows-LanguageFeatures-OCR-cs-cz-Package-amd64.cab" Downloading "Microsoft-Windows-LanguageFeatures-TextToSpeech-cs-cz-Package-amd64.cab" SHA-1 Hash: e7b2986577196a4dd08693327a7209dae02545e5 Microsoft-Windows-Client-LanguagePack-Package_cs-cz-amd64-cs-cz.esd 4e6b61b4e3654d03d765bbf59ab6fc45eb5b4c2f Microsoft-Windows-LanguageFeatures-Basic-cs-cz-Package-amd64.cab 4ef0b346760bb818b830b218296c5bfd7b11d38d Microsoft-Windows-LanguageFeatures-Handwriting-cs-cz-Package-amd64.cab 702a8eddbc454768efabb4c04ea540b464a6b7de Microsoft-Windows-LanguageFeatures-OCR-cs-cz-Package-amd64.cab f980392aca2b3d424c8ca73446beb7e8026bb14f Microsoft-Windows-LanguageFeatures-TextToSpeech-cs-cz-Package-amd64.cab
What versions are supported?
Win 10 below 1809 isn't supported (why are you still using it?). Server 2022 isn't here, because the language selection is severely limited.
- Win 11 22H2
- Win 11 22H1
- Win 10 2004, 20H2, 21H1 & 21H2 are the same family
- Win 10 1903 & 1909 are the same family
- Win 10 1809
Do I need ESD2CAB?
Yes, click on the GUI's download link to open abbodi's GitHub.
Why do I get a PowerShell execution policy error?
Your default execution policy prevents unsigned scripts from running. Run the W10_11LP.bat instead.
How do I check if the downloaded packages are the correct version?
When files are downloaded, the script will report the SHA-1 values.
You can compare the SHA-1 against UUPdump (select a Windows build, under Browse Files / Search enter "language").
ie. https://uupdump.net/findfiles.php?id=a84eaaea-f57c-4271-a642-4abf996a7101&q=language
Why do you use a different UUP than abbodi's W10MUI?
Language Packs are some of the first packages built, and they're shared across later versions in the same family. While my reference builds are different, all packages have identical SHA-1 values to abbodi's UUP's. There's no functional difference btw them.
If you're bored, my versions are listed in the script.
What about Win 7 & 8 versions?
There's a different script for you (included in the ZIP), because the languages list isn't the same as W10/11.
While you could just copy the URL from pastebin or other sources, I already wrote a GUI.
View attachment 6952
How hard was it writing the GUI?
When you find a good WinForms code example, it's easy to modify and make your own layout. I moved and sized the visual elements by hand.
If you noticed, the OK button isn't active until the user selects from all menu boxes. And W11 doesn't have a x86 version, checking that option always gets unselected.
I'm surprised no one reported it before...
Function Install-LanguageCabs {
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[String]$Language,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[ValidateScript({
if ($_ -notin 'arm', 'amd64') {
throw "The Arch parameter must be either 'amd64' or 'arm'. amd64 is for standard x64. x86 doesnt work at all from what iv seen."
}
return $true
})]
[String]$Arch,
[Switch]$Install,
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[ValidateScript({
if ($_ -ne ".\") {
New-Item -Path $_ -ItemType Directory -Force -ErrorAction SilentlyContinue
Set-Location $_
} return $true
})]
[String]$Path,
[Parameter(Mandatory=$false)]
[ValidateScript({
if (-not ($_)) {
throw "The Build parameter cannot be empty or null."
}
if (-not ($_ -match '\d{5}\.\d+$')) {
throw "Build string must be 5 digits.anynumberofdigits so i.e 25300.457"
}return $true
})]
[String]$Build,
[Parameter(Mandatory=$true)]
[ValidateScript({
if ($_ -notin '10','11') {
throw "The Os parameter must be either '10' or '11'."
}
return $true
})]
[String]$Os,
[Switch]$LocalRepository,
[Parameter(Mandatory=$false)]
[ValidateScript({
if (-not ($_)) {
throw "The LocalRepositoryPath parameter cannot be empty or null."
}
if (-not (Test-Path "$_\$Os\$Build\$language\*.cab" -PathType Any)) {
throw "The path specified ""$_\$Os\$Build\$language"" in the LocalRepositoryPath parameter does not exist. Or the path doesnt contain any cab files to install."
} return $true
})]
[String]$LocalRepositoryPath,
[Parameter(Mandatory=$true)]
[ValidateScript({
if (-not ($_)) {
throw "The ToastImage parameter cannot be empty or null."
}
if (-not (Test-Path $_ -PathType Any)) {
throw "The path specified ""$_"" in the ToastImage parameter does not exist. It needs to point directly to the full path or a image file."
} return $true
})]
[String]$ToastImage,
[string]$ProgressFile)
Start-Transcript -Path "C:\Temp\Intune\Scripts\TransLogOOBEInstallCAB.log" -Force -Verbose -ErrorAction SilentlyContinue
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
$WebResponse = (Invoke-WebRequest -Uri "https://uupdump.net/known.php?q=windows+$($Os)+$($Build -replace '10.0.','')" -UseBasicParsing -MaximumRedirection 1 -Method GET).Links
#if ($null -ne $WebResponse) {
$href= ($WebResponse | Where-Object { $_.href -match "./selectlang.php\?id"} -ErrorAction SilentlyContinue)
$UpdateID = (($href | Where-Object {$_.outerHTML -match $Arch}).href).split("=")[1]
try {
$Links = (Invoke-WebRequest -UseBasicParsing -Uri "https://uupdump.net/get.php?id=$UpdateID&pack=$Language&edition=core" -Method GET -MaximumRedirection 1).Links
}
catch {
$_.Exception.Message
$Install = $null
$Links=$null
}
if($Links) {
$Lang = $Language.Split('-')[0]
foreach ($link in ($Links.outerHTML -match ".*(Language|LanguageFeatures).*$Lang-.*")) {
$URL = $link.Split('"')[1]
$Filename = $link.Split('>')[1].Split('<')[0] -replace '\s',''
$PackageName = ($Filename -split ('[_-]' + $Lang))[0]
$Filename = $Filename.Replace($PackageName,[cultureinfo]::GetCultureInfo('en-US').TextInfo.ToTitleCase($PackageName))
$Filename = $Filename.TrimStart('Cabs_')
$Filename = $Filename.Replace('Ocr','OCR')
foreach ($word in ('Pack','Feat','ToSpeech')) {
$Filename = $Filename.Replace($word.ToLower(),$word)
}
Write-Host $filename
if (-not (Test-Path -LiteralPath $Filename -PathType Leaf)) {
try {
if ($LocalRepository){
Copy-Item -LiteralPath "$LocalRepositoryPath\$Os\$Build\$language\*.cab" -Destination .\ -Force
}
else {
Invoke-WebRequest -UseBasicParsing -Uri $URL -Headers $Headers -OutFile $Filename -ErrorAction SilentlyContinue
Write-Host "Downloading: $Filename to path: $Path" -ForegroundColor Blue
}
}
catch {
$_.Exception.Message
}
}
else {
Write-Host "No download needed, File: $Filename exist in path: $Path" -ForegroundColor Green
}
}
}
else {
Write-Host "Was a problem downloading from: " -NoNewline -ForegroundColor Red
Write-Host "https://uupdump.net/get.php?id=$UpdateID&pack=$Language&edition=core"
if (Test-Path -Path $path -Filter "\(Microsoft-Windows-LanguageFeatures*.cab)|(\Microsoft-Windows-Client*.esd)|(\Microsoft-Windows-Client*.cab)") {
Write-Host "But! The files in the current directory match what we are after so no need for downloading anything!" -ForegroundColor Green
$Install = $True
}
elseif($LocalRepository) {
if(Test-Path -Path $LocalRepositoryPath\$Os\$Build\$language -Filter "\(Microsoft-Windows-LanguageFeatures*.cab)|(\Microsoft-Windows-Client*.esd)|(\Microsoft-Windows-Client*.cab)") {
Write-Host "But! Since you specified a local repository to download from and we can access them there, everything is good!" -ForegroundColor Green
Copy-Item -LiteralPath "$LocalRepositoryPath\$Os\$Build\$language\*.cab" -Destination .\ -Force -Verbose
$Install = $True
}
}
else{
Write-Host "And no local files was found in $path and no LocalRepository was specified" -ForegroundColor Red
Write-Host "Will have to abort Language CAB Install and retry on next attempt" -ForegroundColor Red
$Install = $null
Exit 2
}
}
if ($Install) {
if(-Not $LocalRepository) {
try{
if(Test-Path -Path $path -Filter "\Microsoft-Windows-Client-LanguagePack-Package*.cab") {
}
elseif(!(Test-Path -Path $path -Filter "\Microsoft-Windows-Client-LanguagePack-Package*.cab") -and (Test-Path -Path $path -Filter "\Microsoft-Windows-Client-LanguagePack-Package*.esd")) {
Invoke-WebRequest -UseBasicParsing -Uri 'https://github.com/abbodi1406/WHD/raw/master/scripts/ESD2CAB-CAB2ESD-2.zip' -Headers $Headers -OutFile "$path\ESD2CAB.zip" -ErrorAction SilentlyContinue
Start-Sleep 1
Expand-Archive -Path "$path\ESD2CAB.zip" -DestinationPath "$path\" -Force -ErrorAction Stop
Start-Sleep 1
$file = Get-Content -Path ".\esd2cab_CLI.cmd"
$file = $file | Where-Object { $_ -notmatch "echo Press any key to exit\.\.\." } | Where-Object { $_ -notmatch "pause >nul" } #Removes the need to interact with the ESD to CAB conversion
Set-Content -Path ".\esd2cab_CLI.cmd" -Value $file
Start-Sleep 1
.\esd2cab_CLI.cmd
}
else {
Write-Host "Should be .cab files in this current directory.." -ForegroundColor Red
}
}
catch {
$_.Exception.Message
}
}
Remove-PSDrive -Name HKCR -ErrorAction SilentlyContinue
$command = @(
"dism /online /add-package /packagepath:$path\Microsoft-Windows-Client-LanguagePack-Package_$Language-$arch-$Language.cab /norestart /LogPath:$path\$Language-CAB.log /LogLevel:3",
"dism /online /add-package /packagepath:$path\Microsoft-Windows-LanguageFeatures-Basic-$Language-Package-$arch.cab /norestart /LogPath:$path\$Language-CAB.log /LogLevel:3",
"dism /online /add-package /packagepath:$path\Microsoft-Windows-LanguageFeatures-Handwriting-$Language-Package-$arch.cab /norestart /LogPath:$path\$Language-CAB.log /LogLevel:3",
"dism /online /add-package /packagepath:$path\Microsoft-Windows-LanguageFeatures-OCR-$Language-Package-$arch.cab /norestart /LogPath:$path\$Language-CAB.log /LogLevel:3",
"dism /online /add-package /packagepath:$path\Microsoft-Windows-LanguageFeatures-TextToSpeech-$Language-Package-$arch.cab /norestart /LogPath:$path\$Language-CAB.log /LogLevel:3"
)
if($Language -eq "ja-jp" -or $Language -eq "en-us" -or $Language -eq "en-uk") {
$command += "dism /online /add-package /packagepath:$path\Microsoft-Windows-LanguageFeatures-Speech-$Language-Package-$arch.cab /norestart /LogPath:$path\$Language-CAB.log /LogLevel:3"
}
Get-Job | Remove-Job -Force -Confirm:$false
Remove-item "$path\ErrorObject.log" -Force -Recurse -Confirm:$false -ErrorAction SilentlyContinue
Remove-item "$path\$Language-CAB.log" -Force -Recurse -Confirm:$false -ErrorAction SilentlyContinue
$elapsedTime = Measure-Command {
$jobList = @()
for ($i = 0; $i -lt ($command.Length); $i++) {
$JobName = switch ($i) {
0 { "($language)-Client-LanguagePack" }
1 { "LanguageFeatures-Basic" }
2 { "LanguageFeatures-Handwriting" }
3 { "LanguageFeatures-OCR" }
4 { "LanguageFeatures-TextToSpeech" }
5 { "LanguageFeatures-Speech" }
}
$job = Start-Job -Name $jobName -ScriptBlock {
param($command,$JobName,$Path)
try {
$logData = @{
StartTimestamp = Get-Date
Command = $Command
EndTimestamp = ""
ElapsedTime = ""
InstallStatus = ""
}
$DISM = & cmd /c "$command"
$logData.EndTimestamp = Get-Date
$logData.ElapsedTime = [datetime]::ParseExact($logData.EndTimestamp.ToString("yyyy-MM-dd hh:mm:ss.ffff"), "yyyy-MM-dd hh:mm:ss.ffff", $null) - [datetime]::ParseExact($logData.StartTimestamp.ToString("yyyy-MM-dd hh:mm:ss.ffff"), "yyyy-MM-dd hh:mm:ss.ffff", $null)
$logData.StartTimestamp = $logData.StartTimestamp.ToString("yyyy-MM-dd hh:mm:ss.ffff", [System.Globalization.CultureInfo]::InvariantCulture)
$logData.EndTimestamp = $logData.EndTimestamp.ToString("yyyy-MM-dd hh:mm:ss.ffff", [System.Globalization.CultureInfo]::InvariantCulture)
}
catch {
$_ | Select-Object * | Out-File "$path\ErrorObject.log" -Append -Force
$logData.InstallStatus = "Error.."
}
$logData.InstallStatus = "OK"
Return $logData
} -ArgumentList $command[$i].ToString(), $JobName,$path
$jobList += $job
}
$timeout = [DateTime]::Now.AddMinutes(45)
while ($jobList) {
if ([DateTime]::Now -gt $timeout) {
Write-Error "The job has exceeded the 45 minutes timeout limit."
break
}
foreach ($job in $jobList) {
if ($job.State -eq "Completed") {
# Get the job output
$output = Receive-Job $job | Out-String -Stream
Write-Output "Job $($job.Name) output:"
$output | ForEach-Object {
if ($_ -match '^(\S+)\s+(\S.*)$') {
Write-Host $Matches[1] -ForegroundColor Green -NoNewline
Write-Host ' ' $Matches[2]
}
else {
Write-Host $_
}
}
$jobList = $jobList | Where-Object { $_.InstanceId -ne $job.InstanceId }
$job.Dispose()
}
}
Start-Sleep -Milliseconds 100
}
}
if ($elapsedTime.Minutes -ge 45) {
Write-Error "Will have to abort since Install CAB job never finished, will try again.."
Exit 3
}
else {
if ($elapsedTime.Minutes -le 9) {
Write-Host "Elapsed total time for complete language ($Language) CAB installation: 0$($elapsedTime.Hours):0$($elapsedTime.Minutes):$($elapsedTime.Seconds).$($elapsedTime.Milliseconds)" -ForegroundColor Green
}
else {
Write-Host "Elapsed total time for complete language ($Language) CAB installation: 0$($elapsedTime.Hours):$($elapsedTime.Minutes):$($elapsedTime.Seconds).$($elapsedTime.Milliseconds)" -ForegroundColor Green
}
if(Get-Content -Path "$path\ErrorObject.log" -ErrorAction SilentlyContinue) {
Write-Warning "The Install CAB jobs finished in time but left errors.."
Write-Warning "Printing first 50 rows here:"
Get-Content -Path "$path\ErrorObject.log" -TotalCount 50 | Out-Host
}
else{
Write-Host "Looks like Install CAB files worked! Good job!" -ForegroundColor Green
Update-ExitCode -Path $progressFile -Part "Install-LanguageCabs" -ExitCode "Job says OK!"
}
}
New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT -erroraction silentlycontinue | Out-Null
New-item 'HKCR:\ToastReboot' -force -ErrorAction SilentlyContinue | Out-Null
set-itemproperty 'HKCR:\ToastReboot' -name '(DEFAULT)' -value 'url:ToastReboot' -force -ErrorAction SilentlyContinue | Out-Null
set-itemproperty 'HKCR:\ToastReboot' -name 'URL Protocol' -value '' -force -ErrorAction SilentlyContinue | Out-Null
new-itemproperty -path 'HKCR:\ToastReboot' -propertytype dword -name 'EditFlags' -value 2162688 -ErrorAction SilentlyContinue | Out-Null
New-item 'HKCR:\ToastReboot\Shell\Open\command' -force -ErrorAction SilentlyContinue | Out-Null
set-itemproperty 'HKCR:\ToastReboot\Shell\Open\command' -name '(DEFAULT)' -value 'C:\Windows\System32\shutdown.exe -r -t 00' -force -ErrorAction SilentlyContinue #Pressing restart now on the toast will do just that :)
if (-not (Get-Command "Invoke-AsCurrentUser_WithArgs" -ErrorAction SilentlyContinue)) {
$url = "https://raw.githubusercontent.com/ztrhgf/RunAsUser/master/Public/Invoke-AsCurrentUser.ps1"
$localFile = "$env:Temp\Invoke-AsCurrentUser_WithArgs.ps1"
$status = Invoke-WebRequest $url -PassThru -OutFile $localFile
if ($status.StatusCode -eq "200" -or ($status.StatusDescription -eq "OK") -or (Test-Path "$env:Temp\Invoke-AsCurrentUser_WithArgs.ps1")) {
(Get-Content $localFile -Raw) -replace "Invoke-AsCurrentUser", "Invoke-AsCurrentUser_WithArgs" | Set-Content $localFile
$newFunc = Get-Content $localFile -Raw
Invoke-Command -ScriptBlock ([Scriptblock]::Create($newFunc))
Invoke-Expression $newFunc -Verbose
}
}
[hashtable]$Argument = @{
Os = $Os
ToastImage = $ToastImage
}
Invoke-AsCurrentUser_WithArgs -UseWindowsPowerShell -Argument $argument -ScriptBlock {
Param ([string]$Os,[string]$ToastImage)
Import-Module -Name BurntToast -ErrorAction SilentlyContinue -Force -Verbose
$burn = Get-Command "Submit-BTNotification" -ErrorAction SilentlyContinue
if (!($burn)) {
Import-Module $((Get-ChildItem -Path "C:\WINDOWS\system32\config\systemprofile\Documents\PowerShell\Modules\BurntToast\0.*.5\BurntToast.psd1").FullName) -Force # SYSTEM user has installed the module here.
}
Remove-BTNotification -UniqueIdentifier "LXP" -Confirm:$false -ErrorAction SilentlyContinue
Remove-BTNotification -Group "LXP" -ErrorAction SilentlyContinue
Remove-BTNotification -UniqueIdentifier "LXP" -ErrorAction SilentlyContinue
$heroimage = New-BTImage -Source $ToastImage -HeroImage
$Text1 = New-BTText -Content "Good news! Windows $Os language installation has finished in the backgroud, a restart is needed."
$Button = New-BTButton -Content "Restart Later" -snooze -id "SnoozeTime"
$Button2 = New-BTButton -Content "Restart Now" -Arguments "ToastReboot:" -ActivationType Protocol
$audio = New-BTAudio -Silent
$action = New-BTAction -Buttons $Button, $Button2
$Binding = New-BTBinding -Children $text1 -HeroImage $heroimage
$Visual = New-BTVisual -BindingGeneric $Binding
$Content = New-BTContent -Visual $Visual -Actions $action -Audio $audio -Duration Long
Submit-BTNotification -Content $Content -UniqueIdentifier "LXP"
}
}
else {
try{
if(Get-ChildItem -Path .\ -Filter "*.cab") {
Invoke-WebRequest -UseBasicParsing -Uri 'https://github.com/abbodi1406/WHD/raw/master/scripts/ESD2CAB-CAB2ESD-2.zip' -OutFile .\ESD2CAB.zip
Start-Sleep 1
Expand-Archive -Path .\ESD2CAB.zip -DestinationPath .\ -Force -ErrorAction Stop
Start-Sleep 1
$file = Get-Content -Path ".\esd2cab_CLI.cmd"
$file = $file | Where-Object { $_ -notmatch "echo Press any key to exit\.\.\." } | Where-Object { $_ -notmatch "pause >nul" } #Removes the need to interact with the ESD to CAB conversion
Set-Content -Path ".\esd2cab_CLI.cmd" -Value $file
Start-Sleep 1
Write-Host "Running: .\esd2cab_CLI.cmd" -ForegroundColor Green
.\esd2cab_CLI.cmd
}
else {
Write-Host "Should be .cab files in this current directory but none was found, its a mystery!" -ForegroundColor Red
}
}
catch {
$_.Exception.Message
}
}
Stop-Transcript -ErrorAction SilentlyContinue
}
Hi how to run this script ? i downloaded from github but it exit with errorsHi!
EDIT4: The full working solution is now at: https://github.com/Harze2k/PowerShell/blob/main/Intune/Install-LanguageCabs
Got limited here with the size of the code haha Also bonus, the script is actually working, running parallel jobs to finish as quick as possible. \o/
Watch out for when uudump change their site though, might mess things up though..
Yes as Garlin said basically its for more of a enterprise deployment with more functions that are chained together. So if that's the case just use his tool But since you tested the code for update exit code is just a quick little function to write in a xml file like this:Hi how to run this script ? i downloaded from github but it exit with errors
+ if ((Update-ExitCode -Path $progressFile -Part "Install-LanguageCabs" ...
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Update-ExitCode:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
TaskPath TaskName State
-------- -------- -----
\ asSystem_InstallOffice_FixPins... Ready
\ asSystem_InstallOffice_FixPins... Ready
\ asSystem_InstallOffice_FixPins... Ready
function Update-ExitCode {
[CmdletBinding()]
param ([Parameter(Mandatory=$true, Position=0)]
[string]$Path,
[Parameter(Mandatory=$true)]
[string]$Part,
[Parameter(Mandatory=$false)]
[string]$ExitCode,
[Parameter(Mandatory=$false)]
[switch]$ReturnOnly)
try {
# Read the contents of the JSON file
$json = Get-Content -Path $Path -Raw | ConvertFrom-Json
}
catch {
Write-Warning "Error reading JSON file: $_"
return
}
# Find the item with the matching Part value
$item = $json.Progress | Where-Object { $_.Part -eq $Part }
if (!$item) {
Write-Warning "Part '$Part' not found in JSON file"
return
}
# If the ReturnOnly switch is specified, output the ExitCode value and return
if ($ReturnOnly) {
Write-Output $item.ExitCode
return
}
# Update the ExitCode value if a new value is provided
if ($ExitCode) {
$item.ExitCode = $ExitCode
}
Write-Output "Part: $($item.Part) ExitCode: $($item.ExitCode)"
try {
# Write the updated JSON back to the file
$json | ConvertTo-Json -Depth 100 | Set-Content $Path
}
catch {
Write-Warning "Error writing JSON file: $_"
}
}
Function Set-CheckProgressJSON {
param([string]$ProgressFile)
@"
{
"Progress": [
{
"Part": "SendFirstLogs",
"ExitCode": ""
},
{
"Part": "InstallOffice",
"ExitCode": ""
},
{
"Part": "PinIcons",
"ExitCode": ""
},
{
"Part": "Install-LanguageCabs",
"ExitCode": ""
},
{
"Part": "SendLastLogs",
"ExitCode": ""
}
]
}
"@ | Out-File -FilePath $progressFile -Force
}