Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions Config/Languages/en-US/Chrome.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
74 changes: 74 additions & 0 deletions Scripts/FileIO/LoadLanguageFile.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -71,6 +73,7 @@ function Import-LanguageContent {
LanguageCode = $LanguageFolder
Features = $features.Features
Categories = $categories.Categories
Chrome = if ($chrome) { $chrome.Chrome } else { $null }
Fallback = $null
}
}
Expand Down Expand Up @@ -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 = '(?<attr>\b(?:Content|Text|Header|ToolTip|Title))="(?<value>[^"]*)"'
return [regex]::Replace($markup, $pattern, $evaluator)
}

<#
.SYNOPSIS
Returns the localized display name for a feature category.
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/SetWindowThemeResources.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/Show-AboutDialog.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/Show-AppSelectionWindow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/Show-ApplyModal.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/Show-Bubble.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/Show-ConfigWindow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/Show-MainWindow.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/Show-MessageBox.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Scripts/GUI/Show-RestoreBackupDialog.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
82 changes: 82 additions & 0 deletions Tests/Unit/Test-Localization.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 & <bad>' = 'Peligro & <malo>'
}
Features = $null
Categories = $null
Fallback = $null
}
$script:sample = Join-Path $TestDrive 'sample.xaml'
}

It 'substitutes a translated attribute' {
'<Window><Button Content="Apply changes" /></Window>' |
Set-Content -LiteralPath $script:sample -Encoding utf8

Get-LocalizedXaml -Path $script:sample | Should -Match 'Content="Aplicar cambios"'
}

It 'leaves untranslated text alone' {
'<Window><Button Content="Not in catalogue" /></Window>' |
Set-Content -LiteralPath $script:sample -Encoding utf8

Get-LocalizedXaml -Path $script:sample | Should -Match 'Content="Not in catalogue"'
}

It 'does not touch bindings' {
'<Window><Button Content="{Binding Apply}" /></Window>' |
Set-Content -LiteralPath $script:sample -Encoding utf8

Get-LocalizedXaml -Path $script:sample | Should -BeLike '*{Binding Apply}*'
}

It 'XML-encodes a translation containing markup characters' {
'<Window><Button Content="Risky &amp; &lt;bad&gt;" /></Window>' |
Set-Content -LiteralPath $script:sample -Encoding utf8

$result = Get-LocalizedXaml -Path $script:sample
$result | Should -BeLike '*Peligro &amp; &lt;malo&gt;*'
$result | Should -Not -BeLike '*Peligro & <malo>*'
}

It 'returns the markup unchanged when no catalogue is loaded' {
$script:Language = $null
'<Window><Button Content="Apply changes" /></Window>' |
Set-Content -LiteralPath $script:sample -Encoding utf8

Get-LocalizedXaml -Path $script:sample | Should -BeLike '*Content="Apply changes"*'
}
}

Context 'the shipped en-US catalogue' {
It 'covers every feature in Features.json' {
$catalogue = LoadJsonFile -filePath (Join-Path $script:realLanguagesPath 'en-US\Features.json')
Expand Down Expand Up @@ -187,5 +242,32 @@ Describe 'WinSwift localization' {

$missing | Should -BeNullOrEmpty -Because "categories absent from the en-US catalogue: $($missing -join ', ')"
}

It 'covers every translatable string in the schemas' {
# Guards against a new XAML string being added without a catalogue
# entry. Glyph character references and bindings are not text.
$catalogue = LoadJsonFile -filePath (Join-Path $script:realLanguagesPath 'en-US\Chrome.json')
$schemasPath = Join-Path (Split-Path (Split-Path $script:realLanguagesPath -Parent) -Parent) 'Schemas'

$missing = [System.Collections.Generic.List[string]]::new()
foreach ($schema in Get-ChildItem $schemasPath -Filter *.xaml) {
$markup = Get-Content -LiteralPath $schema.FullName -Raw
foreach ($match in [regex]::Matches($markup, '(?:Content|Text|Header|ToolTip|Title)="([^"]*)"')) {
$raw = $match.Groups[1].Value
if ($raw.Length -lt 2) { continue }
if ($raw -match '^\s*\{') { continue }
if ($raw -match '^(&#x?[0-9A-Fa-f]+;\s*)+$') { continue }

$decoded = [System.Net.WebUtility]::HtmlDecode($raw)
if ($decoded -match '^[\W\d_]+$') { continue }

if (-not $catalogue.Chrome.PSObject.Properties[$decoded]) {
$missing.Add("$($schema.Name): $decoded")
}
}
}

$missing | Should -BeNullOrEmpty -Because "schema strings absent from Chrome.json: $($missing -join '; ')"
}
}
}
Loading