Skip to content

Commit 81844d2

Browse files
committed
🚜 [refactor] Stabilize module-root discovery and implement robust localization loader
- 🚜 [refactor] Add Resolve-LocalizedMessagesFile, Import-LocalizedMessagesFromFile and Initialize-ColorScriptsLocalization to reliably locate and import Messages.psd1 (culture-folder enumeration, case-insensitive matching, ProviderPath resolution, root fallback). - ✨ [feat] Embed default English messages ($script:EmbeddedDefaultMessages) and fall back gracefully when no localized resources are found; preserve ModuleRoot and emit a warning when falling back. - 🛠️ [fix] Harden module import tracing and null handling for $moduleInfo; write detailed cs-module-root-debug.log entries for ModuleInfo, PSScriptRoot, candidate evaluation and localization import outcomes. - 🧪 [test] Add comprehensive "Localization resolution" unit tests (culture enumeration, root fallback, ConvertFrom-StringData payload import, Import-PowerShellDataFile import, error-recovery path) and initialize localization in test setup; record module root in-module scope for deterministic imports. - 🔧 [build] Bump ModuleVersion to 2025.11.02.1734 and synchronize ReleaseNotes and HelpInfo UICultureVersion entries across localized HelpInfo.xml files. - 📝 [docs] Refresh dist release notes and PowerShellGallery metadata to reflect the localization refactor and version bump. Signed-off-by: Nick2bad4u <20943337+Nick2bad4u@users.noreply.github.com>
1 parent 6ded0ac commit 81844d2

15 files changed

Lines changed: 474 additions & 122 deletions

ColorScripts-Enhanced/ColorScripts-Enhanced.psd1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
RootModule = 'ColorScripts-Enhanced.psm1'
1212

1313
# Version number of this module.
14-
ModuleVersion = '2025.11.02.1436'
14+
ModuleVersion = '2025.11.02.1734'
1515

1616
# Supported PSEditions
1717
CompatiblePSEditions = @('Desktop', 'Core')
@@ -211,7 +211,7 @@ PERFECT FOR
211211

212212
# ReleaseNotes of this module
213213
ReleaseNotes = @'
214-
Version 2025.11.02.1436:
214+
Version 2025.11.02.1734:
215215
- Enhanced caching system with OS-wide cache in AppData
216216
- 6-19x performance improvement
217217
- Cache stored in centralized location

ColorScripts-Enhanced/ColorScripts-Enhanced.psm1

Lines changed: 299 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,14 @@ $moduleInfo = $ExecutionContext.SessionState.Module
4242
$moduleRootCandidates = @()
4343
$moduleRootDebugPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'cs-module-root-debug.log'
4444
"--- Import begin: $(Get-Date -Format o) ---" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
45-
"ModuleInfo.ModuleBase = $($moduleInfo?.ModuleBase)" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
46-
"ModuleInfo.Path = $($moduleInfo?.Path)" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
45+
if ($moduleInfo) {
46+
"ModuleInfo.ModuleBase = $($moduleInfo.ModuleBase)" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
47+
"ModuleInfo.Path = $($moduleInfo.Path)" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
48+
}
49+
else {
50+
"ModuleInfo.ModuleBase = <null>" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
51+
"ModuleInfo.Path = <null>" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
52+
}
4753
"PSScriptRoot = $PSScriptRoot" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
4854

4955
if ($moduleInfo) {
@@ -131,54 +137,315 @@ foreach ($cultureName in $cultureFallback) {
131137

132138
$cultureFallback = $cultureFallbackClean
133139

134-
$script:Messages = $null
135-
$localizedDataLoaded = $false
136-
$searchedPaths = @()
140+
$script:EmbeddedDefaultMessages = ConvertFrom-StringData @'
141+
# ColorScripts-Enhanced Localized Messages
142+
# English (en-US) - Default Language
143+
144+
# Error Messages
145+
UnableToPrepareCacheDirectory = Unable to prepare cache directory '{0}': {1}
146+
FailedToParseConfigurationFile = Failed to parse configuration file at '{0}': {1}. Using defaults.
147+
UnableToResolveCachePath = Unable to resolve cache path '{0}'.
148+
ConfiguredCachePathInvalid = Configured cache path '{0}' could not be resolved. Falling back to default locations.
149+
UnableToResolveOutputPath = Unable to resolve output path '{0}'.
150+
UnableToDetermineConfigurationDirectory = Unable to determine configuration directory for ColorScripts-Enhanced.
151+
ConfigurationRootCouldNotBeResolved = Configuration root could not be resolved.
152+
UnableToResolveProfilePath = Unable to resolve profile path '{0}'.
153+
FailedToExecuteColorscript = Failed to execute colorscript '{0}': {1}
154+
FailedToBuildCacheForScript = Failed to build cache for $($selection.Name).
155+
CacheBuildFailedForScript = Cache build failed for {0}: {1}
156+
ScriptAlreadyExists = Script '{0}' already exists. Use -Force to overwrite.
157+
ProfilePathNotDefinedForScope = Profile path for scope '{0}' is not defined.
158+
ScriptPathNotFound = Script path not found.
159+
ScriptExitedWithCode = Script exited with code {0}.
160+
CacheFileNotFound = Cache file not found.
161+
NoChangesApplied = No changes applied.
162+
UnableToRetrieveFileInfo = Unable to retrieve file info for '{0}': {1}
163+
UnableToReadCacheInfo = Unable to read cache info for '{0}': {1}
164+
165+
# Warning Messages
166+
NoColorscriptsFoundMatchingCriteria = No colorscripts found matching the specified criteria.
167+
NoScriptsMatchedSpecifiedFilters = No scripts matched the specified filters.
168+
NoColorscriptsAvailableWithFilters = No colorscripts available with the specified filters.
169+
NoColorscriptsFoundInScriptsPath = No colorscripts found in $script:ScriptsPath
170+
NoScriptsSelectedForCacheBuild = No scripts selected for cache build.
171+
ScriptNotFound = Script not found: {0}
172+
ColorscriptNotFoundWithFilters = Colorscript '{0}' not found with the specified filters.
173+
CachePathNotFound = Cache path not found: {0}
174+
NoCacheFilesFound = No cache files found at {0}.
175+
ProfileUpdatesNotSupportedInRemote = Profile updates are not supported in remote sessions.
176+
ScriptSkippedByFilter = Script '{0}' does not satisfy the specified filters and will be skipped.
177+
178+
# Status Messages
179+
DisplayingColorscripts = `nDisplaying $totalCount colorscripts...
180+
CacheBuildSummary = `nCache Build Summary:
181+
FailedScripts = `nFailed scripts:
182+
TotalScriptsProcessed = `nTotal scripts processed: $totalCount
183+
DisplayingContinuously = Displaying continuously (Ctrl+C to stop)`n
184+
FinishedDisplayingAll = Finished displaying all $totalCount colorscripts!
185+
Quitting = `nQuitting...
186+
CurrentIndexOfTotal = [$currentIndex/$totalCount]
187+
FailedScriptDetails = - $($failure.Name): $($failure.StdErr)
188+
MultipleColorscriptsMatched = Multiple colorscripts matched the provided name pattern(s): {0}. Displaying '{1}'.
189+
StatusCached = Cached
190+
StatusSkippedUpToDate = Skipped (up-to-date)
191+
StatusSkippedByUser = Skipped by user
192+
StatusFailed = Failed
193+
StatusUpToDateSkipped = Up-to-date (skipped)
194+
195+
# Interactive Messages
196+
PressSpacebarToContinue = Press [Spacebar] to continue to next, [Q] to quit`n
197+
PressSpacebarForNext = Press [Spacebar] for next, [Q] to quit...
198+
199+
# Success Messages
200+
ProfileSnippetAdded = [OK] Added ColorScripts-Enhanced startup snippet to $profilePath
201+
ProfileAlreadyContainsSnippet = Profile already contains ColorScripts-Enhanced snippet.
202+
ProfileAlreadyImportsModule = Profile already imports ColorScripts-Enhanced.
203+
ModuleLoadedSuccessfully = ColorScripts-Enhanced module loaded successfully.
204+
RemoteSessionDetected = Remote session detected.
205+
ProfileAlreadyConfigured = Profile already configured.
206+
ProfileSnippetAddedMessage = ColorScripts-Enhanced profile snippet added.
207+
208+
# Help/Instruction Messages
209+
SpecifyNameToSelectScripts = Specify -Name to select scripts when -All is explicitly disabled.
210+
SpecifyAllOrNameToClearCache = Specify -All or -Name to clear cache entries.
211+
UsePassThruForDetailedResults = Use -PassThru to see detailed results`n
212+
213+
# UI Elements
214+
215+
# Miscellaneous
216+
'@
217+
218+
function Resolve-LocalizedMessagesFile {
219+
param(
220+
[Parameter(Mandatory)][string]$BaseDirectory,
221+
[Parameter(Mandatory)][string[]]$CultureFallback
222+
)
223+
224+
$messagesFileName = 'Messages.psd1'
137225

138-
foreach ($candidatePath in $moduleRootCandidates) {
139-
$searchedPaths += $candidatePath
140-
"Evaluating candidate: ${candidatePath}" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
226+
$directoryEntries = Get-ChildItem -LiteralPath $BaseDirectory -Directory -ErrorAction SilentlyContinue
227+
if (-not $directoryEntries) {
228+
$directoryEntries = @()
229+
}
141230

142-
$hasLocalizedFile = $false
143-
foreach ($cultureName in $cultureFallback) {
144-
$localizedFile = Join-Path -Path $candidatePath -ChildPath (Join-Path -Path $cultureName -ChildPath 'Messages.psd1')
145-
if (Test-Path -LiteralPath $localizedFile) {
146-
$hasLocalizedFile = $true
147-
break
231+
foreach ($cultureName in $CultureFallback) {
232+
if ([string]::IsNullOrWhiteSpace($cultureName)) {
233+
continue
234+
}
235+
236+
$resolvedDirectory = $null
237+
$directCulturePath = Join-Path -Path $BaseDirectory -ChildPath $cultureName
238+
239+
if (Test-Path -LiteralPath $directCulturePath -PathType Container) {
240+
$resolvedResult = Resolve-Path -LiteralPath $directCulturePath -ErrorAction SilentlyContinue
241+
if ($resolvedResult) {
242+
$resolvedDirectory = $resolvedResult.ProviderPath
243+
}
244+
else {
245+
$resolvedDirectory = $directCulturePath
246+
}
247+
}
248+
249+
if (-not $resolvedDirectory) {
250+
$matchingDirectory = $null
251+
foreach ($entry in $directoryEntries) {
252+
if ($entry.Name.Equals($cultureName, [System.StringComparison]::OrdinalIgnoreCase)) {
253+
$matchingDirectory = $entry.FullName
254+
break
255+
}
256+
}
257+
258+
if ($matchingDirectory) {
259+
$resolvedDirectory = $matchingDirectory
260+
}
261+
}
262+
263+
if (-not $resolvedDirectory) {
264+
continue
265+
}
266+
267+
$candidateFile = Join-Path -Path $resolvedDirectory -ChildPath $messagesFileName
268+
269+
if (-not (Test-Path -LiteralPath $candidateFile -PathType Leaf)) {
270+
$filesInDirectory = Get-ChildItem -LiteralPath $resolvedDirectory -File -ErrorAction SilentlyContinue
271+
if (-not $filesInDirectory) {
272+
$filesInDirectory = @()
273+
}
274+
275+
$candidateFile = $null
276+
foreach ($fileEntry in $filesInDirectory) {
277+
if ($fileEntry.Name.Equals($messagesFileName, [System.StringComparison]::OrdinalIgnoreCase)) {
278+
$candidateFile = $fileEntry.FullName
279+
break
280+
}
281+
}
282+
}
283+
284+
if ($candidateFile -and (Test-Path -LiteralPath $candidateFile -PathType Leaf)) {
285+
$resolvedFile = $candidateFile
286+
$resolvedFilePath = Resolve-Path -LiteralPath $candidateFile -ErrorAction SilentlyContinue
287+
if ($resolvedFilePath) {
288+
$resolvedFile = $resolvedFilePath.ProviderPath
289+
}
290+
291+
return [pscustomobject]@{
292+
FilePath = $resolvedFile
293+
CultureName = $cultureName
294+
}
148295
}
149296
}
150297

151-
if (-not $hasLocalizedFile) {
152-
$rootFallback = Join-Path -Path $candidatePath -ChildPath 'Messages.psd1'
153-
if (Test-Path -LiteralPath $rootFallback) {
154-
$hasLocalizedFile = $true
155-
"Found root fallback at ${rootFallback}" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
298+
$rootFile = Join-Path -Path $BaseDirectory -ChildPath $messagesFileName
299+
300+
if (-not (Test-Path -LiteralPath $rootFile -PathType Leaf)) {
301+
$rootFiles = Get-ChildItem -LiteralPath $BaseDirectory -File -ErrorAction SilentlyContinue
302+
if (-not $rootFiles) {
303+
$rootFiles = @()
304+
}
305+
306+
$rootFile = $null
307+
foreach ($fileEntry in $rootFiles) {
308+
if ($fileEntry.Name.Equals($messagesFileName, [System.StringComparison]::OrdinalIgnoreCase)) {
309+
$rootFile = $fileEntry.FullName
310+
break
311+
}
156312
}
157313
}
158314

159-
if (-not $hasLocalizedFile) {
160-
"No localized data found under ${candidatePath}" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
161-
continue
315+
if ($rootFile -and (Test-Path -LiteralPath $rootFile -PathType Leaf)) {
316+
$resolvedRootFile = $rootFile
317+
$resolvedRootResult = Resolve-Path -LiteralPath $rootFile -ErrorAction SilentlyContinue
318+
if ($resolvedRootResult) {
319+
$resolvedRootFile = $resolvedRootResult.ProviderPath
320+
}
321+
322+
return [pscustomobject]@{
323+
FilePath = $resolvedRootFile
324+
CultureName = $null
325+
}
162326
}
163327

164-
try {
165-
Import-LocalizedData -BindingVariable "script:Messages" -FileName "Messages" -BaseDirectory $candidatePath -ErrorAction Stop
328+
return $null
329+
}
330+
331+
function Import-LocalizedMessagesFromFile {
332+
param(
333+
[Parameter(Mandatory)][string]$FilePath
334+
)
335+
336+
if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) {
337+
throw [System.IO.FileNotFoundException]::new("Localization file '$FilePath' was not found.")
338+
}
339+
340+
$rawContent = Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8 -ErrorAction Stop
341+
342+
$messagesBody = $null
343+
$match = [System.Text.RegularExpressions.Regex]::Match(
344+
$rawContent,
345+
"ConvertFrom-StringData\s*@'(?<body>.*)'@",
346+
[System.Text.RegularExpressions.RegexOptions]::Singleline
347+
)
348+
349+
if ($match.Success) {
350+
$messagesBody = $match.Groups['body'].Value
351+
}
352+
353+
if ($messagesBody) {
354+
$script:Messages = ConvertFrom-StringData -StringData $messagesBody
355+
return
356+
}
357+
358+
$data = Import-PowerShellDataFile -Path $FilePath
359+
if ($data -isnot [hashtable]) {
360+
throw [System.InvalidOperationException]::new("Localization file '$FilePath' did not return a hashtable.")
361+
}
362+
363+
$script:Messages = $data
364+
}
365+
366+
function Initialize-ColorScriptsLocalization {
367+
param(
368+
[Parameter()][string[]]$CandidateRoots,
369+
[Parameter()][string[]]$CultureFallbackOverride
370+
)
371+
372+
$pathsToEvaluate = @()
373+
if ($CandidateRoots) {
374+
$pathsToEvaluate = $CandidateRoots
375+
}
376+
377+
$fallbackCultures = if ($CultureFallbackOverride) { $CultureFallbackOverride } else { $cultureFallback }
378+
379+
$script:Messages = $null
380+
$localizedDataLoaded = $false
381+
$searchedPaths = @()
382+
383+
foreach ($candidatePath in $pathsToEvaluate) {
384+
$searchedPaths += $candidatePath
385+
"Evaluating candidate: ${candidatePath}" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
386+
387+
$resolvedLocalization = Resolve-LocalizedMessagesFile -BaseDirectory $candidatePath -CultureFallback $fallbackCultures
388+
389+
if (-not $resolvedLocalization) {
390+
"No localized data found under ${candidatePath}" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
391+
continue
392+
}
393+
394+
$importSucceeded = $true
395+
try {
396+
Import-LocalizedMessagesFromFile -FilePath $resolvedLocalization.FilePath
397+
}
398+
catch {
399+
$importSucceeded = $false
400+
"Localization import failed for ${candidatePath}: $($_.Exception.Message)" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
401+
}
402+
403+
if (-not $importSucceeded) {
404+
continue
405+
}
406+
166407
$script:ModuleRoot = $candidatePath
167408
$localizedDataLoaded = $true
168-
"Localization loaded from ${candidatePath}" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
409+
410+
$logMessage = "Resolved localization using file: $($resolvedLocalization.FilePath)"
411+
if ($resolvedLocalization.CultureName) {
412+
$logMessage += " (culture: $($resolvedLocalization.CultureName))"
413+
}
414+
415+
$logMessage | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
416+
"Localization loaded from ${candidatePath} (method: direct-file)" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
169417
break
170418
}
171-
catch {
172-
"Import-LocalizedData failed for ${candidatePath}: $($_.Exception.Message)" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
173-
continue
419+
420+
if (-not $localizedDataLoaded) {
421+
$searchedDescription = if ($searchedPaths) { ($searchedPaths -join ', ') } else { '<none>' }
422+
"Localization fallback engaged. Searched paths: $searchedDescription" | Out-File -FilePath $moduleRootDebugPath -Encoding utf8 -Append
423+
424+
$fallbackRoot = $searchedPaths | Where-Object { Test-Path -LiteralPath $_ -PathType Container } | Select-Object -First 1
425+
if (-not $fallbackRoot) {
426+
$resolvedRoot = Resolve-Path -LiteralPath $PSScriptRoot -ErrorAction SilentlyContinue
427+
if ($resolvedRoot) {
428+
$fallbackRoot = $resolvedRoot.ProviderPath
429+
}
430+
else {
431+
$fallbackRoot = $PSScriptRoot
432+
}
433+
}
434+
435+
$script:ModuleRoot = $fallbackRoot
436+
$script:Messages = $script:EmbeddedDefaultMessages.Clone()
437+
Write-Warning "Localization resources were not found. Falling back to built-in English messages."
174438
}
175-
}
176439

177-
if (-not $localizedDataLoaded) {
178-
$searchedDescription = if ($searchedPaths) { ($searchedPaths -join ', ') } else { '<none>' }
179-
throw [System.IO.FileNotFoundException]::new("Cannot find the PowerShell data file 'Messages.psd1'. Searched paths: $searchedDescription")
440+
[pscustomobject]@{
441+
LocalizedDataLoaded = $localizedDataLoaded
442+
SearchedPaths = $searchedPaths
443+
ModuleRoot = $script:ModuleRoot
444+
}
180445
}
181446

447+
$null = Initialize-ColorScriptsLocalization -CandidateRoots ($moduleRootCandidates | Select-Object -Unique)
448+
182449
$script:ScriptsPath = Join-Path -Path $script:ModuleRoot -ChildPath "Scripts"
183450
$script:MetadataPath = Join-Path -Path $script:ModuleRoot -ChildPath "ScriptMetadata.psd1"
184451

ColorScripts-Enhanced/de/ColorScripts-Enhanced_f77548d7-23eb-48ce-a6e0-f64b4758d995_HelpInfo.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<SupportedUICultures>
55
<UICulture>
66
<UICultureName>de</UICultureName>
7-
<UICultureVersion>2025.11.02.1436</UICultureVersion>
7+
<UICultureVersion>2025.11.02.1734</UICultureVersion>
88
</UICulture>
99
</SupportedUICultures>
1010
</HelpInfo>

0 commit comments

Comments
 (0)