chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$EnglishFilePath,
|
||||
[string]$LanguagesDirectory,
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationSourceTools.ps1')
|
||||
|
||||
$resolvedRepositoryRoot = Resolve-TranslationSyncRepositoryRoot -RepositoryRoot $RepositoryRoot
|
||||
$resolvedEnglishFilePath = Resolve-EnglishLanguageFilePath -ResolvedRepositoryRoot $resolvedRepositoryRoot -EnglishFilePath $EnglishFilePath
|
||||
$resolvedLanguagesDirectory = if ([string]::IsNullOrWhiteSpace($LanguagesDirectory)) {
|
||||
Split-Path -Path $resolvedEnglishFilePath -Parent
|
||||
}
|
||||
else {
|
||||
Get-FullPath -Path $LanguagesDirectory
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $resolvedLanguagesDirectory -PathType Container)) {
|
||||
throw "Languages directory not found: $resolvedLanguagesDirectory"
|
||||
}
|
||||
|
||||
$englishMap = Read-OrderedJsonMap -Path $resolvedEnglishFilePath
|
||||
$englishSections = Split-TranslationMapAtBoundary -Map $englishMap
|
||||
if (-not $englishSections.HasBoundary) {
|
||||
Write-Output 'The English translation file does not contain a legacy boundary marker. No alignment to export.'
|
||||
return
|
||||
}
|
||||
|
||||
$englishActiveOrder = @($englishSections.ActiveMap.Keys)
|
||||
$englishLegacyOrder = @($englishSections.LegacyMap.Keys)
|
||||
$englishActiveSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($key in $englishActiveOrder) {
|
||||
[void]$englishActiveSet.Add([string]$key)
|
||||
}
|
||||
|
||||
$englishLegacySet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($key in $englishLegacyOrder) {
|
||||
[void]$englishLegacySet.Add([string]$key)
|
||||
}
|
||||
|
||||
$reports = New-Object System.Collections.Generic.List[object]
|
||||
$languageFiles = Get-ChildItem -Path $resolvedLanguagesDirectory -Filter 'lang_*.json' | Sort-Object Name
|
||||
foreach ($languageFile in $languageFiles) {
|
||||
if ($languageFile.FullName -ieq $resolvedEnglishFilePath) {
|
||||
continue
|
||||
}
|
||||
|
||||
$languageMap = Read-OrderedJsonMapPermissive -Path $languageFile.FullName
|
||||
$languageSections = Split-TranslationMapAtBoundary -Map $languageMap
|
||||
|
||||
$missingActiveKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($key in $englishActiveOrder) {
|
||||
if (-not $languageMap.Contains($key)) {
|
||||
$missingActiveKeys.Add([string]$key)
|
||||
}
|
||||
}
|
||||
|
||||
$presentActiveInExpectedOrder = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($key in $englishActiveOrder) {
|
||||
if ($languageMap.Contains($key)) {
|
||||
$presentActiveInExpectedOrder.Add([string]$key)
|
||||
}
|
||||
}
|
||||
|
||||
$currentActiveInEnglishSet = New-Object System.Collections.Generic.List[string]
|
||||
$extraActiveKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $languageSections.ActiveMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($englishActiveSet.Contains($key)) {
|
||||
$currentActiveInEnglishSet.Add($key)
|
||||
}
|
||||
elseif ($key -cne (Get-TranslationLegacyBoundaryKey)) {
|
||||
$extraActiveKeys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
$presentLegacyInExpectedOrder = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($key in $englishLegacyOrder) {
|
||||
if ($languageMap.Contains($key)) {
|
||||
$presentLegacyInExpectedOrder.Add([string]$key)
|
||||
}
|
||||
}
|
||||
|
||||
$currentLegacyInEnglishSet = New-Object System.Collections.Generic.List[string]
|
||||
$extraLegacyKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $languageSections.LegacyMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($englishLegacySet.Contains($key)) {
|
||||
$currentLegacyInEnglishSet.Add($key)
|
||||
}
|
||||
else {
|
||||
$extraLegacyKeys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
$reportEntry = [pscustomobject]@{
|
||||
languageFile = (Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $languageFile.FullName)
|
||||
hasBoundary = $languageSections.HasBoundary
|
||||
activeOrderAligned = [System.Linq.Enumerable]::SequenceEqual($currentActiveInEnglishSet.ToArray(), $presentActiveInExpectedOrder.ToArray())
|
||||
legacyOrderAligned = [System.Linq.Enumerable]::SequenceEqual($currentLegacyInEnglishSet.ToArray(), $presentLegacyInExpectedOrder.ToArray())
|
||||
activeKeyCount = $currentActiveInEnglishSet.Count
|
||||
legacyKeyCount = $currentLegacyInEnglishSet.Count
|
||||
missingActiveKeyCount = $missingActiveKeys.Count
|
||||
extraActiveKeyCount = $extraActiveKeys.Count
|
||||
extraLegacyKeyCount = $extraLegacyKeys.Count
|
||||
missingActiveKeys = $missingActiveKeys.ToArray()
|
||||
extraActiveKeys = $extraActiveKeys.ToArray()
|
||||
extraLegacyKeys = $extraLegacyKeys.ToArray()
|
||||
}
|
||||
|
||||
$reports.Add($reportEntry)
|
||||
}
|
||||
|
||||
$report = [pscustomobject]@{
|
||||
generatedAt = (Get-Date).ToString('o')
|
||||
repositoryRoot = $resolvedRepositoryRoot
|
||||
englishFile = (Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $resolvedEnglishFilePath)
|
||||
boundaryKey = Get-TranslationLegacyBoundaryKey
|
||||
englishActiveKeyCount = $englishActiveOrder.Count
|
||||
englishLegacyKeyCount = $englishLegacyOrder.Count
|
||||
languageCount = $reports.Count
|
||||
languages = $reports.ToArray()
|
||||
}
|
||||
|
||||
$resolvedOutputPath = if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||
Join-Path $resolvedRepositoryRoot 'artifacts\translation\translation-boundary-alignment.json'
|
||||
}
|
||||
else {
|
||||
Get-FullPath -Path $OutputPath
|
||||
}
|
||||
|
||||
New-Utf8File -Path $resolvedOutputPath -Content ($report | ConvertTo-Json -Depth 6)
|
||||
|
||||
Write-Output 'Translation boundary alignment summary'
|
||||
Write-Output "Repository root: $resolvedRepositoryRoot"
|
||||
Write-Output "English file: $resolvedEnglishFilePath"
|
||||
Write-Output "Language files analyzed: $($reports.Count)"
|
||||
Write-Output "English active keys: $($englishActiveOrder.Count)"
|
||||
Write-Output "English legacy keys: $($englishLegacyOrder.Count)"
|
||||
Write-Output "Report: $resolvedOutputPath"
|
||||
@@ -0,0 +1,128 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$TranslationFilePath,
|
||||
[string]$BeforeRef = 'HEAD~1',
|
||||
[string]$AfterRef = 'HEAD',
|
||||
[string]$OutputPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationSourceTools.ps1')
|
||||
|
||||
$resolvedRepositoryRoot = Resolve-TranslationSyncRepositoryRoot -RepositoryRoot $RepositoryRoot
|
||||
$resolvedTranslationFilePath = if ([string]::IsNullOrWhiteSpace($TranslationFilePath)) {
|
||||
Join-Path $resolvedRepositoryRoot 'src\Languages\lang_en.json'
|
||||
}
|
||||
else {
|
||||
Get-FullPath -Path $TranslationFilePath
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $resolvedTranslationFilePath -PathType Leaf)) {
|
||||
throw "Translation file not found: $resolvedTranslationFilePath"
|
||||
}
|
||||
|
||||
$relativePath = Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $resolvedTranslationFilePath
|
||||
|
||||
function Get-TranslationMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Ref,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepoRelativePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResolvedFilePath
|
||||
)
|
||||
|
||||
if ($Ref -in @('WORKTREE', 'CURRENT', 'FILE')) {
|
||||
return Read-OrderedJsonMap -Path $ResolvedFilePath
|
||||
}
|
||||
|
||||
$gitObject = '{0}:{1}' -f $Ref, $RepoRelativePath
|
||||
$content = & git -C $RepoRoot show $gitObject 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Unable to read '$RepoRelativePath' from git ref '$Ref'."
|
||||
}
|
||||
|
||||
return Convert-JsonContentToOrderedMap -Content ([string]::Join([Environment]::NewLine, @($content))) -Path $gitObject -DetectDuplicates
|
||||
}
|
||||
|
||||
$beforeMap = Get-TranslationMap -RepoRoot $resolvedRepositoryRoot -Ref $BeforeRef -RepoRelativePath $relativePath -ResolvedFilePath $resolvedTranslationFilePath
|
||||
$afterMap = Get-TranslationMap -RepoRoot $resolvedRepositoryRoot -Ref $AfterRef -RepoRelativePath $relativePath -ResolvedFilePath $resolvedTranslationFilePath
|
||||
|
||||
$removedKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $beforeMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if (-not $afterMap.Contains($key)) {
|
||||
$removedKeys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
$addedKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $afterMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if (-not $beforeMap.Contains($key)) {
|
||||
$addedKeys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
$changedValues = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($entry in $beforeMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($afterMap.Contains($key)) {
|
||||
$beforeValue = [string]$entry.Value
|
||||
$afterValue = [string]$afterMap[$key]
|
||||
if ($beforeValue -cne $afterValue) {
|
||||
$changedValues.Add([pscustomobject]@{
|
||||
Key = $key
|
||||
Before = $beforeValue
|
||||
After = $afterValue
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$removedKeysArray = $removedKeys.ToArray()
|
||||
$addedKeysArray = $addedKeys.ToArray()
|
||||
$changedValuesArray = $changedValues.ToArray()
|
||||
|
||||
$report = [pscustomobject]@{
|
||||
generatedAt = (Get-Date).ToString('o')
|
||||
repositoryRoot = $resolvedRepositoryRoot
|
||||
translationFile = $relativePath
|
||||
beforeRef = $BeforeRef
|
||||
afterRef = $AfterRef
|
||||
removedKeyCount = $removedKeys.Count
|
||||
addedKeyCount = $addedKeys.Count
|
||||
changedValueCount = $changedValues.Count
|
||||
removedKeys = $removedKeysArray
|
||||
addedKeys = $addedKeysArray
|
||||
changedValues = $changedValuesArray
|
||||
}
|
||||
|
||||
$resolvedOutputPath = if ([string]::IsNullOrWhiteSpace($OutputPath)) {
|
||||
Join-Path $resolvedRepositoryRoot ('artifacts\translation\{0}-{1}-to-{2}.json' -f ([IO.Path]::GetFileNameWithoutExtension($relativePath)), (Get-SafeLabel -Value $BeforeRef), (Get-SafeLabel -Value $AfterRef))
|
||||
}
|
||||
else {
|
||||
Get-FullPath -Path $OutputPath
|
||||
}
|
||||
|
||||
New-Utf8File -Path $resolvedOutputPath -Content ($report | ConvertTo-Json -Depth 6)
|
||||
|
||||
Write-Output "Translation key diff summary"
|
||||
Write-Output "Repository root: $resolvedRepositoryRoot"
|
||||
Write-Output "Translation file: $relativePath"
|
||||
Write-Output "Before ref: $BeforeRef"
|
||||
Write-Output "After ref: $AfterRef"
|
||||
Write-Output "Removed keys: $($removedKeys.Count)"
|
||||
Write-Output "Added keys: $($addedKeys.Count)"
|
||||
Write-Output "Changed values: $($changedValues.Count)"
|
||||
Write-Output "Report: $resolvedOutputPath"
|
||||
@@ -0,0 +1,587 @@
|
||||
[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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
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}',
|
||||
'WinGet COM API',
|
||||
'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 'it' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'DEBUG BUILD'
|
||||
)) {
|
||||
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',
|
||||
'Machine | Global',
|
||||
'Navigation',
|
||||
'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
|
||||
}
|
||||
|
||||
if ($LanguageCode -ceq 'tl' -and $SourceValue -ceq $TargetValue -and $SourceValue -in @(
|
||||
'Machine | Global'
|
||||
)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$script:ProjectRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
$script:LanguageRemap = [ordered]@{
|
||||
'pt-BR' = 'pt_BR'
|
||||
'pt-PT' = 'pt_PT'
|
||||
'nn-NO' = 'nn'
|
||||
'uk' = 'ua'
|
||||
'zh-Hans' = 'zh_CN'
|
||||
'zh-Hant' = 'zh_TW'
|
||||
}
|
||||
|
||||
$script:LanguageFlagsRemap = [ordered]@{
|
||||
'af' = 'za'
|
||||
'ar' = 'sa'
|
||||
'bs' = 'ba'
|
||||
'ca' = 'ad'
|
||||
'cs' = 'cz'
|
||||
'da' = 'dk'
|
||||
'el' = 'gr'
|
||||
'en' = 'gb'
|
||||
'eo' = 'https://upload.wikimedia.org/wikipedia/commons/f/f5/Flag_of_Esperanto.svg'
|
||||
'es-MX' = 'mx'
|
||||
'et' = 'ee'
|
||||
'fa' = 'ir'
|
||||
'fil' = 'ph'
|
||||
'gl' = 'es'
|
||||
'he' = 'il'
|
||||
'hi' = 'in'
|
||||
'ja' = 'jp'
|
||||
'ka' = 'ge'
|
||||
'ko' = 'kr'
|
||||
'ku' = 'iq'
|
||||
'mr' = 'in'
|
||||
'nb' = 'no'
|
||||
'nn' = 'no'
|
||||
'pt_BR' = 'br'
|
||||
'pt_PT' = 'pt'
|
||||
'si' = 'lk'
|
||||
'sr' = 'rs'
|
||||
'sv' = 'se'
|
||||
'sl' = 'si'
|
||||
'ta' = 'in'
|
||||
'vi' = 'vn'
|
||||
'zh_CN' = 'cn'
|
||||
'zh_TW' = 'tw'
|
||||
'zh' = 'cn'
|
||||
'bn' = 'bd'
|
||||
'tg' = 'ph'
|
||||
'sq' = 'al'
|
||||
'kn' = 'in'
|
||||
'sa' = 'in'
|
||||
'gu' = 'in'
|
||||
'ur' = 'pk'
|
||||
'be' = 'by'
|
||||
}
|
||||
|
||||
function Get-ProjectRoot {
|
||||
return $script:ProjectRoot
|
||||
}
|
||||
|
||||
function Get-ContributorsListPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\UniGetUI.Core.Data\Assets\Data\Contributors.list'
|
||||
}
|
||||
|
||||
function Get-TranslatorsJsonPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\Languages\Data\Translators.json'
|
||||
}
|
||||
|
||||
function Get-TranslatedPercentagesJsonPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\Languages\Data\TranslatedPercentages.json'
|
||||
}
|
||||
|
||||
function Get-LanguagesReferenceJsonPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\Languages\Data\LanguagesReference.json'
|
||||
}
|
||||
|
||||
function Get-LanguagesDirectoryPath {
|
||||
return Join-Path (Get-ProjectRoot) 'src\Languages'
|
||||
}
|
||||
|
||||
function Read-JsonDictionary {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
$content = [System.IO.File]::ReadAllText($Path)
|
||||
if ([string]::IsNullOrWhiteSpace($content)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
$parsed = $content | ConvertFrom-Json -AsHashtable
|
||||
if ($null -eq $parsed) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
if (-not ($parsed -is [System.Collections.IDictionary])) {
|
||||
throw "JSON root must be an object: $Path"
|
||||
}
|
||||
|
||||
return $parsed
|
||||
}
|
||||
|
||||
function Get-ContributorsList {
|
||||
$path = Get-ContributorsListPath
|
||||
if (-not (Test-Path -Path $path -PathType Leaf)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @(
|
||||
Get-Content -Path $path -Encoding UTF8 |
|
||||
ForEach-Object { $_.Trim() } |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
)
|
||||
}
|
||||
|
||||
function Get-LanguageCredits {
|
||||
return Read-JsonDictionary -Path (Get-TranslatorsJsonPath)
|
||||
}
|
||||
|
||||
function Get-TranslatedPercentages {
|
||||
return Read-JsonDictionary -Path (Get-TranslatedPercentagesJsonPath)
|
||||
}
|
||||
|
||||
function Get-LanguageReference {
|
||||
return Read-JsonDictionary -Path (Get-LanguagesReferenceJsonPath)
|
||||
}
|
||||
|
||||
function Get-LanguageRemap {
|
||||
return [ordered]@{} + $script:LanguageRemap
|
||||
}
|
||||
|
||||
function Get-LanguageFlagsRemap {
|
||||
return [ordered]@{} + $script:LanguageFlagsRemap
|
||||
}
|
||||
|
||||
function Get-TranslatorsFromCredits {
|
||||
param(
|
||||
[AllowNull()]
|
||||
[string]$Credits
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Credits)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$contributors = Get-ContributorsList
|
||||
$translatorLookup = @{}
|
||||
foreach ($translator in ($Credits -split ',')) {
|
||||
$trimmed = $translator.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($trimmed)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$wasPrefixed = $trimmed.StartsWith('@', [System.StringComparison]::Ordinal)
|
||||
if ($wasPrefixed) {
|
||||
$trimmed = $trimmed.Substring(1)
|
||||
}
|
||||
|
||||
$link = ''
|
||||
if ($wasPrefixed -or ($contributors -contains $trimmed)) {
|
||||
$link = "https://github.com/$trimmed"
|
||||
}
|
||||
|
||||
$translatorLookup[$trimmed] = [pscustomobject]@{
|
||||
name = $trimmed
|
||||
link = $link
|
||||
}
|
||||
}
|
||||
|
||||
return @(
|
||||
$translatorLookup.Keys |
|
||||
Sort-Object { $_.ToLowerInvariant() } |
|
||||
ForEach-Object { $translatorLookup[$_] }
|
||||
)
|
||||
}
|
||||
|
||||
function ConvertTo-TranslatorMarkdown {
|
||||
param(
|
||||
[AllowNull()]
|
||||
[object]$Translators
|
||||
)
|
||||
|
||||
if ($null -eq $Translators) {
|
||||
return ''
|
||||
}
|
||||
|
||||
$translatorItems = @()
|
||||
if ($Translators -is [string]) {
|
||||
$translatorItems = Get-TranslatorsFromCredits -Credits $Translators
|
||||
}
|
||||
else {
|
||||
$translatorItems = @($Translators)
|
||||
}
|
||||
|
||||
$formatted = foreach ($translator in $translatorItems) {
|
||||
$name = [string]$translator.name
|
||||
$link = if ($null -eq $translator.link) { '' } else { [string]$translator.link }
|
||||
if ([string]::IsNullOrWhiteSpace($link)) {
|
||||
$name
|
||||
}
|
||||
else {
|
||||
"[$name]($link)"
|
||||
}
|
||||
}
|
||||
|
||||
return ($formatted -join ', ')
|
||||
}
|
||||
|
||||
function Get-LanguageFilePathMap {
|
||||
param(
|
||||
[switch]$AbsolutePaths
|
||||
)
|
||||
|
||||
$languageReference = Get-LanguageReference
|
||||
$languagesDirectory = Get-LanguagesDirectoryPath
|
||||
$result = [ordered]@{}
|
||||
|
||||
foreach ($entry in $languageReference.GetEnumerator()) {
|
||||
$code = [string]$entry.Key
|
||||
if ($code -eq 'default') {
|
||||
continue
|
||||
}
|
||||
|
||||
$fileName = "lang_$code.json"
|
||||
$result[$code] = if ($AbsolutePaths.IsPresent) { Join-Path $languagesDirectory $fileName } else { $fileName }
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Get-MarkdownSupportLangs {
|
||||
return Get-MarkdownTranslationsTable
|
||||
}
|
||||
|
||||
function Get-MarkdownTranslationsTable {
|
||||
param(
|
||||
[switch]$IncludeZeroPercent,
|
||||
|
||||
[switch]$IncludeLanguageCode,
|
||||
|
||||
[switch]$IncludeFileColumn,
|
||||
|
||||
[bool]$IncludeTranslatedColumn = $true,
|
||||
|
||||
[bool]$IncludeCreditsColumn = $true,
|
||||
|
||||
[string]$MissingCreditsText = ''
|
||||
)
|
||||
|
||||
$languageReference = Get-LanguageReference
|
||||
$translationPercentages = Get-TranslatedPercentages
|
||||
$languageCredits = Get-LanguageCredits
|
||||
$flagRemap = Get-LanguageFlagsRemap
|
||||
$languagesDirectory = Get-LanguagesDirectoryPath
|
||||
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
$headers = @('Language')
|
||||
$alignments = @(':--')
|
||||
|
||||
if ($IncludeLanguageCode.IsPresent) {
|
||||
$headers += 'Code'
|
||||
$alignments += ':--'
|
||||
}
|
||||
|
||||
if ($IncludeTranslatedColumn) {
|
||||
$headers += 'Translated'
|
||||
$alignments += ':--'
|
||||
}
|
||||
|
||||
if ($IncludeFileColumn.IsPresent) {
|
||||
$headers += 'File'
|
||||
$alignments += ':--'
|
||||
}
|
||||
|
||||
if ($IncludeCreditsColumn) {
|
||||
$headers += 'Contributor(s)'
|
||||
$alignments += '---'
|
||||
}
|
||||
|
||||
$lines.Add('| ' + ($headers -join ' | ') + ' |')
|
||||
$lines.Add('| ' + ($alignments -join ' | ') + ' |')
|
||||
|
||||
foreach ($entry in $languageReference.GetEnumerator()) {
|
||||
$languageCode = [string]$entry.Key
|
||||
if ($languageCode -eq 'default') {
|
||||
continue
|
||||
}
|
||||
|
||||
$languageFilePath = Join-Path $languagesDirectory ("lang_$languageCode.json")
|
||||
if (-not (Test-Path -Path $languageFilePath -PathType Leaf)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$percentage = if ($translationPercentages.Contains($languageCode)) { [string]$translationPercentages[$languageCode] } else { '100%' }
|
||||
if ($percentage -eq '0%' -and -not $IncludeZeroPercent.IsPresent) {
|
||||
continue
|
||||
}
|
||||
|
||||
$languageName = [string]$entry.Value
|
||||
$flag = if ($flagRemap.Contains($languageCode)) { [string]$flagRemap[$languageCode] } else { $languageCode }
|
||||
$credits = ''
|
||||
if ($IncludeCreditsColumn) {
|
||||
$credits = if ($languageCredits.Contains($languageCode)) {
|
||||
ConvertTo-TranslatorMarkdown -Translators $languageCredits[$languageCode]
|
||||
}
|
||||
else {
|
||||
''
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($credits) -and -not [string]::IsNullOrWhiteSpace($MissingCreditsText)) {
|
||||
$credits = $MissingCreditsText
|
||||
}
|
||||
}
|
||||
|
||||
$flagImageSource = if ([string]$flag -match '^[a-z]+://') { [string]$flag } else { "https://flagcdn.com/$flag.svg" }
|
||||
|
||||
$row = New-Object System.Collections.Generic.List[string]
|
||||
$row.Add("<img src='$flagImageSource' width=20> $languageName")
|
||||
|
||||
if ($IncludeLanguageCode.IsPresent) {
|
||||
$row.Add(('`{0}`' -f $languageCode))
|
||||
}
|
||||
|
||||
if ($IncludeTranslatedColumn) {
|
||||
$row.Add($percentage)
|
||||
}
|
||||
|
||||
if ($IncludeFileColumn.IsPresent) {
|
||||
$relativeLanguageFilePath = (Join-Path 'src/Languages' ("lang_{0}.json" -f $languageCode)) -replace '\\', '/'
|
||||
$row.Add("[lang_$languageCode.json]($relativeLanguageFilePath)")
|
||||
}
|
||||
|
||||
if ($IncludeCreditsColumn) {
|
||||
$row.Add($credits)
|
||||
}
|
||||
|
||||
$lines.Add('| ' + ($row -join ' | ') + ' |')
|
||||
}
|
||||
|
||||
$lines.Add('')
|
||||
return ($lines -join [Environment]::NewLine)
|
||||
}
|
||||
|
||||
function Get-TranslationDocumentationMarkdown {
|
||||
$coverageTable = Get-MarkdownTranslationsTable -IncludeZeroPercent -IncludeLanguageCode -IncludeFileColumn -IncludeCreditsColumn:$false
|
||||
$contributorsTable = Get-MarkdownTranslationsTable -IncludeZeroPercent -IncludeLanguageCode -IncludeTranslatedColumn:$false
|
||||
|
||||
$sections = @(
|
||||
'# Translations',
|
||||
'',
|
||||
'UniGetUI includes translations for the languages listed below.',
|
||||
'',
|
||||
'This page lists the supported languages, each locale file, current completion status, and the credited contributors for each translation.',
|
||||
'',
|
||||
'If you would like to help improve a translation or report an issue, please open an issue or submit a pull request.',
|
||||
'',
|
||||
'Translation discussion and coordination also happens in [GitHub discussion #4510](https://github.com/Devolutions/UniGetUI/discussions/4510).',
|
||||
'',
|
||||
'## Language Coverage',
|
||||
'',
|
||||
$coverageTable.TrimEnd(),
|
||||
'',
|
||||
'## Contributors',
|
||||
'',
|
||||
'We are grateful to everyone who contributes translations to UniGetUI. Contributor credits are sourced from [Translators.json](src/Languages/Data/Translators.json). If you would like to be added to or removed from the list for a particular language, please open a pull request.',
|
||||
'',
|
||||
$contributorsTable.TrimEnd(),
|
||||
'',
|
||||
'## Maintaining Translation Data',
|
||||
'',
|
||||
'The tables in this document are generated from the checked-in translation metadata. Completion is computed against the active English keys in `lang_en.json`; keys below the legacy boundary marker are excluded from the percentage.',
|
||||
'',
|
||||
'To refresh translated percentages, contributor metadata, and this document after locale changes, run:',
|
||||
'',
|
||||
'```powershell',
|
||||
'pwsh ./scripts/translation/Sync-TranslationMetadata.ps1 -AllLanguages -UpdateTranslationDoc',
|
||||
'```',
|
||||
'',
|
||||
'To inspect the current status without modifying files, run:',
|
||||
'',
|
||||
'```powershell',
|
||||
'pwsh ./scripts/translation/Get-TranslationStatus.ps1 -OutputFormat Markdown -OnlyIncomplete',
|
||||
'```',
|
||||
''
|
||||
)
|
||||
|
||||
return (($sections -join [Environment]::NewLine) + [Environment]::NewLine)
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Get-ProjectRoot',
|
||||
'Get-ContributorsListPath',
|
||||
'Get-TranslatorsJsonPath',
|
||||
'Get-TranslatedPercentagesJsonPath',
|
||||
'Get-LanguagesReferenceJsonPath',
|
||||
'Get-LanguagesDirectoryPath',
|
||||
'Get-ContributorsList',
|
||||
'Get-LanguageCredits',
|
||||
'Get-TranslatedPercentages',
|
||||
'Get-LanguageReference',
|
||||
'Get-LanguageRemap',
|
||||
'Get-LanguageFlagsRemap',
|
||||
'Get-TranslatorsFromCredits',
|
||||
'ConvertTo-TranslatorMarkdown',
|
||||
'Get-MarkdownSupportLangs',
|
||||
'Get-MarkdownTranslationsTable',
|
||||
'Get-TranslationDocumentationMarkdown',
|
||||
'Get-LanguageFilePathMap'
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$AsJson,
|
||||
[switch]$AbsolutePaths
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'LanguageData.psm1') -Force
|
||||
|
||||
$languageFileMap = Get-LanguageFilePathMap -AbsolutePaths:$AbsolutePaths.IsPresent
|
||||
|
||||
if ($AsJson.IsPresent) {
|
||||
$languageFileMap | ConvertTo-Json -Depth 5
|
||||
return
|
||||
}
|
||||
|
||||
$languageFileMap
|
||||
@@ -0,0 +1,497 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$script:TranslationLegacyBoundaryKey = '__LEGACY_TRANSLATION_KEYS_BELOW__'
|
||||
$script:TranslationLegacyBoundaryValue = 'Legacy translation keys below are kept for backward compatibility with older UniGetUI builds. Do not translate or remove yet.'
|
||||
|
||||
function Assert-Command {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
if (-not (Get-Command -Name $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Required command not found on PATH: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-FullPath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
return [System.IO.Path]::GetFullPath($Path)
|
||||
}
|
||||
|
||||
function New-Utf8File {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
$directory = Split-Path -Path $Path -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($Path, $Content, $encoding)
|
||||
}
|
||||
|
||||
function New-OrderedStringMap {
|
||||
return [System.Collections.Specialized.OrderedDictionary]::new([System.StringComparer]::Ordinal)
|
||||
}
|
||||
|
||||
function Assert-NoDuplicateJsonKeys {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$pattern = [regex]'(?m)^\s*"((?:\\.|[^"])*)"\s*:'
|
||||
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
$duplicates = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($match in $pattern.Matches($Content)) {
|
||||
$key = [regex]::Unescape($match.Groups[1].Value)
|
||||
if (-not $seen.Add($key) -and -not $duplicates.Contains($key)) {
|
||||
$duplicates.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
if ($duplicates.Count -gt 0) {
|
||||
throw "JSON file contains duplicate key(s): $($duplicates -join ', '). Path: $Path"
|
||||
}
|
||||
}
|
||||
|
||||
function Read-OrderedJsonMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
||||
return (New-OrderedStringMap)
|
||||
}
|
||||
|
||||
$content = [System.IO.File]::ReadAllText((Get-FullPath -Path $Path))
|
||||
return Convert-JsonContentToOrderedMap -Content $content -Path $Path -DetectDuplicates
|
||||
}
|
||||
|
||||
function Read-OrderedJsonMapPermissive {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
||||
return (New-OrderedStringMap)
|
||||
}
|
||||
|
||||
$content = [System.IO.File]::ReadAllText((Get-FullPath -Path $Path))
|
||||
return Convert-JsonContentToOrderedMap -Content $content -Path $Path
|
||||
}
|
||||
|
||||
function Convert-JsonContentToOrderedMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[switch]$DetectDuplicates
|
||||
)
|
||||
|
||||
$result = New-OrderedStringMap
|
||||
if ([string]::IsNullOrWhiteSpace($Content)) {
|
||||
return $result
|
||||
}
|
||||
|
||||
if ($DetectDuplicates) {
|
||||
Assert-NoDuplicateJsonKeys -Content $Content -Path $Path
|
||||
}
|
||||
|
||||
$document = [System.Text.Json.JsonDocument]::Parse($Content)
|
||||
try {
|
||||
if ($document.RootElement.ValueKind -eq [System.Text.Json.JsonValueKind]::Null) {
|
||||
return $result
|
||||
}
|
||||
|
||||
if ($document.RootElement.ValueKind -ne [System.Text.Json.JsonValueKind]::Object) {
|
||||
throw "JSON file must contain a flat object: $Path"
|
||||
}
|
||||
|
||||
foreach ($property in $document.RootElement.EnumerateObject()) {
|
||||
if ($property.Value.ValueKind -eq [System.Text.Json.JsonValueKind]::Object -or $property.Value.ValueKind -eq [System.Text.Json.JsonValueKind]::Array) {
|
||||
throw "JSON file must contain a flat object with string values only: $Path"
|
||||
}
|
||||
|
||||
if ($property.Value.ValueKind -eq [System.Text.Json.JsonValueKind]::String) {
|
||||
$result[$property.Name] = $property.Value.GetString()
|
||||
continue
|
||||
}
|
||||
|
||||
if ($property.Value.ValueKind -eq [System.Text.Json.JsonValueKind]::Null) {
|
||||
$result[$property.Name] = ''
|
||||
continue
|
||||
}
|
||||
|
||||
$result[$property.Name] = $property.Value.ToString()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$document.Dispose()
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function ConvertTo-JsonStringLiteral {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
return ($Value | ConvertTo-Json -Compress)
|
||||
}
|
||||
|
||||
function Write-OrderedJsonMap {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Map
|
||||
)
|
||||
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
$lines.Add('{')
|
||||
|
||||
$index = 0
|
||||
$count = $Map.Count
|
||||
foreach ($entry in $Map.GetEnumerator()) {
|
||||
$index += 1
|
||||
$line = ' {0}: {1}' -f (ConvertTo-JsonStringLiteral -Value ([string]$entry.Key)), (ConvertTo-JsonStringLiteral -Value ([string]$entry.Value))
|
||||
if ($index -lt $count) {
|
||||
$line += ','
|
||||
}
|
||||
|
||||
$lines.Add($line)
|
||||
}
|
||||
|
||||
$lines.Add('}')
|
||||
New-Utf8File -Path $Path -Content (($lines -join "`r`n") + "`r`n")
|
||||
}
|
||||
|
||||
function Get-TranslationLegacyBoundaryKey {
|
||||
return $script:TranslationLegacyBoundaryKey
|
||||
}
|
||||
|
||||
function Get-TranslationLegacyBoundaryValue {
|
||||
return $script:TranslationLegacyBoundaryValue
|
||||
}
|
||||
|
||||
function Split-TranslationMapAtBoundary {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Map
|
||||
)
|
||||
|
||||
$boundaryKey = Get-TranslationLegacyBoundaryKey
|
||||
$activeMap = New-OrderedStringMap
|
||||
$legacyMap = New-OrderedStringMap
|
||||
$hasBoundary = $false
|
||||
$boundaryReached = $false
|
||||
|
||||
foreach ($entry in $Map.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
$value = [string]$entry.Value
|
||||
|
||||
if ($key -ceq $boundaryKey) {
|
||||
$hasBoundary = $true
|
||||
$boundaryReached = $true
|
||||
continue
|
||||
}
|
||||
|
||||
if ($boundaryReached) {
|
||||
$legacyMap[$key] = $value
|
||||
}
|
||||
else {
|
||||
$activeMap[$key] = $value
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
HasBoundary = $hasBoundary
|
||||
ActiveMap = $activeMap
|
||||
LegacyMap = $legacyMap
|
||||
}
|
||||
}
|
||||
|
||||
function Join-TranslationMapWithBoundary {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$ActiveMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$LegacyMap,
|
||||
|
||||
[switch]$IncludeBoundary
|
||||
)
|
||||
|
||||
$result = New-OrderedStringMap
|
||||
foreach ($entry in $ActiveMap.GetEnumerator()) {
|
||||
$result[[string]$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
|
||||
if ($IncludeBoundary) {
|
||||
$result[(Get-TranslationLegacyBoundaryKey)] = Get-TranslationLegacyBoundaryValue
|
||||
}
|
||||
|
||||
foreach ($entry in $LegacyMap.GetEnumerator()) {
|
||||
$result[[string]$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function Test-OrderedStringMapsEqual {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Left,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Right
|
||||
)
|
||||
|
||||
if ($Left.Count -ne $Right.Count) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$leftEntries = @($Left.GetEnumerator())
|
||||
$rightEntries = @($Right.GetEnumerator())
|
||||
for ($index = 0; $index -lt $leftEntries.Count; $index += 1) {
|
||||
if ([string]$leftEntries[$index].Key -cne [string]$rightEntries[$index].Key) {
|
||||
return $false
|
||||
}
|
||||
|
||||
if ([string]$leftEntries[$index].Value -cne [string]$rightEntries[$index].Value) {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
function Invoke-CirupJson {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Arguments
|
||||
)
|
||||
|
||||
Assert-Command -Name 'cirup'
|
||||
|
||||
$output = & cirup @Arguments --output-format json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "cirup command failed: cirup $($Arguments -join ' ')"
|
||||
}
|
||||
|
||||
$jsonText = [string]::Join([Environment]::NewLine, @($output)).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($jsonText)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$parsed = $jsonText | ConvertFrom-Json -AsHashtable
|
||||
return @($parsed)
|
||||
}
|
||||
|
||||
function Get-RepositoryRoot {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$WorkingDirectory
|
||||
)
|
||||
|
||||
$repoRoot = & git -C $WorkingDirectory rev-parse --show-toplevel
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
|
||||
throw "Unable to resolve git repository root from '$WorkingDirectory'."
|
||||
}
|
||||
|
||||
return $repoRoot.Trim()
|
||||
}
|
||||
|
||||
function Get-RepoRelativePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepositoryRoot,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath
|
||||
)
|
||||
|
||||
$repoRootFull = Get-FullPath -Path $RepositoryRoot
|
||||
$filePathFull = Get-FullPath -Path $FilePath
|
||||
$repoRootWithSeparator = $repoRootFull.TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + [System.IO.Path]::DirectorySeparatorChar
|
||||
|
||||
if (-not $filePathFull.StartsWith($repoRootWithSeparator, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "File '$filePathFull' is not located under repository root '$repoRootFull'."
|
||||
}
|
||||
|
||||
return [System.IO.Path]::GetRelativePath($repoRootFull, $filePathFull).Replace('\', '/')
|
||||
}
|
||||
|
||||
function Get-SafeLabel {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$safe = $Value -replace '[^A-Za-z0-9._-]+', '-'
|
||||
$safe = $safe.Trim('-')
|
||||
if ([string]::IsNullOrWhiteSpace($safe)) {
|
||||
return 'base'
|
||||
}
|
||||
|
||||
return $safe
|
||||
}
|
||||
|
||||
function Get-PatchBaseName {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$NeutralJsonPath
|
||||
)
|
||||
|
||||
$stem = [System.IO.Path]::GetFileNameWithoutExtension($NeutralJsonPath)
|
||||
if ($stem -match '^(.*)_en$' -and -not [string]::IsNullOrWhiteSpace($matches[1])) {
|
||||
return $matches[1]
|
||||
}
|
||||
|
||||
return $stem
|
||||
}
|
||||
|
||||
function Get-LanguagesReferencePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$NeutralJsonPath
|
||||
)
|
||||
|
||||
$languagesDirectory = Split-Path -Path (Get-FullPath -Path $NeutralJsonPath) -Parent
|
||||
return Join-Path $languagesDirectory 'Data\LanguagesReference.json'
|
||||
}
|
||||
|
||||
function Assert-LanguageCodeKnown {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguagesReferencePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LanguageCode
|
||||
)
|
||||
|
||||
$referenceMap = Read-OrderedJsonMap -Path $LanguagesReferencePath
|
||||
if (-not $referenceMap.Contains($LanguageCode)) {
|
||||
throw "Unknown language code '$LanguageCode'. Expected one of the language codes from $LanguagesReferencePath"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-NeedsTranslation {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Key,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourceMap,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$TargetMap
|
||||
)
|
||||
|
||||
if (-not $TargetMap.Contains($Key)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
$targetValue = [string]$TargetMap[$Key]
|
||||
if ([string]::IsNullOrWhiteSpace($targetValue)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return $targetValue -ceq [string]$SourceMap[$Key]
|
||||
}
|
||||
|
||||
function Get-PlaceholderTokens {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$tokens = foreach ($match in [regex]::Matches($Value, '\{[A-Za-z0-9_]+(?:,[^}:]+)?(?::[^}]+)?\}')) {
|
||||
$match.Value
|
||||
}
|
||||
|
||||
return @($tokens | Sort-Object)
|
||||
}
|
||||
|
||||
function Get-HtmlTokens {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$tokens = foreach ($match in [regex]::Matches($Value, '</?[^>]+?>')) {
|
||||
$match.Value
|
||||
}
|
||||
|
||||
return @($tokens | Sort-Object)
|
||||
}
|
||||
|
||||
function Get-NewlineCount {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
return ([regex]::Matches($Value, "`r`n|`n")).Count
|
||||
}
|
||||
|
||||
function Assert-TranslationStructure {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$SourceValue,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$TranslatedValue,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Key
|
||||
)
|
||||
|
||||
$sourcePlaceholderSignature = (Get-PlaceholderTokens -Value $SourceValue) -join "`n"
|
||||
$translatedPlaceholderSignature = (Get-PlaceholderTokens -Value $TranslatedValue) -join "`n"
|
||||
if ($sourcePlaceholderSignature -ne $translatedPlaceholderSignature) {
|
||||
throw "Placeholder mismatch for key '$Key'."
|
||||
}
|
||||
|
||||
$sourceHtmlSignature = (Get-HtmlTokens -Value $SourceValue) -join "`n"
|
||||
$translatedHtmlSignature = (Get-HtmlTokens -Value $TranslatedValue) -join "`n"
|
||||
if ($sourceHtmlSignature -ne $translatedHtmlSignature) {
|
||||
throw "HTML fragment mismatch for key '$Key'."
|
||||
}
|
||||
|
||||
if ((Get-NewlineCount -Value $SourceValue) -ne (Get-NewlineCount -Value $TranslatedValue)) {
|
||||
throw "Line-break mismatch for key '$Key'."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
. (Join-Path $PSScriptRoot 'TranslationJsonTools.ps1')
|
||||
|
||||
$script:SupportedSourceExtensions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
[void]$script:SupportedSourceExtensions.Add('.cs')
|
||||
[void]$script:SupportedSourceExtensions.Add('.xaml')
|
||||
[void]$script:SupportedSourceExtensions.Add('.axaml')
|
||||
|
||||
$script:ExcludedDirectoryNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($name in @('.git', '.vs', 'bin', 'obj', 'generated', 'node_modules', 'packages')) {
|
||||
[void]$script:ExcludedDirectoryNames.Add($name)
|
||||
}
|
||||
|
||||
function Resolve-TranslationSyncRepositoryRoot {
|
||||
param(
|
||||
[string]$RepositoryRoot
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($RepositoryRoot)) {
|
||||
return Get-FullPath -Path $RepositoryRoot
|
||||
}
|
||||
|
||||
return [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
}
|
||||
|
||||
function Resolve-EnglishLanguageFilePath {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResolvedRepositoryRoot,
|
||||
|
||||
[string]$EnglishFilePath
|
||||
)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($EnglishFilePath)) {
|
||||
return Get-FullPath -Path $EnglishFilePath
|
||||
}
|
||||
|
||||
return Join-Path $ResolvedRepositoryRoot 'src\Languages\lang_en.json'
|
||||
}
|
||||
|
||||
function Test-TranslationSourceFileIncluded {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.IO.FileInfo]$File,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResolvedRepositoryRoot
|
||||
)
|
||||
|
||||
if (-not $script:SupportedSourceExtensions.Contains($File.Extension)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($ResolvedRepositoryRoot, $File.FullName).Replace('\', '/')
|
||||
foreach ($segment in $relativePath.Split('/')) {
|
||||
if ($script:ExcludedDirectoryNames.Contains($segment)) {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($relativePath -like 'src/Languages/lang_*.json') {
|
||||
return $false
|
||||
}
|
||||
|
||||
return $relativePath.StartsWith('src/', [System.StringComparison]::OrdinalIgnoreCase)
|
||||
}
|
||||
|
||||
function Get-TranslationSourceFiles {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ResolvedRepositoryRoot
|
||||
)
|
||||
|
||||
return @(
|
||||
Get-ChildItem -Path $ResolvedRepositoryRoot -File -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object { Test-TranslationSourceFileIncluded -File $_ -ResolvedRepositoryRoot $ResolvedRepositoryRoot } |
|
||||
Sort-Object FullName
|
||||
)
|
||||
}
|
||||
|
||||
function Get-LineNumberFromIndex {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Text,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$Index
|
||||
)
|
||||
|
||||
if ($Index -le 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return ([regex]::Matches($Text.Substring(0, $Index), "`r`n|`n")).Count + 1
|
||||
}
|
||||
|
||||
function Convert-CSharpStringLiteralValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Literal
|
||||
)
|
||||
|
||||
if ($Literal.StartsWith('@"', [System.StringComparison]::Ordinal)) {
|
||||
return $Literal.Substring(2, $Literal.Length - 3).Replace('""', '"')
|
||||
}
|
||||
|
||||
return [regex]::Unescape($Literal.Substring(1, $Literal.Length - 2))
|
||||
}
|
||||
|
||||
function Add-TranslationSourceKey {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Key,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourcePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$LineNumber,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$KeyOrder,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourcesByKey
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Key)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $SourcesByKey.Contains($Key)) {
|
||||
$SourcesByKey[$Key] = New-Object System.Collections.Generic.List[string]
|
||||
$KeyOrder.Add($Key)
|
||||
}
|
||||
|
||||
$location = '{0}:{1}' -f $SourcePath.Replace('\', '/'), $LineNumber
|
||||
$locations = [System.Collections.Generic.List[string]]$SourcesByKey[$Key]
|
||||
if (-not $locations.Contains($location)) {
|
||||
$locations.Add($location)
|
||||
}
|
||||
}
|
||||
|
||||
function Add-TranslationSourceWarning {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Type,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourcePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$LineNumber,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Message,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$Warnings
|
||||
)
|
||||
|
||||
$Warnings.Add([pscustomobject]@{
|
||||
Type = $Type
|
||||
Path = $SourcePath.Replace('\', '/')
|
||||
Line = $LineNumber
|
||||
Message = $Message
|
||||
})
|
||||
}
|
||||
|
||||
function Get-CSharpTranslationMatches {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$KeyOrder,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourcesByKey,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$Warnings
|
||||
)
|
||||
|
||||
$literalPattern = [regex]'CoreTools\s*\.\s*(?:Translate|AutoTranslated)\(\s*(?<literal>@"(?:[^"]|"")*"|"(?:\\.|[^"\\])*")'
|
||||
foreach ($match in $literalPattern.Matches($Content)) {
|
||||
$literal = [string]$match.Groups['literal'].Value
|
||||
$lineNumber = Get-LineNumberFromIndex -Text $Content -Index $match.Index
|
||||
Add-TranslationSourceKey -Key (Convert-CSharpStringLiteralValue -Literal $literal) -SourcePath $FilePath -LineNumber $lineNumber -KeyOrder $KeyOrder -SourcesByKey $SourcesByKey
|
||||
}
|
||||
|
||||
$interpolatedPattern = [regex]'CoreTools\s*\.\s*Translate\(\s*\$@?"'
|
||||
foreach ($match in $interpolatedPattern.Matches($Content)) {
|
||||
$lineNumber = Get-LineNumberFromIndex -Text $Content -Index $match.Index
|
||||
Add-TranslationSourceWarning -Type 'InterpolatedTranslateCall' -SourcePath $FilePath -LineNumber $lineNumber -Message 'Interpolated CoreTools.Translate call is not synchronized automatically.' -Warnings $Warnings
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TranslatedTextBlockMatches {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$KeyOrder,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourcesByKey
|
||||
)
|
||||
|
||||
$translatedTextBlockPattern = [regex]'<(?<tag>[A-Za-z0-9_:.\-]+TranslatedTextBlock)\b[^>]*\bText="(?<text>[^"]*)"'
|
||||
foreach ($match in $translatedTextBlockPattern.Matches($Content)) {
|
||||
$lineNumber = Get-LineNumberFromIndex -Text $Content -Index $match.Index
|
||||
$text = [System.Net.WebUtility]::HtmlDecode([string]$match.Groups['text'].Value)
|
||||
Add-TranslationSourceKey -Key $text -SourcePath $FilePath -LineNumber $lineNumber -KeyOrder $KeyOrder -SourcesByKey $SourcesByKey
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AvaloniaTranslateMarkupMatches {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$KeyOrder,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$SourcesByKey,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IList]$Warnings
|
||||
)
|
||||
|
||||
$markupPattern = [regex]'\{t:Translate(?<body>[^}]*)\}'
|
||||
foreach ($match in $markupPattern.Matches($Content)) {
|
||||
$body = [string]$match.Groups['body'].Value
|
||||
$trimmedBody = $body.Trim()
|
||||
$lineNumber = Get-LineNumberFromIndex -Text $Content -Index $match.Index
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($trimmedBody)) {
|
||||
Add-TranslationSourceWarning -Type 'EmptyMarkupTranslate' -SourcePath $FilePath -LineNumber $lineNumber -Message 'Empty {t:Translate} markup extension was ignored.' -Warnings $Warnings
|
||||
continue
|
||||
}
|
||||
|
||||
$namedMatch = [regex]::Match($trimmedBody, '^Text\s*=\s*(?:''(?<single>[^'']*)''|"(?<double>[^"]*)")$')
|
||||
if ($namedMatch.Success) {
|
||||
$value = if ($namedMatch.Groups['single'].Success) { $namedMatch.Groups['single'].Value } else { $namedMatch.Groups['double'].Value }
|
||||
Add-TranslationSourceKey -Key ([System.Net.WebUtility]::HtmlDecode($value)) -SourcePath $FilePath -LineNumber $lineNumber -KeyOrder $KeyOrder -SourcesByKey $SourcesByKey
|
||||
continue
|
||||
}
|
||||
|
||||
Add-TranslationSourceKey -Key ([System.Net.WebUtility]::HtmlDecode($trimmedBody)) -SourcePath $FilePath -LineNumber $lineNumber -KeyOrder $KeyOrder -SourcesByKey $SourcesByKey
|
||||
}
|
||||
}
|
||||
|
||||
function Get-TranslationSourceSnapshot {
|
||||
param(
|
||||
[string]$RepositoryRoot
|
||||
)
|
||||
|
||||
$resolvedRepositoryRoot = Resolve-TranslationSyncRepositoryRoot -RepositoryRoot $RepositoryRoot
|
||||
$sourceFiles = Get-TranslationSourceFiles -ResolvedRepositoryRoot $resolvedRepositoryRoot
|
||||
$keyOrder = New-Object System.Collections.Generic.List[string]
|
||||
$sourcesByKey = [ordered]@{}
|
||||
$warnings = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
foreach ($file in $sourceFiles) {
|
||||
$relativePath = [System.IO.Path]::GetRelativePath($resolvedRepositoryRoot, $file.FullName).Replace('\', '/')
|
||||
$content = [System.IO.File]::ReadAllText($file.FullName)
|
||||
|
||||
switch ($file.Extension.ToLowerInvariant()) {
|
||||
'.cs' {
|
||||
Get-CSharpTranslationMatches -FilePath $relativePath -Content $content -KeyOrder $keyOrder -SourcesByKey $sourcesByKey -Warnings $warnings
|
||||
}
|
||||
'.xaml' {
|
||||
Get-TranslatedTextBlockMatches -FilePath $relativePath -Content $content -KeyOrder $keyOrder -SourcesByKey $sourcesByKey
|
||||
}
|
||||
'.axaml' {
|
||||
Get-TranslatedTextBlockMatches -FilePath $relativePath -Content $content -KeyOrder $keyOrder -SourcesByKey $sourcesByKey
|
||||
Get-AvaloniaTranslateMarkupMatches -FilePath $relativePath -Content $content -KeyOrder $keyOrder -SourcesByKey $sourcesByKey -Warnings $warnings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$orderedKeyMap = New-OrderedStringMap
|
||||
foreach ($key in $keyOrder) {
|
||||
$orderedKeyMap[$key] = $key
|
||||
}
|
||||
|
||||
$sourceFilePaths = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($sourceFile in $sourceFiles) {
|
||||
$sourceFilePaths.Add($sourceFile.FullName)
|
||||
}
|
||||
|
||||
$warningItems = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($warning in $warnings) {
|
||||
$warningItems.Add($warning)
|
||||
}
|
||||
|
||||
$sourceFileArray = $sourceFilePaths.ToArray()
|
||||
$keyOrderArray = $keyOrder.ToArray()
|
||||
$warningArray = $warningItems.ToArray()
|
||||
|
||||
$snapshot = New-Object -TypeName psobject
|
||||
$snapshot | Add-Member -NotePropertyName RepositoryRoot -NotePropertyValue $resolvedRepositoryRoot
|
||||
$snapshot | Add-Member -NotePropertyName SourceFiles -NotePropertyValue $sourceFileArray
|
||||
$snapshot | Add-Member -NotePropertyName KeyOrder -NotePropertyValue $keyOrderArray
|
||||
$snapshot | Add-Member -NotePropertyName Keys -NotePropertyValue $orderedKeyMap
|
||||
$snapshot | Add-Member -NotePropertyName SourcesByKey -NotePropertyValue $sourcesByKey
|
||||
$snapshot | Add-Member -NotePropertyName Warnings -NotePropertyValue $warningArray
|
||||
|
||||
return $snapshot
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BatchDir,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputTranslatedPatch
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
|
||||
$batchRoot = Get-FullPath -Path $BatchDir
|
||||
$outputPath = Get-FullPath -Path $OutputTranslatedPatch
|
||||
$manifestPath = Join-Path $batchRoot 'manifest.json'
|
||||
|
||||
if (-not (Test-Path -Path $manifestPath -PathType Leaf)) {
|
||||
throw "Batch manifest not found: $manifestPath"
|
||||
}
|
||||
|
||||
$manifest = Get-Content -Path $manifestPath -Raw | ConvertFrom-Json
|
||||
$mergedMap = New-OrderedStringMap
|
||||
|
||||
foreach ($batch in @($manifest.batches | Sort-Object batchNumber)) {
|
||||
$translatedPath = Join-Path $batchRoot ([string]$batch.translated)
|
||||
if (-not (Test-Path -Path $translatedPath -PathType Leaf)) {
|
||||
throw "Translated batch file not found: $translatedPath"
|
||||
}
|
||||
|
||||
$translatedMap = Read-OrderedJsonMap -Path $translatedPath
|
||||
foreach ($entry in $translatedMap.GetEnumerator()) {
|
||||
$mergedMap[[string]$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
}
|
||||
|
||||
Write-OrderedJsonMap -Path $outputPath -Map $mergedMap
|
||||
Write-Output "Merged $($manifest.batchCount) translation batch(es) into $outputPath"
|
||||
@@ -0,0 +1,36 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$EnglishFilePath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationSourceTools.ps1')
|
||||
|
||||
$resolvedRepositoryRoot = Resolve-TranslationSyncRepositoryRoot -RepositoryRoot $RepositoryRoot
|
||||
$resolvedEnglishFilePath = Resolve-EnglishLanguageFilePath -ResolvedRepositoryRoot $resolvedRepositoryRoot -EnglishFilePath $EnglishFilePath
|
||||
|
||||
if (-not (Test-Path -Path $resolvedEnglishFilePath -PathType Leaf)) {
|
||||
throw "English translation file not found: $resolvedEnglishFilePath"
|
||||
}
|
||||
|
||||
$snapshot = Get-TranslationSourceSnapshot -RepositoryRoot $resolvedRepositoryRoot
|
||||
$englishTranslations = Read-OrderedJsonMapPermissive -Path $resolvedEnglishFilePath
|
||||
$extractedKeySet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($key in $snapshot.KeyOrder) {
|
||||
[void]$extractedKeySet.Add([string]$key)
|
||||
}
|
||||
|
||||
$unusedKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $englishTranslations.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if (-not $extractedKeySet.Contains($key)) {
|
||||
$unusedKeys.Add($key)
|
||||
Write-Output "Unused key: $key"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "Scan completed. Checked $(@($snapshot.SourceFiles).Count) file(s); found $($unusedKeys.Count) unused key(s)."
|
||||
@@ -0,0 +1,92 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$EnglishFilePath,
|
||||
[string]$LegacyRef = 'HEAD~1',
|
||||
[switch]$CheckOnly
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationSourceTools.ps1')
|
||||
|
||||
$resolvedRepositoryRoot = Resolve-TranslationSyncRepositoryRoot -RepositoryRoot $RepositoryRoot
|
||||
$resolvedEnglishFilePath = Resolve-EnglishLanguageFilePath -ResolvedRepositoryRoot $resolvedRepositoryRoot -EnglishFilePath $EnglishFilePath
|
||||
|
||||
if (-not (Test-Path -Path $resolvedEnglishFilePath -PathType Leaf)) {
|
||||
throw "English translation file not found: $resolvedEnglishFilePath"
|
||||
}
|
||||
|
||||
$relativeEnglishPath = Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $resolvedEnglishFilePath
|
||||
$gitObject = '{0}:{1}' -f $LegacyRef, $relativeEnglishPath
|
||||
$legacyContent = & git -C $resolvedRepositoryRoot show $gitObject 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Unable to read '$relativeEnglishPath' from git ref '$LegacyRef'."
|
||||
}
|
||||
|
||||
$snapshot = Get-TranslationSourceSnapshot -RepositoryRoot $resolvedRepositoryRoot
|
||||
$currentMap = Read-OrderedJsonMap -Path $resolvedEnglishFilePath
|
||||
$legacySourceMap = Convert-JsonContentToOrderedMap -Content ([string]::Join([Environment]::NewLine, @($legacyContent))) -Path $gitObject -DetectDuplicates
|
||||
|
||||
$extractedKeySet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($key in $snapshot.KeyOrder) {
|
||||
[void]$extractedKeySet.Add([string]$key)
|
||||
}
|
||||
|
||||
$mergedLegacyMap = New-OrderedStringMap
|
||||
foreach ($entry in $legacySourceMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($key -ceq (Get-TranslationLegacyBoundaryKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $extractedKeySet.Contains($key)) {
|
||||
$mergedLegacyMap[$key] = if ($currentMap.Contains($key)) { [string]$currentMap[$key] } else { [string]$entry.Value }
|
||||
}
|
||||
}
|
||||
|
||||
$currentSections = Split-TranslationMapAtBoundary -Map $currentMap
|
||||
foreach ($entry in $currentSections.LegacyMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if (-not $mergedLegacyMap.Contains($key)) {
|
||||
$mergedLegacyMap[$key] = [string]$entry.Value
|
||||
}
|
||||
}
|
||||
|
||||
$updatedActiveMap = New-OrderedStringMap
|
||||
foreach ($key in $snapshot.KeyOrder) {
|
||||
$updatedActiveMap[$key] = if ($currentMap.Contains($key)) { [string]$currentMap[$key] } else { $key }
|
||||
}
|
||||
|
||||
$updatedMap = Join-TranslationMapWithBoundary -ActiveMap $updatedActiveMap -LegacyMap $mergedLegacyMap -IncludeBoundary
|
||||
$hasChanges = -not (Test-OrderedStringMapsEqual -Left $currentMap -Right $updatedMap)
|
||||
|
||||
Write-Output 'English legacy boundary summary'
|
||||
Write-Output "Repository root: $resolvedRepositoryRoot"
|
||||
Write-Output "English file: $resolvedEnglishFilePath"
|
||||
Write-Output "Legacy ref: $LegacyRef"
|
||||
Write-Output "Active keys: $($updatedActiveMap.Count)"
|
||||
Write-Output "Legacy keys restored: $($mergedLegacyMap.Count)"
|
||||
Write-Output "Needs update: $hasChanges"
|
||||
|
||||
if ($CheckOnly) {
|
||||
if ($hasChanges) {
|
||||
throw 'English legacy boundary layout is not in the expected state.'
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $hasChanges) {
|
||||
Write-Output ''
|
||||
Write-Output 'The English file already matches the expected boundary layout.'
|
||||
return
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($resolvedEnglishFilePath, 'Populate the English legacy translation tail')) {
|
||||
Write-OrderedJsonMap -Path $resolvedEnglishFilePath -Map $updatedMap
|
||||
Write-Output ''
|
||||
Write-Output 'The English legacy boundary layout was updated successfully.'
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$EnglishFilePath,
|
||||
[string]$LanguagesDirectory,
|
||||
[string]$InventoryOutputPath,
|
||||
[switch]$IncludeEnglish,
|
||||
[switch]$CheckOnly
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationSourceTools.ps1')
|
||||
|
||||
$resolvedRepositoryRoot = Resolve-TranslationSyncRepositoryRoot -RepositoryRoot $RepositoryRoot
|
||||
$resolvedEnglishFilePath = Resolve-EnglishLanguageFilePath -ResolvedRepositoryRoot $resolvedRepositoryRoot -EnglishFilePath $EnglishFilePath
|
||||
$resolvedLanguagesDirectory = if ([string]::IsNullOrWhiteSpace($LanguagesDirectory)) {
|
||||
Split-Path -Path $resolvedEnglishFilePath -Parent
|
||||
}
|
||||
else {
|
||||
Get-FullPath -Path $LanguagesDirectory
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $resolvedLanguagesDirectory -PathType Container)) {
|
||||
throw "Languages directory not found: $resolvedLanguagesDirectory"
|
||||
}
|
||||
|
||||
$englishMap = Read-OrderedJsonMap -Path $resolvedEnglishFilePath
|
||||
$englishSections = Split-TranslationMapAtBoundary -Map $englishMap
|
||||
if (-not $englishSections.HasBoundary) {
|
||||
Write-Output 'The English translation file does not contain a legacy boundary marker. No reordering needed.'
|
||||
return
|
||||
}
|
||||
|
||||
$englishActiveOrder = @($englishSections.ActiveMap.Keys)
|
||||
$englishLegacyOrder = @($englishSections.LegacyMap.Keys)
|
||||
$englishKeySet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($key in $englishActiveOrder) {
|
||||
[void]$englishKeySet.Add([string]$key)
|
||||
}
|
||||
foreach ($key in $englishLegacyOrder) {
|
||||
[void]$englishKeySet.Add([string]$key)
|
||||
}
|
||||
|
||||
$filesToProcess = Get-ChildItem -Path $resolvedLanguagesDirectory -Filter 'lang_*.json' | Sort-Object Name
|
||||
if (-not $IncludeEnglish) {
|
||||
$filesToProcess = @($filesToProcess | Where-Object { $_.FullName -ine $resolvedEnglishFilePath })
|
||||
}
|
||||
|
||||
$changedFiles = New-Object System.Collections.Generic.List[string]
|
||||
$reports = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
foreach ($languageFile in $filesToProcess) {
|
||||
$languageMap = Read-OrderedJsonMapPermissive -Path $languageFile.FullName
|
||||
|
||||
$activeOutput = New-OrderedStringMap
|
||||
foreach ($key in $englishActiveOrder) {
|
||||
if ($languageMap.Contains($key)) {
|
||||
$activeOutput[$key] = [string]$languageMap[$key]
|
||||
}
|
||||
}
|
||||
|
||||
$legacyOutput = New-OrderedStringMap
|
||||
foreach ($key in $englishLegacyOrder) {
|
||||
if ($languageMap.Contains($key)) {
|
||||
$legacyOutput[$key] = [string]$languageMap[$key]
|
||||
}
|
||||
}
|
||||
|
||||
$unmappedKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $languageMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($key -ceq (Get-TranslationLegacyBoundaryKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $englishKeySet.Contains($key)) {
|
||||
$legacyOutput[$key] = [string]$entry.Value
|
||||
$unmappedKeys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
$updatedMap = Join-TranslationMapWithBoundary -ActiveMap $activeOutput -LegacyMap $legacyOutput -IncludeBoundary
|
||||
$hasChanges = -not (Test-OrderedStringMapsEqual -Left $languageMap -Right $updatedMap)
|
||||
|
||||
if ($hasChanges) {
|
||||
$changedFiles.Add((Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $languageFile.FullName))
|
||||
if (-not $CheckOnly -and $PSCmdlet.ShouldProcess($languageFile.FullName, 'Reorder translation file to the English legacy boundary layout')) {
|
||||
Write-OrderedJsonMap -Path $languageFile.FullName -Map $updatedMap
|
||||
}
|
||||
}
|
||||
|
||||
$reports.Add([pscustomobject]@{
|
||||
languageFile = (Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $languageFile.FullName)
|
||||
changed = $hasChanges
|
||||
activeKeyCount = $activeOutput.Count
|
||||
legacyKeyCount = $legacyOutput.Count
|
||||
unmappedKeyCount = $unmappedKeys.Count
|
||||
unmappedKeys = $unmappedKeys.ToArray()
|
||||
})
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($InventoryOutputPath)) {
|
||||
$resolvedInventoryOutputPath = Get-FullPath -Path $InventoryOutputPath
|
||||
$report = [pscustomobject]@{
|
||||
generatedAt = (Get-Date).ToString('o')
|
||||
repositoryRoot = $resolvedRepositoryRoot
|
||||
englishFile = (Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $resolvedEnglishFilePath)
|
||||
languagesDirectory = (Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $resolvedLanguagesDirectory)
|
||||
includeEnglish = [bool]$IncludeEnglish
|
||||
changedFileCount = $changedFiles.Count
|
||||
changedFiles = $changedFiles.ToArray()
|
||||
files = $reports.ToArray()
|
||||
}
|
||||
|
||||
New-Utf8File -Path $resolvedInventoryOutputPath -Content ($report | ConvertTo-Json -Depth 6)
|
||||
}
|
||||
|
||||
Write-Output 'Translation boundary reorder summary'
|
||||
Write-Output "Repository root: $resolvedRepositoryRoot"
|
||||
Write-Output "Languages directory: $resolvedLanguagesDirectory"
|
||||
Write-Output "Include English: $([bool]$IncludeEnglish)"
|
||||
Write-Output "Files processed: $(@($filesToProcess).Count)"
|
||||
Write-Output "Files changed: $($changedFiles.Count)"
|
||||
|
||||
if ($changedFiles.Count -gt 0) {
|
||||
Write-Output ''
|
||||
Write-Output 'Changed files:'
|
||||
foreach ($path in $changedFiles) {
|
||||
Write-Output (" * {0}" -f $path)
|
||||
}
|
||||
}
|
||||
|
||||
if ($CheckOnly -and $changedFiles.Count -gt 0) {
|
||||
throw 'One or more translation files are not aligned to the English legacy boundary layout.'
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SourcePatch,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputDir,
|
||||
|
||||
[string]$TranslatedPatch,
|
||||
|
||||
[string]$ReferencePatch,
|
||||
|
||||
[ValidateRange(1, 100)]
|
||||
[int]$BatchSize = 100,
|
||||
|
||||
[switch]$Clean
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
|
||||
function New-BatchFileName {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$BatchNumber,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Kind
|
||||
)
|
||||
|
||||
return ('batch.{0:D2}.{1}.json' -f $BatchNumber, $Kind)
|
||||
}
|
||||
|
||||
$sourcePatchPath = Get-FullPath -Path $SourcePatch
|
||||
$outputRoot = Get-FullPath -Path $OutputDir
|
||||
$translatedPatchPath = if ([string]::IsNullOrWhiteSpace($TranslatedPatch)) { $null } else { Get-FullPath -Path $TranslatedPatch }
|
||||
$referencePatchPath = if ([string]::IsNullOrWhiteSpace($ReferencePatch)) { $null } else { Get-FullPath -Path $ReferencePatch }
|
||||
|
||||
if (-not (Test-Path -Path $sourcePatchPath -PathType Leaf)) {
|
||||
throw "Source patch not found: $sourcePatchPath"
|
||||
}
|
||||
|
||||
if ($null -ne $translatedPatchPath -and -not (Test-Path -Path $translatedPatchPath -PathType Leaf)) {
|
||||
throw "Translated patch not found: $translatedPatchPath"
|
||||
}
|
||||
|
||||
if ($null -ne $referencePatchPath -and -not (Test-Path -Path $referencePatchPath -PathType Leaf)) {
|
||||
throw "Reference patch not found: $referencePatchPath"
|
||||
}
|
||||
|
||||
if ($Clean -and (Test-Path -Path $outputRoot)) {
|
||||
Remove-Item -Path $outputRoot -Recurse -Force
|
||||
}
|
||||
|
||||
New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
|
||||
|
||||
$sourceMap = Read-OrderedJsonMap -Path $sourcePatchPath
|
||||
$translatedMap = if ($null -eq $translatedPatchPath) { New-OrderedStringMap } else { Read-OrderedJsonMap -Path $translatedPatchPath }
|
||||
$referenceMap = if ($null -eq $referencePatchPath) { New-OrderedStringMap } else { Read-OrderedJsonMap -Path $referencePatchPath }
|
||||
|
||||
$entries = @($sourceMap.GetEnumerator())
|
||||
$batchCount = if ($entries.Count -eq 0) { 0 } else { [int][Math]::Ceiling($entries.Count / $BatchSize) }
|
||||
$manifestBatches = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
for ($batchIndex = 0; $batchIndex -lt $batchCount; $batchIndex += 1) {
|
||||
$batchNumber = $batchIndex + 1
|
||||
$startIndex = $batchIndex * $BatchSize
|
||||
$endIndexExclusive = [Math]::Min($startIndex + $BatchSize, $entries.Count)
|
||||
|
||||
$batchSourceMap = New-OrderedStringMap
|
||||
$batchTranslatedMap = New-OrderedStringMap
|
||||
$batchReferenceMap = New-OrderedStringMap
|
||||
$batchKeys = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
for ($entryIndex = $startIndex; $entryIndex -lt $endIndexExclusive; $entryIndex += 1) {
|
||||
$entry = $entries[$entryIndex]
|
||||
$key = [string]$entry.Key
|
||||
$value = [string]$entry.Value
|
||||
|
||||
$batchSourceMap[$key] = $value
|
||||
$batchKeys.Add($key)
|
||||
|
||||
if ($translatedMap.Contains($key)) {
|
||||
$batchTranslatedMap[$key] = [string]$translatedMap[$key]
|
||||
}
|
||||
|
||||
if ($referenceMap.Contains($key)) {
|
||||
$batchReferenceMap[$key] = [string]$referenceMap[$key]
|
||||
}
|
||||
}
|
||||
|
||||
$sourceFileName = New-BatchFileName -BatchNumber $batchNumber -Kind 'source'
|
||||
$translatedFileName = New-BatchFileName -BatchNumber $batchNumber -Kind 'translated'
|
||||
$referenceFileName = New-BatchFileName -BatchNumber $batchNumber -Kind 'reference'
|
||||
|
||||
Write-OrderedJsonMap -Path (Join-Path $outputRoot $sourceFileName) -Map $batchSourceMap
|
||||
Write-OrderedJsonMap -Path (Join-Path $outputRoot $translatedFileName) -Map $batchTranslatedMap
|
||||
Write-OrderedJsonMap -Path (Join-Path $outputRoot $referenceFileName) -Map $batchReferenceMap
|
||||
|
||||
$manifestBatches.Add([pscustomobject]@{
|
||||
batchNumber = $batchNumber
|
||||
keyCount = $batchSourceMap.Count
|
||||
source = $sourceFileName
|
||||
translated = $translatedFileName
|
||||
reference = $referenceFileName
|
||||
firstKey = if ($batchKeys.Count -gt 0) { $batchKeys[0] } else { $null }
|
||||
lastKey = if ($batchKeys.Count -gt 0) { $batchKeys[$batchKeys.Count - 1] } else { $null }
|
||||
}) | Out-Null
|
||||
}
|
||||
|
||||
$manifest = [pscustomobject]@{
|
||||
generatedAt = (Get-Date).ToString('o')
|
||||
sourcePatch = $sourcePatchPath
|
||||
translatedPatch = $translatedPatchPath
|
||||
referencePatch = $referencePatchPath
|
||||
batchSize = $BatchSize
|
||||
totalKeys = $sourceMap.Count
|
||||
batchCount = $batchCount
|
||||
batches = $manifestBatches.ToArray()
|
||||
}
|
||||
|
||||
New-Utf8File -Path (Join-Path $outputRoot 'manifest.json') -Content (($manifest | ConvertTo-Json -Depth 6) + "`r`n")
|
||||
|
||||
Write-Output "Created $batchCount translation batch(es) in $outputRoot"
|
||||
@@ -0,0 +1,346 @@
|
||||
[CmdletBinding(DefaultParameterSetName = 'Selected', SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[Parameter(ParameterSetName = 'Selected', Mandatory = $true)]
|
||||
[string[]]$LanguageCodes,
|
||||
|
||||
[Parameter(ParameterSetName = 'All')]
|
||||
[switch]$AllLanguages,
|
||||
|
||||
[switch]$CheckOnly,
|
||||
|
||||
[switch]$UpdateTranslationDoc,
|
||||
|
||||
[switch]$UpdateReadme,
|
||||
|
||||
[string]$ReadmePath = (Join-Path $PSScriptRoot '..\..\README.md'),
|
||||
|
||||
[string]$TranslationDocPath = (Join-Path $PSScriptRoot '..\..\TRANSLATION.md'),
|
||||
|
||||
[string]$TranslatorsPath = (Join-Path $PSScriptRoot '..\..\src\Languages\Data\Translators.json'),
|
||||
|
||||
[string]$TranslatedPercentagesPath = (Join-Path $PSScriptRoot '..\..\src\Languages\Data\TranslatedPercentages.json')
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'Languages\LanguageData.psm1') -Force
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
|
||||
$script:TranslatorCreditsKey = '0 0 0 Contributors, please add your names/usernames separated by comas (for credit purposes). DO NOT Translate this entry'
|
||||
|
||||
function Read-JsonObject {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
$content = [System.IO.File]::ReadAllText($Path)
|
||||
if ([string]::IsNullOrWhiteSpace($content)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
$parsed = $content | ConvertFrom-Json -AsHashtable
|
||||
if ($null -eq $parsed) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
if (-not ($parsed -is [System.Collections.IDictionary])) {
|
||||
throw "JSON root must be an object: $Path"
|
||||
}
|
||||
|
||||
return $parsed
|
||||
}
|
||||
|
||||
function ConvertTo-NormalizedJson {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowNull()]
|
||||
[object]$Value
|
||||
)
|
||||
|
||||
return ($Value | ConvertTo-Json -Depth 10)
|
||||
}
|
||||
|
||||
function ConvertTo-TranslatorMetadataJson {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Map
|
||||
)
|
||||
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
$lines.Add('{')
|
||||
|
||||
$rootEntries = @($Map.GetEnumerator())
|
||||
for ($index = 0; $index -lt $rootEntries.Count; $index++) {
|
||||
$entry = $rootEntries[$index]
|
||||
$key = ConvertTo-JsonStringLiteral -Value ([string]$entry.Key)
|
||||
$translators = @($entry.Value)
|
||||
$isLastRootEntry = $index -eq ($rootEntries.Count - 1)
|
||||
|
||||
if ($translators.Count -eq 0) {
|
||||
$line = ' {0}: []' -f $key
|
||||
if (-not $isLastRootEntry) {
|
||||
$line += ','
|
||||
}
|
||||
|
||||
$lines.Add($line)
|
||||
continue
|
||||
}
|
||||
|
||||
$lines.Add(' {0}: [' -f $key)
|
||||
for ($translatorIndex = 0; $translatorIndex -lt $translators.Count; $translatorIndex++) {
|
||||
$translator = $translators[$translatorIndex]
|
||||
$isLastTranslator = $translatorIndex -eq ($translators.Count - 1)
|
||||
$name = ConvertTo-JsonStringLiteral -Value ([string]$translator['name'])
|
||||
$link = ConvertTo-JsonStringLiteral -Value ([string]$translator['link'])
|
||||
|
||||
$lines.Add(' {')
|
||||
$lines.Add(' "name": {0},' -f $name)
|
||||
$lines.Add(' "link": {0}' -f $link)
|
||||
|
||||
$translatorClosing = ' }'
|
||||
if (-not $isLastTranslator) {
|
||||
$translatorClosing += ','
|
||||
}
|
||||
|
||||
$lines.Add($translatorClosing)
|
||||
}
|
||||
|
||||
$rootClosing = ' ]'
|
||||
if (-not $isLastRootEntry) {
|
||||
$rootClosing += ','
|
||||
}
|
||||
|
||||
$lines.Add($rootClosing)
|
||||
}
|
||||
|
||||
$lines.Add('}')
|
||||
return (($lines -join "`r`n") + "`r`n")
|
||||
}
|
||||
|
||||
function Get-RequestedLanguageCodes {
|
||||
$languageReference = Get-LanguageReference
|
||||
$codesToUpdate = @()
|
||||
|
||||
if ($AllLanguages.IsPresent) {
|
||||
$codesToUpdate = @(
|
||||
$languageReference.Keys |
|
||||
Where-Object { $_ -ne 'default' } |
|
||||
Sort-Object
|
||||
)
|
||||
}
|
||||
else {
|
||||
$codesToUpdate = @(
|
||||
$LanguageCodes |
|
||||
ForEach-Object { $_.Trim() } |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
Sort-Object -Unique
|
||||
)
|
||||
}
|
||||
|
||||
if ($codesToUpdate.Count -eq 0) {
|
||||
throw 'No language codes were provided.'
|
||||
}
|
||||
|
||||
foreach ($code in $codesToUpdate) {
|
||||
if (-not $languageReference.Contains($code)) {
|
||||
throw "Unknown language code '$code'."
|
||||
}
|
||||
}
|
||||
|
||||
return $codesToUpdate
|
||||
}
|
||||
|
||||
function Get-ExpectedTranslationPercentages {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$CodesToUpdate,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$includeEnglish = $AllLanguages.IsPresent -or ($CodesToUpdate -contains 'en')
|
||||
$statusJson = & (Join-Path $PSScriptRoot 'Get-TranslationStatus.ps1') -OutputFormat Json -IncludeEnglish:$includeEnglish
|
||||
$statusRows = $statusJson | ConvertFrom-Json -AsHashtable
|
||||
if ($null -eq $statusRows) {
|
||||
throw 'Could not load translation status data.'
|
||||
}
|
||||
|
||||
$statusByCode = @{}
|
||||
foreach ($row in $statusRows) {
|
||||
$statusByCode[[string]$row.Code] = $row
|
||||
}
|
||||
|
||||
$storedPercentages = Read-JsonObject -Path $Path
|
||||
$updatedPercentages = [ordered]@{}
|
||||
foreach ($entry in $storedPercentages.GetEnumerator()) {
|
||||
$updatedPercentages[[string]$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
|
||||
foreach ($code in $CodesToUpdate) {
|
||||
if (-not $statusByCode.ContainsKey($code)) {
|
||||
throw "Language code '$code' was not found in the computed translation status."
|
||||
}
|
||||
|
||||
$updatedPercentages[$code] = [string]$statusByCode[$code].Completion
|
||||
}
|
||||
|
||||
return $updatedPercentages
|
||||
}
|
||||
|
||||
function Get-ExpectedTranslatorMetadata {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$CodesToUpdate,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$languagesDirectory = Get-LanguagesDirectoryPath
|
||||
$englishLanguageFilePath = Join-Path $languagesDirectory 'lang_en.json'
|
||||
$storedTranslators = Read-JsonObject -Path $Path
|
||||
$updatedTranslators = [ordered]@{}
|
||||
foreach ($entry in $storedTranslators.GetEnumerator()) {
|
||||
$updatedTranslators[[string]$entry.Key] = @($entry.Value)
|
||||
}
|
||||
|
||||
$englishLanguageMap = Read-OrderedJsonMap -Path $englishLanguageFilePath
|
||||
$hasCreditsKey = $englishLanguageMap.Contains($script:TranslatorCreditsKey)
|
||||
|
||||
foreach ($code in $CodesToUpdate) {
|
||||
$languageFilePath = Join-Path $languagesDirectory ("lang_{0}.json" -f $code)
|
||||
if (-not (Test-Path -Path $languageFilePath -PathType Leaf)) {
|
||||
throw "Language file not found: $languageFilePath"
|
||||
}
|
||||
|
||||
$languageMap = Read-OrderedJsonMap -Path $languageFilePath
|
||||
if (-not $hasCreditsKey -or -not $languageMap.Contains($script:TranslatorCreditsKey)) {
|
||||
if (-not $updatedTranslators.Contains($code)) {
|
||||
$updatedTranslators[$code] = @()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
$englishCreditsSignature = Get-TranslatorCreditsSignature -Credits ([string]$englishLanguageMap[$script:TranslatorCreditsKey])
|
||||
$credits = [string]$languageMap[$script:TranslatorCreditsKey]
|
||||
$creditsSignature = Get-TranslatorCreditsSignature -Credits $credits
|
||||
$shouldPreserveExisting = $code -ne 'en' -and $creditsSignature -eq $englishCreditsSignature
|
||||
|
||||
if ($shouldPreserveExisting) {
|
||||
if (-not $updatedTranslators.Contains($code)) {
|
||||
$updatedTranslators[$code] = @()
|
||||
}
|
||||
|
||||
Write-Warning "Preserving existing translator metadata for '$code' because its locale credits currently match the English fallback credits."
|
||||
continue
|
||||
}
|
||||
|
||||
$updatedTranslators[$code] = @(ConvertTo-TranslatorMetadataEntries -Credits $credits)
|
||||
}
|
||||
|
||||
return $updatedTranslators
|
||||
}
|
||||
|
||||
function Get-TranslatorCreditsSignature {
|
||||
param(
|
||||
[AllowNull()]
|
||||
[string]$Credits
|
||||
)
|
||||
|
||||
$entries = @(Get-TranslatorsFromCredits -Credits $Credits)
|
||||
if ($entries.Count -eq 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return @(
|
||||
$entries |
|
||||
ForEach-Object { ([string]$_.name).Trim().ToLowerInvariant() }
|
||||
) -join "`n"
|
||||
}
|
||||
|
||||
function ConvertTo-TranslatorMetadataEntries {
|
||||
param(
|
||||
[AllowNull()]
|
||||
[string]$Credits
|
||||
)
|
||||
|
||||
return @(
|
||||
Get-TranslatorsFromCredits -Credits $Credits |
|
||||
ForEach-Object {
|
||||
[ordered]@{
|
||||
name = [string]$_.name
|
||||
link = [string]$_.link
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Write-TranslationDocumentation {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
New-Utf8File -Path $Path -Content (Get-TranslationDocumentationMarkdown)
|
||||
}
|
||||
|
||||
$shouldUpdateTranslationDoc = $UpdateTranslationDoc.IsPresent
|
||||
if ($UpdateReadme.IsPresent) {
|
||||
Write-Warning 'The -UpdateReadme switch is deprecated. Updating TRANSLATION.md instead.'
|
||||
$shouldUpdateTranslationDoc = $true
|
||||
}
|
||||
|
||||
$codesToUpdate = Get-RequestedLanguageCodes
|
||||
$expectedPercentages = Get-ExpectedTranslationPercentages -CodesToUpdate $codesToUpdate -Path $TranslatedPercentagesPath
|
||||
$expectedTranslators = Get-ExpectedTranslatorMetadata -CodesToUpdate $codesToUpdate -Path $TranslatorsPath
|
||||
|
||||
$actualPercentages = Read-JsonObject -Path $TranslatedPercentagesPath
|
||||
$actualTranslators = Read-JsonObject -Path $TranslatorsPath
|
||||
|
||||
$driftedFiles = New-Object System.Collections.Generic.List[string]
|
||||
if ((ConvertTo-NormalizedJson -Value $actualPercentages) -ne (ConvertTo-NormalizedJson -Value $expectedPercentages)) {
|
||||
$driftedFiles.Add((Get-TranslatedPercentagesJsonPath))
|
||||
}
|
||||
|
||||
if ((ConvertTo-NormalizedJson -Value $actualTranslators) -ne (ConvertTo-NormalizedJson -Value $expectedTranslators)) {
|
||||
$driftedFiles.Add((Get-TranslatorsJsonPath))
|
||||
}
|
||||
|
||||
Write-Output ('Translation metadata scope: ' + ($codesToUpdate -join ', '))
|
||||
Write-Output ('Metadata drift detected: ' + $driftedFiles.Count)
|
||||
|
||||
if ($driftedFiles.Count -gt 0) {
|
||||
foreach ($path in $driftedFiles) {
|
||||
Write-Output (' * ' + $path)
|
||||
}
|
||||
}
|
||||
|
||||
if ($CheckOnly.IsPresent) {
|
||||
if ($driftedFiles.Count -gt 0) {
|
||||
throw 'Translation metadata is out of date. Run scripts/translation/Sync-TranslationMetadata.ps1 to refresh it.'
|
||||
}
|
||||
|
||||
Write-Output 'Translation metadata is up to date.'
|
||||
return
|
||||
}
|
||||
|
||||
if ($driftedFiles.Contains((Get-TranslatedPercentagesJsonPath)) -and $PSCmdlet.ShouldProcess($TranslatedPercentagesPath, 'Update translated percentages metadata')) {
|
||||
New-Utf8File -Path $TranslatedPercentagesPath -Content ((ConvertTo-NormalizedJson -Value $expectedPercentages) + "`r`n")
|
||||
}
|
||||
|
||||
if ($driftedFiles.Contains((Get-TranslatorsJsonPath)) -and $PSCmdlet.ShouldProcess($TranslatorsPath, 'Update translator metadata')) {
|
||||
New-Utf8File -Path $TranslatorsPath -Content (ConvertTo-TranslatorMetadataJson -Map $expectedTranslators)
|
||||
}
|
||||
|
||||
if ($shouldUpdateTranslationDoc -and $PSCmdlet.ShouldProcess($TranslationDocPath, 'Update translation documentation')) {
|
||||
Write-TranslationDocumentation -Path $TranslationDocPath
|
||||
}
|
||||
|
||||
Write-Output 'Translation metadata sync completed.'
|
||||
@@ -0,0 +1,212 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$RepositoryRoot,
|
||||
[string]$EnglishFilePath,
|
||||
[string]$InventoryOutputPath,
|
||||
[switch]$UseLegacyBoundary,
|
||||
[switch]$CheckOnly
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationSourceTools.ps1')
|
||||
|
||||
$resolvedRepositoryRoot = Resolve-TranslationSyncRepositoryRoot -RepositoryRoot $RepositoryRoot
|
||||
$resolvedEnglishFilePath = Resolve-EnglishLanguageFilePath -ResolvedRepositoryRoot $resolvedRepositoryRoot -EnglishFilePath $EnglishFilePath
|
||||
|
||||
if (-not (Test-Path -Path $resolvedEnglishFilePath -PathType Leaf)) {
|
||||
throw "English translation file not found: $resolvedEnglishFilePath"
|
||||
}
|
||||
|
||||
$snapshot = Get-TranslationSourceSnapshot -RepositoryRoot $resolvedRepositoryRoot
|
||||
$currentMap = Read-OrderedJsonMapPermissive -Path $resolvedEnglishFilePath
|
||||
$currentSections = Split-TranslationMapAtBoundary -Map $currentMap
|
||||
$preserveLegacyBoundary = $UseLegacyBoundary -or $currentSections.HasBoundary
|
||||
|
||||
$extractedKeySet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($key in $snapshot.KeyOrder) {
|
||||
[void]$extractedKeySet.Add([string]$key)
|
||||
}
|
||||
|
||||
$currentKeySet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
foreach ($entry in $currentMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($key -ceq (Get-TranslationLegacyBoundaryKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
[void]$currentKeySet.Add($key)
|
||||
}
|
||||
|
||||
$missingKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($key in $snapshot.KeyOrder) {
|
||||
if (-not $currentKeySet.Contains($key)) {
|
||||
$missingKeys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
$unusedKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $currentMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($key -ceq (Get-TranslationLegacyBoundaryKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $extractedKeySet.Contains($key)) {
|
||||
$unusedKeys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
$preservedLegacyMap = New-OrderedStringMap
|
||||
$misplacedLegacyKeys = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $currentSections.ActiveMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if (-not $extractedKeySet.Contains($key)) {
|
||||
$misplacedLegacyKeys.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
$activeKeysBelowBoundary = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in $currentSections.LegacyMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($extractedKeySet.Contains($key)) {
|
||||
$activeKeysBelowBoundary.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entry in $currentMap.GetEnumerator()) {
|
||||
$key = [string]$entry.Key
|
||||
if ($key -ceq (Get-TranslationLegacyBoundaryKey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $extractedKeySet.Contains($key)) {
|
||||
$preservedLegacyMap[$key] = [string]$entry.Value
|
||||
}
|
||||
}
|
||||
|
||||
$syncedActiveMap = New-OrderedStringMap
|
||||
foreach ($key in $snapshot.KeyOrder) {
|
||||
$syncedActiveMap[$key] = if ($currentKeySet.Contains($key)) { [string]$currentMap[$key] } else { $key }
|
||||
}
|
||||
|
||||
$syncedMap = if ($preserveLegacyBoundary) {
|
||||
Join-TranslationMapWithBoundary -ActiveMap $syncedActiveMap -LegacyMap $preservedLegacyMap -IncludeBoundary
|
||||
}
|
||||
else {
|
||||
$syncedActiveMap
|
||||
}
|
||||
|
||||
$hasChanges = -not (Test-OrderedStringMapsEqual -Left $currentMap -Right $syncedMap)
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($InventoryOutputPath)) {
|
||||
$resolvedInventoryOutputPath = Get-FullPath -Path $InventoryOutputPath
|
||||
$inventory = [pscustomobject]@{
|
||||
generatedAt = (Get-Date).ToString('o')
|
||||
repositoryRoot = $resolvedRepositoryRoot
|
||||
englishFile = (Get-RepoRelativePath -RepositoryRoot $resolvedRepositoryRoot -FilePath $resolvedEnglishFilePath)
|
||||
legacyBoundaryEnabled = $preserveLegacyBoundary
|
||||
legacyBoundaryPresent = $currentSections.HasBoundary
|
||||
legacyBoundaryKey = Get-TranslationLegacyBoundaryKey
|
||||
scannedSourceFileCount = @($snapshot.SourceFiles).Count
|
||||
extractedKeyCount = @($snapshot.KeyOrder).Count
|
||||
activeKeyCount = $syncedActiveMap.Count
|
||||
legacyKeyCount = $preservedLegacyMap.Count
|
||||
missingKeyCount = $missingKeys.Count
|
||||
unusedKeyCount = $unusedKeys.Count
|
||||
misplacedLegacyKeyCount = $misplacedLegacyKeys.Count
|
||||
activeKeysBelowBoundaryCount = $activeKeysBelowBoundary.Count
|
||||
warningCount = @($snapshot.Warnings).Count
|
||||
missingKeys = $missingKeys.ToArray()
|
||||
unusedKeys = $unusedKeys.ToArray()
|
||||
legacyKeys = @($preservedLegacyMap.Keys)
|
||||
misplacedLegacyKeys = $misplacedLegacyKeys.ToArray()
|
||||
activeKeysBelowBoundary = $activeKeysBelowBoundary.ToArray()
|
||||
warnings = @($snapshot.Warnings | Sort-Object Path, Line, Type | ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
Path = $_.Path
|
||||
Line = $_.Line
|
||||
Type = $_.Type
|
||||
Message = $_.Message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
New-Utf8File -Path $resolvedInventoryOutputPath -Content ($inventory | ConvertTo-Json -Depth 6)
|
||||
}
|
||||
|
||||
Write-Output "Translation source synchronization summary"
|
||||
Write-Output "Repository root: $resolvedRepositoryRoot"
|
||||
Write-Output "English file: $resolvedEnglishFilePath"
|
||||
Write-Output "Legacy boundary enabled: $preserveLegacyBoundary"
|
||||
Write-Output "Legacy boundary present: $($currentSections.HasBoundary)"
|
||||
Write-Output "Scanned source files: $(@($snapshot.SourceFiles).Count)"
|
||||
Write-Output "Extracted keys: $(@($snapshot.KeyOrder).Count)"
|
||||
Write-Output "Missing English keys: $($missingKeys.Count)"
|
||||
Write-Output "Unused English keys: $($unusedKeys.Count)"
|
||||
Write-Output "Legacy English keys: $($preservedLegacyMap.Count)"
|
||||
Write-Output "Misplaced legacy keys: $($misplacedLegacyKeys.Count)"
|
||||
Write-Output "Active keys below boundary: $($activeKeysBelowBoundary.Count)"
|
||||
Write-Output "Warnings: $(@($snapshot.Warnings).Count)"
|
||||
|
||||
if ($missingKeys.Count -gt 0) {
|
||||
Write-Output ''
|
||||
Write-Output 'Missing keys to add:'
|
||||
foreach ($key in $missingKeys) {
|
||||
Write-Output (" + {0}" -f $key)
|
||||
}
|
||||
}
|
||||
|
||||
if ((-not $preserveLegacyBoundary) -and $unusedKeys.Count -gt 0) {
|
||||
Write-Output ''
|
||||
Write-Output 'Unused keys to remove:'
|
||||
foreach ($key in $unusedKeys) {
|
||||
Write-Output (" - {0}" -f $key)
|
||||
}
|
||||
}
|
||||
|
||||
if ($misplacedLegacyKeys.Count -gt 0) {
|
||||
Write-Output ''
|
||||
Write-Output 'Legacy keys that should move below the boundary:'
|
||||
foreach ($key in $misplacedLegacyKeys) {
|
||||
Write-Output (" v {0}" -f $key)
|
||||
}
|
||||
}
|
||||
|
||||
if ($activeKeysBelowBoundary.Count -gt 0) {
|
||||
Write-Output ''
|
||||
Write-Output 'Active keys currently below the boundary:'
|
||||
foreach ($key in $activeKeysBelowBoundary) {
|
||||
Write-Output (" ^ {0}" -f $key)
|
||||
}
|
||||
}
|
||||
|
||||
if (@($snapshot.Warnings).Count -gt 0) {
|
||||
Write-Output ''
|
||||
Write-Output 'Warnings:'
|
||||
foreach ($warning in $snapshot.Warnings | Sort-Object Path, Line, Type) {
|
||||
Write-Output (" ! {0}:{1} [{2}] {3}" -f $warning.Path, $warning.Line, $warning.Type, $warning.Message)
|
||||
}
|
||||
}
|
||||
|
||||
if ($CheckOnly) {
|
||||
if ($hasChanges) {
|
||||
throw 'lang_en.json is out of sync with extracted translation sources.'
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $hasChanges) {
|
||||
Write-Output ''
|
||||
Write-Output 'lang_en.json is already synchronized with extracted source usage.'
|
||||
return
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($resolvedEnglishFilePath, 'Synchronize English translation keys from source usage')) {
|
||||
Write-OrderedJsonMap -Path $resolvedEnglishFilePath -Map $syncedMap
|
||||
Write-Output ''
|
||||
Write-Output 'lang_en.json was synchronized successfully.'
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
[CmdletBinding(DefaultParameterSetName = 'Selected')]
|
||||
param(
|
||||
[Parameter(ParameterSetName = 'Selected', Mandatory = $true)]
|
||||
[string[]]$LanguageCodes,
|
||||
|
||||
[Parameter(ParameterSetName = 'All')]
|
||||
[switch]$AllLanguages,
|
||||
|
||||
[switch]$UpdateTranslationDoc,
|
||||
|
||||
[switch]$UpdateReadme,
|
||||
|
||||
[string]$ReadmePath = (Join-Path $PSScriptRoot '..\..\README.md'),
|
||||
|
||||
[string]$TranslationDocPath = (Join-Path $PSScriptRoot '..\..\TRANSLATION.md'),
|
||||
|
||||
[string]$TranslatedPercentagesPath = (Join-Path $PSScriptRoot '..\..\src\Languages\Data\TranslatedPercentages.json')
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'Languages\LanguageData.psm1') -Force
|
||||
|
||||
function Read-JsonObject {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -Path $Path -PathType Leaf)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
$content = [System.IO.File]::ReadAllText($Path)
|
||||
if ([string]::IsNullOrWhiteSpace($content)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
$parsed = $content | ConvertFrom-Json -AsHashtable
|
||||
if ($null -eq $parsed) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
if (-not ($parsed -is [System.Collections.IDictionary])) {
|
||||
throw "JSON root must be an object: $Path"
|
||||
}
|
||||
|
||||
return $parsed
|
||||
}
|
||||
|
||||
function Write-Utf8File {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[AllowEmptyString()]
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
$encoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
|
||||
}
|
||||
|
||||
function Write-TranslationDocumentation {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$encoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($Path, (Get-TranslationDocumentationMarkdown), $encoding)
|
||||
}
|
||||
|
||||
$shouldUpdateTranslationDoc = $UpdateTranslationDoc.IsPresent
|
||||
if ($UpdateReadme.IsPresent) {
|
||||
Write-Warning 'The -UpdateReadme switch is deprecated. Updating TRANSLATION.md instead.'
|
||||
$shouldUpdateTranslationDoc = $true
|
||||
}
|
||||
|
||||
$statusJson = & (Join-Path $PSScriptRoot 'Get-TranslationStatus.ps1') -OutputFormat Json
|
||||
$statusRows = $statusJson | ConvertFrom-Json -AsHashtable
|
||||
if ($null -eq $statusRows) {
|
||||
throw 'Could not load translation status data.'
|
||||
}
|
||||
|
||||
$statusByCode = @{}
|
||||
foreach ($row in $statusRows) {
|
||||
$statusByCode[[string]$row.Code] = $row
|
||||
}
|
||||
|
||||
$codesToUpdate = @()
|
||||
if ($AllLanguages.IsPresent) {
|
||||
$codesToUpdate = @($statusByCode.Keys | Sort-Object)
|
||||
}
|
||||
else {
|
||||
$codesToUpdate = @($LanguageCodes | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique)
|
||||
}
|
||||
|
||||
if ($codesToUpdate.Count -eq 0) {
|
||||
throw 'No language codes were provided.'
|
||||
}
|
||||
|
||||
$storedPercentages = Read-JsonObject -Path $TranslatedPercentagesPath
|
||||
$updatedPercentages = [ordered]@{}
|
||||
foreach ($entry in $storedPercentages.GetEnumerator()) {
|
||||
$updatedPercentages[[string]$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
|
||||
foreach ($code in $codesToUpdate) {
|
||||
if (-not $statusByCode.ContainsKey($code)) {
|
||||
throw "Language code '$code' was not found in the computed translation status."
|
||||
}
|
||||
|
||||
$updatedPercentages[$code] = [string]$statusByCode[$code].Completion
|
||||
}
|
||||
|
||||
$percentagesJson = ($updatedPercentages | ConvertTo-Json -Depth 5)
|
||||
Write-Utf8File -Path $TranslatedPercentagesPath -Content $percentagesJson
|
||||
|
||||
if ($shouldUpdateTranslationDoc) {
|
||||
Write-TranslationDocumentation -Path $TranslationDocPath
|
||||
}
|
||||
|
||||
Write-Output ('Updated translation status metadata for: ' + ($codesToUpdate -join ', '))
|
||||
@@ -0,0 +1,143 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationJsonTools.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Languages\TranslationSourceTools.ps1')
|
||||
|
||||
$repoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
|
||||
$syncScript = Join-Path $repoRoot 'scripts\translation\Sync-TranslationSources.ps1'
|
||||
|
||||
if (-not (Test-Path -Path $syncScript -PathType Leaf)) {
|
||||
throw "Sync script not found: $syncScript"
|
||||
}
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ('translation-source-sync-{0}' -f [System.Guid]::NewGuid().ToString('N'))
|
||||
|
||||
try {
|
||||
$sourceDir = Join-Path $tempRoot 'src\Sample'
|
||||
$languageDir = Join-Path $tempRoot 'src\Languages'
|
||||
New-Item -Path $sourceDir -ItemType Directory -Force | Out-Null
|
||||
New-Item -Path $languageDir -ItemType Directory -Force | Out-Null
|
||||
|
||||
New-Utf8File -Path (Join-Path $sourceDir 'Strings.cs') -Content @'
|
||||
using UniGetUI.Core.Tools;
|
||||
|
||||
namespace Sample;
|
||||
|
||||
internal static class Strings
|
||||
{
|
||||
public static void Use(int value)
|
||||
{
|
||||
_ = CoreTools.Translate("Literal from code");
|
||||
_ = CoreTools.AutoTranslated("Auto translated literal");
|
||||
_ = CoreTools.Translate($"Interpolated {value}");
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
New-Utf8File -Path (Join-Path $sourceDir 'View.xaml') -Content @'
|
||||
<Page xmlns:widgets="using:UniGetUI.Interface.Widgets">
|
||||
<widgets:TranslatedTextBlock Text="WinUI label" />
|
||||
</Page>
|
||||
'@
|
||||
|
||||
New-Utf8File -Path (Join-Path $sourceDir 'View.axaml') -Content @'
|
||||
<UserControl xmlns:t="clr-namespace:UniGetUI.Avalonia.MarkupExtensions"
|
||||
xmlns:settings="clr-namespace:UniGetUI.Avalonia.Views.Controls.Settings">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{t:Translate About UniGetUI}" />
|
||||
<TextBlock Text="{t:Translate Text='A, B, C'}" />
|
||||
<settings:TranslatedTextBlock Text="Avalonia header" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
'@
|
||||
|
||||
$englishPath = Join-Path $languageDir 'lang_en.json'
|
||||
$initialMap = New-OrderedStringMap
|
||||
$initialMap['Literal from code'] = 'Literal from code'
|
||||
$initialMap['Obsolete key'] = 'Obsolete key'
|
||||
Write-OrderedJsonMap -Path $englishPath -Map $initialMap
|
||||
|
||||
$snapshot = Get-TranslationSourceSnapshot -RepositoryRoot $tempRoot
|
||||
if (@($snapshot.Warnings).Count -ne 1) {
|
||||
throw "Expected exactly one warning for interpolated CoreTools.Translate, found $(@($snapshot.Warnings).Count)."
|
||||
}
|
||||
|
||||
$threwOnCheck = $false
|
||||
try {
|
||||
& $syncScript -RepositoryRoot $tempRoot -EnglishFilePath $englishPath -CheckOnly | Out-Null
|
||||
}
|
||||
catch {
|
||||
if ($_.Exception.Message -notlike '*out of sync*') {
|
||||
throw
|
||||
}
|
||||
|
||||
$threwOnCheck = $true
|
||||
}
|
||||
|
||||
if (-not $threwOnCheck) {
|
||||
throw 'Expected check-only mode to detect synchronization differences.'
|
||||
}
|
||||
|
||||
& $syncScript -RepositoryRoot $tempRoot -EnglishFilePath $englishPath | Out-Null
|
||||
|
||||
$updatedMap = Read-OrderedJsonMap -Path $englishPath
|
||||
$expectedKeys = @(
|
||||
'Literal from code',
|
||||
'Auto translated literal',
|
||||
'WinUI label',
|
||||
'Avalonia header',
|
||||
'About UniGetUI',
|
||||
'A, B, C'
|
||||
)
|
||||
|
||||
if ($updatedMap.Count -ne $expectedKeys.Count) {
|
||||
throw "Expected $($expectedKeys.Count) synchronized keys, found $($updatedMap.Count)."
|
||||
}
|
||||
|
||||
foreach ($expectedKey in $expectedKeys) {
|
||||
if (-not $updatedMap.Contains($expectedKey)) {
|
||||
throw "Expected synchronized English file to contain key '$expectedKey'."
|
||||
}
|
||||
}
|
||||
|
||||
if ($updatedMap.Contains('Obsolete key')) {
|
||||
throw 'Obsolete key should have been removed during synchronization.'
|
||||
}
|
||||
|
||||
& $syncScript -RepositoryRoot $tempRoot -EnglishFilePath $englishPath -CheckOnly | Out-Null
|
||||
|
||||
$boundaryKey = Get-TranslationLegacyBoundaryKey
|
||||
$boundaryMap = New-OrderedStringMap
|
||||
$boundaryMap['Literal from code'] = 'Literal from code'
|
||||
$boundaryMap['Obsolete key'] = 'Obsolete key'
|
||||
Write-OrderedJsonMap -Path $englishPath -Map $boundaryMap
|
||||
|
||||
& $syncScript -RepositoryRoot $tempRoot -EnglishFilePath $englishPath -UseLegacyBoundary | Out-Null
|
||||
|
||||
$boundaryUpdatedMap = Read-OrderedJsonMap -Path $englishPath
|
||||
$boundarySections = Split-TranslationMapAtBoundary -Map $boundaryUpdatedMap
|
||||
|
||||
if (-not $boundarySections.HasBoundary) {
|
||||
throw 'Expected the synchronized English file to include the legacy boundary marker.'
|
||||
}
|
||||
|
||||
if (-not $boundarySections.LegacyMap.Contains('Obsolete key')) {
|
||||
throw 'Obsolete key should have been preserved below the legacy boundary.'
|
||||
}
|
||||
|
||||
if ($boundarySections.ActiveMap.Contains('Obsolete key')) {
|
||||
throw 'Obsolete key should not remain in the active key section.'
|
||||
}
|
||||
|
||||
if (-not $boundaryUpdatedMap.Contains($boundaryKey)) {
|
||||
throw 'Expected the legacy boundary marker key to exist in the synchronized English file.'
|
||||
}
|
||||
|
||||
& $syncScript -RepositoryRoot $tempRoot -EnglishFilePath $englishPath -UseLegacyBoundary -CheckOnly | Out-Null
|
||||
|
||||
Write-Output 'Translation source sync smoke test completed successfully.'
|
||||
}
|
||||
finally {
|
||||
Remove-Item -Path $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$LanguagesDirectory
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$simplePlaceholderPattern = '^(?<name>[A-Za-z0-9_]+)(?:,[^}:]+)?(?::[^}]+)?$'
|
||||
$icuControlPattern = '^(?<name>[A-Za-z0-9_]+)\s*,\s*(?<kind>plural|select|selectordinal)\s*,(?<body>[\s\S]*)$'
|
||||
|
||||
function Get-RepositoryRoot {
|
||||
return [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
|
||||
}
|
||||
|
||||
function Resolve-LanguagesDirectory {
|
||||
if (-not [string]::IsNullOrWhiteSpace($LanguagesDirectory)) {
|
||||
return [System.IO.Path]::GetFullPath($LanguagesDirectory)
|
||||
}
|
||||
|
||||
return Join-Path (Get-RepositoryRoot) 'src\Languages'
|
||||
}
|
||||
|
||||
function Read-JsonObject {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$content = [System.IO.File]::ReadAllText($Path)
|
||||
if ([string]::IsNullOrWhiteSpace($content)) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
Assert-NoDuplicateJsonKeys -Content $content -Path $Path
|
||||
|
||||
$parsed = $content | ConvertFrom-Json -AsHashtable
|
||||
if ($null -eq $parsed) {
|
||||
return [ordered]@{}
|
||||
}
|
||||
|
||||
if (-not ($parsed -is [System.Collections.IDictionary])) {
|
||||
throw "JSON root must be an object: $Path"
|
||||
}
|
||||
|
||||
return $parsed
|
||||
}
|
||||
|
||||
function Assert-NoDuplicateJsonKeys {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$pattern = [regex]'(?m)^\s*"((?:\\.|[^"])*)"\s*:'
|
||||
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
||||
$duplicates = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
foreach ($match in $pattern.Matches($Content)) {
|
||||
$key = [regex]::Unescape($match.Groups[1].Value)
|
||||
if (-not $seen.Add($key) -and -not $duplicates.Contains($key)) {
|
||||
$duplicates.Add($key)
|
||||
}
|
||||
}
|
||||
|
||||
if ($duplicates.Count -gt 0) {
|
||||
throw "JSON file contains duplicate key(s): $($duplicates -join ', '). Path: $Path"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-BraceBlock {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Text,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$StartIndex
|
||||
)
|
||||
|
||||
if ($Text[$StartIndex] -ne '{') {
|
||||
throw "Expected '{' at index $StartIndex."
|
||||
}
|
||||
|
||||
$depth = 0
|
||||
for ($index = $StartIndex; $index -lt $Text.Length; $index++) {
|
||||
if ($Text[$index] -eq '{') {
|
||||
$depth += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if ($Text[$index] -ne '}') {
|
||||
continue
|
||||
}
|
||||
|
||||
$depth -= 1
|
||||
if ($depth -eq 0) {
|
||||
return [pscustomobject]@{
|
||||
Content = $Text.Substring($StartIndex + 1, $index - $StartIndex - 1)
|
||||
EndIndex = $index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Add-TokenCount {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$Counts,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Token,
|
||||
|
||||
[int]$Amount = 1
|
||||
)
|
||||
|
||||
if ($Counts.Contains($Token)) {
|
||||
$Counts[$Token] = [int]$Counts[$Token] + $Amount
|
||||
}
|
||||
else {
|
||||
$Counts[$Token] = $Amount
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SimplePlaceholderToken {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Content
|
||||
)
|
||||
|
||||
$trimmed = $Content.Trim()
|
||||
if ($trimmed -notmatch $simplePlaceholderPattern) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return '{' + $matches.name + '}'
|
||||
}
|
||||
|
||||
function Get-IcuBranchTokenCounts {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Body
|
||||
)
|
||||
|
||||
$maxCounts = [ordered]@{}
|
||||
$index = 0
|
||||
while ($index -lt $Body.Length) {
|
||||
while ($index -lt $Body.Length -and [char]::IsWhiteSpace($Body[$index])) {
|
||||
$index += 1
|
||||
}
|
||||
|
||||
if ($index -ge $Body.Length) {
|
||||
break
|
||||
}
|
||||
|
||||
if ($Body[$index] -eq '{') {
|
||||
$messageBlock = Get-BraceBlock -Text $Body -StartIndex $index
|
||||
if ($null -eq $messageBlock) {
|
||||
$index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
$branchCounts = Get-TokenCounts -Text $messageBlock.Content
|
||||
foreach ($token in $branchCounts.Keys) {
|
||||
$branchCount = [int]$branchCounts[$token]
|
||||
if ($maxCounts.Contains($token)) {
|
||||
$maxCounts[$token] = [Math]::Max([int]$maxCounts[$token], $branchCount)
|
||||
}
|
||||
else {
|
||||
$maxCounts[$token] = $branchCount
|
||||
}
|
||||
}
|
||||
|
||||
$index = $messageBlock.EndIndex + 1
|
||||
continue
|
||||
}
|
||||
|
||||
while ($index -lt $Body.Length -and -not [char]::IsWhiteSpace($Body[$index]) -and $Body[$index] -ne '{') {
|
||||
$index += 1
|
||||
}
|
||||
}
|
||||
|
||||
return $maxCounts
|
||||
}
|
||||
|
||||
function Get-TokenCounts {
|
||||
param(
|
||||
[AllowEmptyString()]
|
||||
[string]$Text
|
||||
)
|
||||
|
||||
$counts = [ordered]@{}
|
||||
if ($null -eq $Text) {
|
||||
return $counts
|
||||
}
|
||||
|
||||
$index = 0
|
||||
while ($index -lt $Text.Length) {
|
||||
if ($Text[$index] -ne '{') {
|
||||
$index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
$block = Get-BraceBlock -Text $Text -StartIndex $index
|
||||
if ($null -eq $block) {
|
||||
$index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
$content = $block.Content
|
||||
if ($content.Trim() -match $icuControlPattern) {
|
||||
$branchCounts = Get-IcuBranchTokenCounts -Body $matches.body
|
||||
foreach ($token in $branchCounts.Keys) {
|
||||
Add-TokenCount -Counts $counts -Token $token -Amount ([int]$branchCounts[$token])
|
||||
}
|
||||
}
|
||||
else {
|
||||
$token = Get-SimplePlaceholderToken -Content $content
|
||||
if ($null -ne $token) {
|
||||
Add-TokenCount -Counts $counts -Token $token
|
||||
}
|
||||
}
|
||||
|
||||
$index = $block.EndIndex + 1
|
||||
}
|
||||
|
||||
return $counts
|
||||
}
|
||||
|
||||
function Get-TokenMismatches {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$ExpectedCounts,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Collections.IDictionary]$ActualCounts
|
||||
)
|
||||
|
||||
$mismatches = New-Object System.Collections.Generic.List[string]
|
||||
$allTokens = @($ExpectedCounts.Keys + $ActualCounts.Keys | Sort-Object -Unique)
|
||||
|
||||
foreach ($token in $allTokens) {
|
||||
$expected = if ($ExpectedCounts.Contains($token)) { [int]$ExpectedCounts[$token] } else { 0 }
|
||||
$actual = if ($ActualCounts.Contains($token)) { [int]$ActualCounts[$token] } else { 0 }
|
||||
|
||||
if ($expected -eq $actual) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($actual -lt $expected) {
|
||||
$mismatches.Add("missing $token (expected $expected, found $actual)")
|
||||
}
|
||||
else {
|
||||
$mismatches.Add("unexpected $token (expected $expected, found $actual)")
|
||||
}
|
||||
}
|
||||
|
||||
return @($mismatches)
|
||||
}
|
||||
|
||||
$issues = 0
|
||||
$validatedFiles = 0
|
||||
|
||||
try {
|
||||
$resolvedLanguagesDirectory = Resolve-LanguagesDirectory
|
||||
if (-not (Test-Path -Path $resolvedLanguagesDirectory -PathType Container)) {
|
||||
throw "Languages directory not found: $resolvedLanguagesDirectory"
|
||||
}
|
||||
|
||||
$languageFiles = Get-ChildItem -Path $resolvedLanguagesDirectory -Filter 'lang_*.json' -File | Sort-Object Name
|
||||
foreach ($languageFile in $languageFiles) {
|
||||
try {
|
||||
$translations = Read-JsonObject -Path $languageFile.FullName
|
||||
}
|
||||
catch {
|
||||
$issues += 1
|
||||
Write-Output "[$($languageFile.Name)] Failed to parse JSON: $($_.Exception.Message)"
|
||||
continue
|
||||
}
|
||||
|
||||
$validatedFiles += 1
|
||||
foreach ($entry in $translations.GetEnumerator()) {
|
||||
$sourceText = [string]$entry.Key
|
||||
$translatedText = if ($null -eq $entry.Value) { '' } else { [string]$entry.Value }
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($translatedText)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$expectedCounts = Get-TokenCounts -Text $sourceText
|
||||
$actualCounts = Get-TokenCounts -Text $translatedText
|
||||
$mismatches = @(Get-TokenMismatches -ExpectedCounts $expectedCounts -ActualCounts $actualCounts)
|
||||
|
||||
if ($mismatches.Count -eq 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
$issues += 1
|
||||
Write-Output "[$($languageFile.Name)] Placeholder mismatch for key: $sourceText"
|
||||
Write-Output " Translation: $translatedText"
|
||||
foreach ($mismatch in $mismatches) {
|
||||
Write-Output " $mismatch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($issues -gt 0) {
|
||||
Write-Output "Translation verification failed with $issues issue(s)."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Output "Validated $validatedFiles translation file(s); no placeholder issues found."
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
Reference in New Issue
Block a user