Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ utfx = { version = "0.1" }
uuid = { version = "1.18", features = ["v4"] }
# dsc-lib, dsc-lib-jsonschema
url = { version = "2.5" }
# dsc-lib
# dsc-lib, dsc-lib-jsonschema
urlencoding = { version = "2.1" }
# dsc-lib
which = { version = "8.0" }
Expand Down
190 changes: 190 additions & 0 deletions build.helpers.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,16 @@ function Install-PowerShellTestPrerequisite {
Write-Verbose "Installing module 'Pester'"
Install-PSResource Pester -WarningAction Ignore -Repository $repository -TrustRepository
}

if (-not (Get-Module -ListAvailable -Name YaYaml)) {
Write-Verbose "Installing module 'YaYaml'"
Install-PSResource YaYaml -WarningAction Ignore -Repository $repository -TrustRepository
}

if (-not (Get-Module -ListAvailable -Name PSToml)) {
Write-Verbose "Installing module 'PSToml'"
Install-PSResource PSToml -WarningAction Ignore -Repository $repository -TrustRepository
}
}
}

Expand Down Expand Up @@ -1539,6 +1549,186 @@ function Export-RustDocs {
}
#endregion Documenting project functions

#region Rust i18n type definitions and functions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used anywhere outside of the i18n tests? If not, would prefer to keep that code together in the test ps1 rather than creating a dependency on the helper module for that test

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put it here for easier access to checking translations during normal development workflow, since the i18n tests are in the dsc project - when working locally I was testing against the dsc-lib-jsonschema and dsc-lib crates independently.

Happy to move it though if that's preferable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My preference would be to move it since someone reading this in the future wouldn't be aware of the separation of the code. If it was used across multiple scripts, then it would make more sense to me to have it centralized here.

class DscProjectRustTranslationInfo {
[System.Collections.Generic.Dictionary[string, string]] $Table
[System.Collections.Generic.HashSet[string]] $DuplicateTranslations
[System.Collections.Generic.HashSet[string]] $MissingTranslations
[System.Collections.Generic.HashSet[string]] $UnusedTranslations
[System.Collections.Generic.HashSet[string]] $UsedTranslations

[string] GetTranslationString([string]$translationKey) {
if ($null -eq $this.Table) {
$this.Initialize()
}
return $this.Table[$translationKey]
}

[void] Initialize() {
if ($null -ne $this.Table) {
return
}
$this.Table = [System.Collections.Generic.Dictionary[string, string]]::new()
}

[void] ProcessData(
[System.Collections.Specialized.OrderedDictionary]$data,
[string]$prefix,
[int]$version
) {
foreach ($key in $data.Keys) {
$keyData = $data[$key]
$workingKey = @($prefix, $key) -join '.'

if ($keyData -is [System.Collections.Specialized.OrderedDictionary]) {
$this.ProcessData($keyData, $workingKey, $version)
} elseif ($keyData -is [string]) {
if ($key -match '^[a-zA-Z]+-[a-zA-Z]+' -and $version -eq 2) {
if ($key -eq 'en-us') {
if (-not [string]::IsNullOrEmpty($this.Table[$prefix])) {
$this.DuplicateTranslations.Add($prefix)
}
$this.Table[$prefix] = $keyData
}
continue
}

if (-not [string]::IsNullOrEmpty($this.Table[$workingKey])) {
$this.DuplicateTranslations.Add($workingKey)
}
$this.Table[$workingKey] = $keyData
}
}
}

[void] LoadData([System.Collections.Specialized.OrderedDictionary]$data) {
$this.Initialize()

[ValidateRange(1,2)][int]$version = 1
if ($data['_version'] -is [int]) {
$version = $data['_version']
}

foreach ($key in $data.Keys) {
if ($key -eq '_version') {
continue
}
$keyData = $data[$key]

if ($keyData -is [string]) {
if (-not [string]::IsNullOrEmpty($this.Table[$key])) {
$this.DuplicateTranslations.Add($key)
}
$this.Table[$key] = $keyData
} elseif ($keyData -is [System.Collections.Specialized.OrderedDictionary]) {
$this.ProcessData($keyData, $key, $version)
}
}
}

[void] LoadFile([System.IO.FileInfo]$file) {
$content = Get-Content -Path $file.FullName -Raw
$extension = $file.Extension.Substring(1)

$fileData = switch ($extension) {
'toml' {
$content | PSToml\ConvertFrom-Toml
break
}
'yaml' {
$content | YaYaml\ConvertFrom-Yaml
break
}
default {
throw "Unsupported translation file format '$extension' - must be TOML or YAML."
}
}

$this.LoadData($fileData)
}

[void] CheckTranslations([System.IO.DirectoryInfo]$projectFolder) {
$this.UsedTranslations = Get-TranslationKey -ProjectDirectory $projectFolder
$definedKeys = [System.Collections.Generic.HashSet[string]]$this.Table.Keys
$this.MissingTranslations = $this.UsedTranslations.Where({ $_ -notin $definedKeys })
$this.UnusedTranslations = $definedKeys.Where({ $_ -notin $this.UsedTranslations })
}

DscProjectRustTranslationInfo([System.Collections.Specialized.OrderedDictionary]$data) {
$this.LoadData($data)
}

DscProjectRustTranslationInfo([System.IO.FileInfo]$file) {
$this.LoadFile($file)
}
DscProjectRustTranslationInfo([System.IO.DirectoryInfo]$directory) {
$localesFolder = if ($directory.BaseName -eq 'locales') {
$directory
} else {
Join-Path -Path $directory -ChildPath 'locales'
}
$projectFolder = Split-Path -Path $localesFolder -Parent

if (-not (Test-Path $localesFolder)) {
throw "Unable to find valid locales folder from in directory '$directory'"
}

$tomlFile = Join-Path -Path $localesFolder -ChildPath 'en-us.toml'
if (Test-Path -Path $tomlFile) {
$this.LoadFile((Get-Item -Path $tomlFile))
}
$yamlFiles = Get-ChildItem -Path $localesFolder | Where-Object Extension -match 'ya?ml'
foreach ($yamlFile in $yamlFiles) {
$this.LoadFile($yamlFile)
}

$this.CheckTranslations($projectFolder)
}
}

function Get-TranslationKey {
[cmdletbinding()]
[OutputType([string[]])]
param(
[Parameter(Mandatory)]
[string]$ProjectDirectory
)

begin {
$patterns = @{
t = '(?s)\bt\!\(\s*"(?<key>.*?)".*?\)'
panic_t = '(?s)\bpanic_t\!\(\s*"(?<key>.*?)".*?\)'
assert_t = '(?s)\bassert_t\!\(\s*.*?,\s*"(?<key>.*?)".*?\)'
}
[string[]]$keys = @()
}

process {
if (-not (Test-Path $ProjectDirectory -PathType Container)) {
throw "Invalid target, '$ProjectDirectory' isn't a directory or doesn't exist."
}

Get-ChildItem -Recurse -Path $ProjectDirectory -Include *.rs -File | ForEach-Object {
$file = $_
$content = Get-Content -Path $file -Raw
foreach ($pattern in $patterns.keys) {
($content | Select-String -Pattern $patterns[$pattern] -AllMatches).Matches | ForEach-Object {
if ($null -ne $_) {
$key = $_.Groups['key'].Value
$keys += $key
}
}
}
}
}

end {
$keys
}
}
#endregion Rust i18n type definitions and functions


#region Test project functions
function Test-RustProject {
[CmdletBinding()]
Expand Down
96 changes: 57 additions & 39 deletions dsc/tests/dsc_i18n.tests.ps1
Original file line number Diff line number Diff line change
@@ -1,58 +1,76 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

using module ../../build.helpers.psm1

BeforeDiscovery {
$tomls = Get-ChildItem $PSScriptRoot/../../en-us.toml -Recurse -File
# Limit the folders to recursively search for rust i18n translation strings
$rootFolders = @(
'adapters'
'dsc'
'extensions'
'grammars'
'lib'
'pal'
'resources'
'tools'
'y2j'
)
$localeFolders = $rootFolders | ForEach-Object -Process {
Get-ChildItem $PSScriptRoot/../../$_/locales -Recurse -Directory
}

$projects = @()
$tomls | ForEach-Object {
$projectName = (Split-Path $_ -Parent | Split-Path -Parent)
$projects += @{ project = $projectName; toml = $_ }
$localeFolders | ForEach-Object -Process {
$projects += @{
project = Split-Path $_ -Parent
translationInfo = [DscProjectRustTranslationInfo]::new($_)
}
}
}

Describe 'Internationalization tests' {
It 'Project <project> uses i18n strings from <toml>' -TestCases $projects {
param($project, $toml)

$i18n = [System.Collections.Hashtable]::new([System.StringComparer]::Ordinal)
$prefix = ''
Get-Content -Path $toml | ForEach-Object {
if ($_ -match '\[(?<prefix>.*?)\]') {
$prefix = $Matches['prefix']
}
elseif ($_ -match '^(?<key>\w+)\s?=\s?"(?<value>.*?)"') {
$key = $prefix + '.' + $Matches['key']
$i18n[$key] = 0
Context '<project>' -ForEach $projects {
It 'Uses translation strings' {
$check = @{
Not = $true
BeNullOrEmpty = $true
Because = "'$project' defines at least one translation file"
}
$translationInfo.UsedTranslations | Should @check
}
It 'Does not define any duplicate translation strings' {
$check = @{
BeNullOrEmpty = $true
Because = (@(
"The following translation keys are defined more than once:"
$translationInfo.DuplicateTranslations | ConvertTo-Json -Depth 2
) -join ' ')
}

$patterns = @{
t = '(?s)\bt\!\(\s*"(?<key>.*?)".*?\)'
panic_t = '(?s)\bpanic_t\!\(\s*"(?<key>.*?)".*?\)'
assert_t = '(?s)\bassert_t\!\(\s*.*?,\s*"(?<key>.*?)".*?\)'
$translationInfo.DuplicateTranslations | Should @check
}

$missing = @()
Get-ChildItem -Recurse -Path $project -Include *.rs -File | ForEach-Object {
$content = Get-Content -Path $_ -Raw
foreach ($pattern in $patterns.keys) {
($content | Select-String -Pattern $patterns[$pattern] -AllMatches).Matches | ForEach-Object {
# write-verbose -verbose "Line: $_"
if ($null -ne $_) {
$key = $_.Groups['key'].Value
if ($i18n.ContainsKey($key)) {
$i18n[$key] = 1
}
else {
$missing += $key
}
}
}
It 'Uses every defined translation string' {
$check = @{
BeNullOrEmpty = $true
Because = (@(
"The following translation keys are defined but not used:"
$translationInfo.UnusedTranslations | ConvertTo-Json -Depth 2
) -join ' ')
}
$translationInfo.UnusedTranslations | Should @check
}

$missing | Should -BeNullOrEmpty -Because "The following i18n keys are missing from $toml :`n$($missing | Out-String)"
$unused = $i18n.GetEnumerator() | Where-Object { $_.Value -eq 0 } | ForEach-Object { $_.Key }
$unused | Should -BeNullOrEmpty -Because "The following i18n keys are unused in the project:`n$($unused | Out-String)"
It 'Defines every used translation string' {
$check = @{
BeNullOrEmpty = $true
Because = (@(
"The following translation keys are used but not defined:"
$translationInfo.MissingTranslations | ConvertTo-Json -Depth 2
) -join ' ')
}
$translationInfo.MissingTranslations | Should @check
}
}
}
1 change: 1 addition & 0 deletions lib/dsc-lib-jsonschema/.clippy.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
doc-valid-idents = ["IntelliSense", ".."]
2 changes: 2 additions & 0 deletions lib/dsc-lib-jsonschema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ edition = "2024"
doctest = false # Disable doc tests by default for compilation speed

[dependencies]
jsonschema = { workspace = true }
regex = { workspace = true }
rust-i18n = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }

[dev-dependencies]
# Helps review complex comparisons, like schemas
Expand Down
Loading