[CmdletBinding()]
param(
[ValidateSet('zh-CN', 'en-US')][string]$Language = 'zh-CN',
[string]$ResourceDirectory = (Join-Path $PSScriptRoot '../Localization'),
[string]$OutputPath,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Read-Resources([string]$Path) {
$document = [System.Text.Json.JsonDocument]::Parse([IO.File]::ReadAllText($Path))
try {
if ($document.RootElement.ValueKind -ne [System.Text.Json.JsonValueKind]::Object) { throw "Resource root must be an object: $Path" }
$entries = [Collections.Generic.Dictionary[string,string]]::new([StringComparer]::Ordinal)
foreach ($property in $document.RootElement.EnumerateObject()) {
if ($property.Name -notmatch '^[A-Z][A-Za-z0-9]*$' -or $property.Name -in @('Language', 'Brand')) { throw "Invalid/reserved resource key: $($property.Name) in $Path" }
if ($property.Value.ValueKind -ne [System.Text.Json.JsonValueKind]::String) { throw "Resource must be a string: $($property.Name) in $Path" }
if ($entries.ContainsKey($property.Name)) { throw "Duplicate resource key: $($property.Name) in $Path" }
$value = $property.Value.GetString()
if ([string]::IsNullOrWhiteSpace($value)) { throw "Empty resource: $($property.Name) in $Path" }
$entries.Add($property.Name, $value)
}
return ,$entries
}
finally { $document.Dispose() }
}
function Get-Placeholders([string]$Value, [string]$Key) {
# Validate the same composite-format syntax that the C# runtime will use.
try { $null = [string]::Format([Globalization.CultureInfo]::InvariantCulture, $Value, [object[]](@(0) * 32)) }
catch { throw "Invalid format string for $Key : $Value" }
$unescaped = $Value.Replace('{{', '').Replace('}}', '')
return (([regex]::Matches($unescaped, '\{([0-9]+)(?:,[^}:]+)?(?::[^}]+)?\}') | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique) -join ',')
}
function CSharp-Literal([string]$Value) {
$result = $Value.Replace('\', '\\').Replace('"', '\"').Replace("`r", '\r').Replace("`n", '\n').Replace("`t", '\t')
return '"' + $result + '"'
}
$files = @(Get-ChildItem -LiteralPath $ResourceDirectory -Filter '*.zh-CN.json' | Sort-Object Name)
if ($files.Count -eq 0) { throw "No localization resources found: $ResourceDirectory" }
$all = [Collections.Generic.SortedDictionary[string,string]]::new([StringComparer]::Ordinal)
$domains = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
foreach ($file in $files) {
$domain = $file.Name.Substring(0, $file.Name.Length - '.zh-CN.json'.Length)
$null = $domains.Add($domain)
$englishPath = Join-Path $ResourceDirectory ($domain + '.en-US.json')
if (-not (Test-Path -LiteralPath $englishPath)) { throw "Missing English resource file: $englishPath" }
$chinese = Read-Resources $file.FullName
$english = Read-Resources $englishPath
if ($chinese.Count -ne $english.Count) { throw "Resource key count mismatch: $domain" }
foreach ($key in $chinese.Keys) {
if (-not $english.ContainsKey($key)) { throw "Missing English resource: $key" }
if ((Get-Placeholders $chinese[$key] $key) -cne (Get-Placeholders $english[$key] $key)) { throw "Resource placeholder mismatch: $key" }
if ($all.ContainsKey($key)) { throw "Duplicate resource key across domains: $key" }
$all.Add($key, $(if ($Language -eq 'zh-CN') { $chinese[$key] } else { $english[$key] }))
}
}
foreach ($file in Get-ChildItem -LiteralPath $ResourceDirectory -Filter '*.en-US.json') {
if (-not $domains.Contains($file.Name.Substring(0, $file.Name.Length - '.en-US.json'.Length))) { throw "Missing Chinese resource counterpart: $($file.Name)" }
}
if ($ValidateOnly) { Write-Host "XFE AMS resources: $($all.Count) matching keys validated."; return }
if ([string]::IsNullOrWhiteSpace($OutputPath)) { throw 'OutputPath is required unless ValidateOnly is set.' }
$lines = [Collections.Generic.List[string]]::new()
$lines.Add('// Generated from Localization/*.json. Edit resources, not this file.')
$lines.Add('namespace AutoMiningScript { public partial class Program { public static partial class L {')
$lines.Add('public const string Brand = "XFE AMS";')
$lines.Add('public const string Language = ' + (CSharp-Literal $Language) + ';')
foreach ($entry in $all.GetEnumerator()) { $lines.Add('public const string ' + $entry.Key + ' = ' + (CSharp-Literal $entry.Value) + ';') }
$lines.Add('} } }')
$content = ($lines -join "`n") + "`n"
$fullPath = [IO.Path]::GetFullPath($OutputPath)
$null = [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($fullPath))
if (-not [IO.File]::Exists($fullPath) -or [IO.File]::ReadAllText($fullPath) -cne $content) { [IO.File]::WriteAllText($fullPath, $content, [Text.UTF8Encoding]::new($false)) }
Write-Host "XFE AMS $Language resources: $($all.Count) strings."
[CmdletBinding()]
param(
[ValidateSet('zh-CN', 'en-US')][string]$Language = 'zh-CN',
[string]$ResourceDirectory = (Join-Path $PSScriptRoot '../Localization'),
[string]$OutputPath,
[switch]$ValidateOnly
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
function Read-Resources([string]$Path) {
$document = [System.Text.Json.JsonDocument]::Parse([IO.File]::ReadAllText($Path))
try {
if ($document.RootElement.ValueKind -ne [System.Text.Json.JsonValueKind]::Object) { throw "Resource root must be an object: $Path" }
$entries = [Collections.Generic.Dictionary[string,string]]::new([StringComparer]::Ordinal)
foreach ($property in $document.RootElement.EnumerateObject()) {
if ($property.Name -notmatch '^[A-Z][A-Za-z0-9]*$' -or $property.Name -in @('Language', 'Brand')) { throw "Invalid/reserved resource key: $($property.Name) in $Path" }
if ($property.Value.ValueKind -ne [System.Text.Json.JsonValueKind]::String) { throw "Resource must be a string: $($property.Name) in $Path" }
if ($entries.ContainsKey($property.Name)) { throw "Duplicate resource key: $($property.Name) in $Path" }
$value = $property.Value.GetString()
if ([string]::IsNullOrWhiteSpace($value)) { throw "Empty resource: $($property.Name) in $Path" }
$entries.Add($property.Name, $value)
}
return ,$entries
}
finally { $document.Dispose() }
}
function Get-Placeholders([string]$Value, [string]$Key) {
# Validate the same composite-format syntax that the C# runtime will use.
try { $null = [string]::Format([Globalization.CultureInfo]::InvariantCulture, $Value, [object[]](@(0) * 32)) }
catch { throw "Invalid format string for $Key : $Value" }
$unescaped = $Value.Replace('{{', '').Replace('}}', '')
return (([regex]::Matches($unescaped, '\{([0-9]+)(?:,[^}:]+)?(?::[^}]+)?\}') | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique) -join ',')
}
function CSharp-Literal([string]$Value) {
$result = $Value.Replace('\', '\\').Replace('"', '\"').Replace("`r", '\r').Replace("`n", '\n').Replace("`t", '\t')
return '"' + $result + '"'
}
$files = @(Get-ChildItem -LiteralPath $ResourceDirectory -Filter '*.zh-CN.json' | Sort-Object Name)
if ($files.Count -eq 0) { throw "No localization resources found: $ResourceDirectory" }
$all = [Collections.Generic.SortedDictionary[string,string]]::new([StringComparer]::Ordinal)
$domains = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
foreach ($file in $files) {
$domain = $file.Name.Substring(0, $file.Name.Length - '.zh-CN.json'.Length)
$null = $domains.Add($domain)
$englishPath = Join-Path $ResourceDirectory ($domain + '.en-US.json')
if (-not (Test-Path -LiteralPath $englishPath)) { throw "Missing English resource file: $englishPath" }
$chinese = Read-Resources $file.FullName
$english = Read-Resources $englishPath
if ($chinese.Count -ne $english.Count) { throw "Resource key count mismatch: $domain" }
foreach ($key in $chinese.Keys) {
if (-not $english.ContainsKey($key)) { throw "Missing English resource: $key" }
if ((Get-Placeholders $chinese[$key] $key) -cne (Get-Placeholders $english[$key] $key)) { throw "Resource placeholder mismatch: $key" }
if ($all.ContainsKey($key)) { throw "Duplicate resource key across domains: $key" }
$all.Add($key, $(if ($Language -eq 'zh-CN') { $chinese[$key] } else { $english[$key] }))
}
}
foreach ($file in Get-ChildItem -LiteralPath $ResourceDirectory -Filter '*.en-US.json') {
if (-not $domains.Contains($file.Name.Substring(0, $file.Name.Length - '.en-US.json'.Length))) { throw "Missing Chinese resource counterpart: $($file.Name)" }
}
if ($ValidateOnly) { Write-Host "XFE AMS resources: $($all.Count) matching keys validated."; return }
if ([string]::IsNullOrWhiteSpace($OutputPath)) { throw 'OutputPath is required unless ValidateOnly is set.' }
$lines = [Collections.Generic.List[string]]::new()
$lines.Add('// Generated from Localization/*.json. Edit resources, not this file.')
$lines.Add('namespace AutoMiningScript { public partial class Program { public static partial class L {')
$lines.Add('public const string Brand = "XFE AMS";')
$lines.Add('public const string Language = ' + (CSharp-Literal $Language) + ';')
foreach ($entry in $all.GetEnumerator()) { $lines.Add('public const string ' + $entry.Key + ' = ' + (CSharp-Literal $entry.Value) + ';') }
$lines.Add('} } }')
$content = ($lines -join "`n") + "`n"
$fullPath = [IO.Path]::GetFullPath($OutputPath)
$null = [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($fullPath))
if (-not [IO.File]::Exists($fullPath) -or [IO.File]::ReadAllText($fullPath) -cne $content) { [IO.File]::WriteAllText($fullPath, $content, [Text.UTF8Encoding]::new($false)) }
Write-Host "XFE AMS $Language resources: $($all.Count) strings."