Skip to content

Commit 4da8945

Browse files
(MAINT) Reimplement Rust i18n acceptance tests
Prior to this change, the i18n tests used simple string matching to search for translation keys in the `en-us.toml` file and the project Rust code. This change defines a new `DscProjectRustTranslationInfo` class and `Get-TranslationKey` function in the `dsc_i18n` acceptance tests. This change reimplements the i18n tests to use the new class and function for retrieving and analyzing translation strings for Rust projects, building in support for both TOML and YAML translation files. This is required to support the translation strings for JSON Schema annotations, which are lengthy. It enables us to keep the translations for messages in the code in the `en-us.toml` file and all schema-related translation strings in separate YAML files, where the hierarchy and multi-line strings fit more naturally. This change ensures that we can continue to accurately test our translation strings, checking: - That each project with a `locales` folder defines and uses at least one translation string. - That no translation keys are defined more than once across all translation files. - That every defined translation key is used in the Rust code. - That every translation key the Rust code references is defined in a translation file for `en-us`.
1 parent 80f7e18 commit 4da8945

2 files changed

Lines changed: 243 additions & 38 deletions

File tree

build.helpers.psm1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,16 @@ function Install-PowerShellTestPrerequisite {
644644
Write-Verbose "Installing module 'Pester'"
645645
Install-PSResource Pester -WarningAction Ignore -Repository $repository -TrustRepository
646646
}
647+
648+
if (-not (Get-Module -ListAvailable -Name YaYaml)) {
649+
Write-Verbose "Installing module 'YaYaml'"
650+
Install-PSResource YaYaml -WarningAction Ignore -Repository $repository -TrustRepository
651+
}
652+
653+
if (-not (Get-Module -ListAvailable -Name PSToml)) {
654+
Write-Verbose "Installing module 'PSToml'"
655+
Install-PSResource PSToml -WarningAction Ignore -Repository $repository -TrustRepository
656+
}
647657
}
648658
}
649659

dsc/tests/dsc_i18n.tests.ps1

Lines changed: 233 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,252 @@
22
# Licensed under the MIT License.
33

44
BeforeDiscovery {
5-
$tomls = Get-ChildItem $PSScriptRoot/../../en-us.toml -Recurse -File
6-
$projects = @()
7-
$tomls | ForEach-Object {
8-
$projectName = (Split-Path $_ -Parent | Split-Path -Parent)
9-
$projects += @{ project = $projectName; toml = $_ }
10-
}
11-
}
5+
#region Rust i18n type definitions and functions
6+
class DscProjectRustTranslationInfo {
7+
[System.Collections.Generic.Dictionary[string, string]] $Table
8+
[System.Collections.Generic.HashSet[string]] $DuplicateTranslations
9+
[System.Collections.Generic.HashSet[string]] $MissingTranslations
10+
[System.Collections.Generic.HashSet[string]] $UnusedTranslations
11+
[System.Collections.Generic.HashSet[string]] $UsedTranslations
1212

13-
Describe 'Internationalization tests' {
14-
It 'Project <project> uses i18n strings from <toml>' -TestCases $projects {
15-
param($project, $toml)
13+
[string] GetTranslationString([string]$translationKey) {
14+
if ($null -eq $this.Table) {
15+
$this.Initialize()
16+
}
17+
return $this.Table[$translationKey]
18+
}
1619

17-
$i18n = [System.Collections.Hashtable]::new([System.StringComparer]::Ordinal)
18-
$prefix = ''
19-
Get-Content -Path $toml | ForEach-Object {
20-
if ($_ -match '\[(?<prefix>.*?)\]') {
21-
$prefix = $Matches['prefix']
20+
[void] Initialize() {
21+
if ($null -ne $this.Table) {
22+
return
2223
}
23-
elseif ($_ -match '^(?<key>\w+)\s?=\s?"(?<value>.*?)"') {
24-
$key = $prefix + '.' + $Matches['key']
25-
$i18n[$key] = 0
24+
$this.Table = [System.Collections.Generic.Dictionary[string, string]]::new()
25+
}
26+
27+
[void] ProcessData(
28+
[System.Collections.Specialized.OrderedDictionary]$data,
29+
[string]$prefix,
30+
[int]$version
31+
) {
32+
foreach ($key in $data.Keys) {
33+
$keyData = $data[$key]
34+
$workingKey = @($prefix, $key) -join '.'
35+
36+
if ($keyData -is [System.Collections.Specialized.OrderedDictionary]) {
37+
$this.ProcessData($keyData, $workingKey, $version)
38+
} elseif ($keyData -is [string]) {
39+
if ($key -match '^[a-zA-Z]+-[a-zA-Z]+' -and $version -eq 2) {
40+
if ($key -eq 'en-us') {
41+
if (-not [string]::IsNullOrEmpty($this.Table[$prefix])) {
42+
$this.DuplicateTranslations.Add($prefix)
43+
}
44+
$this.Table[$prefix] = $keyData
45+
}
46+
continue
47+
}
48+
49+
if (-not [string]::IsNullOrEmpty($this.Table[$workingKey])) {
50+
$this.DuplicateTranslations.Add($workingKey)
51+
}
52+
$this.Table[$workingKey] = $keyData
53+
}
2654
}
2755
}
2856

29-
$patterns = @{
30-
t = '(?s)\bt\!\(\s*"(?<key>.*?)".*?\)'
31-
panic_t = '(?s)\bpanic_t\!\(\s*"(?<key>.*?)".*?\)'
32-
assert_t = '(?s)\bassert_t\!\(\s*.*?,\s*"(?<key>.*?)".*?\)'
57+
[void] LoadData([System.Collections.Specialized.OrderedDictionary]$data) {
58+
$this.Initialize()
59+
60+
[ValidateRange(1,2)][int]$version = 1
61+
if ($data['_version'] -is [int]) {
62+
$version = $data['_version']
63+
}
64+
65+
foreach ($key in $data.Keys) {
66+
if ($key -eq '_version') {
67+
continue
68+
}
69+
$keyData = $data[$key]
70+
71+
if ($keyData -is [string]) {
72+
if (-not [string]::IsNullOrEmpty($this.Table[$key])) {
73+
$this.DuplicateTranslations.Add($key)
74+
}
75+
$this.Table[$key] = $keyData
76+
} elseif ($keyData -is [System.Collections.Specialized.OrderedDictionary]) {
77+
$this.ProcessData($keyData, $key, $version)
78+
}
79+
}
3380
}
3481

35-
$missing = @()
36-
Get-ChildItem -Recurse -Path $project -Include *.rs -File | ForEach-Object {
37-
$content = Get-Content -Path $_ -Raw
38-
foreach ($pattern in $patterns.keys) {
39-
($content | Select-String -Pattern $patterns[$pattern] -AllMatches).Matches | ForEach-Object {
40-
# write-verbose -verbose "Line: $_"
41-
if ($null -ne $_) {
42-
$key = $_.Groups['key'].Value
43-
if ($i18n.ContainsKey($key)) {
44-
$i18n[$key] = 1
45-
}
46-
else {
47-
$missing += $key
82+
[void] LoadFile([System.IO.FileInfo]$file) {
83+
$content = Get-Content -Path $file.FullName -Raw
84+
$extension = $file.Extension.Substring(1)
85+
86+
$fileData = switch ($extension) {
87+
'toml' {
88+
$content | PSToml\ConvertFrom-Toml
89+
break
90+
}
91+
'yaml' {
92+
$content | YaYaml\ConvertFrom-Yaml
93+
break
94+
}
95+
default {
96+
throw "Unsupported translation file format '$extension' - must be TOML or YAML."
97+
}
98+
}
99+
100+
$this.LoadData($fileData)
101+
}
102+
103+
[void] CheckTranslations([System.IO.DirectoryInfo]$projectFolder) {
104+
$this.UsedTranslations = Get-TranslationKey -ProjectDirectory $projectFolder
105+
$definedKeys = [System.Collections.Generic.HashSet[string]]$this.Table.Keys
106+
$this.MissingTranslations = $this.UsedTranslations.Where({ $_ -notin $definedKeys })
107+
$this.UnusedTranslations = $definedKeys.Where({ $_ -notin $this.UsedTranslations })
108+
}
109+
110+
DscProjectRustTranslationInfo([System.Collections.Specialized.OrderedDictionary]$data) {
111+
$this.LoadData($data)
112+
}
113+
114+
DscProjectRustTranslationInfo([System.IO.FileInfo]$file) {
115+
$this.LoadFile($file)
116+
}
117+
DscProjectRustTranslationInfo([System.IO.DirectoryInfo]$directory) {
118+
$localesFolder = if ($directory.BaseName -eq 'locales') {
119+
$directory
120+
} else {
121+
Join-Path -Path $directory -ChildPath 'locales'
122+
}
123+
$projectFolder = Split-Path -Path $localesFolder -Parent
124+
125+
if (-not (Test-Path $localesFolder)) {
126+
throw "Unable to find valid locales folder from in directory '$directory'"
127+
}
128+
129+
$tomlFile = Join-Path -Path $localesFolder -ChildPath 'en-us.toml'
130+
if (Test-Path -Path $tomlFile) {
131+
$this.LoadFile((Get-Item -Path $tomlFile))
132+
}
133+
$yamlFiles = Get-ChildItem -Path $localesFolder | Where-Object Extension -match 'ya?ml'
134+
foreach ($yamlFile in $yamlFiles) {
135+
$this.LoadFile($yamlFile)
136+
}
137+
138+
$this.CheckTranslations($projectFolder)
139+
}
140+
}
141+
142+
function Get-TranslationKey {
143+
[cmdletbinding()]
144+
[OutputType([string[]])]
145+
param(
146+
[Parameter(Mandatory)]
147+
[string]$ProjectDirectory
148+
)
149+
150+
begin {
151+
$patterns = @{
152+
t = '(?s)\bt\!\(\s*"(?<key>.*?)".*?\)'
153+
panic_t = '(?s)\bpanic_t\!\(\s*"(?<key>.*?)".*?\)'
154+
assert_t = '(?s)\bassert_t\!\(\s*.*?,\s*"(?<key>.*?)".*?\)'
155+
}
156+
[string[]]$keys = @()
157+
}
158+
159+
process {
160+
if (-not (Test-Path $ProjectDirectory -PathType Container)) {
161+
throw "Invalid target, '$ProjectDirectory' isn't a directory or doesn't exist."
162+
}
163+
164+
Get-ChildItem -Recurse -Path $ProjectDirectory -Include *.rs -File | ForEach-Object {
165+
$file = $_
166+
$content = Get-Content -Path $file -Raw
167+
foreach ($pattern in $patterns.keys) {
168+
($content | Select-String -Pattern $patterns[$pattern] -AllMatches).Matches | ForEach-Object {
169+
if ($null -ne $_) {
170+
$key = $_.Groups['key'].Value
171+
$keys += $key
48172
}
49173
}
50174
}
51175
}
52176
}
53177

54-
$missing | Should -BeNullOrEmpty -Because "The following i18n keys are missing from $toml :`n$($missing | Out-String)"
55-
$unused = $i18n.GetEnumerator() | Where-Object { $_.Value -eq 0 } | ForEach-Object { $_.Key }
56-
$unused | Should -BeNullOrEmpty -Because "The following i18n keys are unused in the project:`n$($unused | Out-String)"
178+
end {
179+
$keys
180+
}
181+
}
182+
#endregion Rust i18n type definitions and functions
183+
184+
# Limit the folders to recursively search for rust i18n translation strings
185+
$rootFolders = @(
186+
'adapters'
187+
'dsc'
188+
'extensions'
189+
'grammars'
190+
'lib'
191+
'pal'
192+
'resources'
193+
'tools'
194+
'y2j'
195+
)
196+
$localeFolders = $rootFolders | ForEach-Object -Process {
197+
Get-ChildItem $PSScriptRoot/../../$_/locales -Recurse -Directory
198+
}
199+
200+
$projects = @()
201+
$localeFolders | ForEach-Object -Process {
202+
$projects += @{
203+
project = Split-Path $_ -Parent
204+
translationInfo = [DscProjectRustTranslationInfo]::new($_)
205+
}
206+
}
207+
}
208+
209+
Describe 'Internationalization tests' {
210+
Context '<project>' -ForEach $projects {
211+
It 'Uses translation strings' {
212+
$check = @{
213+
Not = $true
214+
BeNullOrEmpty = $true
215+
Because = "'$project' defines at least one translation file"
216+
}
217+
$translationInfo.UsedTranslations | Should @check
218+
}
219+
It 'Does not define any duplicate translation strings' {
220+
$check = @{
221+
BeNullOrEmpty = $true
222+
Because = (@(
223+
"The following translation keys are defined more than once:"
224+
$translationInfo.DuplicateTranslations | ConvertTo-Json -Depth 2
225+
) -join ' ')
226+
}
227+
228+
$translationInfo.DuplicateTranslations | Should @check
229+
}
230+
231+
It 'Uses every defined translation string' {
232+
$check = @{
233+
BeNullOrEmpty = $true
234+
Because = (@(
235+
"The following translation keys are defined but not used:"
236+
$translationInfo.UnusedTranslations | ConvertTo-Json -Depth 2
237+
) -join ' ')
238+
}
239+
$translationInfo.UnusedTranslations | Should @check
240+
}
241+
242+
It 'Defines every used translation string' {
243+
$check = @{
244+
BeNullOrEmpty = $true
245+
Because = (@(
246+
"The following translation keys are used but not defined:"
247+
$translationInfo.MissingTranslations | ConvertTo-Json -Depth 2
248+
) -join ' ')
249+
}
250+
$translationInfo.MissingTranslations | Should @check
251+
}
57252
}
58253
}

0 commit comments

Comments
 (0)