解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
9.67 KiB
X
# 多语言词表一致性校验:键集合、占位符、格式说明符、孤儿键
# 以基准语言文件(默认 zh-Hans.yml)为准,所有语言必须提供完全相同的键集合;
# metadata-keys.json 登记的是「非源码来源键」(属性元数据、语言名等),它们同样要求全语言齐全,仅豁免孤儿键判定。
# en.yml 为可选:只需列出源文非英文的键,缺失的消息键自动回退英文源文。
#
# 用法:
#   pwsh -File Tools/Scripts/check-locales.ps1                                      # Core 默认目录
#   pwsh -File Tools/Scripts/check-locales.ps1 -LocaleDir src\Lib\Locales -SourceDir src\Lib
#   pwsh -File Tools/Scripts/check-locales.ps1 -LocaleDir src\Lib\Locales -SkipOrphan   # 下游只想校验词表内部一致性
#   pwsh -File Tools/Scripts/check-locales.ps1 -MetadataKeys src\Lib\Locales\metadata-keys.json
param(
    [String]$LocaleDir = "NewLife.Core\Locales",
    [String]$Baseline = "zh-Hans.yml",
    [String[]]$SourceDir = @("NewLife.Core", "NewLife.Core.SourceGenerator"),
    [String]$MetadataKeys,
    [Switch]$SkipOrphan
)

$ErrorActionPreference = "Stop"

# 脚本位于 Tools\Scripts,仓库根目录在其上两级
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$localeDir = if ([IO.Path]::IsPathRooted($LocaleDir)) { $LocaleDir } else { Join-Path $root $LocaleDir }
if (!(Test-Path $localeDir)) {
    Write-Host "未找到语言目录:$localeDir" -ForegroundColor Red
    exit 1
}

$pattern = '^\s*"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"\s*$'
$placeholder = '\{(\d+)(?:,-?\d+)?(:[^}]*)?\}'
$languageCode = '\b[a-z]{2}-[A-Za-z]{2,4}\b'

# 非源码来源键:属性元数据、语言名等,豁免孤儿键判定(仍要求全语言齐全)
if (!$MetadataKeys) {
    $candidate = Join-Path $localeDir "metadata-keys.json"
    if (Test-Path -LiteralPath $candidate) { $MetadataKeys = $candidate }
}
$metadataKeys = [Collections.Generic.HashSet[String]]::new([StringComparer]::Ordinal)
if ($MetadataKeys) {
    $metadataPath = if ([IO.Path]::IsPathRooted($MetadataKeys)) { $MetadataKeys } else { Join-Path $root $MetadataKeys }
    if (!(Test-Path -LiteralPath $metadataPath)) {
        Write-Host "未找到非源码来源键清单:$metadataPath" -ForegroundColor Red
        exit 1
    }
    foreach ($item in (Get-Content -LiteralPath $metadataPath -Encoding UTF8 -Raw | ConvertFrom-Json).keys) {
        if (![String]::IsNullOrEmpty($item)) { [void]$metadataKeys.Add($item) }
    }
}

function Unescape-Yaml([string]$text) {
    # 与源生成器保持一致:还原 \\ \" \n \r \t \uXXXX
    return [regex]::Replace($text, '\\(u[0-9a-fA-F]{4}|.)', {
        param($m)
        $v = $m.Groups[1].Value
        if ($v.Length -eq 5) { return [string][char][Convert]::ToInt32($v.Substring(1), 16) }
        switch -CaseSensitive ($v) {
            'n' { return "`n" }
            'r' { return "`r" }
            't' { return "`t" }
            '\\' { return '\' }
            '"' { return '"' }
            default { return $m.Value }
        }
    })
}

function Get-Placeholders([string]$text) {
    $set = [System.Collections.Generic.HashSet[int]]::new()
    foreach ($m in [regex]::Matches($text, $placeholder)) {
        [void]$set.Add([int]$m.Groups[1].Value)
    }
    return ,$set
}

function Get-PlaceholderSpecs([string]$text) {
    $map = @{}
    foreach ($m in [regex]::Matches($text, $placeholder)) {
        $index = [int]$m.Groups[1].Value
        if (!$map.ContainsKey($index)) { $map[$index] = $m.Groups[2].Value }
    }
    return $map
}

# 语言清单条目:译文中的语言代码集合必须与英文 key 一致,防止译者漏码
function Test-LanguageCodeList([string]$key, [string]$value) {
    if (!$key.StartsWith("UI language. Values:")) { return $true }
    $expected = @([regex]::Matches($key, $languageCode) | ForEach-Object { $_.Value } | Sort-Object -Unique)
    $actual = @([regex]::Matches($value, $languageCode) | ForEach-Object { $_.Value } | Sort-Object -Unique)
    return !(Compare-Object $expected $actual)
}

function Read-Locale([string]$path) {
    $entries = [ordered]@{}
    $errors = @()
    $lineNo = 0
    foreach ($line in Get-Content -LiteralPath $path -Encoding UTF8) {
        $lineNo++
        if ($line.Trim().Length -eq 0 -or $line.TrimStart().StartsWith("#")) { continue }
        $m = [regex]::Match($line, $pattern)
        if (!$m.Success) {
            $errors += "第 $lineNo 行格式非法:$line"
            continue
        }
        $key = Unescape-Yaml $m.Groups[1].Value
        if ($entries.Contains($key)) {
            $errors += "第 $lineNo 行键重复:$key"
            continue
        }
        $entries[$key] = Unescape-Yaml $m.Groups[2].Value
    }
    return @{ Entries = $entries; Errors = $errors }
}

$referencePath = Join-Path $localeDir $Baseline
if (!(Test-Path -LiteralPath $referencePath)) {
    Write-Host "未找到基准语言文件:$referencePath" -ForegroundColor Red
    exit 1
}
$reference = Read-Locale $referencePath
$allErrors = @($reference.Errors | ForEach-Object { "$($Baseline): $_" })
$allWarnings = @()

foreach ($key in $reference.Entries.Keys) {
    if (!(Test-LanguageCodeList $key $reference.Entries[$key])) {
        $allErrors += "$($Baseline): 语言代码清单不一致 → $key"
    }
}

foreach ($file in Get-ChildItem $localeDir -Filter *.yml | Sort-Object Name) {
    if ($file.Name -eq $Baseline) { continue }
    # en 为可选词表:元数据源文非英文时用它提供英文译文,未列出的消息键回退英文源文
    $isEnglish = $file.BaseName -eq "en"
    $locale = Read-Locale $file.FullName
    foreach ($e in $locale.Errors) { $allErrors += "$($file.Name): $e" }

    if (!$isEnglish) {
        foreach ($key in $reference.Entries.Keys) {
            if ($locale.Entries.Contains($key)) { continue }
            $allErrors += "$($file.Name): 缺少键 $key"
        }
    }
    foreach ($key in $locale.Entries.Keys) {
        if (!$reference.Entries.Contains($key)) {
            $allErrors += "$($file.Name): 多余键 $key"
            continue
        }

        $value = $locale.Entries[$key]
        $expected = Get-Placeholders $key
        $actual = Get-Placeholders $value
        if (!$expected.SetEquals($actual)) {
            $allErrors += "$($file.Name): 占位符不一致 → $key"
            continue
        }

        # 格式说明符只在译文保留该占位符时比对,缺失时仅告警
        $enSpecs = Get-PlaceholderSpecs $key
        $trSpecs = Get-PlaceholderSpecs $value
        foreach ($index in $enSpecs.Keys) {
            if ($trSpecs.ContainsKey($index) -and $trSpecs[$index] -ne $enSpecs[$index]) {
                $allWarnings += "$($file.Name): 格式说明符不一致 → $key"
                break
            }
        }

        if (!(Test-LanguageCodeList $key $value)) {
            $allErrors += "$($file.Name): 语言代码清单不一致 → $key"
        }
    }
}

# 孤儿键:基准表中的键在源码里完全找不到(改文案后遗留的死条目)
# 源文范围含 .cs 与 .csproj:程序集标题/描述等元数据写在 csproj 中,同样是词表源文
$orphans = @()
$sourceDirs = @()
if (!$SkipOrphan) {
    $sourceDirs = @($SourceDir | ForEach-Object {
        if ([IO.Path]::IsPathRooted($_)) { $_ } else { Join-Path $root $_ }
    } | Where-Object { Test-Path $_ })

    $sourceText = ($sourceDirs | ForEach-Object { Get-ChildItem $_ -Recurse -Include *.cs, *.csproj } |
        Where-Object { $_.FullName -notmatch '\\(obj|bin|Locales)\\' } |
        ForEach-Object { [IO.File]::ReadAllText($_.FullName) }) -join "`n"

    $orphans = @()
    foreach ($key in $reference.Entries.Keys) {
        # 非源码来源键(元数据、语言名)由外部数据或其它来源引用,不参与源码归属判定
        if ($metadataKeys.Contains($key)) { continue }

        # 源码中的字面量形式:制表符写作 \t,引号写作 \"
        $escaped = $key.Replace('\', '\\').Replace('"', '\"').Replace("`t", '\t').Replace("`r", '\r').Replace("`n", '\n')
        if (!$sourceText.Contains($key) -and !$sourceText.Contains($escaped)) {
            $orphans += $key
        }
    }
    foreach ($o in $orphans) { $allErrors += "孤儿键(源码中找不到):$o" }
}

# 源码消息回填:显式调用 L.T / Localization.Translate 的字面量必须有词条,否则该处永远只能得到英文
if (!$SkipOrphan) {
    $callPattern = '(?:L\.T|L\.TLog|Localization\.Translate(?:ForLog)?)\s*\(\s*(?:[A-Za-z_][\w\.]*\s*,\s*)?"((?:[^"\\]|\\.)*)"'
    $missingMessages = [Collections.Generic.SortedSet[String]]::new([StringComparer]::Ordinal)
    foreach ($file in ($sourceDirs | ForEach-Object { Get-ChildItem $_ -Recurse -Filter *.cs } |
        Where-Object { $_.FullName -notmatch '\\(obj|bin|Locales)\\' })) {
        $text = [IO.File]::ReadAllText($file.FullName)
        foreach ($m in [regex]::Matches($text, $callPattern)) {
            $literal = $m.Groups[1].Value.Replace('\"', '"').Replace('\\', '\').Replace('\n', "`n").Replace('\r', "`r").Replace('\t', "`t")
            if ($literal.Length -eq 0 -or $reference.Entries.Contains($literal)) { continue }
            [void]$missingMessages.Add($literal)
        }
    }
    foreach ($key in $missingMessages) { $allErrors += "缺少词条(源码已调用 L.T):$key" }
}

foreach ($e in $allErrors) { Write-Host $e -ForegroundColor Red }
foreach ($w in $allWarnings) { Write-Host $w -ForegroundColor Yellow }

$count = (Get-ChildItem $localeDir -Filter *.yml).Count
if ($allErrors.Count -gt 0) {
    Write-Host "多语言词表校验未通过:$count 个语言,$($allErrors.Count) 个错误" -ForegroundColor Red
    exit 1
}

Write-Host "多语言词表校验通过:$count 个语言,基准 $($reference.Entries.Count) 条" -ForegroundColor Green