mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-10-08 06:26:45 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user