Files
wehub-resource-sync 643e9f9fcb
.NET Tests / test-codebase (push) Waiting to run
Translation Validation / validate-translations (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:01:52 +08:00

588 lines
16 KiB
PowerShell

[CmdletBinding()]
param(
[ValidateSet('Table', 'Json', 'Markdown')]
[string]$OutputFormat = 'Table',
[switch]$IncludeEnglish,
[switch]$OnlyIncomplete,
[string]$OutputPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot 'Languages\LanguageData.psm1') -Force
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
function Read-LanguageMap {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -Path $Path -PathType Leaf)) {
return (New-OrderedStringMap)
}
return (Read-OrderedJsonMap -Path $Path)
}
function Get-LanguageMapSections {
param(
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$Map
)
$sections = Split-TranslationMapAtBoundary -Map $Map
$fullMap = Join-TranslationMapWithBoundary -ActiveMap $sections.ActiveMap -LegacyMap $sections.LegacyMap
return [pscustomobject]@{
HasBoundary = $sections.HasBoundary
FullMap = $fullMap
ActiveMap = $sections.ActiveMap
LegacyMap = $sections.LegacyMap
}
}
function Get-PercentageNumber {
param(
[AllowNull()]
[object]$Value
)
if ($null -eq $Value) {
return $null
}
$text = [string]$Value
if ([string]::IsNullOrWhiteSpace($text)) {
return $null
}
$trimmed = $text.Trim()
if ($trimmed.EndsWith('%', [System.StringComparison]::Ordinal)) {
$trimmed = $trimmed.Substring(0, $trimmed.Length - 1)
}
$result = 0
if ([int]::TryParse($trimmed, [ref]$result)) {
return $result
}
return $null
}
function Get-CompletionPercentage {
param(
[int]$Completed,
[int]$Total
)
if ($Total -le 0) {
return 0
}
if ($Completed -ge $Total) {
return 100
}
$rounded = [int][Math]::Round(($Completed / $Total) * 100, 0, [MidpointRounding]::AwayFromZero)
return [Math]::Min($rounded, 99)
}
function Test-KnownPlaceholderLocale {
param(
[Parameter(Mandatory = $true)]
[string]$LanguageCode
)
return $false
}
function Test-IntentionalSourceEqualValue {
param(
[Parameter(Mandatory = $true)]
[string]$LanguageCode,
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$SourceValue,
[Parameter(Mandatory = $false)]
[AllowEmptyString()]
[string]$TargetValue = ''
)
if ($SourceValue -in @(
'MSI',
'MSIX',
'OK',
'UniGetUI',
'UniGetUI - {0} {1}',
'your@email.com',
'{0}: {1}',
'{0}: {1}, {2}'
)) {
return $true
}
if ($LanguageCode -ceq 'it' -and $SourceValue -ceq 'No' -and $TargetValue -ceq 'No') {
return $true
}
if ($LanguageCode -ceq 'ca' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'1 - Errors',
'Error',
'Global',
'Local',
'Manifest',
'Manifests',
'No',
'Notes:',
'Text'
)) {
return $true
}
if ($LanguageCode -ceq 'pt_PT' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Global',
'Local'
)) {
return $true
}
if ($LanguageCode -ceq 'hr' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Manifest',
'Status',
'URL'
)) {
return $true
}
if ($LanguageCode -ceq 'es' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Error',
'Global',
'Local',
'No',
'URL'
)) {
return $true
}
if ($LanguageCode -ceq 'es-MX' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Error',
'Global',
'Local',
'No',
'URL',
'Url'
)) {
return $true
}
if ($LanguageCode -ceq 'fr' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Ascendant',
'Descendant',
'Global',
'Local',
'Portable',
'Source',
'Sources',
'Verbose',
'Version',
'installation',
'option',
'version {0}',
'{0} minutes'
)) {
return $true
}
if ($LanguageCode -ceq 'nl' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'1 week',
'Filters',
'Manifest',
'Status',
'Updates',
'URL',
'update',
'website',
'{0} status'
)) {
return $true
}
if ($LanguageCode -ceq 'sk' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Manifest',
'Text'
)) {
return $true
}
if ($LanguageCode -ceq 'de' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Global',
'Manifest',
'Name',
'Repository',
'Start',
'Status',
'Text',
'Updates',
'URL',
'Verbose',
'Version',
'Version:',
'optional',
'{package} Installation',
'{package} Update'
)) {
return $true
}
if ($LanguageCode -ceq 'sv' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Global',
'Manifest',
'Start',
'Status',
'Text',
'Version',
'Version:',
'installation',
'version {0}',
'{0} installation',
'{0} status',
'{pm} version:'
)) {
return $true
}
if ($LanguageCode -ceq 'id' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Global',
'Grid',
'Manifest',
'Status',
'URL',
'Verbose',
'{0} status'
)) {
return $true
}
if ($LanguageCode -ceq 'fil' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
'Android Subsystem',
'Default',
'Error',
'Global',
'Grid',
'Machine | Global',
'Manifest',
'OK',
'Ok',
'Package',
'Password',
'Portable',
'Portable mode',
'PreRelease',
'Repository',
'Source',
'Source:',
'Telemetry',
'Text',
'URL',
'UniGetUI',
'UniGetUI - {0} {1}',
'User',
'Username',
'Verbose',
'website',
'library'
)) {
return $true
}
return $false
}
. (Join-Path $PSScriptRoot 'Languages\IntentionalSourceEqualValues.ps1')
function Get-TranslationStatusRow {
param(
[Parameter(Mandatory = $true)]
[string]$LanguageCode,
[Parameter(Mandatory = $true)]
[string]$LanguageName,
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$NeutralMap,
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$NeutralLegacyMap,
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$TargetMap,
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$TargetLegacyMap,
[AllowNull()]
[Nullable[int]]$StoredPercentage,
[Parameter(Mandatory = $true)]
[bool]$HasFile,
[Parameter(Mandatory = $true)]
[bool]$HasBoundary
)
$totalKeys = $NeutralMap.Count
$missingKeys = 0
$emptyKeys = 0
$sourceEqualKeys = 0
$translatedKeys = 0
$extraKeys = 0
foreach ($entry in $NeutralMap.GetEnumerator()) {
$key = [string]$entry.Key
$sourceValue = if ($null -eq $entry.Value) { '' } else { [string]$entry.Value }
if (-not $TargetMap.Contains($key)) {
$missingKeys += 1
continue
}
$targetValue = if ($null -eq $TargetMap[$key]) { '' } else { [string]$TargetMap[$key] }
if ([string]::IsNullOrWhiteSpace($targetValue)) {
$emptyKeys += 1
continue
}
if ($LanguageCode -ne 'en' -and $targetValue -ceq $sourceValue) {
if (Test-IntentionalSourceEqualValue -LanguageCode $LanguageCode -SourceValue $sourceValue -TargetValue $targetValue) {
$translatedKeys += 1
continue
}
$sourceEqualKeys += 1
continue
}
$translatedKeys += 1
}
$knownKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
foreach ($key in $NeutralMap.Keys) {
[void]$knownKeys.Add([string]$key)
}
foreach ($key in $NeutralLegacyMap.Keys) {
[void]$knownKeys.Add([string]$key)
}
foreach ($key in $TargetMap.Keys) {
if (-not $knownKeys.Contains([string]$key)) {
$extraKeys += 1
}
}
$isKnownPlaceholder = Test-KnownPlaceholderLocale -LanguageCode $LanguageCode
$sourceEqualThreshold = if ($totalKeys -le 0) {
0
}
else {
[int][Math]::Max(
[double]($totalKeys - 5),
[Math]::Ceiling($totalKeys * 0.98)
)
}
$potentialFallbackRegression = $HasFile -and -not $isKnownPlaceholder -and $sourceEqualKeys -ge $sourceEqualThreshold -and $translatedKeys -le 5
$completion = Get-CompletionPercentage -Completed $translatedKeys -Total $totalKeys
$storedText = if ($null -eq $StoredPercentage) { '' } else { '{0}%' -f $StoredPercentage }
$delta = if ($null -eq $StoredPercentage) { $null } else { $completion - $StoredPercentage }
return [pscustomobject]@{
Code = $LanguageCode
Language = $LanguageName
HasFile = $HasFile
HasBoundary = $HasBoundary
TotalKeys = $totalKeys
ActiveKeys = $totalKeys
Legacy = $TargetLegacyMap.Count
Translated = $translatedKeys
Missing = $missingKeys
Empty = $emptyKeys
SourceEqual = $sourceEqualKeys
Untranslated = $missingKeys + $emptyKeys + $sourceEqualKeys
Extra = $extraKeys
Completion = '{0}%' -f $completion
CompletionValue = $completion
Stored = $storedText
StoredValue = $StoredPercentage
Delta = $delta
KnownPlaceholder = $isKnownPlaceholder
PotentialFallbackRegression = $potentialFallbackRegression
}
}
function Convert-RowsToMarkdown {
param(
[Parameter(Mandatory = $true)]
[object[]]$Rows
)
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add('| Code | Language | Completion | Stored | Delta | Translated | Missing | Empty | Source Equal | Extra |')
$lines.Add('| :-- | :-- | --: | --: | --: | --: | --: | --: | --: | --: |')
foreach ($row in $Rows) {
$deltaText = if ($null -eq $row.Delta) { '' } elseif ($row.Delta -gt 0) { '+{0}' -f $row.Delta } else { [string]$row.Delta }
$storedText = if ([string]::IsNullOrWhiteSpace([string]$row.Stored)) { '' } else { [string]$row.Stored }
$lines.Add("| $($row.Code) | $($row.Language) | $($row.Completion) | $storedText | $deltaText | $($row.Translated) | $($row.Missing) | $($row.Empty) | $($row.SourceEqual) | $($row.Extra) |")
}
return ($lines -join [Environment]::NewLine)
}
function Write-OutputContent {
param(
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$Content
)
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
Write-Output $Content
return
}
$resolvedOutputPath = [System.IO.Path]::GetFullPath($OutputPath)
$directory = Split-Path -Path $resolvedOutputPath -Parent
if (-not [string]::IsNullOrWhiteSpace($directory)) {
New-Item -Path $directory -ItemType Directory -Force | Out-Null
}
$encoding = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($resolvedOutputPath, $Content + [Environment]::NewLine, $encoding)
Write-Output "Wrote translation status summary to: $resolvedOutputPath"
}
function Get-OverviewLines {
param(
[Parameter(Mandatory = $true)]
[object[]]$Rows
)
if ($Rows.Count -eq 0) {
return @('Languages: 0', 'Incomplete: 0', 'Fully translated: 0')
}
$incompleteCount = @($Rows | Where-Object { $_.Untranslated -gt 0 }).Count
$completeCount = @($Rows | Where-Object { $_.Untranslated -eq 0 }).Count
$totalUntranslated = ($Rows | Measure-Object -Property Untranslated -Sum).Sum
$totalMissing = ($Rows | Measure-Object -Property Missing -Sum).Sum
$totalEmpty = ($Rows | Measure-Object -Property Empty -Sum).Sum
$totalSourceEqual = ($Rows | Measure-Object -Property SourceEqual -Sum).Sum
$placeholderRows = @($Rows | Where-Object { $_.KnownPlaceholder })
$potentialRegressionRows = @($Rows | Where-Object { $_.PotentialFallbackRegression })
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add(('Languages: {0}' -f $Rows.Count))
$lines.Add(('Incomplete: {0}' -f $incompleteCount))
$lines.Add(('Fully translated: {0}' -f $completeCount))
$lines.Add(('Outstanding entries: {0} (missing {1}, empty {2}, source-equal {3})' -f $totalUntranslated, $totalMissing, $totalEmpty, $totalSourceEqual))
if ($placeholderRows.Count -gt 0) {
$lines.Add(('Known placeholders: {0}' -f (($placeholderRows | ForEach-Object { $_.Code }) -join ', ')))
}
if ($potentialRegressionRows.Count -gt 0) {
$lines.Add(('Potential fallback regressions: {0}' -f (($potentialRegressionRows | ForEach-Object { $_.Code }) -join ', ')))
}
return $lines.ToArray()
}
$languagesDirectory = Get-LanguagesDirectoryPath
$neutralPath = Join-Path $languagesDirectory 'lang_en.json'
if (-not (Test-Path -Path $neutralPath -PathType Leaf)) {
throw "Neutral language file not found: $neutralPath"
}
$neutralSections = Get-LanguageMapSections -Map (Read-LanguageMap -Path $neutralPath)
$neutralMap = $neutralSections.ActiveMap
$neutralLegacyMap = $neutralSections.LegacyMap
$languageReference = Get-LanguageReference
$storedPercentages = Get-TranslatedPercentages
$rows = New-Object System.Collections.Generic.List[object]
foreach ($entry in $languageReference.GetEnumerator()) {
$languageCode = [string]$entry.Key
if ($languageCode -eq 'default') {
continue
}
if (-not $IncludeEnglish.IsPresent -and $languageCode -eq 'en') {
continue
}
$languageFilePath = Join-Path $languagesDirectory ("lang_{0}.json" -f $languageCode)
$hasFile = Test-Path -Path $languageFilePath -PathType Leaf
$targetSections = if ($hasFile) { Get-LanguageMapSections -Map (Read-LanguageMap -Path $languageFilePath) } else { [pscustomobject]@{ HasBoundary = $false; FullMap = (New-OrderedStringMap); ActiveMap = (New-OrderedStringMap); LegacyMap = (New-OrderedStringMap) } }
$storedPercentage = if ($storedPercentages.Contains($languageCode)) { Get-PercentageNumber -Value $storedPercentages[$languageCode] } elseif ($languageCode -eq 'en') { 100 } else { $null }
$row = Get-TranslationStatusRow -LanguageCode $languageCode -LanguageName ([string]$entry.Value) -NeutralMap $neutralMap -NeutralLegacyMap $neutralLegacyMap -TargetMap $targetSections.FullMap -TargetLegacyMap $targetSections.LegacyMap -StoredPercentage $storedPercentage -HasFile:$hasFile -HasBoundary:$targetSections.HasBoundary
if ($OnlyIncomplete.IsPresent -and $row.Untranslated -eq 0) {
continue
}
$rows.Add($row)
}
$orderedRows = @(
$rows |
Sort-Object @{ Expression = 'CompletionValue'; Descending = $false }, @{ Expression = 'Code'; Descending = $false }
)
switch ($OutputFormat) {
'Json' {
$json = if ($orderedRows.Count -eq 0) {
'[]'
}
else {
$orderedRows | ConvertTo-Json -Depth 5
}
Write-OutputContent -Content $json
}
'Markdown' {
$overview = Get-OverviewLines -Rows $orderedRows
$markdownLines = @(
'## Translation Status Overview',
''
) + @($overview | ForEach-Object { '- ' + $_ }) + @(
'',
(Convert-RowsToMarkdown -Rows $orderedRows)
)
$markdown = $markdownLines -join [Environment]::NewLine
Write-OutputContent -Content $markdown
}
default {
$overview = Get-OverviewLines -Rows $orderedRows
$tableText = $orderedRows |
Select-Object Code, Language, Completion, Stored, Delta, Translated, Missing, Empty, SourceEqual, Extra |
Format-Table -AutoSize |
Out-String
$content = (@(
$overview
) + @(
'',
$tableText.TrimEnd()
)) -join [Environment]::NewLine
Write-OutputContent -Content $content
}
}