Add localization framework for the GUI (en-US baseline) (#764)

Credit to @jmolinap2 for their work in #643, which was used as inspiration.
This commit is contained in:
Sashank
2026-09-10 17:48:43 +02:00
committed by GitHub
parent 6012b02ea2
commit 32024662f3
62 changed files with 2947 additions and 742 deletions
+2 -2
View File
@@ -50,7 +50,7 @@ function Remove-SelectedApps {
$appIndex++
if ($script:ApplySubStepCallback -and $appCount -gt 1) {
& $script:ApplySubStepCallback "Removing apps ($appIndex/$appCount)" $appIndex $appCount
& $script:ApplySubStepCallback (Get-Translation -Key 'RemovingAppsSubStep' -FormatArgs @($appIndex, $appCount)) $appIndex $appCount
}
Write-Host "Removing $app"
@@ -329,7 +329,7 @@ function Get-AppRemovalMethod {
#>
function Request-EdgeForceRemove {
if ($script:GuiWindow) {
$result = Show-MessageBox -Message 'Unable to uninstall Microsoft Edge via WinGet. Would you like to forcefully uninstall it? NOT RECOMMENDED!' -Title 'Force Uninstall Microsoft Edge?' -Button 'YesNo' -Icon 'Warning'
$result = Show-MessageBox -Message (Get-Translation -Key 'ForceRemoveEdgeMessage') -Title (Get-Translation -Key 'ForceRemoveEdgeTitle') -Button 'YesNo' -Icon 'Warning'
if ($result -eq 'Yes') {
Write-Host ""
return (Invoke-ForceRemoveEdge)
+5 -5
View File
@@ -265,7 +265,7 @@ function Invoke-ApplyFeatures {
# Resolve display name for the progress indicator
$f = $script:Features[$featureId]
$displayName = $f.ApplyText
$displayName = Get-Translation -Key $featureId -Field 'ApplyText' -Section 'Features'
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $TotalSteps $displayName
@@ -309,8 +309,8 @@ function Invoke-UndoFeatures {
if ($script:CancelRequested) { return }
$f = if ($script:Features.ContainsKey($featureId)) { $script:Features[$featureId] } else { $null }
$undoLabel = if ($f -and $f.UndoLabel) { $f.UndoLabel } else { $featureId }
$undoText = if ($f -and $f.ApplyUndoText) { $f.ApplyUndoText } else { $undoLabel }
$undoLabel = if ($f -and $f.UndoLabel) { Get-Translation -Key $featureId -Field 'UndoLabel' -Section 'Features' } else { $featureId }
$undoText = if ($f -and $f.ApplyUndoText) { Get-Translation -Key $featureId -Field 'ApplyUndoText' -Section 'Features' } else { $undoLabel }
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $TotalSteps $undoText
@@ -391,7 +391,7 @@ function Invoke-AllChanges {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $totalSteps "Creating registry backup..."
& $script:ApplyProgressCallback $step $totalSteps (Get-Translation -Key 'ApplyCreatingRegistryBackup')
}
if ($script:Params.ContainsKey("WhatIf")) {
@@ -421,7 +421,7 @@ function Invoke-AllChanges {
if ($script:CancelRequested) { return }
$step++
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $totalSteps "Creating system restore point, this may take a moment..."
& $script:ApplyProgressCallback $step $totalSteps (Get-Translation -Key 'ApplyCreatingRestorePoint')
}
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Create system restore point" -ForegroundColor Cyan
@@ -89,7 +89,7 @@ function Invoke-SystemRestorePoint {
# Ensure that the user is aware if creating a restore point failed, and give them the option to continue without a restore point or cancel the script
if ($failed) {
if ($script:GuiWindow) {
$result = Show-MessageBox "Failed to create a system restore point. Do you want to continue without a restore point?" "Restore Point Creation Failed" "YesNo" "Warning"
$result = Show-MessageBox (Get-Translation -Key 'RestorePointFailedMessage') (Get-Translation -Key 'RestorePointFailedTitle') "YesNo" "Warning"
if ($result -ne "Yes") {
$script:CancelRequested = $true
+59 -13
View File
@@ -1,3 +1,11 @@
<#
.SYNOPSIS
Extracts and deduplicates the SelectedFeatures list from a backup, validating each entry.
.DESCRIPTION
Returns an empty SelectedFeatures array with an error when the property is missing
entirely, and flags (without stopping) any entry that isn't a non-empty string.
#>
function Get-NormalizedSelectedFeatureIdsFromBackup {
param(
[Parameter(Mandatory)]
@@ -10,7 +18,7 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
$hasInvalidSelectedFeatureId = $false
if (-not $Backup.PSObject.Properties['SelectedFeatures']) {
$errors.Add('Missing property: SelectedFeatures')
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('SelectedFeatures')))
return [PSCustomObject]@{
SelectedFeatures = $selectedFeatures.ToArray()
Errors = $errors.ToArray()
@@ -30,7 +38,7 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
}
if ($hasInvalidSelectedFeatureId) {
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
$errors.Add((Get-Translation -Key 'BackupSelectedFeaturesMustBeStrings'))
}
return [PSCustomObject]@{
@@ -39,6 +47,14 @@ function Get-NormalizedSelectedFeatureIdsFromBackup {
}
}
<#
.SYNOPSIS
Extracts and deduplicates the SelectedUndoFeatures list from a backup, validating each entry.
.DESCRIPTION
Unlike SelectedFeatures, this property is optional: a missing property returns an
empty array with no error, since not every backup has undone features to restore.
#>
function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
param(
[Parameter(Mandatory)]
@@ -71,7 +87,7 @@ function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
}
if ($hasInvalidSelectedUndoFeatureId) {
$errors.Add('SelectedUndoFeatures must contain non-empty string feature IDs.')
$errors.Add((Get-Translation -Key 'BackupSelectedUndoFeaturesMustBeStrings'))
}
return [PSCustomObject]@{
@@ -80,6 +96,15 @@ function Get-NormalizedSelectedUndoFeatureIdsFromBackup {
}
}
<#
.SYNOPSIS
Normalizes a raw registry key snapshot into a consistent shape, recursing into sub-keys.
.DESCRIPTION
Fills in defaults for optional properties (Exists, Values, SubKeys) so downstream
validation can read a predictable shape regardless of what the backup file omitted.
Throws if Path is missing, since every snapshot must be anchored to a real key.
#>
function Normalize-RegistryKeySnapshot {
param(
[Parameter(Mandatory)]
@@ -87,7 +112,7 @@ function Normalize-RegistryKeySnapshot {
)
if (-not $Snapshot.PSObject.Properties['Path'] -or [string]::IsNullOrWhiteSpace([string]$Snapshot.Path)) {
throw 'Backup validation failed: Registry key snapshot is missing Path.'
throw (Get-Translation -Key 'BackupSnapshotMissingPath')
}
$exists = $false
@@ -127,6 +152,16 @@ function Normalize-RegistryKeySnapshot {
}
}
<#
.SYNOPSIS
Validates that a backup's registry snapshots only contain paths and values the
selected (and undo) features are actually allowed to touch.
.DESCRIPTION
Builds an allow-list from the capture plans of the selected features, then checks
every snapshot against it. Returns an array of translated error strings; an empty
array means the backup is valid.
#>
function Test-RegistryBackupMatchesSelectedFeatures {
param(
[Parameter(Mandatory)]
@@ -145,7 +180,7 @@ function Test-RegistryBackupMatchesSelectedFeatures {
$errors = New-Object System.Collections.Generic.List[string]
if (-not $script:Features -or $script:Features.Count -eq 0) {
$errors.Add('Unable to validate registry backup allowlist because feature definitions are not loaded.')
$errors.Add((Get-Translation -Key 'BackupFeatureCatalogNotLoaded'))
return $errors.ToArray()
}
@@ -161,7 +196,7 @@ function Test-RegistryBackupMatchesSelectedFeatures {
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) {
$errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from the selected features.')
$errors.Add((Get-Translation -Key 'BackupNoAllowedRegistryPaths'))
}
foreach ($rootSnapshot in @($RegistryKeys)) {
@@ -171,6 +206,15 @@ function Test-RegistryBackupMatchesSelectedFeatures {
return $errors.ToArray()
}
<#
.SYNOPSIS
Resolves selected feature IDs to their catalog entries, keeping only the ones with a
registry-backed apply or undo key relevant to the requested direction.
.DESCRIPTION
Appends a translated error to the caller-supplied Errors list for any feature ID that
isn't in the current catalog, rather than failing the whole validation outright.
#>
function Get-SelectedRegistryFeaturesForBackupValidation {
param(
[Parameter(Mandatory)]
@@ -186,11 +230,13 @@ function Get-SelectedRegistryFeaturesForBackupValidation {
if ($null -eq $Errors -or -not ($Errors -is [System.Collections.IList])) {
throw 'Get-SelectedRegistryFeaturesForBackupValidation requires Errors to be a mutable list collection.'
}
# Intentionally not localized: this throw signals a programming error (a caller passing the
# wrong collection type), not a condition a user's backup file can trigger.
$selectedRegistryFeatures = New-Object System.Collections.Generic.List[object]
foreach ($featureId in @($SelectedFeatureIds)) {
if (-not $script:Features.ContainsKey($featureId)) {
$Errors.Add("Selected feature '$featureId' was not found in the current feature catalog.")
$Errors.Add((Get-Translation -Key 'BackupFeatureNotInCatalog' -FormatArgs @($featureId)))
continue
}
@@ -293,13 +339,13 @@ function Test-RegistrySnapshotAgainstAllowList {
$snapshotPath = [string]$Snapshot.Path
$normalizedPath = Get-NormalizedRegistryPathKey -Path $snapshotPath
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
$Errors.Add("Backup contains unsupported registry path '$snapshotPath'.")
$Errors.Add((Get-Translation -Key 'BackupUnsupportedRegistryPath' -FormatArgs @($snapshotPath)))
return
}
$planMatch = Find-RegistryAllowListPlanMatch -NormalizedPath $normalizedPath -PlanMap $PlanMap
if ($null -eq $planMatch) {
$Errors.Add("Backup contains unexpected registry path '$snapshotPath' that is not allowed by SelectedFeatures.")
$Errors.Add((Get-Translation -Key 'BackupUnexpectedRegistryPath' -FormatArgs @($snapshotPath)))
return
}
@@ -308,21 +354,21 @@ function Test-RegistrySnapshotAgainstAllowList {
$valueExists = [bool]$valueSnapshot.Exists
if (-not (Test-RegistryValueAllowedByPlan -PlanMatch $planMatch -ValueName $valueName)) {
$Errors.Add("Backup contains unexpected value '$valueName' under '$snapshotPath'.")
$Errors.Add((Get-Translation -Key 'BackupUnexpectedValue' -FormatArgs @($valueName, $snapshotPath)))
}
$kindName = if ($valueSnapshot.PSObject.Properties['Kind']) { [string]$valueSnapshot.Kind } else { '' }
$valueReference = Get-RegistryValueReferenceForError -SnapshotPath $snapshotPath -ValueName $valueName
if ($valueExists) {
if (-not (Test-RegistryValueKindNameSupported -KindName $kindName)) {
$Errors.Add("Backup contains unsupported registry value kind '$kindName' for '$valueReference'.")
$Errors.Add((Get-Translation -Key 'BackupUnsupportedValueKind' -FormatArgs @($kindName, $valueReference)))
}
elseif (-not (Test-RegistryValueDataMatchesKind -KindName $kindName -Data $valueSnapshot.Data)) {
$Errors.Add("Backup contains invalid registry data for kind '$kindName' at '$valueReference'.")
$Errors.Add((Get-Translation -Key 'BackupInvalidValueData' -FormatArgs @($kindName, $valueReference)))
}
}
elseif (-not [string]::IsNullOrWhiteSpace($kindName)) {
$Errors.Add("Backup value '$valueReference' must not define Kind when Exists is false.")
$Errors.Add((Get-Translation -Key 'BackupValueMustNotDefineKind' -FormatArgs @($valueReference)))
}
}
+9 -9
View File
@@ -319,7 +319,7 @@ function Restore-StartMenuFromBackup {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "No start menu backup file found for user $userName."
Message = Get-Translation -Key 'StartMenuNoBackupFoundForUser' -FormatArgs @($userName)
}
}
@@ -328,7 +328,7 @@ function Restore-StartMenuFromBackup {
return [PSCustomObject]@{
UserName = $userName
Result = $true
Message = "[WhatIf] Restored start menu for user $userName."
Message = Get-Translation -Key 'StartMenuWhatIfRestoredForUser' -FormatArgs @($userName)
}
}
@@ -336,7 +336,7 @@ function Restore-StartMenuFromBackup {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "No start menu backup file found for user $userName."
Message = Get-Translation -Key 'StartMenuNoBackupFoundForUser' -FormatArgs @($userName)
}
}
@@ -349,14 +349,14 @@ function Restore-StartMenuFromBackup {
return [PSCustomObject]@{
UserName = $userName
Result = $true
Message = "Restored start menu for user $userName."
Message = Get-Translation -Key 'StartMenuRestoredForUser' -FormatArgs @($userName)
}
}
catch {
return [PSCustomObject]@{
UserName = $userName
Result = $false
Message = "Failed to restore start menu for user $userName. $($_.Exception.Message)"
Message = Get-Translation -Key 'StartMenuRestoreFailedForUser' -FormatArgs @($userName, $_.Exception.Message)
}
}
}
@@ -440,7 +440,7 @@ function Restore-StartMenuForAllUsers {
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $true
Message = '[WhatIf] Removed start2.bin for the default user profile.'
Message = Get-Translation -Key 'StartMenuWhatIfRemovedDefaultProfile'
}
}
else {
@@ -449,14 +449,14 @@ function Restore-StartMenuForAllUsers {
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $true
Message = 'Removed start2.bin for the default user profile.'
Message = Get-Translation -Key 'StartMenuRemovedDefaultProfile'
}
}
catch {
$results += [PSCustomObject]@{
UserName = 'Default'
Result = $false
Message = "Failed to remove start2.bin for the default user profile. $($_.Exception.Message)"
Message = Get-Translation -Key 'StartMenuRemoveFailedDefaultProfile' -FormatArgs @($_.Exception.Message)
}
}
}
@@ -467,7 +467,7 @@ function Restore-StartMenuForAllUsers {
$results += [PSCustomObject]@{
UserName = 'unknown'
Result = $false
Message = 'No user start menu locations were found.'
Message = Get-Translation -Key 'StartMenuNoUserLocationsFound'
}
}
+16 -16
View File
@@ -21,14 +21,14 @@ function Import-RegistryBackup {
)
if (-not (Test-Path -LiteralPath $FilePath)) {
throw "Backup file was not found: $FilePath"
throw (Get-Translation -Key 'BackupFileNotFound' -FormatArgs @($FilePath))
}
try {
$rawBackup = Get-Content -LiteralPath $FilePath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
}
catch {
throw "Failed to read backup file '$FilePath'. The file is not valid JSON."
throw (Get-Translation -Key 'BackupFileNotValidJson' -FormatArgs @($FilePath))
}
return ConvertTo-NormalizedRegistryBackup -Backup $rawBackup
@@ -61,22 +61,22 @@ function ConvertTo-NormalizedRegistryBackup {
$errors = New-Object System.Collections.Generic.List[string]
if (-not $Backup.PSObject.Properties['Version']) {
$errors.Add('Missing property: Version')
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('Version')))
}
elseif ([string]$Backup.Version -ne '1.0') {
$errors.Add("Unsupported backup version '$($Backup.Version)'.")
$errors.Add((Get-Translation -Key 'BackupUnsupportedVersion' -FormatArgs @($Backup.Version)))
}
if (-not $Backup.PSObject.Properties['BackupType']) {
$errors.Add('Missing property: BackupType')
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('BackupType')))
}
elseif ([string]$Backup.BackupType -ne 'RegistryState') {
$errors.Add("Unsupported BackupType '$($Backup.BackupType)'.")
$errors.Add((Get-Translation -Key 'BackupUnsupportedType' -FormatArgs @($Backup.BackupType)))
}
$normalizedTarget = ''
if (-not $Backup.PSObject.Properties['Target'] -or [string]::IsNullOrWhiteSpace([string]$Backup.Target)) {
$errors.Add('Missing property: Target')
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('Target')))
}
else {
$normalizedTarget = [string]$Backup.Target
@@ -88,27 +88,27 @@ function ConvertTo-NormalizedRegistryBackup {
$targetUserName = $normalizedTarget.Substring(5)
$targetValidation = Test-TargetUserName -UserName $targetUserName
if (-not $targetValidation.IsValid) {
$errors.Add("Invalid user '$normalizedTarget'")
$errors.Add((Get-Translation -Key 'BackupInvalidUser' -FormatArgs @($normalizedTarget)))
}
}
elseif ($normalizedTarget -like 'CurrentUser:*') {
$targetCurrentUserName = $normalizedTarget.Substring(12)
if (Test-RunningAsSystem) {
$errors.Add("Backup was made for '$targetCurrentUserName' and is user-scoped. Re-run as that user; SYSTEM cannot restore a CurrentUser backup.")
$errors.Add((Get-Translation -Key 'BackupUserScopedWrongContext' -FormatArgs @($targetCurrentUserName)))
}
elseif ([string]::IsNullOrWhiteSpace($targetCurrentUserName) -or
-not (Test-UserNameMatch -UserNameA $targetCurrentUserName -UserNameB $env:USERNAME)) {
$errors.Add("Backup was made for '$targetCurrentUserName', this does not match current user '$env:USERNAME'.")
$errors.Add((Get-Translation -Key 'BackupUserMismatch' -FormatArgs @($targetCurrentUserName, $env:USERNAME)))
}
}
else {
$errors.Add("Unsupported Target '$normalizedTarget'.")
$errors.Add((Get-Translation -Key 'BackupUnsupportedTarget' -FormatArgs @($normalizedTarget)))
}
}
$registryKeys = @()
if (-not $Backup.PSObject.Properties['RegistryKeys']) {
$errors.Add('Missing property: RegistryKeys')
$errors.Add((Get-Translation -Key 'BackupMissingProperty' -FormatArgs @('RegistryKeys')))
}
else {
$registryKeys = @($Backup.RegistryKeys)
@@ -133,7 +133,7 @@ function ConvertTo-NormalizedRegistryBackup {
$allSelectedFeatures = @($selectedFeatures) + @($selectedUndoFeatures)
if ($allSelectedFeatures.Count -eq 0) {
$errors.Add('Backup must contain at least one feature ID in SelectedFeatures or SelectedUndoFeatures.')
$errors.Add((Get-Translation -Key 'BackupMissingSelectedFeatures'))
}
else {
try {
@@ -143,17 +143,17 @@ function ConvertTo-NormalizedRegistryBackup {
}
}
catch {
$errors.Add("Failed to validate backup: $($_.Exception.Message)")
$errors.Add((Get-Translation -Key 'BackupAllowlistValidationFailed' -FormatArgs @($_.Exception.Message)))
}
}
if ($errors.Count -gt 0) {
Write-Error "Backup validation failed: $($errors -join ' ')"
if ($errors.Count -eq 1) {
throw ("Validation failed: $($errors[0])")
throw (Get-Translation -Key 'BackupValidationSingleError' -FormatArgs @($errors[0]))
}
else {
throw ("Validation failed with $($errors.Count) errors. See console output for details.")
throw (Get-Translation -Key 'BackupValidationMultipleErrors' -FormatArgs @($errors.Count))
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ function Import-JsonFile {
}
try {
$jsonContent = Get-Content -Path $filePath -Raw | ConvertFrom-Json
$jsonContent = Get-Content -Path $filePath -Raw -Encoding UTF8 | ConvertFrom-Json
# Validate version if specified
if ($expectedVersion -and $jsonContent.Version -and $jsonContent.Version -ne $expectedVersion) {
+424
View File
@@ -0,0 +1,424 @@
<#
.SYNOPSIS
Resolves a language code to an available Config/Languages folder, or falls back to en-US.
#>
function Resolve-LanguageFolder {
param(
[Parameter(Mandatory)]
[string]$LanguageCode,
[string]$LanguagesPath = $script:LanguagesPath
)
$exactPath = Join-Path $LanguagesPath $LanguageCode
if (Test-Path $exactPath -PathType Container) {
return $LanguageCode
}
$languagePrefix = ($LanguageCode -split '-')[0]
$prefixMatch = Get-ChildItem -Path $LanguagesPath -Directory -Filter "$languagePrefix-*" -ErrorAction SilentlyContinue | Select-Object -First 1
if ($prefixMatch) {
return $prefixMatch.Name
}
return 'en-US'
}
<#
.SYNOPSIS
Loads Chrome/Features/Categories JSON for a language folder, or returns $null on failure.
#>
function Import-LanguageContent {
param(
[Parameter(Mandatory)]
[string]$LanguageFolder,
[string]$LanguagesPath = $script:LanguagesPath
)
$folderPath = Join-Path $LanguagesPath $LanguageFolder
$chrome = Import-JsonFile -filePath (Join-Path $folderPath 'Chrome.json')
$features = Import-JsonFile -filePath (Join-Path $folderPath 'Features.json')
$categories = Import-JsonFile -filePath (Join-Path $folderPath 'Categories.json')
if (-not $chrome -or -not $features -or -not $categories) {
return $null
}
return [PSCustomObject]@{
LanguageCode = $LanguageFolder
Chrome = $chrome
Features = $features.Features
UiGroups = $features.UiGroups
Categories = $categories
}
}
<#
.SYNOPSIS
Loads the active language content, falling back to en-US when the requested language fails to load.
#>
function Import-LanguageFile {
param(
[string]$LanguageCode = ([System.Globalization.CultureInfo]::CurrentUICulture.Name),
[string]$LanguagesPath = $script:LanguagesPath
)
$resolvedFolder = Resolve-LanguageFolder -LanguageCode $LanguageCode -LanguagesPath $LanguagesPath
$content = Import-LanguageContent -LanguageFolder $resolvedFolder -LanguagesPath $LanguagesPath
if (-not $content -and $resolvedFolder -ne 'en-US') {
Write-Warning "Failed to load language '$resolvedFolder', falling back to en-US."
$content = Import-LanguageContent -LanguageFolder 'en-US' -LanguagesPath $LanguagesPath
}
if (-not $content) {
Write-Error "Unable to load the en-US language files. The GUI cannot continue without them."
return $null
}
if ($content.LanguageCode -ne 'en-US') {
$content | Add-Member -MemberType NoteProperty -Name 'Fallback' -Value (Import-LanguageContent -LanguageFolder 'en-US' -LanguagesPath $LanguagesPath)
}
return $content
}
<#
.SYNOPSIS
Returns the CLDR plural category ('one', 'other', etc.) for a count in the given language.
.DESCRIPTION
Each language's CLDR plural rule is its own case, keyed by language prefix ('en' matches
both 'en-US' and 'en-GB'). A language without a case falls back to the English rule, which
is also correct for Dutch and German but wrong for languages with 3+ plural categories or
a different singular/plural split.
#>
function Get-PluralCategory {
param(
[Parameter(Mandatory)]
[string]$LanguageCode,
[Parameter(Mandatory)]
[int]$Count
)
$languagePrefix = ($LanguageCode -split '-')[0].ToLowerInvariant()
switch ($languagePrefix) {
# English CLDR rule: singular only for exactly 1, plural otherwise. Also correct for Dutch/German.
default {
if ($Count -eq 1) {
return 'one'
}
return 'other'
}
}
}
<#
.SYNOPSIS
Returns a language and its Fallback (if any) as an ordered lookup chain, skipping nulls.
#>
function Get-LanguageFallbackChain {
param(
[object]$Lang
)
return @($Lang, $Lang.Fallback) | Where-Object { $_ }
}
<#
.SYNOPSIS
Finds the named section object (Chrome, Features, UiGroups, or Categories) that owns a key.
.DESCRIPTION
A FeatureId, GroupId, and CategoryId aren't guaranteed to be disjoint (Config/Features.json
can and does reuse the same string as both a GroupId and a FeatureId), so the default search
order below is only a fallback for callers who don't know which section their key belongs to.
Pass -Section to look in exactly one section instead of guessing from the order.
#>
function Find-TranslationSection {
param(
[Parameter(Mandatory)]
[object]$Lang,
[Parameter(Mandatory)]
[string]$Key,
[ValidateSet('', 'Chrome', 'Features', 'UiGroups', 'Categories')]
[string]$Section = ''
)
if ($Section) {
$sectionObject = $Lang.$Section
if ($sectionObject -and $sectionObject.PSObject.Properties[$Key]) {
return $sectionObject
}
return $null
}
foreach ($sectionName in 'Chrome', 'Features', 'UiGroups', 'Categories') {
$sectionObject = $Lang.$sectionName
if ($sectionObject -and $sectionObject.PSObject.Properties[$Key]) {
return $sectionObject
}
}
return $null
}
<#
.SYNOPSIS
Looks up a translated value from the active language, falling back to en-US, then to the key itself.
.DESCRIPTION
Chrome.json keys are flat strings, so -Field is omitted for those. Features/UiGroups/Categories
keys (FeatureId/GroupId/CategoryId) resolve to an object, so -Field picks the property on it
(Label, ToolTip, ApplyText, UndoLabel, ApplyUndoText). One generic lookup covers every section
instead of a separate function per section.
A FeatureId and a GroupId aren't guaranteed to be distinct strings, so pass -Section
('Features', 'UiGroups', or 'Categories') whenever the caller knows which one it means,
rather than relying on Find-TranslationSection's search order to guess correctly.
When -Count is supplied, tries the plural-suffixed key ("$Key`_$category") before the bare key,
so callers don't need to add a plural variant for every string, only the ones that need one.
.OUTPUTS
System.String. The translated string, or the original Key if no language has it.
#>
function Get-Translation {
param(
[Parameter(Mandatory)]
[string]$Key,
[string]$Field = $null,
[object]$Lang = $script:Lang,
[Nullable[int]]$Count = $null,
[object[]]$FormatArgs = $null,
[ValidateSet('', 'Chrome', 'Features', 'UiGroups', 'Categories')]
[string]$Section = ''
)
if (-not $Lang) {
return $Key
}
$lookupKeys = @($Key)
if ($null -ne $Count) {
$category = Get-PluralCategory -LanguageCode $Lang.LanguageCode -Count $Count
$lookupKeys = @("${Key}_$category", $Key)
}
$resolved = $Key
:langLoop foreach ($candidateLang in (Get-LanguageFallbackChain -Lang $Lang)) {
foreach ($lookupKey in $lookupKeys) {
$sectionObject = Find-TranslationSection -Lang $candidateLang -Key $lookupKey -Section $Section
if (-not $sectionObject) { continue }
$entry = $sectionObject.$lookupKey
if ($Field) {
if ($entry.PSObject.Properties[$Field]) {
$resolved = [string]$entry.$Field
break langLoop
}
continue
}
$resolved = [string]$entry
break langLoop
}
}
if ($null -ne $FormatArgs -and $FormatArgs.Count -gt 0) {
try {
return $resolved -f $FormatArgs
}
catch {
Write-Warning "Translation '$Key' has a placeholder mismatch with its format arguments: $($_.Exception.Message)"
return $resolved
}
}
return $resolved
}
<#
.SYNOPSIS
Looks up a translated label for one value of a UiGroup, falling back to en-US, then to the raw label.
.DESCRIPTION
UiGroups[GroupId].Values in the language files is a FeatureId -> Label map, one level deeper
than Get-Translation's -Field lookup handles, so this has its own small fallback walk instead
of overloading Get-Translation's flat-field shape.
#>
function Get-GroupValueTranslation {
param(
[Parameter(Mandatory)]
[string]$GroupId,
[Parameter(Mandatory)]
[string]$FeatureId,
[Parameter(Mandatory)]
[string]$FallbackLabel,
[object]$Lang = $script:Lang
)
if (-not $Lang) {
return $FallbackLabel
}
foreach ($candidateLang in (Get-LanguageFallbackChain -Lang $Lang)) {
if (-not $candidateLang.UiGroups) { continue }
$group = $candidateLang.UiGroups.$GroupId
if (-not $group -or -not $group.Values) { continue }
$valueEntry = $group.Values.PSObject.Properties[$FeatureId]
if ($valueEntry) {
return [string]$valueEntry.Value
}
}
return $FallbackLabel
}
<#
.SYNOPSIS
Flattens a loaded language object into a sorted set of dotted key paths.
.DESCRIPTION
Chrome.json keys are already flat and are used as-is. Features/UiGroups/Categories
keys are one level deeper (EntryId -> {Field: value}), so each field becomes its own
"Section.EntryId.Field" path (e.g. "Features.DisableTelemetry.Label"). UiGroups' nested
Values map becomes "UiGroups.GroupId.Values.FeatureId". Used by Test-LanguageKeyCoverage
to diff one language's key set against another's.
#>
function Get-LanguageKeyPaths {
param(
[Parameter(Mandatory)]
[object]$Lang
)
$paths = New-Object System.Collections.Generic.List[string]
foreach ($chromeKey in @($Lang.Chrome.PSObject.Properties.Name)) {
if ([string]::IsNullOrEmpty($chromeKey)) { continue }
$paths.Add($chromeKey)
}
foreach ($sectionName in 'Features', 'Categories') {
$section = $Lang.$sectionName
if (-not $section) { continue }
foreach ($entryId in @($section.PSObject.Properties.Name)) {
if ([string]::IsNullOrEmpty($entryId)) { continue }
foreach ($field in @($section.$entryId.PSObject.Properties.Name)) {
if ([string]::IsNullOrEmpty($field)) { continue }
$paths.Add("$sectionName.$entryId.$field")
}
}
}
foreach ($groupId in @($Lang.UiGroups.PSObject.Properties.Name)) {
if ([string]::IsNullOrEmpty($groupId)) { continue }
$group = $Lang.UiGroups.$groupId
foreach ($field in @($group.PSObject.Properties.Name)) {
if ([string]::IsNullOrEmpty($field)) { continue }
if ($field -eq 'Values') {
foreach ($featureId in @($group.Values.PSObject.Properties.Name)) {
if ([string]::IsNullOrEmpty($featureId)) { continue }
$paths.Add("UiGroups.$groupId.Values.$featureId")
}
continue
}
$paths.Add("UiGroups.$groupId.$field")
}
}
return @($paths | Sort-Object -Unique)
}
<#
.SYNOPSIS
Diffs a language's key set against a baseline language, reporting missing and extra keys.
.DESCRIPTION
Loads both languages fresh from disk (independent of any already-cached $script:Lang),
flattens each with Get-LanguageKeyPaths, and compares the two sets. Intended to run
against en-US as the baseline; useful today (catches a key present in one en-US file but
not another) and becomes the real translation-completeness check once non-English
language folders are contributed.
.OUTPUTS
PSCustomObject with ResolvedLanguageCode (the folder actually compared, after the same
exact/prefix/en-US resolution Import-LanguageFile applies), MissingKeys (present in
Baseline, absent from LanguageCode), and ExtraKeys (present in LanguageCode, absent from
Baseline) properties, the latter two sorted arrays.
#>
function Test-LanguageKeyCoverage {
param(
[Parameter(Mandatory)]
[string]$LanguageCode,
[string]$BaselineLanguageCode = 'en-US',
[string]$LanguagesPath = $script:LanguagesPath
)
$resolvedLanguageCode = Resolve-LanguageFolder -LanguageCode $LanguageCode -LanguagesPath $LanguagesPath
$baseline = Import-LanguageContent -LanguageFolder $BaselineLanguageCode -LanguagesPath $LanguagesPath
$target = Import-LanguageContent -LanguageFolder $resolvedLanguageCode -LanguagesPath $LanguagesPath
if (-not $baseline -or -not $target) {
Write-Error "Unable to load language content for coverage comparison ('$BaselineLanguageCode' vs '$resolvedLanguageCode')."
return $null
}
$baselineKeys = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-LanguageKeyPaths -Lang $baseline))
$targetKeys = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-LanguageKeyPaths -Lang $target))
$missingKeys = @($baselineKeys | Where-Object { -not $targetKeys.Contains($_) } | Sort-Object)
$extraKeys = @($targetKeys | Where-Object { -not $baselineKeys.Contains($_) } | Sort-Object)
return [PSCustomObject]@{
ResolvedLanguageCode = $resolvedLanguageCode
MissingKeys = $missingKeys
ExtraKeys = $extraKeys
}
}
<#
.SYNOPSIS
Substitutes %LANG:Key% markers in XAML text with translated, XML-escaped values.
.DESCRIPTION
Only resolves flat Chrome.json keys, since XAML markers never reference a Feature/Category/
UiGroup field directly (those get their text from Get-Translation calls in the GUI scripts
that build dynamic controls). Runs a single pass over every marker rather than sequential
.Replace() calls, so an already-substituted value can't be re-matched by a later key.
After substitution, scans for any %LANG:...% text that survived unresolved and throws,
since Get-Translation's key-as-fallback behavior means a missing key would otherwise render
as plain, un-marked text (e.g. "TitleBarClose" instead of a visible error) rather than being
caught here. Checks both the active language and its Fallback before flagging a key missing,
so a partially-translated language degrades to en-US text instead of failing to load.
#>
function ConvertTo-LocalizedXaml {
param(
[Parameter(Mandatory)]
[string]$Xaml,
[object]$Lang = $script:Lang
)
$missingKeys = New-Object System.Collections.Generic.List[string]
$evaluator = [System.Text.RegularExpressions.MatchEvaluator]{
param($match)
$key = $match.Groups[1].Value
$resolvesSomewhere = @(Get-LanguageFallbackChain -Lang $Lang) | Where-Object { $_.Chrome -and $_.Chrome.PSObject.Properties[$key] }
if (-not $resolvesSomewhere) { $missingKeys.Add($key) }
$value = if ($resolvesSomewhere) { Get-Translation -Key $key -Lang $Lang } else { $key }
return [System.Security.SecurityElement]::Escape($value)
}
$result = [System.Text.RegularExpressions.Regex]::Replace($Xaml, '%LANG:([A-Za-z0-9_]+)%', $evaluator)
if ($missingKeys.Count -gt 0) {
throw "Unresolved localization marker(s) in XAML, key(s) not found in any language: $($missingKeys -join ', ')"
}
return $result
}
+17 -8
View File
@@ -142,6 +142,15 @@ function Update-AppsPanelSort {
}
}
<#
.SYNOPSIS
Updates the "N apps selected" status text and shows or hides the app-removal scope section.
.DESCRIPTION
The scope section only makes sense once at least one app is selected, so it's hidden
entirely at zero and its description text is refreshed via Update-AppRemovalScopeDescription
whenever the selection count changes.
#>
function Update-AppSelectionStatus {
param(
[System.Windows.Controls.Panel]$AppsPanel,
@@ -158,7 +167,7 @@ function Update-AppSelectionStatus {
$selectedCount++
}
}
$AppSelectionStatus.Text = "$selectedCount app(s) selected for removal"
$AppSelectionStatus.Text = Get-Translation -Key 'AppSelectionStatusSelected' -Count $selectedCount -FormatArgs @($selectedCount)
if ($AppRemovalScopeCombo -and $AppRemovalScopeSection -and $AppRemovalScopeDescription) {
if ($selectedCount -gt 0) {
@@ -189,13 +198,13 @@ function Update-AppRemovalScopeDescription {
# Content is the display text and will change once translated; Name is stable.
switch ($selectedItem.Name) {
"AppRemovalScopeAllUsers" {
$AppRemovalScopeDescription.Text = "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users."
$AppRemovalScopeDescription.Text = Get-Translation -Key 'AppRemovalScopeDescriptionAllUsers'
}
"AppRemovalScopeCurrentUser" {
$AppRemovalScopeDescription.Text = "Apps will only be removed for the current user."
$AppRemovalScopeDescription.Text = Get-Translation -Key 'AppRemovalScopeDescriptionCurrentUser'
}
"AppRemovalScopeTargetUser" {
$AppRemovalScopeDescription.Text = "Apps will only be removed for the specified target user."
$AppRemovalScopeDescription.Text = Get-Translation -Key 'AppRemovalScopeDescriptionTargetUser'
}
}
}
@@ -448,9 +457,9 @@ function Add-AppsToMainWindow {
$dot.Style = $Window.Resources['AppRecommendationDotStyle']
$dot.Fill = switch ($app.Recommendation) { 'safe' { $brushSafe } 'unsafe' { $brushUnsafe } default { $brushDefault } }
$dot.ToolTip = switch ($app.Recommendation) {
'safe' { '[Recommended] Safe to remove for most users' }
'unsafe' { '[Not Recommended] Only remove if you know what you are doing' }
default { "[Optional] Can be safely removed if you don't need this app" }
'safe' { Get-Translation -Key 'AppLegendRecommendedTooltip' }
'unsafe' { Get-Translation -Key 'AppLegendNotRecommendedTooltip' }
default { Get-Translation -Key 'AppLegendOptionalTooltip' }
}
[System.Windows.Controls.Grid]::SetColumn($dot, 0)
@@ -608,7 +617,7 @@ function Initialize-MainWindowApps {
if ($null -eq $listOfApps) {
Write-Warning "WinGet returned no data (command timed out or failed)"
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'AppSelectionWinGetLoadFailedMessage') -Title (Get-Translation -Key 'ErrorTitle') -Button 'OK' -Icon 'Error' | Out-Null
$OnlyInstalledAppsBox.IsChecked = $false
}
}
+33 -9
View File
@@ -96,6 +96,17 @@ function Get-PendingTweakActions {
return @($actions.ToArray())
}
<#
.SYNOPSIS
Builds the plain-text list of pending changes: apps to remove and tweaks to apply or undo.
.PARAMETER ShowCurrentlyAppliedTweaksCheckBox
When checked, pending tweak actions are computed against the system's live applied
state instead of the controls' initial values.
.OUTPUTS
System.String[]. One line per pending change, or an empty array if nothing is selected.
#>
function New-Overview {
param(
[System.Windows.Window]$Window,
@@ -114,12 +125,12 @@ function New-Overview {
}
}
if ($selectedAppsCount -gt 0) {
$changesList += "Remove $selectedAppsCount application(s)"
$changesList += Get-Translation -Key 'DeployChangeRemoveApps' -Count $selectedAppsCount -FormatArgs @($selectedAppsCount)
}
foreach ($tweakAction in @(Get-PendingTweakActions -Window $Window -ShowAppliedTweaksMode:$showAppliedTweaksMode)) {
if ($tweakAction.Action -eq 'Undo') {
$changesList += "Undo: $($tweakAction.Label)"
$changesList += Get-Translation -Key 'DeployChangeUndoPrefix' -FormatArgs @($tweakAction.Label)
}
else {
$changesList += $tweakAction.Label
@@ -129,6 +140,11 @@ function New-Overview {
return $changesList
}
<#
.SYNOPSIS
Shows the pending-changes list from New-Overview in a message box, or a
"no changes selected" message when there's nothing to show.
#>
function Invoke-ShowChangesOverview {
param(
[System.Windows.Window]$Window,
@@ -139,12 +155,12 @@ function Invoke-ShowChangesOverview {
$changesList = New-Overview -Window $Window -AppsPanel $AppsPanel -ShowCurrentlyAppliedTweaksCheckBox $ShowCurrentlyAppliedTweaksCheckBox
if ($changesList.Count -eq 0) {
Show-MessageBox -Message 'No changes have been selected.' -Title 'Selected Changes' -Button 'OK' -Icon 'Information'
Show-MessageBox -Message (Get-Translation -Key 'DeployNoChangesSelectedMessage') -Title (Get-Translation -Key 'DeploySelectedChangesTitle') -Button 'OK' -Icon 'Information'
return
}
$message = ($changesList | ForEach-Object { "$([char]0x2022) $_" }) -join "`n"
Show-MessageBox -Message $message -Title 'Selected Changes' -Button 'OK' -Icon 'None' -Width 600
Show-MessageBox -Message $message -Title (Get-Translation -Key 'DeploySelectedChangesTitle') -Button 'OK' -Icon 'None' -Width 600
}
<#
@@ -437,6 +453,14 @@ function Update-AppliedTweaksUserModeState {
}
}
<#
.SYNOPSIS
Updates the deployment-target description text to match the selected user option.
.DESCRIPTION
Falls back to a generic description (current user, other user, or Sysprep default
profile) when a username can't be resolved yet, and to a specific one once it can.
#>
function Update-UserSelectionDescription {
param(
[System.Windows.Window]$Window,
@@ -449,23 +473,23 @@ function Update-UserSelectionDescription {
0 {
$currentUserName = Get-UserName
if ([string]::IsNullOrWhiteSpace($currentUserName)) {
$UserSelectionDescription.Text = "The currently logged-in user profile"
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionCurrentUser'
}
else {
$UserSelectionDescription.Text = "The currently logged-in user profile: $currentUserName"
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionCurrentUserNamed' -FormatArgs @($currentUserName)
}
}
1 {
$targetUserName = $OtherUsernameTextBox.Text.Trim()
if ([string]::IsNullOrWhiteSpace($targetUserName)) {
$UserSelectionDescription.Text = "A different user profile on this system"
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionOtherUser'
}
else {
$UserSelectionDescription.Text = "A different user profile on this system: $targetUserName"
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionOtherUserNamed' -FormatArgs @($targetUserName)
}
}
default {
$UserSelectionDescription.Text = "The default user template, affecting all new users created after this point. Useful for Sysprep deployment."
$UserSelectionDescription.Text = Get-Translation -Key 'DeployUserDescriptionSysprep'
}
}
+29 -22
View File
@@ -154,7 +154,8 @@ function New-DynamicTweakControls {
Returns the existing category panel or creates and registers a new one.
.PARAMETER CategoryObj
The category definition containing Name and Icon properties.
The category definition, with Name and Icon for internal use (the map key, the
wiki-link slug) and CategoryId for the translated header shown to the user.
.OUTPUTS
System.Windows.Controls.StackPanel. The category's content panel.
@@ -162,6 +163,7 @@ function New-DynamicTweakControls {
function Get-OrCreateCategoryCard($categoryObj) {
$categoryName = $categoryObj.Name
$categoryIcon = $categoryObj.Icon
$categoryLabel = Get-Translation -Key $categoryObj.CategoryId -Field 'Label' -Section 'Categories'
if ($script:CategoryCardMap.ContainsKey($categoryName)) { return $script:CategoryCardMap[$categoryName] }
@@ -193,7 +195,7 @@ function New-DynamicTweakControls {
$headerRow.Children.Add($icon) | Out-Null
$header = New-Object System.Windows.Controls.TextBlock
$header.Text = $categoryName
$header.Text = $categoryLabel
$header.Style = $Window.Resources['CategoryHeaderTextBlock']
$headerRow.Children.Add($header) | Out-Null
@@ -203,7 +205,7 @@ function New-DynamicTweakControls {
$helpBtn = New-Object System.Windows.Controls.Button
$helpBtn.Content = $helpIcon
$helpBtn.ToolTip = "Open the wiki for more info on '$categoryName' tweaks"
$helpBtn.ToolTip = Get-Translation -Key 'TweaksCategoryHelpTooltip' -FormatArgs @($categoryLabel)
$helpBtn.Tag = (Get-WikiUrlForCategory -category $categoryName)
$helpBtn.Style = $Window.Resources['CategoryHelpLinkButtonStyle']
$helpBtn.Add_Click({
@@ -347,17 +349,18 @@ function New-DynamicTweakControls {
if ($featureMap.ContainsKey($soleFid)) {
$soleFeature = $featureMap[$soleFid]
$opt = 'Apply'
if ($soleFeature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($soleFeature.FeatureId -match '^Enable') { $opt = 'Enable' }
$items = @('No Change', $opt)
$soleFeatureLabel = Get-Translation -Key $soleFeature.FeatureId -Field 'Label' -Section 'Features'
$opt = Get-Translation -Key 'TweaksOptionApply'
if ($soleFeature.FeatureId -match '^Disable') { $opt = Get-Translation -Key 'TweaksOptionDisable' } elseif ($soleFeature.FeatureId -match '^Enable') { $opt = Get-Translation -Key 'TweaksOptionEnable' }
$items = @((Get-Translation -Key 'TweaksOptionNoChange'), $opt)
$comboName = ("Feature_{0}_Combo" -f $soleFeature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $soleFeature.Label -comboName $comboName -items $items
$combo = New-LabeledCombo -parent $panel -labelText $soleFeatureLabel -comboName $comboName -items $items
# attach tooltip from Features.json if present
if ($soleFeature.ToolTip -or $soleFeature.DisableWhenApplied -eq $true) {
$tooltipText = $soleFeature.ToolTip
$tooltipText = Get-Translation -Key $soleFeature.FeatureId -Field 'ToolTip' -Section 'Features'
if ($soleFeature.DisableWhenApplied -eq $true) {
$tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change."
$tooltipText = Get-Translation -Key 'TweaksAlreadyAppliedTooltip'
}
$tipBlock = New-Object System.Windows.Controls.TextBlock
$tipBlock.Text = $tooltipText
@@ -369,19 +372,20 @@ function New-DynamicTweakControls {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
}
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $soleFeature.FeatureId; Label = $soleFeature.Label; CategoryId = $categoryId }
$script:UiControlMappings[$comboName] = @{ Type = 'feature'; FeatureId = $soleFeature.FeatureId; Label = $soleFeatureLabel; CategoryId = $categoryId }
}
continue
}
$items = @('No Change') + ($filteredValues | ForEach-Object { $_.Label })
$groupLabel = Get-Translation -Key $group.GroupId -Field 'Label' -Section 'UiGroups'
$items = @((Get-Translation -Key 'TweaksOptionNoChange')) + ($filteredValues | ForEach-Object { Get-GroupValueTranslation -GroupId $group.GroupId -FeatureId $_.FeatureIds[0] -FallbackLabel $_.Label })
$comboName = 'Group_{0}Combo' -f $group.GroupId
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $group.Label -comboName $comboName -items $items
$combo = New-LabeledCombo -parent $panel -labelText $groupLabel -comboName $comboName -items $items
# attach tooltip from UiGroups if present
if ($group.ToolTip) {
$tipBlock = New-Object System.Windows.Controls.TextBlock
$tipBlock.Text = $group.ToolTip
$tipBlock.Text = Get-Translation -Key $group.GroupId -Field 'ToolTip' -Section 'UiGroups'
$tipBlock.TextWrapping = 'Wrap'
$tipBlock.MaxWidth = 420
$combo.ToolTip = $tipBlock
@@ -389,21 +393,22 @@ function New-DynamicTweakControls {
try { $lblBorderObj = $Window.FindName("$comboName`_LabelBorder") } catch {}
if ($lblBorderObj) { $lblBorderObj.ToolTip = $tipBlock }
}
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $filteredValues; Label = $group.Label; CategoryId = $categoryId }
$script:UiControlMappings[$comboName] = @{ Type = 'group'; Values = $filteredValues; Label = $groupLabel; CategoryId = $categoryId }
}
elseif ($item.Type -eq 'feature') {
$feature = $item.Data
$opt = 'Apply'
if ($feature.FeatureId -match '^Disable') { $opt = 'Disable' } elseif ($feature.FeatureId -match '^Enable') { $opt = 'Enable' }
$items = @('No Change', $opt)
$featureLabel = Get-Translation -Key $feature.FeatureId -Field 'Label' -Section 'Features'
$opt = Get-Translation -Key 'TweaksOptionApply'
if ($feature.FeatureId -match '^Disable') { $opt = Get-Translation -Key 'TweaksOptionDisable' } elseif ($feature.FeatureId -match '^Enable') { $opt = Get-Translation -Key 'TweaksOptionEnable' }
$items = @((Get-Translation -Key 'TweaksOptionNoChange'), $opt)
$comboName = ("Feature_{0}_Combo" -f $feature.FeatureId) -replace '[^a-zA-Z0-9_]', ''
if (-not $panel) { $panel = Get-OrCreateCategoryCard -categoryObj $categoryObj }
$combo = New-LabeledCombo -parent $panel -labelText $feature.Label -comboName $comboName -items $items
$combo = New-LabeledCombo -parent $panel -labelText $featureLabel -comboName $comboName -items $items
# attach tooltip from Features.json if present, and include the disabled-state reason
if ($feature.ToolTip -or $feature.DisableWhenApplied -eq $true) {
$tooltipText = $feature.ToolTip
$tooltipText = Get-Translation -Key $feature.FeatureId -Field 'ToolTip' -Section 'Features'
if ($feature.DisableWhenApplied -eq $true) {
$tooltipText = "This tweak is already applied and cannot be undone automatically. Visit the Win11Debloat wiki for instructions on how to manually revert this change."
$tooltipText = Get-Translation -Key 'TweaksAlreadyAppliedTooltip'
}
$tipBlock = New-Object System.Windows.Controls.TextBlock
@@ -425,8 +430,10 @@ function New-DynamicTweakControls {
$script:FeatureLabelLookup = @{}
$script:UndoFeatureLabelLookup = @{}
foreach ($f in $featuresJson.Features) {
$script:FeatureLabelLookup[$f.FeatureId] = $f.Label
$script:UndoFeatureLabelLookup[$f.FeatureId] = $f.UndoLabel
$script:FeatureLabelLookup[$f.FeatureId] = Get-Translation -Key $f.FeatureId -Field 'Label' -Section 'Features'
if ($f.UndoLabel) {
$script:UndoFeatureLabelLookup[$f.FeatureId] = Get-Translation -Key $f.FeatureId -Field 'UndoLabel' -Section 'Features'
}
}
}
@@ -81,8 +81,9 @@ function Test-RestoreDialogFeatureCanAutoRevert {
Resolves a human-readable label for a feature shown in the restore dialog.
.DESCRIPTION
Returns the feature's Label from Features.json when found, falling back
to the raw FeatureId string. For null/empty FeatureIds returns 'Unknown feature'.
Returns the feature's translated Label when found, falling back to the
raw FeatureId string. For null/empty FeatureIds returns the translated
"unknown feature" placeholder.
.PARAMETER FeatureId
The feature identifier to resolve a label for.
@@ -97,12 +98,12 @@ function Get-RestoreDialogFeatureDisplayLabel {
)
if ([string]::IsNullOrWhiteSpace($FeatureId)) {
return 'Unknown feature'
return Get-Translation -Key 'RestoreUnknownFeature'
}
$featureDefinition = Get-RestoreDialogFeatureDefinition -FeatureId $FeatureId -Features $Features
if ($featureDefinition) {
return [string]$featureDefinition.Label
return Get-Translation -Key $FeatureId -Field 'Label' -Section 'Features'
}
return $FeatureId
+1
View File
@@ -32,6 +32,7 @@ function Show-AboutDialog {
# Load XAML from file
$xaml = Get-Content -Path $script:AboutWindowSchema -Raw
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$aboutWindow = [System.Windows.Markup.XamlReader]::Load($reader)
+2 -1
View File
@@ -24,6 +24,7 @@ function Show-AppSelectionWindow {
# Load XAML from file
$xaml = Get-Content -Path $script:AppSelectionSchema -Raw
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$window = [System.Windows.Markup.XamlReader]::Load($reader)
@@ -72,7 +73,7 @@ function Show-AppSelectionWindow {
$listOfApps = Get-WingetInstalledApps -TimeOut 10 -NonBlocking
if ($null -eq $listOfApps) {
# Show error that the script was unable to get list of apps from WinGet
Show-MessageBox -Message 'Unable to load list of installed apps via WinGet.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $window | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'AppSelectionWinGetLoadFailedMessage') -Title (Get-Translation -Key 'ErrorTitle') -Button 'OK' -Icon 'Error' -Owner $window | Out-Null
$onlyInstalledBox.IsChecked = $false
}
}
+19 -18
View File
@@ -37,6 +37,7 @@ function Show-ApplyModal {
# Load XAML from file
$xaml = Get-Content -Path $script:ApplyChangesWindowSchema -Raw
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$applyWindow = [System.Windows.Markup.XamlReader]::Load($reader)
@@ -74,8 +75,8 @@ function Show-ApplyModal {
# Initialize in-progress state
$script:ApplyInProgressPanel.Visibility = 'Visible'
$script:ApplyCompletionPanel.Visibility = 'Collapsed'
$script:ApplyStepNameEl.Text = "Preparing..."
$script:ApplyStepCounterEl.Text = "Preparing..."
$script:ApplyStepNameEl.Text = Get-Translation -Key 'ApplyPreparing'
$script:ApplyStepCounterEl.Text = Get-Translation -Key 'ApplyStepCounter' -FormatArgs @(0, 0)
$script:ApplyProgressBarEl.Value = 0
$script:ApplyModalInErrorState = $false
@@ -83,7 +84,7 @@ function Show-ApplyModal {
$script:ApplyProgressCallback = {
param($currentStep, $totalSteps, $stepName)
$script:ApplyStepNameEl.Text = $stepName
$script:ApplyStepCounterEl.Text = "Step $currentStep of $totalSteps"
$script:ApplyStepCounterEl.Text = Get-Translation -Key 'ApplyStepCounter' -FormatArgs @($currentStep, $totalSteps)
# Store current step/total in Tag properties for sub-step interpolation
$script:ApplyStepCounterEl.Tag = $currentStep
$script:ApplyProgressBarEl.Tag = $totalSteps
@@ -140,8 +141,8 @@ function Show-ApplyModal {
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
$script:ApplyCompletionTitleEl.Text = "Cancelled"
$script:ApplyCompletionMessageEl.Text = "Script execution was cancelled by the user."
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleCancelled'
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageCancelled'
} elseif ($failureCount -gt 0 -or $appRemovalVerificationUnavailable) {
if ($failureCount -gt 0) {
Write-Host "Script completed with $failureCount error(s)."
@@ -150,17 +151,17 @@ function Show-ApplyModal {
$script:ApplyCompletionIconEl.Text = [char]0xE7BA
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#e8912d"))
if ($failureCount -eq 0 -and $appRemovalVerificationUnavailable) {
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
$script:ApplyCompletionMessageEl.Text = "All changes were applied without errors, but Win11Debloat could not confirm that all selected apps were successfully uninstalled."
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleSuccess'
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageVerificationUnavailable'
}
else {
$script:ApplyCompletionTitleEl.Text = "Changes Applied with Errors"
$script:ApplyCompletionMessageEl.Text = "$failureCount change(s) failed. See console for details."
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleErrors'
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageFailures' -Count $failureCount -FormatArgs @($failureCount)
}
} else {
Write-Host "All changes have been applied successfully!"
$script:ApplyCompletionTitleEl.Text = "Changes Applied"
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleSuccess'
# Show completion message with reboot instructions if any applied features require reboot
if ($InvokeRestartExplorer) {
@@ -179,7 +180,7 @@ function Show-ApplyModal {
$applyRebootPanel.Visibility = 'Visible'
}
else {
$script:ApplyCompletionMessageEl.Text = "Your system is ready. Thanks for using Win11Debloat!"
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageReady'
}
}
}
@@ -191,24 +192,24 @@ function Show-ApplyModal {
$script:ApplyCompletionPanel.Visibility = 'Visible'
$script:ApplyCompletionIconEl.Text = [char]0xEA39
$script:ApplyCompletionIconEl.Foreground = [System.Windows.Media.SolidColorBrush]::new([System.Windows.Media.ColorConverter]::ConvertFromString("#c42b1c"))
$script:ApplyCompletionTitleEl.Text = "Error"
$script:ApplyCompletionMessageEl.Text = "An error occurred while applying changes: $($_.Exception.Message)"
$script:ApplyCompletionTitleEl.Text = Get-Translation -Key 'ApplyCompletionTitleError'
$script:ApplyCompletionMessageEl.Text = Get-Translation -Key 'ApplyCompletionMessageError' -FormatArgs @($_.Exception.Message)
# Set error state to change Kofi button to report link
$script:ApplyModalInErrorState = $true
# Update Kofi button to be a report issue button
$applyKofiBtn.Content = $null
$reportText = [System.Windows.Controls.TextBlock]::new()
$reportText.Text = 'Report a bug'
$reportText.Text = Get-Translation -Key 'MenuReportBug'
$reportText.VerticalAlignment = 'Center'
$reportText.FontSize = 14
$reportText.Margin = [System.Windows.Thickness]::new(0, 0, 0, 1)
$applyKofiBtn.Content = $reportText
[System.Windows.Automation.AutomationProperties]::SetName($applyKofiBtn, 'Report a bug')
[System.Windows.Automation.AutomationProperties]::SetName($applyKofiBtn, (Get-Translation -Key 'MenuReportBug'))
$applyWindow.Dispatcher.Invoke([System.Windows.Threading.DispatcherPriority]::Render, [action]{})
}
+4 -3
View File
@@ -72,8 +72,8 @@ function Hide-Bubble {
parameter is mandatory.
.PARAMETER Message
The text message to display inside the bubble. Defaults to
'View the selected changes here'.
The text message to display inside the bubble. Defaults to the
translated "view the selected changes" prompt.
.PARAMETER DurationSeconds
The number of seconds the bubble remains visible before auto-hiding.
@@ -91,7 +91,7 @@ function Show-Bubble {
[System.Windows.Controls.Control]$TargetControl,
[Parameter(Mandatory=$false)]
[string]$Message = 'View the selected changes here',
[string]$Message = (Get-Translation -Key 'BubbleViewChanges'),
[Parameter(Mandatory=$false)]
[int]$DurationSeconds = 5
@@ -104,6 +104,7 @@ function Show-Bubble {
Hide-Bubble -Immediate
$xaml = Get-Content -Path $script:BubbleHintSchema -Raw
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$bubblePanel = [System.Windows.Markup.XamlReader]::Load($reader)
+63 -29
View File
@@ -1,3 +1,26 @@
<#
.SYNOPSIS
Maps an internal import/export category ID ('Applications', 'System Tweaks',
'Deployment Settings') to its Chrome.json translation key.
.DESCRIPTION
The category strings stay as the stable internal IDs used for -contains checks and
hashtable lookups throughout this file; only the label shown to the user is translated.
#>
function Get-ImportExportCategoryLabelKey {
param(
[Parameter(Mandatory)]
[string]$Category
)
switch ($Category) {
'Applications' { return 'ImportExportCategoryApplications' }
'System Tweaks' { return 'ImportExportCategorySystemTweaks' }
'Deployment Settings' { return 'ImportExportCategoryDeploymentSettings' }
default { return $Category }
}
}
<#
.SYNOPSIS
Shows a modal category-selection dialog for importing or exporting configuration.
@@ -11,7 +34,7 @@ function Show-ImportExportConfigWindow {
[string[]]$Categories = @('Applications', 'System Tweaks', 'Deployment Settings'),
[string[]]$DisabledCategories = @(),
[hashtable]$CategoryDetails = @(),
[string]$ActionLabel = 'OK'
[string]$ActionLabel = (Get-Translation -Key 'MessageBoxOk')
)
# Show overlay on owner window
@@ -32,7 +55,7 @@ function Show-ImportExportConfigWindow {
$schemaPath = $script:ImportExportConfigSchema
if (-not $schemaPath -or -not (Test-Path $schemaPath)) {
Show-MessageBox -Message 'Import/Export window schema file could not be found.' -Title 'Error' -Button 'OK' -Icon 'Error' -Owner $Owner | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'ImportExportSchemaMissingMessage') -Title (Get-Translation -Key 'ErrorTitle') -Button 'OK' -Icon 'Error' -Owner $Owner | Out-Null
if ($overlay -and -not $overlayWasAlreadyVisible) {
try { $Owner.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' }) } catch { }
}
@@ -40,6 +63,7 @@ function Show-ImportExportConfigWindow {
}
$xaml = Get-Content -Path $schemaPath -Raw
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$dlg = [System.Windows.Markup.XamlReader]::Load($reader)
@@ -79,7 +103,7 @@ function Show-ImportExportConfigWindow {
# Create checkbox
$cb = New-Object System.Windows.Controls.CheckBox
$cb.Content = $cat
$cb.Content = Get-Translation -Key (Get-ImportExportCategoryLabelKey -Category $cat)
$cb.IsChecked = $true
$cb.Margin = [System.Windows.Thickness]::new(0,0,0,4)
$cb.FontSize = 14
@@ -89,7 +113,7 @@ function Show-ImportExportConfigWindow {
$cb.IsChecked = $false
$cb.IsEnabled = $false
$cb.Opacity = 0.65
$cb.ToolTip = 'No selected settings available in this category.'
$cb.ToolTip = Get-Translation -Key 'ImportExportCategoryDisabledTooltip'
}
$container.Children.Add($cb) | Out-Null
@@ -245,6 +269,11 @@ function Get-AvailableImportExportCategories {
return $availableCategories
}
<#
.SYNOPSIS
Summarizes a deployment settings array into the short description shown under the
"Deployment Settings" checkbox in the import/export category picker.
#>
function Get-DeploymentCategoryDetailString {
param (
[array]$DeploymentSettings
@@ -261,33 +290,38 @@ function Get-DeploymentCategoryDetailString {
if ($lookup.ContainsKey('UserSelectionIndex')) {
switch ([int]$lookup['UserSelectionIndex']) {
0 { $line1 += 'User: Current User' }
1 { $line1 += "User: $(if ($lookup['OtherUsername']) { $lookup['OtherUsername'] } else { 'Other User' })" }
2 { $line1 += 'User: Sysprep' }
0 { $line1 += Get-Translation -Key 'ImportExportDeployUserCurrentUser' }
1 { $line1 += Get-Translation -Key 'ImportExportDeployUserOther' -FormatArgs @($(if ($lookup['OtherUsername']) { $lookup['OtherUsername'] } else { Get-Translation -Key 'ImportExportDeployUserOtherFallback' })) }
2 { $line1 += Get-Translation -Key 'ImportExportDeployUserSysprep' }
}
}
if ($lookup.ContainsKey('AppRemovalScopeIndex')) {
switch ([int]$lookup['AppRemovalScopeIndex']) {
0 { $line1 += 'App Removal: All Users' }
1 { $line1 += 'App Removal: Current User' }
2 { $line1 += "App Removal: $(if ($lookup['OtherUsername']) { $lookup['OtherUsername'] } else { 'Other User' })" }
0 { $line1 += Get-Translation -Key 'ImportExportAppRemovalAllUsers' }
1 { $line1 += Get-Translation -Key 'ImportExportAppRemovalCurrentUser' }
2 { $line1 += Get-Translation -Key 'ImportExportAppRemovalOther' -FormatArgs @($(if ($lookup['OtherUsername']) { $lookup['OtherUsername'] } else { Get-Translation -Key 'ImportExportDeployUserOtherFallback' })) }
}
}
$options = @()
if ($lookup.ContainsKey('CreateRestorePoint') -and [bool]$lookup['CreateRestorePoint']) { $options += 'Restore Point' }
if (-not ($lookup.ContainsKey('SkipRegistryBackup') -and [bool]$lookup['SkipRegistryBackup'])) { $options += 'Registry Backup' }
if ($lookup.ContainsKey('RestartExplorer') -and [bool]$lookup['RestartExplorer']) { $options += 'Restart Explorer' }
if ($lookup.ContainsKey('CreateRestorePoint') -and [bool]$lookup['CreateRestorePoint']) { $options += Get-Translation -Key 'ImportExportOptionRestorePoint' }
if (-not ($lookup.ContainsKey('SkipRegistryBackup') -and [bool]$lookup['SkipRegistryBackup'])) { $options += Get-Translation -Key 'ImportExportOptionRegistryBackup' }
if ($lookup.ContainsKey('RestartExplorer') -and [bool]$lookup['RestartExplorer']) { $options += Get-Translation -Key 'ImportExportOptionRestartExplorer' }
$lines = @()
if ($line1.Count -gt 0) { $lines += $line1 -join ', ' }
if ($options.Count -gt 0) { $lines += "Options: $($options -join ', ')" }
if ($options.Count -gt 0) { $lines += Get-Translation -Key 'ImportExportOptionsPrefix' -FormatArgs @($options -join ', ') }
if ($lines.Count -gt 0) { return $lines -join "`n" }
return 'Default deployment settings'
return Get-Translation -Key 'ImportExportDefaultDeploymentSettings'
}
<#
.SYNOPSIS
Builds the per-category description text shown under each checkbox in the
import/export category picker.
#>
function Build-CategoryDetails {
param (
[int]$AppCount = 0,
@@ -298,17 +332,17 @@ function Build-CategoryDetails {
$details = @{}
if ($AppCount -gt 0) {
$details['Applications'] = "$AppCount app$(if ($AppCount -ne 1) { 's' }) selected"
$details['Applications'] = Get-Translation -Key 'ImportExportAppsSelected' -Count $AppCount -FormatArgs @($AppCount)
}
else {
$details['Applications'] = 'No apps selected'
$details['Applications'] = Get-Translation -Key 'ImportExportNoAppsSelected'
}
if ($TweakCount -gt 0) {
$details['System Tweaks'] = "$TweakCount tweak$(if ($TweakCount -ne 1) { 's' }) selected"
$details['System Tweaks'] = Get-Translation -Key 'ImportExportTweaksSelected' -Count $TweakCount -FormatArgs @($TweakCount)
}
else {
$details['System Tweaks'] = 'No tweaks selected'
$details['System Tweaks'] = Get-Translation -Key 'ImportExportNoTweaksSelected'
}
if ($DeploymentSettings) {
@@ -422,7 +456,7 @@ function Export-Configuration {
$deploymentSettings = Get-DeploymentSettings -Owner $Owner -UserSelectionCombo $UserSelectionCombo -OtherUsernameTextBox $OtherUsernameTextBox
$categoryDetails = Build-CategoryDetails -AppCount $selectedApps.Count -TweakCount $tweakSettings.Count -DeploymentSettings $deploymentSettings
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Export Configuration' -Prompt 'Create a configuration file based on the currently selected settings. You can choose which settings categories you wish to include in the export.' -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel 'Export Settings'
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title (Get-Translation -Key 'ImportExportExportTitle') -Prompt (Get-Translation -Key 'ImportExportExportPrompt') -DisabledCategories $disabledCategories -CategoryDetails $categoryDetails -ActionLabel (Get-Translation -Key 'ImportExportExportActionLabel')
if (-not $categories) {
Write-Host 'Export canceled.'
return
@@ -442,7 +476,7 @@ function Export-Configuration {
# Show native save-file dialog
$saveDialog = New-Object Microsoft.Win32.SaveFileDialog
$saveDialog.Title = 'Export Configuration'
$saveDialog.Title = Get-Translation -Key 'ImportExportSelectExportFileDialogTitle'
$saveDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
$saveDialog.DefaultExt = '.json'
$saveDialog.FileName = "Win11Debloat-Config-$(Get-Date -Format 'yyyyMMdd').json"
@@ -456,17 +490,17 @@ function Export-Configuration {
if ($script:Params.ContainsKey("WhatIf")) {
Write-Host "[WhatIf] Export configuration to '$($saveDialog.FileName)'" -ForegroundColor Cyan
Show-MessageBox -Message "[WhatIf] Configuration would be exported to this file (no file written)." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'ImportExportWhatIfExportMessage') -Title (Get-Translation -Key 'ImportExportExportTitle') -Button 'OK' -Icon 'Information' | Out-Null
return
}
if (Save-ToFile -Config $config -FilePath $saveDialog.FileName) {
Write-Host "Configuration exported successfully: $($saveDialog.FileName)"
Show-MessageBox -Message "Configuration exported successfully." -Title 'Export Configuration' -Button 'OK' -Icon 'Information' | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'ImportExportExportSuccessMessage') -Title (Get-Translation -Key 'ImportExportExportTitle') -Button 'OK' -Icon 'Information' | Out-Null
}
else {
Write-Error "Failed to export configuration to '$($saveDialog.FileName)'"
Show-MessageBox -Message "Failed to export configuration" -Title 'Error' -Button 'OK' -Icon 'Error' | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'ImportExportExportFailedMessage') -Title (Get-Translation -Key 'ErrorTitle') -Button 'OK' -Icon 'Error' | Out-Null
}
}
@@ -488,7 +522,7 @@ function Import-Configuration {
# Show native open-file dialog
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Configuration File'
$openDialog.Title = Get-Translation -Key 'ImportExportSelectImportFileDialogTitle'
$openDialog.Filter = 'JSON files (*.json)|*.json|All files (*.*)|*.*'
$openDialog.DefaultExt = '.json'
@@ -502,14 +536,14 @@ function Import-Configuration {
$config = Import-JsonFile -filePath $openDialog.FileName -expectedVersion '1.0'
if (-not $config) {
Write-Error "Failed to read configuration file '$($openDialog.FileName)'"
Show-MessageBox -Message "Failed to read configuration file" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'ImportExportReadFailedMessage') -Title (Get-Translation -Key 'ImportExportInvalidConfigTitle') -Button 'OK' -Icon 'Error' | Out-Null
return
}
$consistencyError = Test-ConfigConsistency -Config $config
if ($consistencyError) {
Write-Error "Invalid configuration file '$($openDialog.FileName)': $consistencyError"
Show-MessageBox -Message "Invalid configuration file: $consistencyError" -Title 'Invalid Config' -Button 'OK' -Icon 'Error' | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'ImportExportInvalidConfigMessage' -FormatArgs @($consistencyError)) -Title (Get-Translation -Key 'ImportExportInvalidConfigTitle') -Button 'OK' -Icon 'Error' | Out-Null
return
}
@@ -521,7 +555,7 @@ function Import-Configuration {
$tweakCount = @($config.Tweaks | Where-Object { $_ -and $_.Name -and $_.Value -eq $true }).Count
$categoryDetails = Build-CategoryDetails -AppCount $appCount -TweakCount $tweakCount -DeploymentSettings @($config.Deployment)
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title 'Import Configuration' -Prompt 'Choose the settings categories that you wish to import. You can review and modify the imported settings before they are applied.' -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel 'Import Settings'
$categories = Show-ImportExportConfigWindow -Owner $Owner -UsesDarkMode $UsesDarkMode -Title (Get-Translation -Key 'ImportExportImportTitle') -Prompt (Get-Translation -Key 'ImportExportImportPrompt') -Categories $availableCategories -CategoryDetails $categoryDetails -ActionLabel (Get-Translation -Key 'ImportExportImportActionLabel')
if (-not $categories) {
Write-Host 'Import canceled.'
return
@@ -553,7 +587,7 @@ function Import-Configuration {
}
Write-Host 'Configuration imported successfully.'
Show-MessageBox -Message "Configuration imported successfully." -Title 'Import Configuration' -Button 'OK' -Icon 'Information' | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'ImportExportImportSuccessMessage') -Title (Get-Translation -Key 'ImportExportImportTitle') -Button 'OK' -Icon 'Information' | Out-Null
if ($OnImportCompleted) {
& $OnImportCompleted $categories
+16 -16
View File
@@ -10,6 +10,7 @@ function Show-MainWindow {
# ---- Load XAML ----
$xaml = Get-Content -Path $script:MainWindowSchema -Raw
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$window = [System.Windows.Markup.XamlReader]::Load($reader)
@@ -97,7 +98,7 @@ function Show-MainWindow {
Start-Process "explorer.exe" -ArgumentList $logsFolder
}
else {
Show-MessageBox -Message "No logs folder found at: $logsFolder" -Title "Logs" -Button 'OK' -Icon 'Information'
Show-MessageBox -Message (Get-Translation -Key 'LogsFolderNotFoundMessage' -FormatArgs @($logsFolder)) -Title (Get-Translation -Key 'LogsMenuTitle') -Button 'OK' -Icon 'Information'
}
})
@@ -170,7 +171,7 @@ function Show-MainWindow {
}
catch {
Write-Warning "Export configuration failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open export configuration dialog: $($_.Exception.Message)" -Title 'Export Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null
Show-MessageBox -Owner $window -Message (Get-Translation -Key 'ExportConfigFailedMessage' -FormatArgs @($_.Exception.Message)) -Title (Get-Translation -Key 'ExportConfigFailedTitle') -Button 'OK' -Icon 'Error' | Out-Null
}
})
@@ -180,13 +181,13 @@ function Show-MainWindow {
$tabControl.SelectedIndex = 3
Update-NavigationButtons -Window $window -TabControl $tabControl
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
Show-Bubble -TargetControl $reviewChangesBtn -Message (Get-Translation -Key 'BubbleViewChanges')
}) | Out-Null
}
}
catch {
Write-Warning "Import configuration failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open import configuration dialog: $($_.Exception.Message)" -Title 'Import Configuration Failed' -Button 'OK' -Icon 'Error' | Out-Null
Show-MessageBox -Owner $window -Message (Get-Translation -Key 'ImportConfigFailedMessage' -FormatArgs @($_.Exception.Message)) -Title (Get-Translation -Key 'ImportConfigFailedTitle') -Button 'OK' -Icon 'Error' | Out-Null
}
})
@@ -206,7 +207,7 @@ function Show-MainWindow {
}
catch {
Write-Warning "Restore backup action failed: $($_.Exception.Message)"
Show-MessageBox -Owner $window -Message "Unable to open restore backup dialog: $($_.Exception.Message)" -Title 'Restore Backup Failed' -Button 'OK' -Icon 'Error' | Out-Null
Show-MessageBox -Owner $window -Message (Get-Translation -Key 'RestoreBackupFailedMessage' -FormatArgs @($_.Exception.Message)) -Title (Get-Translation -Key 'RestoreBackupFailedTitle') -Button 'OK' -Icon 'Error' | Out-Null
}
})
}
@@ -232,7 +233,7 @@ function Show-MainWindow {
$checkbox.Content = $preset.Name
$checkbox.IsThreeState = $true
$checkbox.Style = $window.Resources['PresetCheckBoxStyle']
$checkbox.ToolTip = "Select $($preset.Name)"
$checkbox.ToolTip = Get-Translation -Key 'AppPresetSelectTooltip' -FormatArgs @($preset.Name)
$checkbox.SetValue([System.Windows.Automation.AutomationProperties]::NameProperty, $preset.Name)
Add-TriStateClickBehavior -CheckBox $checkbox
Add-Member -InputObject $checkbox -MemberType NoteProperty -Name 'PresetAppIds' -Value $preset.AppIds
@@ -501,12 +502,11 @@ function Show-MainWindow {
param($sourceControl, $e)
if ($e.Key -eq [System.Windows.Input.Key]::F -and
([System.Windows.Input.Keyboard]::Modifiers -band [System.Windows.Input.ModifierKeys]::Control)) {
$currentTab = $tabControl.SelectedItem
if ($currentTab.Header -eq "App Removal" -and $appSearchBox) {
if ($tabControl.SelectedIndex -eq 1 -and $appSearchBox) {
$appSearchBox.Focus()
$e.Handled = $true
}
elseif ($currentTab.Header -eq "Tweaks" -and $tweakSearchBox) {
elseif ($tabControl.SelectedIndex -eq 2 -and $tweakSearchBox) {
$tweakSearchBox.Focus()
$e.Handled = $true
}
@@ -599,9 +599,9 @@ function Show-MainWindow {
$usernameValidationMessage.Text
}
else {
"Please enter a valid username."
Get-Translation -Key 'InvalidUsernameFallbackMessage'
}
Show-MessageBox -Message $validationMessage -Title "Invalid Username" -Button 'OK' -Icon 'Warning' | Out-Null
Show-MessageBox -Message $validationMessage -Title (Get-Translation -Key 'InvalidUsernameTitle') -Button 'OK' -Icon 'Warning' | Out-Null
return $false
}
return $true
@@ -638,7 +638,7 @@ function Show-MainWindow {
Invoke-NavigationUpdate
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-Bubble -TargetControl $reviewChangesBtn -Message 'View the selected changes here'
Show-Bubble -TargetControl $reviewChangesBtn -Message (Get-Translation -Key 'BubbleViewChanges')
}) | Out-Null
})
@@ -692,7 +692,7 @@ function Show-MainWindow {
}
if (-not $hasAppSelection -and $selectedForwardFeatureIds.Count -eq 0 -and $script:UndoParams.Count -eq 0) {
Show-MessageBox -Message 'No changes have been selected, please select at least one option to proceed.' -Title 'No Changes Selected' -Button 'OK' -Icon 'Information'
Show-MessageBox -Message (Get-Translation -Key 'NoChangesSelectedMessage') -Title (Get-Translation -Key 'NoChangesSelectedTitle') -Button 'OK' -Icon 'Information'
return
}
@@ -764,7 +764,7 @@ function Show-MainWindow {
if ($userSelectionCombo -and $userSelectionCombo.Items.Count -gt 0) {
$currentUserItem = $userSelectionCombo.Items[0]
if ($currentUserItem -is [System.Windows.Controls.ComboBoxItem]) {
$currentUserItem.Content = "Current User ($(Get-UserName))"
$currentUserItem.Content = Get-Translation -Key 'HomeCurrentUserWithName' -FormatArgs @(Get-UserName)
}
}
@@ -810,7 +810,7 @@ function Show-MainWindow {
catch {
Write-Warning "Error during GUI initialization: $($_.Exception.Message)"
Write-Warning "Stack trace: $($_.Exception.StackTrace)"
Show-MessageBox -Message "An error occurred during initialization: $($_.Exception.Message)" -Title "Initialization Error" -Button 'OK' -Icon 'Error' | Out-Null
Show-MessageBox -Message (Get-Translation -Key 'InitializationErrorMessage' -FormatArgs @($_.Exception.Message)) -Title (Get-Translation -Key 'InitializationErrorTitle') -Button 'OK' -Icon 'Error' | Out-Null
}
})
@@ -900,7 +900,7 @@ function Show-MainWindow {
# If WhatIf mode is enabled, notify the user that no changes will be made
if ($script:Params.ContainsKey("WhatIf")) {
$window.Dispatcher.BeginInvoke([System.Windows.Threading.DispatcherPriority]::Loaded, [action]{
Show-MessageBox -Message "WhatIf mode is enabled. The script will not make any changes to your system in this mode.`n`nYou can observe the actions that would be taken by the script in the console output." -Title 'WhatIf Mode' -Button 'OK' -Icon 'Information' -Owner $window
Show-MessageBox -Message (Get-Translation -Key 'WhatIfModeMessage') -Title (Get-Translation -Key 'WhatIfModeTitle') -Button 'OK' -Icon 'Information' -Owner $window
}) | Out-Null
}
+49 -5
View File
@@ -1,6 +1,15 @@
<#
.SYNOPSIS
Shows a themed Windows 11-style message box.
.DESCRIPTION
Falls back to a plain native MessageBox if the themed dialog's own schema fails to
localize (a bad %LANG:% marker in MessageBox.xaml, not a missing app-wide translation),
so a broken schema can't take down every error dialog in the app at once.
.OUTPUTS
System.String. One of 'OK', 'Cancel', 'Yes', or 'No', matching the pressed button
in both the themed dialog and the native fallback.
#>
function Show-MessageBox {
param (
@@ -50,6 +59,41 @@ function Show-MessageBox {
# Load XAML from file
$xaml = Get-Content -Path $script:MessageBoxSchema -Raw
try {
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
}
catch {
# The themed dialog depends on the schema's own %LANG:% markers resolving. If that ever
# breaks (a bad marker added to MessageBoxSchema, not a missing app-wide translation - that
# never reaches this function), fall back to a plain native MessageBox rather than losing
# the ability to show any error dialog at all.
Write-Warning "Falling back to a native message box, themed dialog failed to localize: $($_.Exception.Message)"
# Hide overlay before showing the fallback (only if this dialog was the one that showed it)
if ($overlay -and -not $overlayWasAlreadyVisible) {
try {
$ownerWindow.Dispatcher.Invoke([action]{ $overlay.Visibility = 'Collapsed' })
}
catch { }
}
$nativeButton = switch ($Button) {
'OKCancel' { [System.Windows.MessageBoxButton]::OKCancel }
'YesNo' { [System.Windows.MessageBoxButton]::YesNo }
default { [System.Windows.MessageBoxButton]::OK }
}
$nativeIcon = switch ($Icon) {
'Warning' { [System.Windows.MessageBoxImage]::Warning }
'Error' { [System.Windows.MessageBoxImage]::Error }
'Question' { [System.Windows.MessageBoxImage]::Question }
'Information' { [System.Windows.MessageBoxImage]::Information }
'Success' { [System.Windows.MessageBoxImage]::Information }
default { [System.Windows.MessageBoxImage]::None }
}
$nativeResult = [System.Windows.MessageBox]::Show($Message, $Title, $nativeButton, $nativeIcon)
return [string]$nativeResult
}
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$msgWindow = [System.Windows.Markup.XamlReader]::Load($reader)
@@ -121,22 +165,22 @@ function Show-MessageBox {
# Configure buttons - store result in window's Tag property
switch ($Button) {
'OK' {
$button1.Content = 'OK'
$button1.Content = Get-Translation -Key 'MessageBoxOk'
$button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() })
$button2.Visibility = 'Collapsed'
# Right-align sole button by moving it to column 1
[System.Windows.Controls.Grid]::SetColumn($button1, 1)
}
'OKCancel' {
$button1.Content = 'OK'
$button2.Content = 'Cancel'
$button1.Content = Get-Translation -Key 'MessageBoxOk'
$button2.Content = Get-Translation -Key 'MessageBoxCancel'
$button1.Add_Click({ $msgWindow.Tag = 'OK'; $msgWindow.Close() })
$button2.Add_Click({ $msgWindow.Tag = 'Cancel'; $msgWindow.Close() })
$button2.Visibility = 'Visible'
}
'YesNo' {
$button1.Content = 'Yes'
$button2.Content = 'No'
$button1.Content = Get-Translation -Key 'MessageBoxYes'
$button2.Content = Get-Translation -Key 'MessageBoxNo'
$button1.Add_Click({ $msgWindow.Tag = 'Yes'; $msgWindow.Close() })
$button2.Add_Click({ $msgWindow.Tag = 'No'; $msgWindow.Close() })
$button2.Visibility = 'Visible'
+375 -368
View File
@@ -44,422 +44,429 @@ function Show-RestoreBackupDialog {
catch { }
}
$schemaPath = $script:RestoreBackupWindowSchema
if (-not $schemaPath -or -not (Test-Path $schemaPath)) {
throw 'Restore backup window schema file could not be found.'
}
$xaml = Get-Content -Path $schemaPath -Raw
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
# Everything from here through ShowDialog() sits inside one outer try/finally. The overlay
# was already made visible above, and nothing hides it again until ShowDialog() returns, so
# a failure anywhere in schema loading, control wiring, or event registration would otherwise
# leave the owner window permanently dimmed.
try {
$window = [System.Windows.Markup.XamlReader]::Load($reader)
}
finally {
$reader.Close()
}
$schemaPath = $script:RestoreBackupWindowSchema
if (-not $schemaPath -or -not (Test-Path $schemaPath)) {
throw 'Restore backup window schema file could not be found.'
}
if ($ownerWindow) {
$xaml = Get-Content -Path $schemaPath -Raw
$xaml = ConvertTo-LocalizedXaml -Xaml $xaml
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
try {
$window.Owner = $ownerWindow
$window = [System.Windows.Markup.XamlReader]::Load($reader)
}
catch { }
}
try {
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
}
catch { }
$titleBar = $window.FindName('TitleBar')
$titleText = $window.FindName('TitleText')
$closeBtn = $window.FindName('CloseBtn')
$backBtn = $window.FindName('BackBtn')
$primaryActionBtn = $window.FindName('PrimaryActionBtn')
$chooseRegistryBtn = $window.FindName('ChooseRegistryBtn')
$chooseStartMenuBtn = $window.FindName('ChooseStartMenuBtn')
$restoreModeTabs = $window.FindName('RestoreModeTabs')
$startMenuIntroPanel = $window.FindName('StartMenuIntroPanel')
$startMenuScopeCombo = $window.FindName('StartMenuScopeCombo')
$startMenuAutoBackupCheck = $window.FindName('StartMenuAutoBackupCheck')
$introInfoPanel = $window.FindName('IntroInfoPanel')
$overviewPanel = $window.FindName('OverviewPanel')
$overviewFeaturesSection = $window.FindName('OverviewFeaturesSection')
$overviewSummaryText = $window.FindName('OverviewSummaryText')
$backupFileText = $window.FindName('BackupFileText')
$backupCreatedText = $window.FindName('BackupCreatedText')
$backupTargetText = $window.FindName('BackupTargetText')
$featuresItemsControl = $window.FindName('FeaturesItemsControl')
$reappliedSeparator = $window.FindName('ReappliedSeparator')
$reappliedPanel = $window.FindName('ReappliedPanel')
$reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl')
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
$nonRevertibleWikiLink = $window.FindName('NonRevertibleWikiLink')
$titleBar.Add_MouseLeftButtonDown({ $window.DragMove() })
$window.Tag = New-RestoreDialogState
$chooseRegistryBtn.IsDefault = $true
$state = @{ WizardStep = 'SelectType'; SelectedRegistryBackup = $null; SelectedStartMenuBackupFilePath = $null }
$getStartMenuScopeInfo = {
$isAllUsersScope = ($startMenuScopeCombo.SelectedItem.Tag -eq 'AllUsers')
$scopeValue = if ($isAllUsersScope) { 'AllUsers' } else { 'CurrentUser' }
$summaryScopeText = if ($isAllUsersScope) { 'all users' } else { 'the current user' }
return [PSCustomObject]@{
Scope = $scopeValue
Target = $scopeValue
SummaryText = $summaryScopeText
}
}
$showStartMenuIntroState = {
$backupFileText.Text = 'Not selected'
$backupCreatedText.Text = 'N/A'
$overviewSummaryText.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Collapsed'
$startMenuIntroPanel.Visibility = 'Visible'
$restoreModeTabs.SelectedIndex = 2
}
$showStartMenuOverviewState = {
param([string]$BackupFilePath)
$scopeInfo = & $getStartMenuScopeInfo
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target $scopeInfo.Target
$overviewSummaryText.Text = "This will replace the current Start Menu pinned apps layout for $($scopeInfo.SummaryText) with the selected backup."
$backupFileText.Text = Split-Path -Path $BackupFilePath -Leaf
$createdText = 'Unknown'
try {
$createdText = (Get-Item -LiteralPath $BackupFilePath -ErrorAction Stop).LastWriteTime.ToString('yyyy-MM-dd HH:mm')
}
catch { }
$backupCreatedText.Text = $createdText
$overviewFeaturesSection.Visibility = 'Collapsed'
$overviewSummaryText.Visibility = 'Visible'
$reappliedSeparator.Visibility = 'Collapsed'
$reappliedPanel.Visibility = 'Collapsed'
$nonRevertibleSeparator.Visibility = 'Collapsed'
$nonRevertiblePanel.Visibility = 'Collapsed'
$introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible'
$restoreModeTabs.SelectedIndex = 1
}
$updateStartMenuOverviewPanel = {
if ($state.WizardStep -ne 'StartMenu') {
return
finally {
$reader.Close()
}
if ([string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
& $showStartMenuIntroState
return
}
& $showStartMenuOverviewState $state.SelectedStartMenuBackupFilePath
}
$updateStartMenuPrimaryActionText = {
if ($state.WizardStep -ne 'StartMenu') {
return
}
$isAutoBackupEnabled = ($startMenuAutoBackupCheck.IsChecked -eq $true)
$hasSelectedManualFile = -not [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)
if ($isAutoBackupEnabled -or $hasSelectedManualFile) {
$primaryActionBtn.Content = 'Restore backup'
}
else {
$primaryActionBtn.Content = 'Select backup file'
}
}
$refreshStartMenuUi = {
& $updateStartMenuOverviewPanel
& $updateStartMenuPrimaryActionText
}
$enterSelectTypeStep = {
$titleText.Text = 'Restore Backup'
$restoreModeTabs.SelectedIndex = 0
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Cancel'
$primaryActionBtn.Visibility = 'Collapsed'
$chooseRegistryBtn.IsDefault = $true
$primaryActionBtn.IsDefault = $false
}
$enterRegistryStep = {
$titleText.Text = 'Restore Registry Backup'
$restoreModeTabs.SelectedIndex = 1
$introInfoPanel.Visibility = 'Visible'
$overviewPanel.Visibility = 'Collapsed'
$overviewFeaturesSection.Visibility = 'Visible'
$overviewSummaryText.Visibility = 'Collapsed'
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Back'
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.Content = 'Select backup file'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
}
$enterStartMenuStep = {
$titleText.Text = 'Restore Start Menu Backup'
$restoreModeTabs.SelectedIndex = 2
$backBtn.Visibility = 'Visible'
$backBtn.Content = 'Back'
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
# Show intro panel so user can configure scope & auto-detect
$startMenuAutoBackupCheck.IsChecked = $true
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
}
$showRegistryOverview = {
param(
[Parameter(Mandatory = $true)]
$SelectedBackup,
[Parameter(Mandatory = $true)]
[string]$SelectedBackupFilePath
)
$createdText = if ([string]::IsNullOrWhiteSpace($SelectedBackup.CreatedAt)) {
'Unknown'
}
else {
if ($ownerWindow) {
try {
[DateTime]::Parse($SelectedBackup.CreatedAt).ToString('yyyy-MM-dd HH:mm')
}
catch {
$SelectedBackup.CreatedAt
$window.Owner = $ownerWindow
}
catch { }
}
$selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
$selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
$seenForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($featureId in $selectedForwardFeatureIds) {
[void]$seenForwardFeatureIds.Add([string]$featureId)
}
$filteredUndoFeatureIds = New-Object System.Collections.Generic.List[string]
foreach ($featureId in $selectedUndoFeatureIds) {
if ($seenForwardFeatureIds.Contains([string]$featureId)) {
continue
}
$filteredUndoFeatureIds.Add([string]$featureId)
}
$forwardFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedForwardFeatureIds -Features $script:Features
$undoFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds @($filteredUndoFeatureIds.ToArray()) -Features $script:Features
$combinedFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds (Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) -Features $script:Features
$revertibleFeaturesList = @($forwardFeatureLists.Revertible)
$reappliedFeaturesList = @($undoFeatureLists.Revertible)
$nonRevertibleFeaturesList = @($combinedFeatureLists.NonRevertible)
Write-Host "Backup overview prepared. Reverted=$($revertibleFeaturesList.Count), ReApplied=$($reappliedFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
if ($revertibleFeaturesList.Count -eq 0 -and $reappliedFeaturesList.Count -eq 0) {
throw 'The selected backup does not contain any changes that can be restored.'
}
$backupFileText.Text = Split-Path $SelectedBackupFilePath -Leaf
$backupCreatedText.Text = $createdText
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
$overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
if ($reappliedFeaturesList.Count -gt 0) { $reappliedPanel.Visibility = 'Visible' } else { $reappliedPanel.Visibility = 'Collapsed' }
if ($revertibleFeaturesList.Count -gt 0 -and $reappliedFeaturesList.Count -gt 0) { $reappliedSeparator.Visibility = 'Visible' } else { $reappliedSeparator.Visibility = 'Collapsed' }
$overviewSummaryText.Visibility = 'Collapsed'
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
$introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible'
return $true
}
$handleRegistryPrimaryAction = {
if ($state.SelectedRegistryBackup) {
$window.Tag = @{
Result = 'RestoreRegistry'
Backup = $state.SelectedRegistryBackup
}
$window.DialogResult = $true
$window.Close()
return
}
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Registry Backup File'
$openDialog.Filter = 'Registry backup (*.json)|*.json'
$openDialog.DefaultExt = '.json'
$openDialog.InitialDirectory = $script:RegistryBackupsPath
if ($openDialog.ShowDialog($window) -ne $true) {
return
}
Write-Host "Backup file selected: $($openDialog.FileName)"
try {
$selectedBackup = Import-RegistryBackup -FilePath $openDialog.FileName
Set-WindowThemeResources -window $window -usesDarkMode $usesDarkMode
}
catch { }
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
$titleBar = $window.FindName('TitleBar')
$titleText = $window.FindName('TitleText')
$closeBtn = $window.FindName('CloseBtn')
$backBtn = $window.FindName('BackBtn')
$primaryActionBtn = $window.FindName('PrimaryActionBtn')
$chooseRegistryBtn = $window.FindName('ChooseRegistryBtn')
$chooseStartMenuBtn = $window.FindName('ChooseStartMenuBtn')
$restoreModeTabs = $window.FindName('RestoreModeTabs')
$startMenuIntroPanel = $window.FindName('StartMenuIntroPanel')
$startMenuScopeCombo = $window.FindName('StartMenuScopeCombo')
$startMenuAutoBackupCheck = $window.FindName('StartMenuAutoBackupCheck')
$introInfoPanel = $window.FindName('IntroInfoPanel')
$overviewPanel = $window.FindName('OverviewPanel')
$overviewFeaturesSection = $window.FindName('OverviewFeaturesSection')
$overviewSummaryText = $window.FindName('OverviewSummaryText')
$backupFileText = $window.FindName('BackupFileText')
$backupCreatedText = $window.FindName('BackupCreatedText')
$backupTargetText = $window.FindName('BackupTargetText')
$featuresItemsControl = $window.FindName('FeaturesItemsControl')
$reappliedSeparator = $window.FindName('ReappliedSeparator')
$reappliedPanel = $window.FindName('ReappliedPanel')
$reappliedFeaturesItemsControl = $window.FindName('ReappliedFeaturesItemsControl')
$nonRevertibleSeparator = $window.FindName('NonRevertibleSeparator')
$nonRevertiblePanel = $window.FindName('NonRevertiblePanel')
$nonRevertibleFeaturesItemsControl = $window.FindName('NonRevertibleFeaturesItemsControl')
$nonRevertibleWikiLink = $window.FindName('NonRevertibleWikiLink')
$titleBar.Add_MouseLeftButtonDown({ $window.DragMove() })
$window.Tag = New-RestoreDialogState
$chooseRegistryBtn.IsDefault = $true
$state = @{ WizardStep = 'SelectType'; SelectedRegistryBackup = $null; SelectedStartMenuBackupFilePath = $null }
$getStartMenuScopeInfo = {
$isAllUsersScope = ($startMenuScopeCombo.SelectedItem.Tag -eq 'AllUsers')
$scopeValue = if ($isAllUsersScope) { 'AllUsers' } else { 'CurrentUser' }
$summaryScopeText = if ($isAllUsersScope) { Get-Translation -Key 'RestoreStartMenuSummaryScopeAllUsers' } else { Get-Translation -Key 'RestoreStartMenuSummaryScopeCurrentUser' }
return [PSCustomObject]@{
Scope = $scopeValue
Target = $scopeValue
SummaryText = $summaryScopeText
}
}
$showStartMenuIntroState = {
$backupFileText.Text = Get-Translation -Key 'RestoreFileNotSelected'
$backupCreatedText.Text = Get-Translation -Key 'RestoreCreatedUnavailable'
$overviewSummaryText.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Collapsed'
$startMenuIntroPanel.Visibility = 'Visible'
$restoreModeTabs.SelectedIndex = 2
}
$showStartMenuOverviewState = {
param([string]$BackupFilePath)
$scopeInfo = & $getStartMenuScopeInfo
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target $scopeInfo.Target
$overviewSummaryText.Text = Get-Translation -Key 'RestoreStartMenuSummary' -FormatArgs @($scopeInfo.SummaryText)
$backupFileText.Text = Split-Path -Path $BackupFilePath -Leaf
$createdText = Get-Translation -Key 'RestoreCreatedUnknown'
try {
$createdText = (Get-Item -LiteralPath $BackupFilePath -ErrorAction Stop).LastWriteTime.ToString('yyyy-MM-dd HH:mm')
}
catch { }
$backupCreatedText.Text = $createdText
$overviewFeaturesSection.Visibility = 'Collapsed'
$overviewSummaryText.Visibility = 'Visible'
$reappliedSeparator.Visibility = 'Collapsed'
$reappliedPanel.Visibility = 'Collapsed'
$nonRevertibleSeparator.Visibility = 'Collapsed'
$nonRevertiblePanel.Visibility = 'Collapsed'
$introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible'
$restoreModeTabs.SelectedIndex = 1
}
$updateStartMenuOverviewPanel = {
if ($state.WizardStep -ne 'StartMenu') {
return
}
}
catch {
Show-MessageBox -Owner $window -Title 'Invalid Backup File' -Message "The selected file could not be loaded:`n$($_.Exception.Message)" -Button 'OK' -Icon 'Error' | Out-Null
return
if ([string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
& $showStartMenuIntroState
return
}
& $showStartMenuOverviewState $state.SelectedStartMenuBackupFilePath
}
$state.SelectedRegistryBackup = $selectedBackup
$primaryActionBtn.Content = 'Restore from backup'
}
$updateStartMenuPrimaryActionText = {
if ($state.WizardStep -ne 'StartMenu') {
return
}
$handleStartMenuPrimaryAction = {
$scope = (& $getStartMenuScopeInfo).Scope
$useManualBackupFile = -not ($startMenuAutoBackupCheck.IsChecked -eq $true)
$isAutoBackupEnabled = ($startMenuAutoBackupCheck.IsChecked -eq $true)
$hasSelectedManualFile = -not [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)
if ($isAutoBackupEnabled -or $hasSelectedManualFile) {
$primaryActionBtn.Content = Get-Translation -Key 'RestorePrimaryActionRestoreBackup'
}
else {
$primaryActionBtn.Content = Get-Translation -Key 'RestorePrimaryActionSelectBackupFile'
}
}
$refreshStartMenuUi = {
& $updateStartMenuOverviewPanel
& $updateStartMenuPrimaryActionText
}
$enterSelectTypeStep = {
$titleText.Text = Get-Translation -Key 'RestoreWindowTitle'
$restoreModeTabs.SelectedIndex = 0
$backBtn.Visibility = 'Visible'
$backBtn.Content = Get-Translation -Key 'RestoreBackBtnCancel'
$primaryActionBtn.Visibility = 'Collapsed'
$chooseRegistryBtn.IsDefault = $true
$primaryActionBtn.IsDefault = $false
}
$enterRegistryStep = {
$titleText.Text = Get-Translation -Key 'RestoreRegistryStepTitle'
$restoreModeTabs.SelectedIndex = 1
$introInfoPanel.Visibility = 'Visible'
$overviewPanel.Visibility = 'Collapsed'
$overviewFeaturesSection.Visibility = 'Visible'
$overviewSummaryText.Visibility = 'Collapsed'
$backBtn.Visibility = 'Visible'
$backBtn.Content = Get-Translation -Key 'RestoreBackBtnBack'
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.Content = Get-Translation -Key 'RestorePrimaryActionSelectBackupFile'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
}
$enterStartMenuStep = {
$titleText.Text = Get-Translation -Key 'RestoreStartMenuStepTitle'
$restoreModeTabs.SelectedIndex = 2
$backBtn.Visibility = 'Visible'
$backBtn.Content = Get-Translation -Key 'RestoreBackBtnBack'
$primaryActionBtn.Visibility = 'Visible'
$primaryActionBtn.IsDefault = $true
$chooseRegistryBtn.IsDefault = $false
# Show intro panel so user can configure scope & auto-detect
$startMenuAutoBackupCheck.IsChecked = $true
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
}
$showRegistryOverview = {
param(
[Parameter(Mandatory = $true)]
$SelectedBackup,
[Parameter(Mandatory = $true)]
[string]$SelectedBackupFilePath
)
$createdText = if ([string]::IsNullOrWhiteSpace($SelectedBackup.CreatedAt)) {
Get-Translation -Key 'RestoreCreatedUnknown'
}
else {
try {
[DateTime]::Parse($SelectedBackup.CreatedAt).ToString('yyyy-MM-dd HH:mm')
}
catch {
$SelectedBackup.CreatedAt
}
}
$selectedForwardFeatureIds = @(Get-SelectedForwardFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
$selectedUndoFeatureIds = @(Get-SelectedUndoFeatureIdsFromBackup -SelectedBackup $SelectedBackup)
$seenForwardFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($featureId in $selectedForwardFeatureIds) {
[void]$seenForwardFeatureIds.Add([string]$featureId)
}
$filteredUndoFeatureIds = New-Object System.Collections.Generic.List[string]
foreach ($featureId in $selectedUndoFeatureIds) {
if ($seenForwardFeatureIds.Contains([string]$featureId)) {
continue
}
$filteredUndoFeatureIds.Add([string]$featureId)
}
$forwardFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds $selectedForwardFeatureIds -Features $script:Features
$undoFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds @($filteredUndoFeatureIds.ToArray()) -Features $script:Features
$combinedFeatureLists = Get-RestoreBackupFeatureLists -SelectedFeatureIds (Get-SelectedFeatureIdsFromBackup -SelectedBackup $SelectedBackup) -Features $script:Features
$revertibleFeaturesList = @($forwardFeatureLists.Revertible)
$reappliedFeaturesList = @($undoFeatureLists.Revertible)
$nonRevertibleFeaturesList = @($combinedFeatureLists.NonRevertible)
Write-Host "Backup overview prepared. Reverted=$($revertibleFeaturesList.Count), ReApplied=$($reappliedFeaturesList.Count), NonRevertible=$($nonRevertibleFeaturesList.Count)"
if ($revertibleFeaturesList.Count -eq 0 -and $reappliedFeaturesList.Count -eq 0) {
throw (Get-Translation -Key 'RestoreNoRestorableChangesError')
}
$backupFileText.Text = Split-Path $SelectedBackupFilePath -Leaf
$backupCreatedText.Text = $createdText
$backupTargetText.Text = Get-FriendlyRegistryBackupTarget -Target ([string]$SelectedBackup.Target)
$featuresItemsControl.ItemsSource = $revertibleFeaturesList
$overviewFeaturesSection.Visibility = if ($revertibleFeaturesList.Count -gt 0) { 'Visible' } else { 'Collapsed' }
$reappliedFeaturesItemsControl.ItemsSource = $reappliedFeaturesList
if ($reappliedFeaturesList.Count -gt 0) { $reappliedPanel.Visibility = 'Visible' } else { $reappliedPanel.Visibility = 'Collapsed' }
if ($revertibleFeaturesList.Count -gt 0 -and $reappliedFeaturesList.Count -gt 0) { $reappliedSeparator.Visibility = 'Visible' } else { $reappliedSeparator.Visibility = 'Collapsed' }
$overviewSummaryText.Visibility = 'Collapsed'
$nonRevertibleFeaturesItemsControl.ItemsSource = $nonRevertibleFeaturesList
$hasNonRevertibleItems = ($nonRevertibleFeaturesList.Count -gt 0)
if ($hasNonRevertibleItems) { $nonRevertiblePanel.Visibility = 'Visible' } else { $nonRevertiblePanel.Visibility = 'Collapsed' }
if ($hasNonRevertibleItems -and ($revertibleFeaturesList.Count -gt 0 -or $reappliedFeaturesList.Count -gt 0)) { $nonRevertibleSeparator.Visibility = 'Visible' } else { $nonRevertibleSeparator.Visibility = 'Collapsed' }
$introInfoPanel.Visibility = 'Collapsed'
$overviewPanel.Visibility = 'Visible'
return $true
}
$handleRegistryPrimaryAction = {
if ($state.SelectedRegistryBackup) {
$window.Tag = @{
Result = 'RestoreRegistry'
Backup = $state.SelectedRegistryBackup
}
$window.DialogResult = $true
$window.Close()
return
}
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = 'Select Start Menu Backup File'
$openDialog.Filter = 'Start Menu backup (*.bak)|*.bak'
$openDialog.InitialDirectory = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$openDialog.DefaultExt = '.bak'
$openDialog.Title = Get-Translation -Key 'RestoreSelectRegistryFileDialogTitle'
$openDialog.Filter = 'Registry backup (*.json)|*.json'
$openDialog.DefaultExt = '.json'
$openDialog.InitialDirectory = $script:RegistryBackupsPath
if ($openDialog.ShowDialog($window) -ne $true) {
return
}
$state.SelectedStartMenuBackupFilePath = $openDialog.FileName
Write-Host "Selected Start Menu backup file: $($state.SelectedStartMenuBackupFilePath)"
& $refreshStartMenuUi
return
}
Write-Host "Backup file selected: $($openDialog.FileName)"
if (-not $useManualBackupFile) {
$scopeInfo = & $getStartMenuScopeInfo
$autoBackupPath = Get-StartMenuBackupPath -Scope $scopeInfo.Scope
if ($null -eq $autoBackupPath) {
$scopeText = $scopeInfo.SummaryText
Show-MessageBox -Owner $window -Title 'No Backup Found' -Message "No Start Menu backup file was found for $scopeText. Uncheck 'Automatically find Start Menu backup' to select a backup file manually." -Button 'OK' -Icon 'Warning' | Out-Null
try {
$selectedBackup = Import-RegistryBackup -FilePath $openDialog.FileName
if (-not (& $showRegistryOverview -SelectedBackup $selectedBackup -SelectedBackupFilePath $openDialog.FileName)) {
return
}
}
catch {
Show-MessageBox -Owner $window -Title (Get-Translation -Key 'RestoreInvalidBackupTitle') -Message (Get-Translation -Key 'RestoreInvalidBackupMessage' -FormatArgs @($_.Exception.Message)) -Button 'OK' -Icon 'Error' | Out-Null
return
}
$state.SelectedStartMenuBackupFilePath = if ($scopeInfo.Scope -eq 'CurrentUser') { $autoBackupPath } else { $null }
$state.SelectedRegistryBackup = $selectedBackup
$primaryActionBtn.Content = Get-Translation -Key 'RestorePrimaryActionRestoreFromBackup'
}
$window.Tag = @{
Result = 'Restore-StartMenu'
StartMenuScope = $scope
UseManualBackupFile = $useManualBackupFile
BackupFilePath = $state.SelectedStartMenuBackupFilePath
$handleStartMenuPrimaryAction = {
$scope = (& $getStartMenuScopeInfo).Scope
$useManualBackupFile = -not ($startMenuAutoBackupCheck.IsChecked -eq $true)
if ($useManualBackupFile -and [string]::IsNullOrWhiteSpace($state.SelectedStartMenuBackupFilePath)) {
$openDialog = New-Object Microsoft.Win32.OpenFileDialog
$openDialog.Title = Get-Translation -Key 'RestoreSelectStartMenuFileDialogTitle'
$openDialog.Filter = 'Start Menu backup (*.bak)|*.bak'
$openDialog.InitialDirectory = "$env:LOCALAPPDATA\Packages\Microsoft.Windows.StartMenuExperienceHost_cw5n1h2txyewy\LocalState"
$openDialog.DefaultExt = '.bak'
if ($openDialog.ShowDialog($window) -ne $true) {
return
}
$state.SelectedStartMenuBackupFilePath = $openDialog.FileName
Write-Host "Selected Start Menu backup file: $($state.SelectedStartMenuBackupFilePath)"
& $refreshStartMenuUi
return
}
if (-not $useManualBackupFile) {
$scopeInfo = & $getStartMenuScopeInfo
$autoBackupPath = Get-StartMenuBackupPath -Scope $scopeInfo.Scope
if ($null -eq $autoBackupPath) {
$scopeText = $scopeInfo.SummaryText
Show-MessageBox -Owner $window -Title (Get-Translation -Key 'RestoreNoBackupFoundTitle') -Message (Get-Translation -Key 'RestoreNoBackupFoundMessage' -FormatArgs @($scopeText)) -Button 'OK' -Icon 'Warning' | Out-Null
return
}
$state.SelectedStartMenuBackupFilePath = if ($scopeInfo.Scope -eq 'CurrentUser') { $autoBackupPath } else { $null }
}
$window.Tag = @{
Result = 'Restore-StartMenu'
StartMenuScope = $scope
UseManualBackupFile = $useManualBackupFile
BackupFilePath = $state.SelectedStartMenuBackupFilePath
}
$window.DialogResult = $true
$window.Close()
}
$window.DialogResult = $true
$window.Close()
}
$setWizardStep = {
param([string]$step)
$setWizardStep = {
param([string]$step)
$state.WizardStep = $step
$state.WizardStep = $step
switch ($step) {
'SelectType' { & $enterSelectTypeStep }
'Registry' { & $enterRegistryStep }
'StartMenu' { & $enterStartMenuStep }
switch ($step) {
'SelectType' { & $enterSelectTypeStep }
'Registry' { & $enterRegistryStep }
'StartMenu' { & $enterStartMenuStep }
}
}
}
$startMenuAutoBackupCheck.Add_Checked({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
$startMenuAutoBackupCheck.Add_Unchecked({
& $refreshStartMenuUi
})
$startMenuAutoBackupCheck.Add_Checked({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
$startMenuAutoBackupCheck.Add_Unchecked({
& $refreshStartMenuUi
})
$startMenuScopeCombo.Add_SelectionChanged({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
$startMenuScopeCombo.Add_SelectionChanged({
$state.SelectedStartMenuBackupFilePath = $null
& $refreshStartMenuUi
})
$nonRevertibleWikiLink.Add_MouseLeftButtonUp({
try {
Start-Process 'https://github.com/Raphire/Win11Debloat/wiki/Reverting-Changes' | Out-Null
}
catch { }
})
$nonRevertibleWikiLink.Add_MouseLeftButtonUp({
try {
Start-Process 'https://github.com/Raphire/Win11Debloat/wiki/Reverting-Changes' | Out-Null
}
catch { }
})
$closeBtn.Add_Click({
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
})
$chooseRegistryBtn.Add_Click({ & $setWizardStep 'Registry' })
$chooseStartMenuBtn.Add_Click({ & $setWizardStep 'StartMenu' })
$backBtn.Add_Click({
if ($state.WizardStep -eq 'SelectType') {
$closeBtn.Add_Click({
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
return
}
})
if ($state.WizardStep -eq 'Registry') {
$state.SelectedRegistryBackup = $null
}
$chooseRegistryBtn.Add_Click({ & $setWizardStep 'Registry' })
$chooseStartMenuBtn.Add_Click({ & $setWizardStep 'StartMenu' })
if ($state.WizardStep -eq 'StartMenu') {
$state.SelectedStartMenuBackupFilePath = $null
$startMenuAutoBackupCheck.IsChecked = $true
}
$backBtn.Add_Click({
if ($state.WizardStep -eq 'SelectType') {
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
return
}
if ($state.WizardStep -eq 'Registry') {
$state.SelectedRegistryBackup = $null
}
if ($state.WizardStep -eq 'StartMenu') {
$state.SelectedStartMenuBackupFilePath = $null
$startMenuAutoBackupCheck.IsChecked = $true
}
& $setWizardStep 'SelectType'
})
$primaryActionBtn.Add_Click({
switch ($state.WizardStep) {
'Registry' { & $handleRegistryPrimaryAction }
'StartMenu' { & $handleStartMenuPrimaryAction }
}
})
$window.Add_KeyDown({
param($source, $e)
if ($e.Key -eq 'Escape') {
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
}
})
& $setWizardStep 'SelectType'
})
$primaryActionBtn.Add_Click({
switch ($state.WizardStep) {
'Registry' { & $handleRegistryPrimaryAction }
'StartMenu' { & $handleStartMenuPrimaryAction }
try {
$null = $window.ShowDialog()
}
})
$window.Add_KeyDown({
param($source, $e)
if ($e.Key -eq 'Escape') {
$window.Tag = New-RestoreDialogState
$window.DialogResult = $false
$window.Close()
catch {
$innerMessage = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { 'None' }
throw "Failed to show restore backup dialog. Error: $($_.Exception.Message) Inner: $innerMessage"
}
})
& $setWizardStep 'SelectType'
try {
$null = $window.ShowDialog()
}
catch {
$innerMessage = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { 'None' }
throw "Failed to show restore backup dialog. Error: $($_.Exception.Message) Inner: $innerMessage"
}
finally {
if ($overlay -and -not $overlayWasAlreadyVisible) {
+11 -11
View File
@@ -35,10 +35,10 @@ function Show-RestoreBackupWindow {
if ($restoreOpResult -and $restoreOpResult.Result) {
$restoreResult.RestoredRegistry = $true
if ($script:Params.ContainsKey("WhatIf")) {
$successMessage = '[WhatIf] Registry backup would be restored (no changes made).'
$successMessage = Get-Translation -Key 'RestoreRegistryWhatIfMessage'
}
else {
$successMessage = 'Registry backup restored successfully. Some changes may require a restart to take effect.'
$successMessage = Get-Translation -Key 'RestoreRegistrySuccessMessage'
}
}
}
@@ -70,22 +70,22 @@ function Show-RestoreBackupWindow {
if ($successCount -eq 0) {
$errorSummary = ($resultEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
throw "Failed to restore the Start Menu backup.`n$errorSummary"
throw (Get-Translation -Key 'RestoreStartMenuFailedMessage' -FormatArgs @($errorSummary))
}
if ($failedEntries.Count -gt 0) {
$failureSummary = ($failedEntries | ForEach-Object { $_.Message }) -join [Environment]::NewLine
$warningMessage = "The Start Menu backup was successfully restored for $successCount user(s).`nSome users could not be restored:`n$failureSummary"
$warningMessage = Get-Translation -Key 'RestoreStartMenuPartialSuccessMessage' -FormatArgs @($successCount, $failureSummary)
}
else {
if ($script:Params.ContainsKey("WhatIf")) {
$successMessage = '[WhatIf] Start Menu backup would be restored (no changes made).'
$successMessage = Get-Translation -Key 'RestoreStartMenuWhatIfMessage'
}
elseif ($scope -eq 'AllUsers') {
$successMessage = "The Start Menu backup was successfully restored for all users. The changes will apply the next time users sign in."
$successMessage = Get-Translation -Key 'RestoreStartMenuAllUsersSuccessMessage'
}
else {
$successMessage = "The Start Menu backup was successfully restored for the current user. The changes will apply the next time you sign in."
$successMessage = Get-Translation -Key 'RestoreStartMenuCurrentUserSuccessMessage'
}
}
@@ -94,19 +94,19 @@ function Show-RestoreBackupWindow {
if ($warningMessage) {
Write-Host "$warningMessage"
Show-MessageBox -Title 'Backup Restored' -Message $warningMessage -Icon Warning
Show-MessageBox -Title (Get-Translation -Key 'RestoreBackupRestoredTitle') -Message $warningMessage -Icon Warning
}
elseif ($successMessage) {
Write-Host "$successMessage"
Show-MessageBox -Title 'Backup Restored' -Message $successMessage -Icon Success
Show-MessageBox -Title (Get-Translation -Key 'RestoreBackupRestoredTitle') -Message $successMessage -Icon Success
}
return $restoreResult
}
catch {
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { 'An unexpected error occurred.' }
$errorMessage = if ($_.Exception.Message) { $_.Exception.Message } else { Get-Translation -Key 'RestoreUnexpectedError' }
Write-Error "Restore operation failed: $errorMessage"
Show-MessageBox -Title 'Error' -Message "Restore failed: $errorMessage" -Icon Error
Show-MessageBox -Title (Get-Translation -Key 'ErrorTitle') -Message (Get-Translation -Key 'RestoreOperationFailedMessage' -FormatArgs @($errorMessage)) -Icon Error
return [PSCustomObject]@{
RestoredRegistry = $false
RestoredStartMenu = $false
+1
View File
@@ -6,6 +6,7 @@ param (
[switch]$Silent,
[switch]$Sysprep,
[string]$LogPath,
[string]$Language,
[string]$User,
[Alias('NoRestartExplorer')]
[switch]$SkipExplorerRestart,
+2 -2
View File
@@ -15,7 +15,7 @@ function Confirm-UnsafeAppRemoval {
# Microsoft Store warning
if ($SelectedApps -contains "Microsoft.WindowsStore") {
$result = Show-MessageBox -Message 'Are you sure that you wish to uninstall the Microsoft Store? This app cannot easily be reinstalled.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $Owner
$result = Show-MessageBox -Message (Get-Translation -Key 'ConfirmRemoveStoreMessage') -Title (Get-Translation -Key 'ConfirmTitle') -Button 'YesNo' -Icon 'Warning' -Owner $Owner
if ($result -ne 'Yes') {
return $false
@@ -24,7 +24,7 @@ function Confirm-UnsafeAppRemoval {
# Windows Terminal warning
if ($SelectedApps -contains "Microsoft.WindowsTerminal") {
$result = Show-MessageBox -Message 'Are you sure that you wish to remove Windows Terminal? Windows Terminal is the default command-line app for Windows. Ensure you are not running Win11Debloat via Windows Terminal before proceeding to avoid a mid-process failure.' -Title 'Are you sure?' -Button 'YesNo' -Icon 'Warning' -Owner $Owner
$result = Show-MessageBox -Message (Get-Translation -Key 'ConfirmRemoveTerminalMessage') -Title (Get-Translation -Key 'ConfirmTitle') -Button 'YesNo' -Icon 'Warning' -Owner $Owner
if ($result -ne 'Yes') {
return $false
@@ -10,37 +10,37 @@ function Get-FriendlyRegistryBackupTarget {
)
if ([string]::IsNullOrWhiteSpace($Target)) {
return 'Unknown'
return Get-Translation -Key 'RestoreTargetUnknown'
}
if ($Target -eq 'DefaultUserProfile') {
return 'Default user profile'
return Get-Translation -Key 'RestoreTargetDefaultUserProfile'
}
if ($Target -eq 'CurrentUser') {
return 'Current user'
return Get-Translation -Key 'RestoreTargetCurrentUser'
}
if ($Target -eq 'AllUsers') {
return 'All users'
return Get-Translation -Key 'RestoreTargetAllUsers'
}
if ($Target -like 'CurrentUser:*') {
$userName = $Target.Substring(12)
if ([string]::IsNullOrWhiteSpace($userName)) {
return 'Current user'
return Get-Translation -Key 'RestoreTargetCurrentUser'
}
return "Current user ($userName)"
return Get-Translation -Key 'RestoreTargetCurrentUserNamed' -FormatArgs @($userName)
}
if ($Target -like 'User:*') {
$userName = $Target.Substring(5)
if ([string]::IsNullOrWhiteSpace($userName)) {
return 'User'
return Get-Translation -Key 'RestoreTargetUser'
}
return "User ($userName)"
return Get-Translation -Key 'RestoreTargetUserNamed' -FormatArgs @($userName)
}
return $Target
+1 -1
View File
@@ -15,7 +15,7 @@ function Get-RebootFeatureLabels {
if ($script:Features.ContainsKey($paramKey) -and $script:Features[$paramKey].RequiresReboot -eq $true) {
$feature = $script:Features[$paramKey]
$isUndo = $script:UndoParams.ContainsKey($paramKey)
$displayLabel = if ($isUndo -and $feature.UndoLabel) { $feature.UndoLabel } else { $feature.Label }
$displayLabel = if ($isUndo -and $feature.UndoLabel) { Get-Translation -Key $paramKey -Field 'UndoLabel' -Section 'Features' } else { Get-Translation -Key $paramKey -Field 'Label' -Section 'Features' }
if (-not [string]::IsNullOrWhiteSpace([string]$displayLabel)) {
[void]$rebootFeatureLabels.Add([string]$displayLabel)
+12 -12
View File
@@ -14,24 +14,24 @@ function Test-ConfigConsistency {
param($Config)
if (-not $Config) {
return 'Configuration is empty or could not be read.'
return Get-Translation -Key 'ConfigEmptyOrUnreadable'
}
if (-not $Config.Version) {
return 'Configuration is missing a Version field.'
return Get-Translation -Key 'ConfigMissingVersion'
}
if (-not $Config.Apps -and -not $Config.Tweaks -and -not $Config.Deployment) {
return 'The configuration file contains no importable data.'
return Get-Translation -Key 'ConfigNoImportableData'
}
if ($null -ne $Config.Apps) {
if ($Config.Apps -isnot [string] -and $Config.Apps -isnot [System.Collections.IEnumerable]) {
return 'Configuration Apps entries must be strings.'
return Get-Translation -Key 'ConfigAppsMustBeStrings'
}
foreach ($app in @($Config.Apps)) {
if ($app -isnot [string]) {
return 'Configuration Apps entries must be strings.'
return Get-Translation -Key 'ConfigAppsMustBeStrings'
}
}
}
@@ -41,13 +41,13 @@ function Test-ConfigConsistency {
if ($null -eq $category) { continue }
if ($category -is [string] -or $category -isnot [System.Collections.IEnumerable]) {
return "Configuration $categoryName entries must contain Name and Value properties."
return Get-Translation -Key 'ConfigEntriesMustHaveNameAndValue' -FormatArgs @($categoryName)
}
foreach ($setting in @($category)) {
$hasName = if ($setting -is [System.Collections.IDictionary]) { $setting.Contains('Name') } else { $null -ne $setting.PSObject.Properties['Name'] }
$hasValue = if ($setting -is [System.Collections.IDictionary]) { $setting.Contains('Value') } else { $null -ne $setting.PSObject.Properties['Value'] }
if (-not $setting -or -not $hasName -or -not $hasValue -or $setting.Name -isnot [string] -or [string]::IsNullOrWhiteSpace($setting.Name)) {
return "Configuration $categoryName entries must contain Name and Value properties."
return Get-Translation -Key 'ConfigEntriesMustHaveNameAndValue' -FormatArgs @($categoryName)
}
}
}
@@ -65,31 +65,31 @@ function Test-ConfigConsistency {
$scopeIndex = $null
if ($hasScope) {
if (-not [int]::TryParse("$($lookup['AppRemovalScopeIndex'])", [ref]$scopeIndex) -or $scopeIndex -notin @(0, 1, 2)) {
return 'AppRemovalScopeIndex must be a supported numeric value (0, 1, or 2).'
return Get-Translation -Key 'ConfigInvalidAppRemovalScopeIndex'
}
}
$userIndex = $null
if ($hasUser) {
if (-not [int]::TryParse("$($lookup['UserSelectionIndex'])", [ref]$userIndex) -or $userIndex -notin @(0, 1, 2)) {
return 'UserSelectionIndex must be a supported numeric value (0, 1, or 2).'
return Get-Translation -Key 'ConfigInvalidUserSelectionIndex'
}
}
# "Current user only" (index 1) is only valid together with "Current User" (index 0)
if ($hasScope -and $scopeIndex -eq 1) {
if (-not $hasUser -or $userIndex -ne 0) {
return "App removal scope 'Current user only' (AppRemovalScopeIndex 1) requires the deployment target 'Current User' (UserSelectionIndex 0)."
return Get-Translation -Key 'ConfigScopeRequiresCurrentUserTarget'
}
}
# "Target user only" (index 2) is only valid together with "Other User" (index 1)
if ($hasScope -and $scopeIndex -eq 2) {
if (-not $hasUser -or $userIndex -ne 1) {
return "App removal scope 'Target user only' (AppRemovalScopeIndex 2) requires the deployment target 'Other User' (UserSelectionIndex 1)."
return Get-Translation -Key 'ConfigScopeRequiresOtherUserTarget'
}
if (-not $lookup.ContainsKey('OtherUsername') -or [string]::IsNullOrWhiteSpace("$($lookup['OtherUsername'])")) {
return "App removal scope 'Target user only' (AppRemovalScopeIndex 2) requires an 'OtherUsername' value."
return Get-Translation -Key 'ConfigScopeRequiresOtherUsername'
}
}
+16 -4
View File
@@ -1,3 +1,15 @@
<#
.SYNOPSIS
Validates a username for the "apply changes to another user" deployment target.
.DESCRIPTION
Rejects an empty name, the current user's own name (that's the "Current User" option
instead), and a name with no matching local profile, in that order.
.OUTPUTS
PSCustomObject with IsValid (bool), UserName (the trimmed input), and Message (a
translated string describing the validation result, success or failure alike).
#>
function Test-TargetUserName {
param(
[AllowNull()]
@@ -11,7 +23,7 @@ function Test-TargetUserName {
return [PSCustomObject]@{
IsValid = $false
UserName = $normalizedUserName
Message = 'Please enter a username'
Message = Get-Translation -Key 'UsernameValidationEmpty'
}
}
@@ -19,7 +31,7 @@ function Test-TargetUserName {
return [PSCustomObject]@{
IsValid = $false
UserName = $normalizedUserName
Message = "Cannot enter your own username, use 'Current User' option instead"
Message = Get-Translation -Key 'UsernameValidationOwnUsername'
}
}
@@ -27,13 +39,13 @@ function Test-TargetUserName {
return [PSCustomObject]@{
IsValid = $false
UserName = $normalizedUserName
Message = 'User not found, please enter a valid username'
Message = Get-Translation -Key 'UsernameValidationNotFound'
}
}
return [PSCustomObject]@{
IsValid = $true
UserName = $normalizedUserName
Message = "User found: $normalizedUserName"
Message = Get-Translation -Key 'UsernameValidationFound' -FormatArgs @($normalizedUserName)
}
}