Skip to content

Commit a7b67ab

Browse files
committed
feat(localization): ✨ Add PowerShell localization parser and tests
- Introduced `LocalizationParser.ps1` to extract localization data from psm1 files. - Added launch configurations for testing the localization parser. - Created tests for the localization parser in `LocalizationParser.tests.ps1`. - Included example localization files for both English and French.
1 parent 896ae0c commit a7b67ab

7 files changed

Lines changed: 243 additions & 17 deletions

File tree

.vscode/launch.json

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@
55
{
66
"version": "0.2.0",
77
"configurations": [
8+
{
9+
"name": "PowerShell Test LocalizationParser",
10+
"type": "PowerShell",
11+
"request": "launch",
12+
"script": "${workspaceFolder}/src/LocalizationParser.ps1",
13+
"args": [
14+
"${workspaceFolder}/tests/fixtures/Example/Example.psm1",
15+
"-Verbose",
16+
"-Debug"
17+
],
18+
"cwd": "${cwd}",
19+
"createTemporaryIntegratedConsole": true
20+
},
21+
{
22+
"name": "PowerShell Test LocalizationParser (fr-FR)",
23+
"type": "PowerShell",
24+
"request": "launch",
25+
"script": "${workspaceFolder}/src/LocalizationParser.ps1",
26+
"args": [
27+
"${workspaceFolder}/tests/fixtures/Example/Example.psm1",
28+
"-UICulture",
29+
"fr-FR",
30+
"-Verbose",
31+
"-Debug"
32+
],
33+
"cwd": "${cwd}",
34+
"createTemporaryIntegratedConsole": true
35+
},
836
{
937
"name": "Run Extension",
1038
"type": "extensionHost",
@@ -18,4 +46,4 @@
1846
"preLaunchTask": "${defaultBuildTask}"
1947
}
2048
]
21-
}
49+
}

src/LocalizationParser.ps1

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<#
2+
This script will take a list of psm1 files that have Import-LocalizedData calls
3+
and attempt to extract the call and get the relevant localization data.
4+
#>
5+
[CmdletBinding()]
6+
param(
7+
[string]$ModuleFile,
8+
[CultureInfo]
9+
$UICulture
10+
)
11+
12+
$resolvedPath = Resolve-Path $ModuleFile
13+
if ($null -eq $resolvedPath.Path) {
14+
Write-Warning "File not found: $ModuleFile"
15+
continue
16+
}
17+
$result = @{}
18+
$file = $resolvedPath.Path
19+
$parentDirectory = Split-Path -Path $file -Parent
20+
# Get path variables
21+
if (Test-Path $file) {
22+
Write-Verbose "Processing file: $file"
23+
$tokens = $null
24+
$errors = $null
25+
$scriptBlock = [System.Management.Automation.Language.Parser]::ParseFile(
26+
$file,
27+
[ref]$tokens,
28+
[ref]$errors
29+
)
30+
# Find the Import-LocalizedData calls
31+
$importLocalizedDataCalls = $scriptBlock.FindAll(
32+
{
33+
param($Ast)
34+
$Ast -is [System.Management.Automation.Language.CommandAst] -and
35+
$Ast.GetCommandName() -eq 'Import-LocalizedData'
36+
},
37+
$true
38+
)
39+
if ($importLocalizedDataCalls.Count -eq 0) {
40+
Write-Warning "No Import-LocalizedData calls found in $file"
41+
continue
42+
}
43+
44+
try {
45+
Write-Verbose "Switching to $parentDirectory"
46+
Push-Location $parentDirectory
47+
foreach ($call in $importLocalizedDataCalls) {
48+
# Here you can add logic to extract and process the localization data
49+
$splat = @{
50+
BaseDirectory = $parentDirectory
51+
}
52+
53+
# We go over each command element and extract the value
54+
$parameterName = $null
55+
foreach ($element in $call.CommandElements[1..$call.CommandElements.Count]) {
56+
Write-Verbose "Checking: $($element.Extent.Text)"
57+
switch ($element) {
58+
{ $_ -is [System.Management.Automation.Language.CommandParameterAst] } {
59+
# This is the command
60+
$parameterName = $element.Extent.Text.Trim('-')
61+
continue
62+
}
63+
{ $_ -is [System.Management.Automation.Language.CommandExpressionAst] } {
64+
# This is an expression, we can ignore it for now
65+
continue
66+
}
67+
{ $_ -is [System.Management.Automation.Language.ParameterAst] } {
68+
# This is a parameter
69+
$parameterName = $element.Extent.Text.Trim('-')
70+
continue
71+
}
72+
{ $_ -is [System.Management.Automation.Language.StringConstantExpressionAst] } {
73+
# Handle string constants
74+
# Remove quotes
75+
$splat[$parameterName] = $element.Extent.Text.Trim('"').Trim("'")
76+
continue
77+
}
78+
{ $_ -is [System.Management.Automation.Language.VariableExpressionAst] } {
79+
# Handle variables
80+
# Look through script block for the last assignment that happens before this point
81+
$lastAssignment = $scriptBlock.FindAll(
82+
{
83+
param($Ast)
84+
$Ast -is [System.Management.Automation.Language.AssignmentStatementAst] -and
85+
$Ast.Left -is [System.Management.Automation.Language.VariableExpressionAst] -and
86+
$Ast.Left.VariablePath.UserPath -eq $element.VariablePath.UserPath -and
87+
$Ast.Extent.StartLineNumber -le $element.Extent.StartLineNumber
88+
},
89+
$true
90+
) | Select-Object -Last 1
91+
if ($lastAssignment) {
92+
if ($element.Splatted) {
93+
# Append all the items of the lastAssignment to the current splat
94+
foreach ($kv in $lastAssignment.Right.Expression.KeyValuePairs) {
95+
$splat[$kv.Item1.Extent.Text] = $kv.Item2.Extent.Text.Trim('"').Trim("'")
96+
}
97+
} else {
98+
$splat[$parameterName] = $lastAssignment.Right.Extent.Text.Trim('"').Trim("'")
99+
}
100+
} else {
101+
$splat[$parameterName] = $element.Extent.Text
102+
}
103+
continue
104+
}
105+
default {
106+
# Handle other cases
107+
throw "Unhandled case: $($element.GetType().Name)"
108+
}
109+
}
110+
}
111+
# Execute the Import-LocalizedData command with the extracted arguments
112+
if ($splat.BindingVariable) {
113+
Write-Verbose "Binding variable found: $($splat.BindingVariable)"
114+
$bindingVariable = $splat.BindingVariable
115+
$splat.Remove('BindingVariable')
116+
}
117+
if ($null -ne $UICulture -and -not [String]::IsNullOrEmpty($UICulture.Name)) {
118+
$splat['UICulture'] = $UICulture.Name
119+
}
120+
Write-Verbose "Running command with splat: $($splat | ConvertTo-Json)"
121+
$data = Import-LocalizedData @splat
122+
$result[$bindingVariable] = $data
123+
}
124+
return $result | ConvertTo-Json
125+
} catch {
126+
Write-Error "Error processing ${file}: $_"
127+
throw $_
128+
} finally {
129+
# Go back to original directory
130+
Write-Verbose "Returning to original directory"
131+
Pop-Location
132+
}
133+
} else {
134+
Write-Warning "File not found: $file"
135+
}

src/extension.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,7 @@ import * as vscode from 'vscode';
55
// This method is called when your extension is activated
66
// Your extension is activated the very first time the command is executed
77
export function activate(context: vscode.ExtensionContext) {
8-
9-
// Use the console to output diagnostic information (console.log) and errors (console.error)
10-
// This line of code will only be executed once when your extension is activated
11-
console.log('Congratulations, your extension "powershelllocalization" is now active!');
12-
13-
// The command has been defined in the package.json file
14-
// Now provide the implementation of the command with registerCommand
15-
// The commandId parameter must match the command field in package.json
16-
const disposable = vscode.commands.registerCommand('powershelllocalization.helloWorld', () => {
17-
// The code you place here will be executed every time your command is executed
18-
// Display a message box to the user
19-
vscode.window.showInformationMessage('Hello World from PowerShellLocalization!');
20-
});
21-
22-
context.subscriptions.push(disposable);
238
}
249

2510
// This method is called when your extension is deactivated
26-
export function deactivate() {}
11+
export function deactivate() { }

tests/LocalizationParser.tests.ps1

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
Describe 'LocalizationParser' {
2+
BeforeAll {
3+
# Arrange
4+
$script:fixturesFolder = Join-Path -Path $PSScriptRoot -ChildPath 'fixtures'
5+
$exampleFolder = Join-Path -Path $script:fixturesFolder -ChildPath 'Example'
6+
$psm1Files = Get-ChildItem -Path $exampleFolder -Filter '*.psm1' -Recurse
7+
function Get-LocalizedText {
8+
param (
9+
[string]$ModuleFile,
10+
[string]$UICulture
11+
)
12+
& "$PSScriptRoot\..\src\LocalizationParser.ps1" -ModuleFile $ModuleFile -UICulture $UICulture
13+
}
14+
15+
# Act
16+
$script:localizedText = Get-LocalizedText -ModuleFile $psm1Files.FullName | ConvertFrom-Json -AsHashtable
17+
}
18+
It 'Can Parse the Example folder' {
19+
# Assert
20+
$script:localizedText | Should -Not -BeNullOrEmpty
21+
$script:localizedText | Should -BeOfType [hashtable]
22+
}
23+
24+
It 'Can parse the output' {
25+
$script:localizedText.ContainsKey('LocalizedData') | Should -BeTrue
26+
$script:localizedText['LocalizedData'] | Should -BeOfType [hashtable]
27+
$script:localizedText['LocalizedData']['Key1'] | Should -Be 'Value1'
28+
$script:localizedText['LocalizedData']['Key2'] | Should -Be 'Value2'
29+
$script:localizedText['LocalizedData']['Key3'] | Should -Be 'Value3'
30+
}
31+
32+
It 'Can parse the splatted version' {
33+
$script:localizedText.ContainsKey('AsSplat') | Should -BeTrue
34+
$script:localizedText['AsSplat']['Key1'] | Should -Be 'Value1'
35+
$script:localizedText['AsSplat']['Key2'] | Should -Be 'Value2'
36+
$script:localizedText['AsSplat']['Key3'] | Should -Be 'Value3'
37+
}
38+
39+
Context 'When UICulture is set' {
40+
BeforeAll {
41+
$script:frText = Get-LocalizedText -ModuleFile $psm1Files.FullName -UICulture "fr-FR" | ConvertFrom-Json -AsHashtable
42+
}
43+
44+
It 'Can parse the output in French' {
45+
$script:frText.ContainsKey('LocalizedData') | Should -BeTrue
46+
$script:frText['LocalizedData'] | Should -BeOfType [hashtable]
47+
$script:frText['LocalizedData']['Key1'] | Should -Be 'Valeur1'
48+
$script:frText['LocalizedData']['Key2'] | Should -Be 'Valeur2'
49+
$script:frText['LocalizedData']['Key3'] | Should -Be 'Valeur3'
50+
}
51+
52+
It 'Can parse the splatted version in French' {
53+
$script:frText.ContainsKey('AsSplat') | Should -BeTrue
54+
$script:frText['AsSplat']['Key1'] | Should -Be 'Valeur1'
55+
$script:frText['AsSplat']['Key2'] | Should -Be 'Valeur2'
56+
$script:frText['AsSplat']['Key3'] | Should -Be 'Valeur3'
57+
}
58+
}
59+
60+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This is a fake psm1
2+
Import-LocalizedData -FileName 'Example.psd1' -BindingVariable 'LocalizedData'
3+
4+
$withSplat = @{
5+
FileName = 'Example.psd1'
6+
BindingVariable = 'AsSplat'
7+
}
8+
Import-LocalizedData @withSplat
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ConvertFrom-StringData @'
2+
Key1 = Value1
3+
Key2 = Value2
4+
Key3 = Value3
5+
'@
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ConvertFrom-StringData @'
2+
Key1 = Valeur1
3+
Key2 = Valeur2
4+
Key3 = Valeur3
5+
'@

0 commit comments

Comments
 (0)