From 07a2128cd62348f06b97db7db9a2822c84f7a092 Mon Sep 17 00:00:00 2001 From: Sashank Bhamidi Date: Fri, 4 Sep 2026 10:55:59 +0530 Subject: [PATCH 01/11] Add localization framework with generic Get-Translation lookup and XAML markers --- Scripts/FileIO/Import-LanguageFile.ps1 | 402 ++++++++++++++++++ Tests/Import-LanguageFile.Tests.ps1 | 223 ++++++++++ .../LanguageLoading/en-US/Categories.json | 5 + .../LanguageLoading/en-US/Chrome.json | 7 + .../LanguageLoading/en-US/Features.json | 13 + .../LanguageLoading/es-ES/Categories.json | 1 + .../LanguageLoading/es-ES/Chrome.json | 3 + .../LanguageLoading/es-ES/Features.json | 4 + 8 files changed, 658 insertions(+) create mode 100644 Scripts/FileIO/Import-LanguageFile.ps1 create mode 100644 Tests/Import-LanguageFile.Tests.ps1 create mode 100644 Tests/TestData/LanguageLoading/en-US/Categories.json create mode 100644 Tests/TestData/LanguageLoading/en-US/Chrome.json create mode 100644 Tests/TestData/LanguageLoading/en-US/Features.json create mode 100644 Tests/TestData/LanguageLoading/es-ES/Categories.json create mode 100644 Tests/TestData/LanguageLoading/es-ES/Chrome.json create mode 100644 Tests/TestData/LanguageLoading/es-ES/Features.json diff --git a/Scripts/FileIO/Import-LanguageFile.ps1 b/Scripts/FileIO/Import-LanguageFile.ps1 new file mode 100644 index 00000000..8b633611 --- /dev/null +++ b/Scripts/FileIO/Import-LanguageFile.ps1 @@ -0,0 +1,402 @@ +<# + .SYNOPSIS + Resolves a language code to an available Config/Languages folder, or falls back to en-US. +#> +function Resolve-LanguageFolder { + param( + [Parameter(Mandatory)] + [string]$LanguageCode, + [string]$LanguagesPath = $script:LanguagesPath + ) + + $exactPath = Join-Path $LanguagesPath $LanguageCode + if (Test-Path $exactPath -PathType Container) { + return $LanguageCode + } + + $languagePrefix = ($LanguageCode -split '-')[0] + $prefixMatch = Get-ChildItem -Path $LanguagesPath -Directory -Filter "$languagePrefix-*" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($prefixMatch) { + return $prefixMatch.Name + } + + return 'en-US' +} + +<# + .SYNOPSIS + Loads Chrome/Features/Categories JSON for a language folder, or returns $null on failure. +#> +function Import-LanguageContent { + param( + [Parameter(Mandatory)] + [string]$LanguageFolder, + [string]$LanguagesPath = $script:LanguagesPath + ) + + $folderPath = Join-Path $LanguagesPath $LanguageFolder + $chrome = Import-JsonFile -filePath (Join-Path $folderPath 'Chrome.json') + $features = Import-JsonFile -filePath (Join-Path $folderPath 'Features.json') + $categories = Import-JsonFile -filePath (Join-Path $folderPath 'Categories.json') + + if (-not $chrome -or -not $features -or -not $categories) { + return $null + } + + return [PSCustomObject]@{ + LanguageCode = $LanguageFolder + Chrome = $chrome + Features = $features.Features + UiGroups = $features.UiGroups + Categories = $categories + } +} + +<# + .SYNOPSIS + Loads the active language content, falling back to en-US when the requested language fails to load. +#> +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) { + Write-Error "Unable to load the en-US language files. The GUI cannot continue without them." + return $null + } + + if ($content.LanguageCode -ne 'en-US') { + $content | Add-Member -MemberType NoteProperty -Name 'Fallback' -Value (Import-LanguageContent -LanguageFolder 'en-US' -LanguagesPath $LanguagesPath) + } + + return $content +} + +<# + .SYNOPSIS + Returns the CLDR plural category ('one', 'other', etc.) for a count in the given language. + + .DESCRIPTION + Each language's CLDR plural rule is its own case, keyed by language prefix ('en' matches + both 'en-US' and 'en-GB'). A language without a case falls back to the English rule, which + is also correct for Dutch and German but wrong for languages with 3+ plural categories or + a different singular/plural split. +#> +function Get-PluralCategory { + param( + [Parameter(Mandatory)] + [string]$LanguageCode, + [Parameter(Mandatory)] + [int]$Count + ) + + $languagePrefix = ($LanguageCode -split '-')[0].ToLowerInvariant() + + switch ($languagePrefix) { + # English CLDR rule: singular only for exactly 1, plural otherwise. Also correct for Dutch/German. + default { + if ($Count -eq 1) { + return 'one' + } + + return 'other' + } + } +} + +<# + .SYNOPSIS + Returns a language and its Fallback (if any) as an ordered lookup chain, skipping nulls. +#> +function Get-LanguageFallbackChain { + param( + [object]$Lang + ) + + return @($Lang, $Lang.Fallback) | Where-Object { $_ } +} + +<# + .SYNOPSIS + Finds the named section object (Chrome, Features, UiGroups, or Categories) that owns a key. +#> +function Find-TranslationSection { + param( + [Parameter(Mandatory)] + [object]$Lang, + [Parameter(Mandatory)] + [string]$Key + ) + + foreach ($sectionName in 'Chrome', 'Features', 'UiGroups', 'Categories') { + $section = $Lang.$sectionName + if ($section -and $section.PSObject.Properties[$Key]) { + return $section + } + } + + return $null +} + +<# + .SYNOPSIS + Looks up a translated value from the active language, falling back to en-US, then to the key itself. + + .DESCRIPTION + Chrome.json keys are flat strings, so -Field is omitted for those. Features/UiGroups/Categories + keys (FeatureId/GroupId/CategoryId) resolve to an object, so -Field picks the property on it + (Label, ToolTip, ApplyText, UndoLabel, ApplyUndoText). One generic lookup covers every section + instead of a separate function per section. + + When -Count is supplied, tries the plural-suffixed key ("$Key`_$category") before the bare key, + so callers don't need to add a plural variant for every string, only the ones that need one. + .OUTPUTS + System.String. The translated string, or the original Key if no language has it. +#> +function Get-Translation { + param( + [Parameter(Mandatory)] + [string]$Key, + [string]$Field = $null, + [object]$Lang = $script:Lang, + [Nullable[int]]$Count = $null, + [object[]]$FormatArgs = $null + ) + + if (-not $Lang) { + return $Key + } + + $lookupKeys = @($Key) + if ($null -ne $Count) { + $category = Get-PluralCategory -LanguageCode $Lang.LanguageCode -Count $Count + $lookupKeys = @("${Key}_$category", $Key) + } + + $resolved = $Key + :langLoop foreach ($candidateLang in (Get-LanguageFallbackChain -Lang $Lang)) { + foreach ($lookupKey in $lookupKeys) { + $section = Find-TranslationSection -Lang $candidateLang -Key $lookupKey + if (-not $section) { continue } + + $entry = $section.$lookupKey + if ($Field) { + if ($entry.PSObject.Properties[$Field]) { + $resolved = [string]$entry.$Field + break langLoop + } + continue + } + + $resolved = [string]$entry + break langLoop + } + } + + if ($null -ne $FormatArgs -and $FormatArgs.Count -gt 0) { + try { + return $resolved -f $FormatArgs + } + catch { + Write-Warning "Translation '$Key' has a placeholder mismatch with its format arguments: $($_.Exception.Message)" + return $resolved + } + } + + return $resolved +} + +<# + .SYNOPSIS + Looks up a translated label for one value of a UiGroup, falling back to en-US, then to the raw label. + + .DESCRIPTION + UiGroups[GroupId].Values in the language files is a FeatureId -> Label map, one level deeper + than Get-Translation's -Field lookup handles, so this has its own small fallback walk instead + of overloading Get-Translation's flat-field shape. +#> +function Get-GroupValueTranslation { + param( + [Parameter(Mandatory)] + [string]$GroupId, + [Parameter(Mandatory)] + [string]$FeatureId, + [Parameter(Mandatory)] + [string]$FallbackLabel, + [object]$Lang = $script:Lang + ) + + if (-not $Lang) { + return $FallbackLabel + } + + foreach ($candidateLang in (Get-LanguageFallbackChain -Lang $Lang)) { + if (-not $candidateLang.UiGroups) { continue } + + $group = $candidateLang.UiGroups.$GroupId + if (-not $group -or -not $group.Values) { continue } + + $valueEntry = $group.Values.PSObject.Properties[$FeatureId] + if ($valueEntry) { + return [string]$valueEntry.Value + } + } + + return $FallbackLabel +} + +<# + .SYNOPSIS + Flattens a loaded language object into a sorted set of dotted key paths. + + .DESCRIPTION + Chrome.json keys are already flat and are used as-is. Features/UiGroups/Categories + keys are one level deeper (EntryId -> {Field: value}), so each field becomes its own + "Section.EntryId.Field" path (e.g. "Features.DisableTelemetry.Label"). UiGroups' nested + Values map becomes "UiGroups.GroupId.Values.FeatureId". Used by Test-LanguageKeyCoverage + to diff one language's key set against another's. +#> +function Get-LanguageKeyPaths { + param( + [Parameter(Mandatory)] + [object]$Lang + ) + + $paths = New-Object System.Collections.Generic.List[string] + + foreach ($chromeKey in @($Lang.Chrome.PSObject.Properties.Name)) { + if ([string]::IsNullOrEmpty($chromeKey)) { continue } + $paths.Add($chromeKey) + } + + foreach ($sectionName in 'Features', 'Categories') { + $section = $Lang.$sectionName + if (-not $section) { continue } + + foreach ($entryId in @($section.PSObject.Properties.Name)) { + if ([string]::IsNullOrEmpty($entryId)) { continue } + foreach ($field in @($section.$entryId.PSObject.Properties.Name)) { + if ([string]::IsNullOrEmpty($field)) { continue } + $paths.Add("$sectionName.$entryId.$field") + } + } + } + + foreach ($groupId in @($Lang.UiGroups.PSObject.Properties.Name)) { + if ([string]::IsNullOrEmpty($groupId)) { continue } + $group = $Lang.UiGroups.$groupId + foreach ($field in @($group.PSObject.Properties.Name)) { + if ([string]::IsNullOrEmpty($field)) { continue } + if ($field -eq 'Values') { + foreach ($featureId in @($group.Values.PSObject.Properties.Name)) { + if ([string]::IsNullOrEmpty($featureId)) { continue } + $paths.Add("UiGroups.$groupId.Values.$featureId") + } + continue + } + $paths.Add("UiGroups.$groupId.$field") + } + } + + return @($paths | Sort-Object -Unique) +} + +<# + .SYNOPSIS + Diffs a language's key set against a baseline language, reporting missing and extra keys. + + .DESCRIPTION + Loads both languages fresh from disk (independent of any already-cached $script:Lang), + flattens each with Get-LanguageKeyPaths, and compares the two sets. Intended to run + against en-US as the baseline; useful today (catches a key present in one en-US file but + not another) and becomes the real translation-completeness check once non-English + language folders are contributed. + + .OUTPUTS + PSCustomObject with ResolvedLanguageCode (the folder actually compared, after the same + exact/prefix/en-US resolution Import-LanguageFile applies), MissingKeys (present in + Baseline, absent from LanguageCode), and ExtraKeys (present in LanguageCode, absent from + Baseline) properties, the latter two sorted arrays. +#> +function Test-LanguageKeyCoverage { + param( + [Parameter(Mandatory)] + [string]$LanguageCode, + [string]$BaselineLanguageCode = 'en-US', + [string]$LanguagesPath = $script:LanguagesPath + ) + + $resolvedLanguageCode = Resolve-LanguageFolder -LanguageCode $LanguageCode -LanguagesPath $LanguagesPath + $baseline = Import-LanguageContent -LanguageFolder $BaselineLanguageCode -LanguagesPath $LanguagesPath + $target = Import-LanguageContent -LanguageFolder $resolvedLanguageCode -LanguagesPath $LanguagesPath + + if (-not $baseline -or -not $target) { + Write-Error "Unable to load language content for coverage comparison ('$BaselineLanguageCode' vs '$resolvedLanguageCode')." + return $null + } + + $baselineKeys = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-LanguageKeyPaths -Lang $baseline)) + $targetKeys = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-LanguageKeyPaths -Lang $target)) + + $missingKeys = @($baselineKeys | Where-Object { -not $targetKeys.Contains($_) } | Sort-Object) + $extraKeys = @($targetKeys | Where-Object { -not $baselineKeys.Contains($_) } | Sort-Object) + + return [PSCustomObject]@{ + ResolvedLanguageCode = $resolvedLanguageCode + MissingKeys = $missingKeys + ExtraKeys = $extraKeys + } +} + +<# + .SYNOPSIS + Substitutes %LANG:Key% markers in XAML text with translated, XML-escaped values. + + .DESCRIPTION + Only resolves flat Chrome.json keys, since XAML markers never reference a Feature/Category/ + UiGroup field directly (those get their text from Get-Translation calls in the GUI scripts + that build dynamic controls). Runs a single pass over every marker rather than sequential + .Replace() calls, so an already-substituted value can't be re-matched by a later key. + + After substitution, scans for any %LANG:...% text that survived unresolved and throws, + since Get-Translation's key-as-fallback behavior means a missing key would otherwise render + as plain, un-marked text (e.g. "TitleBarClose" instead of a visible error) rather than being + caught here. Checks both the active language and its Fallback before flagging a key missing, + so a partially-translated language degrades to en-US text instead of failing to load. +#> +function ConvertTo-LocalizedXaml { + param( + [Parameter(Mandatory)] + [string]$Xaml, + [object]$Lang = $script:Lang + ) + + $missingKeys = New-Object System.Collections.Generic.List[string] + + $evaluator = [System.Text.RegularExpressions.MatchEvaluator]{ + param($match) + $key = $match.Groups[1].Value + $resolvesSomewhere = @(Get-LanguageFallbackChain -Lang $Lang) | Where-Object { $_.Chrome -and $_.Chrome.PSObject.Properties[$key] } + if (-not $resolvesSomewhere) { $missingKeys.Add($key) } + $value = if ($resolvesSomewhere) { Get-Translation -Key $key -Lang $Lang } else { $key } + return [System.Security.SecurityElement]::Escape($value) + } + + $result = [System.Text.RegularExpressions.Regex]::Replace($Xaml, '%LANG:([A-Za-z0-9_]+)%', $evaluator) + + if ($missingKeys.Count -gt 0) { + throw "Unresolved localization marker(s) in XAML, key(s) not found in any language: $($missingKeys -join ', ')" + } + + return $result +} diff --git a/Tests/Import-LanguageFile.Tests.ps1 b/Tests/Import-LanguageFile.Tests.ps1 new file mode 100644 index 00000000..f4a1ac68 --- /dev/null +++ b/Tests/Import-LanguageFile.Tests.ps1 @@ -0,0 +1,223 @@ +BeforeAll { + . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-JsonFile.ps1') + . (Join-Path $PSScriptRoot '..\Scripts\FileIO\Import-LanguageFile.ps1') + $script:LanguagesPath = Join-Path $PSScriptRoot 'TestData\LanguageLoading' +} + +Describe 'Resolve-LanguageFolder' { + It 'resolves ' -ForEach @( + @{ Case = 'an exact match'; LanguageCode = 'en-US'; Expected = 'en-US' } + @{ Case = 'a second exact match'; LanguageCode = 'es-ES'; Expected = 'es-ES' } + @{ Case = 'a language-prefix match when no exact folder exists'; LanguageCode = 'es-AR'; Expected = 'es-ES' } + @{ Case = 'an unrelated locale to the en-US fallback'; LanguageCode = 'fr-FR'; Expected = 'en-US' } + ) { + Resolve-LanguageFolder -LanguageCode $LanguageCode | Should -Be $Expected + } +} + +Describe 'Import-LanguageFile' { + It 'loads all three JSON files for the resolved language' { + $lang = Import-LanguageFile -LanguageCode 'en-US' + + $lang.LanguageCode | Should -Be 'en-US' + $lang.Chrome.TitleBarClose | Should -Be 'Close' + $lang.Features.DisableTelemetry.Label | Should -Be 'Disable telemetry' + $lang.Categories.PrivacySuggestedContent.Label | Should -Be 'Privacy & Suggested Content' + } + + It 'attaches an en-US Fallback when a non-en-US language is loaded' { + $lang = Import-LanguageFile -LanguageCode 'es-ES' + + $lang.LanguageCode | Should -Be 'es-ES' + $lang.Fallback.LanguageCode | Should -Be 'en-US' + } + + It 'does not attach a Fallback when en-US itself is loaded' { + $lang = Import-LanguageFile -LanguageCode 'en-US' + + $lang.PSObject.Properties['Fallback'] | Should -BeNullOrEmpty + } + + It 'falls back to en-US without warning when the requested language has no folder at all' { + # Resolve-LanguageFolder already resolves a nonexistent language straight to 'en-US', + # so there's no real fallback to warn about here, unlike the broken-folder case below. + Mock Write-Warning {} + + $lang = Import-LanguageFile -LanguageCode 'fr-FR' + + $lang.LanguageCode | Should -Be 'en-US' + Should -Invoke Write-Warning -Times 0 -Exactly + } + + It 'warns and falls back to en-US when the requested language folder exists but fails to load' { + Mock Write-Warning {} + Mock Write-Error {} + $brokenFolder = Join-Path $TestDrive 'xx-XX' + New-Item -ItemType Directory -Path $brokenFolder | Out-Null + 'not valid json' | Out-File (Join-Path $brokenFolder 'Chrome.json') + + $lang = Import-LanguageFile -LanguageCode 'xx-XX' -LanguagesPath $TestDrive + + $lang | Should -BeNullOrEmpty + Should -Invoke Write-Warning -ParameterFilter { $Message -match "Failed to load language 'xx-XX'" } -Times 1 -Exactly + } + + It 'reports an error and returns null when en-US itself cannot be loaded' { + Mock Write-Error {} + + $lang = Import-LanguageFile -LanguageCode 'en-US' -LanguagesPath $TestDrive + + $lang | Should -BeNullOrEmpty + Should -Invoke Write-Error -ParameterFilter { $Message -match 'Unable to load the en-US language files' } -Times 1 -Exactly + } +} + +Describe 'Get-PluralCategory' { + It 'returns for a count of ' -ForEach @( + @{ Count = 0; Expected = 'other' } + @{ Count = 1; Expected = 'one' } + @{ Count = 2; Expected = 'other' } + @{ Count = 5; Expected = 'other' } + ) { + Get-PluralCategory -LanguageCode 'en-US' -Count $Count | Should -Be $Expected + } +} + +Describe 'Get-Translation' { + BeforeAll { + $script:EnLang = Import-LanguageFile -LanguageCode 'en-US' + $script:EsLang = Import-LanguageFile -LanguageCode 'es-ES' + } + + It 'looks up a flat Chrome key' { + Get-Translation -Key 'TitleBarClose' -Lang $script:EnLang | Should -Be 'Close' + } + + It 'looks up a Field on a Features entry' { + Get-Translation -Key 'DisableTelemetry' -Field 'Label' -Lang $script:EnLang | Should -Be 'Disable telemetry' + Get-Translation -Key 'DisableTelemetry' -Field 'ToolTip' -Lang $script:EnLang | Should -Be 'Disables telemetry collection.' + } + + It 'looks up a Field on a Categories entry' { + Get-Translation -Key 'PrivacySuggestedContent' -Field 'Label' -Lang $script:EnLang | Should -Be 'Privacy & Suggested Content' + } + + It 'looks up a Field on a UiGroups entry' { + Get-Translation -Key 'SearchIcon' -Field 'Label' -Lang $script:EnLang | Should -Be 'Taskbar search style' + } + + It 'returns the key itself when no language has it' { + Get-Translation -Key 'NoSuchKeyAnywhere' -Lang $script:EnLang | Should -Be 'NoSuchKeyAnywhere' + } + + It 'returns the key itself when the field is missing on an entry that exists' { + Get-Translation -Key 'DisableTelemetry' -Field 'UndoLabel' -Lang $script:EnLang | Should -Be 'DisableTelemetry' + } + + It 'falls back to en-US for a key missing from the active language' { + Get-Translation -Key 'TitleBarOptions' -Lang $script:EsLang | Should -Be 'Options' + } + + It 'prefers the active language over the fallback when both have the key' { + Get-Translation -Key 'TitleBarClose' -Lang $script:EsLang | Should -Be 'Cerrar' + } + + It 'resolves a plural-suffixed key by count, falling back to the bare key' { + Get-Translation -Key 'AppsSelectedCount' -Count 1 -Lang $script:EnLang | Should -Be '{0} app selected' + Get-Translation -Key 'AppsSelectedCount' -Count 5 -Lang $script:EnLang | Should -Be '{0} apps selected' + } + + It 'substitutes FormatArgs into the resolved string' { + Get-Translation -Key 'AppsSelectedCount' -Count 1 -FormatArgs @(1) -Lang $script:EnLang | Should -Be '1 app selected' + Get-Translation -Key 'AppsSelectedCount' -Count 5 -FormatArgs @(5) -Lang $script:EnLang | Should -Be '5 apps selected' + } + + It 'substitutes a zero FormatArgs value instead of treating the array as falsy' { + Get-Translation -Key 'AppsSelectedCount' -Count 0 -FormatArgs @(0) -Lang $script:EnLang | Should -Be '0 apps selected' + } + + It 'returns the unformatted string instead of throwing when FormatArgs is empty' { + Get-Translation -Key 'AppsSelectedCount' -Count 1 -FormatArgs @() -Lang $script:EnLang | Should -Be '{0} app selected' + } + + It 'warns and returns the unformatted string instead of throwing on a placeholder/argument count mismatch' { + Mock Write-Warning {} + + $result = Get-Translation -Key 'TooManyPlaceholders' -FormatArgs @('only one arg') -Lang $script:EnLang + + $result | Should -Be 'Value is {0} and {1}' + Should -Invoke Write-Warning -ParameterFilter { $Message -match 'TooManyPlaceholders' } -Times 1 -Exactly + } +} + +Describe 'Test-LanguageKeyCoverage' { + It 'reports no missing or extra keys when a language is compared against itself' { + $coverage = Test-LanguageKeyCoverage -LanguageCode 'en-US' + + $coverage.MissingKeys | Should -BeNullOrEmpty + $coverage.ExtraKeys | Should -BeNullOrEmpty + } + + It 'flags every key missing from a partially translated language' { + $coverage = Test-LanguageKeyCoverage -LanguageCode 'es-ES' + + $coverage.MissingKeys | Should -Contain 'TitleBarOptions' + $coverage.MissingKeys | Should -Contain 'AppsSelectedCount_one' + $coverage.MissingKeys | Should -Contain 'AppsSelectedCount_other' + $coverage.MissingKeys | Should -Contain 'Features.DisableTelemetry.Label' + $coverage.MissingKeys | Should -Contain 'Features.DisableTelemetry.ToolTip' + $coverage.MissingKeys | Should -Contain 'UiGroups.SearchIcon.Label' + $coverage.MissingKeys | Should -Contain 'Categories.PrivacySuggestedContent.Label' + $coverage.ExtraKeys | Should -BeNullOrEmpty + } + + It 'does not flag a key the target language already has' { + $coverage = Test-LanguageKeyCoverage -LanguageCode 'es-ES' + + $coverage.MissingKeys | Should -Not -Contain 'TitleBarClose' + } + + It 'resolves a regional variant to its language-prefix match instead of erroring' { + $coverage = Test-LanguageKeyCoverage -LanguageCode 'es-AR' + + $coverage.ResolvedLanguageCode | Should -Be 'es-ES' + $coverage.MissingKeys | Should -Contain 'TitleBarOptions' + } +} + +Describe 'ConvertTo-LocalizedXaml' { + BeforeAll { + $script:EnLang = Import-LanguageFile -LanguageCode 'en-US' + $script:EsLang = Import-LanguageFile -LanguageCode 'es-ES' + } + + It 'substitutes every marker and XML-escapes the resolved value' { + $xaml = ' diff --git a/Schemas/BubbleHint.xaml b/Schemas/BubbleHint.xaml index 0693cc20..34cf11e9 100644 --- a/Schemas/BubbleHint.xaml +++ b/Schemas/BubbleHint.xaml @@ -23,7 +23,7 @@ Color="Black"/> diff --git a/Schemas/ImportExportConfigWindow.xaml b/Schemas/ImportExportConfigWindow.xaml index 1fd6ac1d..5fec5233 100644 --- a/Schemas/ImportExportConfigWindow.xaml +++ b/Schemas/ImportExportConfigWindow.xaml @@ -1,6 +1,6 @@ - - @@ -535,12 +535,12 @@ - + - - + + @@ -553,7 +553,7 @@ - +