Files
devolutions--unigetui/.agents/skills/translation-diff-export/scripts/export-translation-diff.ps1
T
wehub-resource-sync 643e9f9fcb
.NET Tests / test-codebase (push) Has been cancelled
Translation Validation / validate-translations (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:52 +08:00

509 lines
15 KiB
PowerShell

param(
[Parameter(Mandatory = $true)]
[string]$NeutralJson,
[Parameter(Mandatory = $true)]
[string]$TargetJson,
[Parameter(Mandatory = $true)]
[string]$Language,
[string]$BaseRef,
[string]$OutputDir = 'generated/translation-diff-export',
[switch]$ActiveOnly,
[switch]$KeepIntermediate
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot 'TranslationDiff.JsonTools.ps1')
function Get-GitJsonMap {
param(
[Parameter(Mandatory = $true)]
[string]$RepositoryRoot,
[Parameter(Mandatory = $true)]
[string]$BaseRef,
[Parameter(Mandatory = $true)]
[string]$RepoRelativePath
)
$gitSpec = '{0}:{1}' -f $BaseRef, $RepoRelativePath
$rawContent = & git -C $RepositoryRoot show $gitSpec 2>$null
if ($LASTEXITCODE -ne 0) {
return $null
}
$jsonText = [string]::Join([Environment]::NewLine, @($rawContent))
if ([string]::IsNullOrWhiteSpace($jsonText)) {
return $null
}
$tempPath = Join-Path $env:TEMP ('translation-diff-{0}.json' -f [System.Guid]::NewGuid().ToString('N'))
try {
New-Utf8File -Path $tempPath -Content $jsonText
return Read-OrderedJsonMap -Path $tempPath
}
finally {
Remove-Item -Path $tempPath -Force -ErrorAction SilentlyContinue
}
}
function Get-CirupKeySet {
param(
[Parameter(Mandatory = $true)]
[string[]]$Arguments
)
$rows = Invoke-CirupJson -Arguments $Arguments
$keys = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
foreach ($row in $rows) {
if (-not ($row -is [System.Collections.IDictionary]) -or -not $row.Contains('name')) {
throw 'cirup returned an unexpected JSON payload.'
}
[void]$keys.Add([string]$row['name'])
}
return $keys
}
function Get-EmptyTargetKeys {
param(
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$SourceMap,
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$TargetMap
)
$keys = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
foreach ($entry in $SourceMap.GetEnumerator()) {
$key = [string]$entry.Key
if (-not $TargetMap.Contains($key)) {
continue
}
if ([string]::IsNullOrWhiteSpace([string]$TargetMap[$key])) {
[void]$keys.Add($key)
}
}
return $keys
}
function Test-IntentionalSourceEqualValue {
param(
[Parameter(Mandatory = $true)]
[string]$LanguageCode,
[Parameter(Mandatory = $true)]
[AllowEmptyString()]
[string]$SourceValue,
[Parameter(Mandatory = $false)]
[AllowEmptyString()]
[string]$TargetValue = ''
)
if ($SourceValue -in @(
'OK',
'UniGetUI',
'UniGetUI - {0} {1}'
)) {
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 '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
}
. ([System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..\..\scripts\translation\Languages\IntentionalSourceEqualValues.ps1')))
function Sync-TranslatedWorkingCopy {
param(
[Parameter(Mandatory = $true)]
[string]$LanguageCode,
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$CurrentSourceMap,
[Parameter(Mandatory = $true)]
[string]$TranslatedPatchPath,
[System.Collections.IDictionary]$PreviousSourceMap
)
$translatedMap = Read-OrderedJsonMap -Path $TranslatedPatchPath
$syncedMap = New-OrderedStringMap
foreach ($entry in $CurrentSourceMap.GetEnumerator()) {
$key = [string]$entry.Key
$sourceValue = [string]$entry.Value
if (-not $translatedMap.Contains($key)) {
continue
}
$translatedValue = [string]$translatedMap[$key]
if ([string]::IsNullOrWhiteSpace($translatedValue)) {
continue
}
if ($translatedValue -ceq $sourceValue) {
if (-not (Test-IntentionalSourceEqualValue -LanguageCode $LanguageCode -SourceValue $sourceValue -TargetValue $translatedValue)) {
continue
}
}
if ($null -ne $PreviousSourceMap -and $PreviousSourceMap.Contains($key)) {
if ($translatedValue -ceq [string]$PreviousSourceMap[$key]) {
continue
}
}
$syncedMap[$key] = $translatedValue
}
Write-OrderedJsonMap -Path $TranslatedPatchPath -Map $syncedMap
}
function Get-FilteredPatchMap {
param(
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$Map,
[Parameter(Mandatory = $true)]
[System.Collections.IDictionary]$NeutralMap,
[Parameter(Mandatory = $true)]
[switch]$ActiveOnly
)
if (-not $ActiveOnly.IsPresent) {
return $Map
}
$activeKeys = Split-TranslationMapAtBoundary -Map $NeutralMap
$filteredMap = New-OrderedStringMap
foreach ($entry in $activeKeys.ActiveMap.GetEnumerator()) {
$key = [string]$entry.Key
if ($Map.Contains($key)) {
$filteredMap[$key] = [string]$Map[$key]
}
}
return $filteredMap
}
Assert-Command -Name 'git'
Assert-Command -Name 'cirup'
$neutralPath = Get-FullPath -Path $NeutralJson
$targetPath = Get-FullPath -Path $TargetJson
if (-not (Test-Path -Path $neutralPath -PathType Leaf)) {
throw "Neutral JSON file not found: $neutralPath"
}
$languageCode = $Language.Trim()
if ([string]::IsNullOrWhiteSpace($languageCode)) {
throw 'Language must not be empty.'
}
$languagesReferencePath = Get-LanguagesReferencePath -NeutralJsonPath $neutralPath
Assert-LanguageCodeKnown -LanguagesReferencePath $languagesReferencePath -LanguageCode $languageCode
$outputRoot = Get-FullPath -Path $OutputDir
$tmpRoot = Join-Path $outputRoot 'tmp'
New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
New-Item -Path $tmpRoot -ItemType Directory -Force | Out-Null
$cirupTargetPath = $targetPath
if (-not (Test-Path -Path $targetPath -PathType Leaf)) {
$cirupTargetPath = Join-Path $tmpRoot 'target.empty.json'
Write-OrderedJsonMap -Path $cirupTargetPath -Map (New-OrderedStringMap)
}
$baseName = Get-PatchBaseName -NeutralJsonPath $neutralPath
$sourcePatchPath = Join-Path $outputRoot ('{0}.diff.{1}.source.json' -f $baseName, $languageCode)
$translatedPatchPath = Join-Path $outputRoot ('{0}.diff.{1}.translated.json' -f $baseName, $languageCode)
$referencePatchPath = Join-Path $outputRoot ('{0}.diff.{1}.reference.json' -f $baseName, $languageCode)
$promptPath = Join-Path $outputRoot ('{0}.diff.{1}.prompt.md' -f $baseName, $languageCode)
$neutralMap = Read-OrderedJsonMap -Path $neutralPath
$targetMap = Read-OrderedJsonMap -Path $targetPath
$baseSummary = 'No git baseline was provided. The patch contains untranslated UniGetUI entries only.'
$changedSourceKeys = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
$missingTargetKeys = Get-CirupKeySet -Arguments @('file-diff', $neutralPath, $cirupTargetPath)
$rawEqualEnglishKeys = Get-CirupKeySet -Arguments @('file-intersect', $neutralPath, $cirupTargetPath)
$equalEnglishKeys = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
$rawEqualEnglishKeys | ForEach-Object {
$key = [string]$_
if (-not (Test-IntentionalSourceEqualValue -LanguageCode $Language -SourceValue ([string]$neutralMap[$key]) -TargetValue ([string]$targetMap[$key]))) {
[void]$equalEnglishKeys.Add($key)
}
}
$emptyTargetKeys = Get-EmptyTargetKeys -SourceMap $neutralMap -TargetMap $targetMap
if (-not [string]::IsNullOrWhiteSpace($BaseRef)) {
$repoRoot = Get-RepositoryRoot -WorkingDirectory (Split-Path -Path $neutralPath -Parent)
$repoRelativeNeutralPath = Get-RepoRelativePath -RepositoryRoot $repoRoot -FilePath $neutralPath
$baseSourceMap = Get-GitJsonMap -RepositoryRoot $repoRoot -BaseRef $BaseRef -RepoRelativePath $repoRelativeNeutralPath
$safeBaseRef = Get-SafeLabel -Value $BaseRef
$baseSnapshotPath = Join-Path $tmpRoot ('{0}.baseline.{1}.json' -f $baseName, $safeBaseRef)
if ($null -ne $baseSourceMap) {
Write-OrderedJsonMap -Path $baseSnapshotPath -Map $baseSourceMap
$baseSummary = "Git baseline '$BaseRef' was used to include keys that are new or whose English value changed since that revision."
}
else {
Write-OrderedJsonMap -Path $baseSnapshotPath -Map (New-OrderedStringMap)
$baseSummary = "Git baseline '$BaseRef' did not contain the neutral JSON file, so all current English entries were treated as new for the changed-source delta."
}
$changedSourceKeys = Get-CirupKeySet -Arguments @('diff-with-base', $baseSnapshotPath, $neutralPath, $neutralPath)
}
$sourcePatchMap = New-OrderedStringMap
$referencePatchMap = New-OrderedStringMap
$keysToTranslate = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::Ordinal)
foreach ($key in $missingTargetKeys) {
[void]$keysToTranslate.Add([string]$key)
}
foreach ($key in $equalEnglishKeys) {
[void]$keysToTranslate.Add([string]$key)
}
foreach ($key in $emptyTargetKeys) {
[void]$keysToTranslate.Add([string]$key)
}
foreach ($key in $changedSourceKeys) {
[void]$keysToTranslate.Add([string]$key)
}
foreach ($entry in $neutralMap.GetEnumerator()) {
$key = [string]$entry.Key
$sourceValue = [string]$entry.Value
if ($keysToTranslate.Contains($key)) {
$sourcePatchMap[$key] = $sourceValue
continue
}
if ($targetMap.Contains($key)) {
$targetValue = [string]$targetMap[$key]
if (-not [string]::IsNullOrWhiteSpace($targetValue) -and $targetValue -cne $sourceValue) {
$referencePatchMap[$key] = $targetValue
}
}
}
$sourcePatchMap = Get-FilteredPatchMap -Map $sourcePatchMap -NeutralMap $neutralMap -ActiveOnly:$ActiveOnly
$referencePatchMap = Get-FilteredPatchMap -Map $referencePatchMap -NeutralMap $neutralMap -ActiveOnly:$ActiveOnly
if (Test-Path -Path $sourcePatchPath -PathType Leaf) {
$previousSourceMap = Read-OrderedJsonMap -Path $sourcePatchPath
}
else {
$previousSourceMap = $null
}
Write-OrderedJsonMap -Path $sourcePatchPath -Map $sourcePatchMap
Write-OrderedJsonMap -Path $referencePatchPath -Map $referencePatchMap
Sync-TranslatedWorkingCopy -LanguageCode $languageCode -CurrentSourceMap $sourcePatchMap -TranslatedPatchPath $translatedPatchPath -PreviousSourceMap $previousSourceMap
$mergedTargetPath = Join-Path ([System.IO.Path]::GetDirectoryName($targetPath)) ('{0}.merged.json' -f [System.IO.Path]::GetFileNameWithoutExtension($targetPath))
$skillsRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
$translateHandoffScript = Join-Path $skillsRoot 'translation-diff-translate\scripts\write-translation-handoff.ps1'
$validationScript = Join-Path $skillsRoot 'translation-diff-import\scripts\validate-language-file.ps1'
if (-not (Test-Path -Path $translateHandoffScript -PathType Leaf)) {
throw "Translation handoff script not found: $translateHandoffScript"
}
& $translateHandoffScript -BaseName $baseName -Language $languageCode -SourcePatch $sourcePatchPath -TranslatedPatch $translatedPatchPath -ReferencePatch $referencePatchPath -TargetJson $targetPath -NeutralJson $neutralPath -MergedTargetJson $mergedTargetPath -ValidationScript $validationScript -OutputPrompt $promptPath -BaseSummary $baseSummary
if (-not $KeepIntermediate.IsPresent) {
Remove-Item -Path $tmpRoot -Recurse -Force -ErrorAction SilentlyContinue
}
Write-Output "Created source patch: $sourcePatchPath"
Write-Output "Refreshed translated working copy: $translatedPatchPath"
Write-Output "Created reference patch: $referencePatchPath"
Write-Output "Created translation handoff prompt: $promptPath"
if ($ActiveOnly.IsPresent) {
Write-Output 'Patch scope: active translation keys only'
}
if ($KeepIntermediate.IsPresent) {
Write-Output "Kept intermediate files under: $tmpRoot"
}