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
+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))
}
}