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
9 changes: 9 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ jobs:
exit 1
}

- name: Run localization tests
shell: pwsh
run: |
$result = Invoke-Pester -Path "Tests/Unit/Test-Localization.ps1" -Output Detailed -PassThru
if ($result.FailedCount -gt 0) {
Write-Error "$($result.FailedCount) test(s) failed in Test-Localization.ps1"
exit 1
}

- name: Run integration tests
shell: pwsh
run: |
Expand Down
17 changes: 17 additions & 0 deletions Config/Languages/en-US/Categories.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Version": "1",
"Categories": {
"AI": "AI",
"Appearance": "Appearance",
"File Explorer": "File Explorer",
"Gaming": "Gaming",
"Multi-tasking": "Multi-tasking",
"Optional Windows Features": "Optional Windows Features",
"Other": "Other",
"Privacy & Suggested Content": "Privacy & Suggested Content",
"Start Menu & Search": "Start Menu & Search",
"System": "System",
"Taskbar": "Taskbar",
"Windows Update": "Windows Update"
}
}
708 changes: 708 additions & 0 deletions Config/Languages/en-US/Features.json

Large diffs are not rendered by default.

237 changes: 237 additions & 0 deletions Scripts/FileIO/LoadLanguageFile.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Loads the localized text catalogue from Config/Languages.
#
# Function names match upstream Raphire/Win11Debloat PR 764 so the two can be
# reconciled later without renaming. The file name follows WinSwift's own
# convention. Feature and category text is keyed by FeatureId and by the English
# category name, both of which are stable identifiers already used elsewhere.

<#
.SYNOPSIS
Resolves a language code to an available Config/Languages folder.

.DESCRIPTION
Prefers an exact match, then any folder sharing the language prefix so that
es-MX resolves to es-ES, then en-US.

.OUTPUTS
System.String. The folder name to load.
#>
function Resolve-LanguageFolder {
param(
[Parameter(Mandatory)]
[string]$LanguageCode,
[string]$LanguagesPath = $script:LanguagesPath
)

if ([string]::IsNullOrWhiteSpace($LanguagesPath)) {
return 'en-US'
}

$exactPath = Join-Path $LanguagesPath $LanguageCode
if (Test-Path -LiteralPath $exactPath -PathType Container) {
return $LanguageCode
}

$languagePrefix = ($LanguageCode -split '-')[0]
if (-not [string]::IsNullOrWhiteSpace($languagePrefix)) {
$prefixMatch = Get-ChildItem -Path $LanguagesPath -Directory -Filter "$languagePrefix-*" -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($prefixMatch) {
return $prefixMatch.Name
}
}

return 'en-US'
}

<#
.SYNOPSIS
Loads the catalogue files for one language folder.

.OUTPUTS
PSCustomObject with LanguageCode, Features and Categories, or $null when a
file is missing or unparsable.
#>
function Import-LanguageContent {
param(
[Parameter(Mandatory)]
[string]$LanguageFolder,
[string]$LanguagesPath = $script:LanguagesPath
)

$folderPath = Join-Path $LanguagesPath $LanguageFolder
$features = LoadJsonFile -filePath (Join-Path $folderPath 'Features.json') -optionalFile
$categories = LoadJsonFile -filePath (Join-Path $folderPath 'Categories.json') -optionalFile

if (-not $features -or -not $categories) {
return $null
}

return [PSCustomObject]@{
LanguageCode = $LanguageFolder
Features = $features.Features
Categories = $categories.Categories
Fallback = $null
}
}

<#
.SYNOPSIS
Loads the active language catalogue, falling back to en-US.

.DESCRIPTION
A language other than en-US also carries the en-US catalogue on its Fallback
property, so a single missing string resolves without the whole language
being rejected.

.OUTPUTS
PSCustomObject, or $null when even en-US cannot be loaded.
#>
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) {
# Not fatal. Callers fall back to the English text in Features.json.
Write-Warning 'Unable to load any language catalogue. Using the text in Features.json.'
return $null
}

if ($content.LanguageCode -ne 'en-US') {
$content.Fallback = Import-LanguageContent -LanguageFolder 'en-US' -LanguagesPath $LanguagesPath
}

return $content
}

<#
.SYNOPSIS
Returns one localized string for a feature.

.DESCRIPTION
Resolution order is the active language, then its en-US fallback, then the
value already carried in Features.json. The last step means a feature added
without a catalogue entry still shows English rather than an empty control.

.PARAMETER FeatureId
The feature to look up.

.PARAMETER Key
Label, ToolTip, ApplyText, UndoLabel or ApplyUndoText.

.OUTPUTS
System.String, or $null when no source defines the key.
#>
function Get-WinSwiftFeatureText {
param(
[Parameter(Mandatory)]
[string]$FeatureId,
[Parameter(Mandatory)]
[ValidateSet('Label', 'ToolTip', 'ApplyText', 'UndoLabel', 'ApplyUndoText')]
[string]$Key
)

foreach ($catalogue in @($script:Language, $script:Language.Fallback)) {
if (-not $catalogue -or -not $catalogue.Features) { continue }

$entry = $catalogue.Features.PSObject.Properties[$FeatureId]
if (-not $entry) { continue }

$value = $entry.Value.PSObject.Properties[$Key]
if ($value -and -not [string]::IsNullOrWhiteSpace([string]$value.Value)) {
return [string]$value.Value
}
}

# Features.json remains the last resort so a missing catalogue is not fatal.
if ($script:Features -and $script:Features.ContainsKey($FeatureId)) {
$fallback = $script:Features[$FeatureId].$Key
if (-not [string]::IsNullOrWhiteSpace([string]$fallback)) {
return [string]$fallback
}
}

return $null
}

<#
.SYNOPSIS
Replaces the text on every loaded feature with its localized value.

.DESCRIPTION
Consumers read $feature.Label, $feature.ApplyText and the rest directly, in
the CLI, the GUI and the XAML bindings alike. Overlaying the values once
here localizes all of them without touching each call site, and without a
call site being missed.

Category is deliberately not overlaid. It is both a display string and the
key features are grouped by, so translating it in place would break
grouping. The GUI resolves it for display through Get-WinSwiftCategoryText.

.OUTPUTS
System.Int32. The number of strings replaced, for logging and tests.
#>
function Update-FeatureTextFromLanguage {
if (-not $script:Features -or $script:Features.Count -eq 0) {
return 0
}

$replaced = 0
foreach ($featureId in @($script:Features.Keys)) {
$feature = $script:Features[$featureId]

foreach ($key in @('Label', 'ToolTip', 'ApplyText', 'UndoLabel', 'ApplyUndoText')) {
$current = [string]$feature.$key
if ([string]::IsNullOrWhiteSpace($current)) { continue }

$localized = Get-WinSwiftFeatureText -FeatureId $featureId -Key $key
if ([string]::IsNullOrWhiteSpace($localized) -or $localized -eq $current) { continue }

$feature.$key = $localized
$replaced++
}
}

return $replaced
}

<#
.SYNOPSIS
Returns the localized display name for a feature category.

.OUTPUTS
System.String. The English category name when no translation exists, so an
untranslated category still groups correctly.
#>
function Get-WinSwiftCategoryText {
param(
[Parameter(Mandatory)]
[AllowEmptyString()]
[string]$Category
)

if ([string]::IsNullOrWhiteSpace($Category)) {
return $Category
}

foreach ($catalogue in @($script:Language, $script:Language.Fallback)) {
if (-not $catalogue -or -not $catalogue.Categories) { continue }

$entry = $catalogue.Categories.PSObject.Properties[$Category]
if ($entry -and -not [string]::IsNullOrWhiteSpace([string]$entry.Value)) {
return [string]$entry.Value
}
}

return $Category
}
11 changes: 10 additions & 1 deletion Scripts/Helpers/Initialize-Environment.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Define script-level variables & paths
$script:Version = "3.3.0"
# Mirrors the WINSWIFT_VERSION constant in WinSwift.ps1. The literal is only
# used when this file is dot-sourced on its own, as the tests do.
$script:Version = if (Get-Variable -Name WINSWIFT_VERSION -Scope Global -ErrorAction SilentlyContinue) {
$global:WINSWIFT_VERSION
} elseif ($WINSWIFT_VERSION) {
$WINSWIFT_VERSION
} else {
"3.4.0"
}
$script:AppVersion = $script:Version
$rootDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$configPath = Join-Path $rootDir 'Config'
Expand All @@ -10,6 +18,7 @@ $scriptsPath = Join-Path $rootDir 'Scripts'
$script:AppsListFilePath = Join-Path $configPath 'Apps.json'
$script:DefaultSettingsFilePath = Join-Path $configPath 'DefaultSettings.json'
$script:FeaturesFilePath = Join-Path $configPath 'Features.json'
$script:LanguagesPath = Join-Path $configPath 'Languages'
$script:SavedSettingsFilePath = Join-Path $configPath 'LastUsedSettings.json'
$script:DefaultLogPath = Join-Path $logsPath 'WinSwift.log'
$script:RegfilesPath = Join-Path $rootDir 'Regfiles'
Expand Down
Loading