fix: Resolve localized strings from the shipped file on every host - #187
Merged
Conversation
PowerShellBuild.psm1 carried a hand-written second copy of every user-facing string in an inline data block, introduced with the comment "Load here in case Import-LocalizedData is not available". The copy had drifted sixteen strings behind en-US/Messages.psd1 -- every certificate and signing message was absent, along with HelpInfoUriRequired, ModuleLandingPageNotFound and ScriptAnalyzerRuleErrorRetry, and PesterVersionNotSupported still named the pre-#182 floor of 5.0.0. The copy was reachable, not dead. Import-LocalizedData runs with -ErrorAction SilentlyContinue, so a lookup that resolves nothing binds the inline copy silently. Probing a module with en-US and de-DE string files from both supported hosts shows they do not agree: pwsh 7.5 powershell.exe 5.1.26100.9168 de-AT -> en-US file de-AT -> inline copy fr-FR -> en-US file fr-FR -> inline copy ja-JP -> en-US file ja-JP -> inline copy PowerShell 7 has a final en-US fallback -- hiding the en-US directory sends fr-FR to the inline copy there too -- and Windows PowerShell 5.1 has none. Against the real module on 5.1 under fr-FR, $LocalizedData held 25 of the 41 shipped strings and reported a Pester floor of 5.0.0. Windows PowerShell 5.1 is a supported host and non-English Windows installations are common. A missing key does not throw: $null -f $arguments returns an empty string. So the symptom was Build-PSBuildUpdatableHelp emitting blank WARNING lines and the signing tasks raising a bare ScriptHalted instead of saying no certificate was found. The inline copy is deleted rather than resynchronized, because a hand-maintained duplicate will drift again, and the comment it was written under guards against nothing: Import-LocalizedData ships in Microsoft.PowerShell.Utility and has since PowerShell 3.0. en-US is now requested by name when the culture lookup binds nothing, with -ErrorAction Stop on that fallback so a module genuinely missing its strings fails at import instead of blanking every message. tests/LocalizedData.tests.ps1 pins both halves. The drift guard asserts the module file defines no string table of its own, and a child-process probe per supported host asserts that importing under a fr-FR UI culture binds every shipped string, key for key. Against the unfixed module the drift guard failed and the Windows PowerShell leg reported 25 keys against 41 expected; the PowerShell 7 leg passed either way, which is exactly why the 5.1 leg is there. Closes #185 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE
…PowerShell Select-Object -Unique compares case-sensitively, so the CI leg that runs the suite under Windows PowerShell 5.1 probed powershell.EXE and powershell.exe as two hosts -- the same executable spelled two ways by the process path and the PATH lookup. Sort-Object -Unique collapses them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE
4 tasks
Publish-PSBuildModule validates -Path against $LocalizedData.PathDoesNotExist in a ValidateScript on a mandatory parameter. en-US/Messages.psd1 never defined that key, so the lookup returned $null, $null -f $argument is an empty string rather than an error, and passing a path that does not exist failed validation with no text at all -- while the very next check in the same script block reported itself properly. Unlike the drift this branch already fixes, that one was not confined to Windows PowerShell on a non-English machine. A key the file never defines is missing on every host and every culture. The new assertion is the other half of the promise this branch makes. The existing guard keeps a second copy of the strings from existing; this one keeps the single copy complete, by scanning the module source for every $LocalizedData reference and requiring Messages.psd1 to define it. The sweep found exactly one missing key and no unused ones, and it is worth having standing: #194 is adding a new string right now. Caught in review of #187. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE
This was referenced Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PowerShellBuild.psm1kept in aninline
data LocalizedData { ConvertFrom-StringData ... }block, leavingen-US/Messages.psd1as the only place a string is written.
Import-LocalizedDataforen-USby name when the UI-culture lookup binds nothing, with-ErrorAction Stopon that fallback so a module genuinely missing its strings fails at importinstead of blanking every message.
tests/LocalizedData.tests.ps1, which pins both the single source of truth and thebehavior under a non-English UI culture on each supported host.
Closes #185.
The copy was reachable, not dead
Import-LocalizedDataruns with-ErrorAction SilentlyContinue, so a lookup that resolves nothingbinds the inline copy and says nothing about it. The two supported hosts do not agree about when
that lookup misses. Probe module with
en-US/Messages.psd1(Source=en-US-file) andde-DE/Messages.psd1(Source=de-DE-file) beside a script that seeds$LocalizedDatawithSource=inline-fallbackand then runs the same splat the module used:PowerShell 7 has a final
en-USfallback: hide theen-USdirectory andfr-FRlands oninline-fallbackthere too. Windows PowerShell 5.1 has no such fallback -- it walks the culture'sown parent chain and stops.
Against the real module on Windows PowerShell 5.1:
Sixteen of forty-one strings missing, and
PesterVersionNotSupportedstill naming the pre-#182floor.
PowerShellVersionis'5.1'withCompatiblePSEditions = @('Desktop', 'Core'), andnon-English Windows installations are common, so this is reachable rather than theoretical.
One correction to the original report: a missing key does not throw.
$null -f $argumentsreturnsan empty string on both hosts. The symptom was quieter and harder to diagnose -- a blank
WARNING:line fromBuild-PSBuildUpdatableHelp, and a bareScriptHaltedwherethrow $LocalizedData.NoCertificateFoundinIB.tasks.ps1meant to explain that no signingcertificate was found.
en-US/Messages.psd1itself was never the problem: the rootBuildtask copies the whole sourcetree, and
Output/PowerShellBuild/0.8.2/en-US/Messages.psd1is present after a build. Only thelookup missed.
Why delete rather than resynchronize
Syncing the inline block and adding a text-comparison test would leave the duplication in place,
and a duplicate that a test keeps honest still has to be edited twice for every new string. The
comment it was written under -- "in case
Import-LocalizedDatais not available" -- guards againstnothing:
Import-LocalizedDatais inMicrosoft.PowerShell.Utilityand has shipped sincePowerShell 3.0, well below this module's 5.1 floor.
Deleting it alone would have left Windows PowerShell 5.1 on a non-English machine with no strings
at all, so the explicit
en-USfallback replaces it. That fallback carries-ErrorAction Stop,which is the "fail loudly" half: a module whose strings really are missing now fails at import
rather than emitting empty messages for the rest of the build.
Red before green
Invoke-Pester -Path ./tests/LocalizedData.tests.ps1against the unfixed module:After the fix, same command:
The PowerShell 7 leg passes either way, which is exactly why the Windows PowerShell leg is there.
The drift guard is the part that fails on every platform, so a future inline copy is caught on
Linux and macOS runners too.
Test Plan
./build.ps1 -Task Teston Windows: 490 passed, 0 failed, 3 skipped (483/0/3 onmain,plus the 7 new tests; the Windows PowerShell leg contributes 3 of them and is absent on
Linux and macOS, where the file contributes 4).
Analyzeclean against the built module..psm1rather than the manifest, so it does not depend onRequiredModulesresolving against a child process's inheritedPSModulePath-- which iswhat a Windows PowerShell child spawned from PowerShell 7 gets, and has nothing to do with
which strings were bound.
Breaking Changes
None. Consumers on an English UI culture see no change; consumers on a non-English Windows
PowerShell 5.1 host stop getting stale and missing messages.
🤖 Generated with Claude Code
https://claude.ai/code/session_01U1Jhu7fgTRJq7LK5MuKteE