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

497 lines
13 KiB
PowerShell

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'."
}
}