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

324 lines
11 KiB
PowerShell

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
}