diff --git a/Config/Languages/en-US/Chrome.json b/Config/Languages/en-US/Chrome.json new file mode 100644 index 0000000..68239da --- /dev/null +++ b/Config/Languages/en-US/Chrome.json @@ -0,0 +1,128 @@ +{ + "Version": "1", + "Chrome": { + "About WinSwift": "About WinSwift", + "Version:": "Version:", + "Author:": "Author:", + "BiosSystem": "BiosSystem", + "Project:": "Project:", + "https://github.com/BiosSystem/WinSwift": "https://github.com/BiosSystem/WinSwift", + "WinSwift is a passion project that I maintain in my free time. If you've found this tool useful, please consider making a small donation to support its development. I really appreciate it!": "WinSwift is a passion project that I maintain in my free time. If you've found this tool useful, please consider making a small donation to support its development. I really appreciate it!", + "Support me on Ko-fi": "Support me on Ko-fi", + "Close": "Close", + "WinSwift Application Selection": "WinSwift Application Selection", + "Check apps that you wish to remove, uncheck apps that you wish to keep": "Check apps that you wish to remove, uncheck apps that you wish to keep", + "Check/Uncheck all": "Check/Uncheck all", + "Loading apps...": "Loading apps...", + "Only show installed apps": "Only show installed apps", + "Confirm": "Confirm", + "Cancel": "Cancel", + "Apply Changes": "Apply Changes", + "Applying Changes": "Applying Changes", + "Preparing...": "Preparing...", + "Step 0 of 0": "Step 0 of 0", + "Changes Applied": "Changes Applied", + "Please note that some changes will only take effect after a reboot. Thanks for using WinSwift!": "Please note that some changes will only take effect after a reboot. Thanks for using WinSwift!", + "A reboot is required for these changes to take effect:": "A reboot is required for these changes to take effect:", + "Support the creator": "Support the creator", + "View the selected changes here": "View the selected changes here", + "Select Settings to Import/Export": "Select Settings to Import/Export", + "OK": "OK", + "WinSwift": "WinSwift", + "Options": "Options", + "Import config": "Import config", + "Export config": "Export config", + "Restore backup": "Restore backup", + "Documentation": "Documentation", + "Report a bug": "Report a bug", + "Logs": "Logs", + "About": "About", + "Home": "Home", + "Welcome to WinSwift": "Welcome to WinSwift", + "Your clean Windows experience is just a few clicks away!": "Your clean Windows experience is just a few clicks away!", + "What user do you want to apply changes to?": "What user do you want to apply changes to?", + "Current User": "Current User", + "Other User": "Other User", + "Windows Default User (Sysprep)": "Windows Default User (Sysprep)", + "Enter username": "Enter username", + "Default Mode": "Default Mode", + "Quickly select the recommended settings": "Quickly select the recommended settings", + "Custom Setup": "Custom Setup", + "Manually select your preferred settings": "Manually select your preferred settings", + "App Removal": "App Removal", + "Select which apps you want to remove": "Select which apps you want to remove", + "Quick Select": "Quick Select", + "Clear all selected apps": "Clear all selected apps", + "Clear Selection": "Clear Selection", + "Default apps": "Default apps", + "Select the apps that are safe to remove for most users": "Select the apps that are safe to remove for most users", + "Previously selected apps": "Previously selected apps", + "Select the apps that were removed the last time WinSwift was run": "Select the apps that were removed the last time WinSwift was run", + "Only show applications that are currently installed on this system": "Only show applications that are currently installed on this system", + "Search apps...": "Search apps...", + "Name": "Name", + "Description": "Description", + "App ID": "App ID", + "Legend:": "Legend:", + "[Recommended] Safe to remove for most users": "[Recommended] Safe to remove for most users", + "Recommended": "Recommended", + "[Optional] Can be safely removed if you don't need this app": "[Optional] Can be safely removed if you don't need this app", + "Optional": "Optional", + "[Not Recommended] Only remove if you know what you are doing": "[Not Recommended] Only remove if you know what you are doing", + "Not Recommended": "Not Recommended", + "Tweaks": "Tweaks", + "System Tweaks": "System Tweaks", + "Select what changes you want to make, you can hover over settings for more information": "Select what changes you want to make, you can hover over settings for more information", + "Clear all selected tweaks": "Clear all selected tweaks", + "Default settings": "Default settings", + "Select the settings that are recommended for most people": "Select the settings that are recommended for most people", + "Previously selected settings": "Previously selected settings", + "Select the settings that were applied the last time WinSwift was run": "Select the settings that were applied the last time WinSwift was run", + "Privacy & suggested content": "Privacy & suggested content", + "Select all tweaks related to privacy & suggested content": "Select all tweaks related to privacy & suggested content", + "AI features": "AI features", + "Select all AI related tweaks": "Select all AI related tweaks", + "Detect applied tweaks": "Detect applied tweaks", + "Detect all tweaks currently applied for the current user": "Detect all tweaks currently applied for the current user", + "Search tweaks...": "Search tweaks...", + "Deployment Settings": "Deployment Settings", + "Configure how the selected changes will be applied to your system": "Configure how the selected changes will be applied to your system", + "Changes will be applied to": "Changes will be applied to", + "The currently logged-in user profile.": "The currently logged-in user profile.", + "Apps will be removed for": "Apps will be removed for", + "All users": "All users", + "Current user only": "Current user only", + "Target user only": "Target user only", + "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users.": "Apps will be removed for all users and from the Windows image to prevent reinstallation for new users.", + "Create a system restore point (Recommended)": "Create a system restore point (Recommended)", + "Restart the Windows Explorer process to apply all changes immediately": "Restart the Windows Explorer process to apply all changes immediately", + "Review selected changes": "Review selected changes", + "Back": "Back", + "Next": "Next", + "MessageBox": "MessageBox", + "Message": "Message", + "Message content goes here": "Message content goes here", + "SelectType": "SelectType", + "Choose what changes you want to restore.": "Choose what changes you want to restore.", + "Restore Registry Backup": "Restore Registry Backup", + "Restore system registry configuration from a backup": "Restore system registry configuration from a backup", + "Restore Start Menu Backup": "Restore Start Menu Backup", + "Restore the Start Menu pinned apps layout from a backup": "Restore the Start Menu pinned apps layout from a backup", + "Registry": "Registry", + "This will restore any system registry changes made by WinSwift to the previous state. You can review the changes after selecting a backup file. Removed applications will need to be reinstalled manually.": "This will restore any system registry changes made by WinSwift to the previous state. You can review the changes after selecting a backup file. Removed applications will need to be reinstalled manually.", + "Warning: Only use backup files generated by WinSwift.": "Warning: Only use backup files generated by WinSwift.", + "File:": "File:", + "Not selected": "Not selected", + "Created:": "Created:", + "N/A": "N/A", + "Target:": "Target:", + "The following changes will be reverted:": "The following changes will be reverted:", + "This will restore the Start Menu pinned apps layout for the current user.": "This will restore the Start Menu pinned apps layout for the current user.", + "The following changes will be re-applied:": "The following changes will be re-applied:", + "The following changes won't be reverted:": "The following changes won't be reverted:", + "Visit the wiki for more information": "Visit the wiki for more information", + "StartMenu": "StartMenu", + "This will restore the Start Menu pinned apps layout for the selected user(s) using a backup that is automatically created by WinSwift. Manually created backups can also be used.": "This will restore the Start Menu pinned apps layout for the selected user(s) using a backup that is automatically created by WinSwift. Manually created backups can also be used.", + "Automatically find Start Menu backup": "Automatically find Start Menu backup" + } +} diff --git a/Scripts/FileIO/LoadLanguageFile.ps1 b/Scripts/FileIO/LoadLanguageFile.ps1 index 7a59b30..ea45626 100644 --- a/Scripts/FileIO/LoadLanguageFile.ps1 +++ b/Scripts/FileIO/LoadLanguageFile.ps1 @@ -62,6 +62,8 @@ function Import-LanguageContent { $folderPath = Join-Path $LanguagesPath $LanguageFolder $features = LoadJsonFile -filePath (Join-Path $folderPath 'Features.json') -optionalFile $categories = LoadJsonFile -filePath (Join-Path $folderPath 'Categories.json') -optionalFile + # Optional: a language may translate features without covering the GUI. + $chrome = LoadJsonFile -filePath (Join-Path $folderPath 'Chrome.json') -optionalFile if (-not $features -or -not $categories) { return $null @@ -71,6 +73,7 @@ function Import-LanguageContent { LanguageCode = $LanguageFolder Features = $features.Features Categories = $categories.Categories + Chrome = if ($chrome) { $chrome.Chrome } else { $null } Fallback = $null } } @@ -205,6 +208,77 @@ function Update-FeatureTextFromLanguage { return $replaced } +<# + .SYNOPSIS + Reads a XAML schema and returns it with its literal text translated. + + .DESCRIPTION + Substitution happens on the markup before it is parsed, rather than by + walking the loaded window. A logical tree does not contain text that lives + inside a Style or a template, so a tree walk reaches only part of the + markup: MainWindow alone carries 113 literal strings but exposes 99 as + logical tree nodes. + + Chrome entries are keyed by their English text, so reordering markup cannot + break them and an untranslated string resolves to itself. Values are read + XML-decoded and written back XML-encoded, so a translation containing an + ampersand or an angle bracket cannot corrupt the markup. + + Only Content, Text, Header, ToolTip and Title are considered, and a value + that opens with a brace is left alone because it is a binding or a resource + reference rather than text. + + .PARAMETER Path + The schema file to read. + + .OUTPUTS + System.String. The markup, translated where a translation exists. +#> +function Get-LocalizedXaml { + param( + [Parameter(Mandatory)] + [string]$Path + ) + + $markup = Get-Content -LiteralPath $Path -Raw + + $chrome = $null + foreach ($catalogue in @($script:Language, $script:Language.Fallback)) { + if ($catalogue -and $catalogue.Chrome) { $chrome = $catalogue.Chrome; break } + } + + if (-not $chrome) { + return $markup + } + + $evaluator = { + param($match) + + $attribute = $match.Groups['attr'].Value + $raw = $match.Groups['value'].Value + + # Bindings and resource references are not text. + if ($raw -match '^\s*\{') { return $match.Value } + + $decoded = [System.Net.WebUtility]::HtmlDecode($raw) + $entry = $chrome.PSObject.Properties[$decoded] + if (-not $entry) { return $match.Value } + + $translated = [string]$entry.Value + if ([string]::IsNullOrWhiteSpace($translated) -or $translated -eq $decoded) { + return $match.Value + } + + # Encode before it goes back into markup, and escape the double quote + # that would otherwise close the attribute. + $encoded = [System.Security.SecurityElement]::Escape($translated) + return ('{0}="{1}"' -f $attribute, $encoded) + } + + $pattern = '(?\b(?:Content|Text|Header|ToolTip|Title))="(?[^"]*)"' + return [regex]::Replace($markup, $pattern, $evaluator) +} + <# .SYNOPSIS Returns the localized display name for a feature category. diff --git a/Scripts/GUI/SetWindowThemeResources.ps1 b/Scripts/GUI/SetWindowThemeResources.ps1 index 610c017..1335e26 100644 --- a/Scripts/GUI/SetWindowThemeResources.ps1 +++ b/Scripts/GUI/SetWindowThemeResources.ps1 @@ -124,7 +124,7 @@ function SetWindowThemeResources { # Load and merge shared styles if ($script:SharedStylesSchema -and (Test-Path $script:SharedStylesSchema)) { - $sharedXaml = Get-Content -Path $script:SharedStylesSchema -Raw + $sharedXaml = Get-LocalizedXaml -Path $script:SharedStylesSchema $sharedReader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($sharedXaml)) try { $sharedDict = [System.Windows.Markup.XamlReader]::Load($sharedReader) diff --git a/Scripts/GUI/Show-AboutDialog.ps1 b/Scripts/GUI/Show-AboutDialog.ps1 index 18e8c94..5fbc02e 100644 --- a/Scripts/GUI/Show-AboutDialog.ps1 +++ b/Scripts/GUI/Show-AboutDialog.ps1 @@ -24,7 +24,7 @@ function Show-AboutDialog { } # Load XAML from file - $xaml = Get-Content -Path $script:AboutWindowSchema -Raw + $xaml = Get-LocalizedXaml -Path $script:AboutWindowSchema $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { $aboutWindow = [System.Windows.Markup.XamlReader]::Load($reader) diff --git a/Scripts/GUI/Show-AppSelectionWindow.ps1 b/Scripts/GUI/Show-AppSelectionWindow.ps1 index 01c3545..0320458 100644 --- a/Scripts/GUI/Show-AppSelectionWindow.ps1 +++ b/Scripts/GUI/Show-AppSelectionWindow.ps1 @@ -17,7 +17,7 @@ function Show-AppSelectionWindow { } # Load XAML from file - $xaml = Get-Content -Path $script:AppSelectionSchema -Raw + $xaml = Get-LocalizedXaml -Path $script:AppSelectionSchema $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { $window = [System.Windows.Markup.XamlReader]::Load($reader) diff --git a/Scripts/GUI/Show-ApplyModal.ps1 b/Scripts/GUI/Show-ApplyModal.ps1 index 70dbc59..a93c39b 100644 --- a/Scripts/GUI/Show-ApplyModal.ps1 +++ b/Scripts/GUI/Show-ApplyModal.ps1 @@ -26,7 +26,7 @@ function Show-ApplyModal { } # Load XAML from file - $xaml = Get-Content -Path $script:ApplyChangesWindowSchema -Raw + $xaml = Get-LocalizedXaml -Path $script:ApplyChangesWindowSchema $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { $applyWindow = [System.Windows.Markup.XamlReader]::Load($reader) diff --git a/Scripts/GUI/Show-Bubble.ps1 b/Scripts/GUI/Show-Bubble.ps1 index c5bcdcd..7886664 100644 --- a/Scripts/GUI/Show-Bubble.ps1 +++ b/Scripts/GUI/Show-Bubble.ps1 @@ -103,7 +103,7 @@ function Show-Bubble { Hide-Bubble -Immediate - $xaml = Get-Content -Path $script:BubbleHintSchema -Raw + $xaml = Get-LocalizedXaml -Path $script:BubbleHintSchema $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { $bubblePanel = [System.Windows.Markup.XamlReader]::Load($reader) diff --git a/Scripts/GUI/Show-ConfigWindow.ps1 b/Scripts/GUI/Show-ConfigWindow.ps1 index 767614e..30dc8d6 100644 --- a/Scripts/GUI/Show-ConfigWindow.ps1 +++ b/Scripts/GUI/Show-ConfigWindow.ps1 @@ -35,7 +35,7 @@ function Show-ImportExportConfigWindow { return $null } - $xaml = Get-Content -Path $schemaPath -Raw + $xaml = Get-LocalizedXaml -Path $schemaPath $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { $dlg = [System.Windows.Markup.XamlReader]::Load($reader) diff --git a/Scripts/GUI/Show-MainWindow.ps1 b/Scripts/GUI/Show-MainWindow.ps1 index 5aa8aa7..50d7f87 100644 --- a/Scripts/GUI/Show-MainWindow.ps1 +++ b/Scripts/GUI/Show-MainWindow.ps1 @@ -5,7 +5,7 @@ function Show-MainWindow { $usesDarkMode = GetSystemUsesDarkMode # ---- Load XAML ---- - $xaml = Get-Content -Path $script:MainWindowSchema -Raw + $xaml = Get-LocalizedXaml -Path $script:MainWindowSchema $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { $window = [System.Windows.Markup.XamlReader]::Load($reader) diff --git a/Scripts/GUI/Show-MessageBox.ps1 b/Scripts/GUI/Show-MessageBox.ps1 index 654758d..651a33c 100644 --- a/Scripts/GUI/Show-MessageBox.ps1 +++ b/Scripts/GUI/Show-MessageBox.ps1 @@ -46,7 +46,7 @@ function Show-MessageBox { } # Load XAML from file - $xaml = Get-Content -Path $script:MessageBoxSchema -Raw + $xaml = Get-LocalizedXaml -Path $script:MessageBoxSchema $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { $msgWindow = [System.Windows.Markup.XamlReader]::Load($reader) diff --git a/Scripts/GUI/Show-RestoreBackupDialog.ps1 b/Scripts/GUI/Show-RestoreBackupDialog.ps1 index 285db50..5f8ab10 100644 --- a/Scripts/GUI/Show-RestoreBackupDialog.ps1 +++ b/Scripts/GUI/Show-RestoreBackupDialog.ps1 @@ -49,7 +49,7 @@ function Show-RestoreBackupDialog { throw 'Restore backup window schema file could not be found.' } - $xaml = Get-Content -Path $schemaPath -Raw + $xaml = Get-LocalizedXaml -Path $schemaPath $reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml)) try { diff --git a/Tests/Unit/Test-Localization.ps1 b/Tests/Unit/Test-Localization.ps1 index 7ebb808..c9a7de1 100644 --- a/Tests/Unit/Test-Localization.ps1 +++ b/Tests/Unit/Test-Localization.ps1 @@ -143,6 +143,61 @@ Describe 'WinSwift localization' { } } + Context 'XAML chrome substitution' { + + BeforeEach { + $script:Language = [PSCustomObject]@{ + LanguageCode = 'xx-XX' + Chrome = [PSCustomObject]@{ + 'Apply changes' = 'Aplicar cambios' + 'Risky & ' = 'Peligro & ' + } + Features = $null + Categories = $null + Fallback = $null + } + $script:sample = Join-Path $TestDrive 'sample.xaml' + } + + It 'substitutes a translated attribute' { + '