214 lines
7.0 KiB
PowerShell
214 lines
7.0 KiB
PowerShell
param(
|
||
[string]$LocalesRoot = "./public/locales",
|
||
[string]$MasterLocale = "en",
|
||
|
||
# Azure OpenAI settings
|
||
[string]$AzureOpenAIEndpoint = "",
|
||
[string]$AzureOpenAIApiKey = "",
|
||
[string]$AzureOpenAIDeployment = "" # e.g. "gpt-4o-mini"
|
||
)
|
||
|
||
if (-not $AzureOpenAIEndpoint -or -not $AzureOpenAIApiKey -or -not $AzureOpenAIDeployment) {
|
||
Write-Host "You must provide -AzureOpenAIEndpoint, -AzureOpenAIApiKey, and -AzureOpenAIDeployment." -ForegroundColor Yellow
|
||
exit 1
|
||
}
|
||
|
||
Write-Host "Current working directory: $(Get-Location)"
|
||
Write-Host "Locales root: $LocalesRoot"
|
||
Write-Host "Master locale: $MasterLocale"
|
||
|
||
# Simple in-memory cache to avoid re-translating identical strings
|
||
$TranslationCache = @{}
|
||
|
||
function Get-FromCache {
|
||
param(
|
||
[string]$text,
|
||
[string]$targetLocale
|
||
)
|
||
|
||
$key = "$targetLocale|$text"
|
||
if ($TranslationCache.ContainsKey($key)) {
|
||
return $TranslationCache[$key]
|
||
}
|
||
return $null
|
||
}
|
||
|
||
function Add-ToCache {
|
||
param(
|
||
[string]$text,
|
||
[string]$targetLocale,
|
||
[string]$translated
|
||
)
|
||
|
||
$key = "$targetLocale|$text"
|
||
$TranslationCache[$key] = $translated
|
||
}
|
||
|
||
function Translate-Text {
|
||
param(
|
||
[string]$text,
|
||
[string]$targetLocale
|
||
)
|
||
|
||
# Check cache first
|
||
$cached = Get-FromCache -text $text -targetLocale $targetLocale
|
||
if ($cached) {
|
||
return $cached
|
||
}
|
||
|
||
$headers = @{
|
||
"api-key" = $AzureOpenAIApiKey
|
||
"Content-Type" = "application/json"
|
||
}
|
||
|
||
$systemPrompt = "You are a translation engine. Translate the user text into $targetLocale. " +
|
||
"Return ONLY the translated text with no commentary, quotes, or extra formatting."
|
||
|
||
$body = @{
|
||
messages = @(
|
||
@{
|
||
role = "system"
|
||
content = $systemPrompt
|
||
},
|
||
@{
|
||
role = "user"
|
||
content = $text
|
||
}
|
||
)
|
||
temperature = 0
|
||
} | ConvertTo-Json -Depth 10
|
||
|
||
$url = "$AzureOpenAIEndpoint/openai/deployments/$AzureOpenAIDeployment/chat/completions?api-version=2024-02-15-preview"
|
||
|
||
$maxRetries = 3
|
||
$delaySeconds = 2
|
||
|
||
for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
|
||
try {
|
||
$response = Invoke-RestMethod `
|
||
-Uri $url `
|
||
-Method Post `
|
||
-Headers $headers `
|
||
-Body $body
|
||
|
||
$translated = $response.choices[0].message.content.Trim()
|
||
Add-ToCache -text $text -targetLocale $targetLocale -translated $translated
|
||
return $translated
|
||
}
|
||
catch {
|
||
Write-Host " Translation error (attempt $attempt): $($_.Exception.Message)" -ForegroundColor Red
|
||
if ($attempt -lt $maxRetries) {
|
||
Start-Sleep -Seconds $delaySeconds
|
||
} else {
|
||
Write-Host " Failed to translate after $maxRetries attempts. Falling back to source text." -ForegroundColor Yellow
|
||
Add-ToCache -text $text -targetLocale $targetLocale -translated $text
|
||
return $text
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function Merge-TranslationObjects {
|
||
param(
|
||
[hashtable]$master,
|
||
[hashtable]$target,
|
||
[string]$locale,
|
||
[string]$path = ""
|
||
)
|
||
|
||
foreach ($key in $master.Keys) {
|
||
$currentPath = if ($path) { "$path.$key" } else { $key }
|
||
|
||
if (-not $target.ContainsKey($key)) {
|
||
if ($master[$key] -is [string]) {
|
||
Write-Host " Missing key: $currentPath → translating to $locale"
|
||
$translated = Translate-Text -text $master[$key] -targetLocale $locale
|
||
$target[$key] = $translated
|
||
}
|
||
elseif ($master[$key] -is [hashtable]) {
|
||
$target[$key] = @{}
|
||
Merge-TranslationObjects -master $master[$key] -target $target[$key] -locale $locale -path $currentPath
|
||
}
|
||
else {
|
||
# Non-string leaf (number, bool, etc.) – just copy
|
||
$target[$key] = $master[$key]
|
||
}
|
||
}
|
||
else {
|
||
# Key exists – preserve existing value, but recurse into nested objects
|
||
if ($master[$key] -is [hashtable] -and $target[$key] -is [hashtable]) {
|
||
Merge-TranslationObjects -master $master[$key] -target $target[$key] -locale $locale -path $currentPath
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
$masterPath = Join-Path $LocalesRoot $MasterLocale
|
||
|
||
if (-not (Test-Path $masterPath)) {
|
||
Write-Host "Master locale folder not found at: $masterPath" -ForegroundColor Red
|
||
exit 1
|
||
}
|
||
|
||
$masterFiles = Get-ChildItem $masterPath -Filter *.json
|
||
|
||
if ($masterFiles.Count -eq 0) {
|
||
Write-Host "No JSON files found in master locale folder: $masterPath" -ForegroundColor Yellow
|
||
exit 0
|
||
}
|
||
|
||
$localeFolders = Get-ChildItem $LocalesRoot -Directory | Where-Object { $_.Name -ne $MasterLocale }
|
||
|
||
foreach ($localeFolder in $localeFolders) {
|
||
$localeName = $localeFolder.Name
|
||
Write-Host ""
|
||
Write-Host "Syncing locale: $localeName" -ForegroundColor Cyan
|
||
|
||
foreach ($file in $masterFiles) {
|
||
$masterFilePath = $file.FullName
|
||
$targetFilePath = Join-Path $localeFolder.FullName $file.Name
|
||
|
||
Write-Host " File: $($file.Name)"
|
||
Write-Host " Reading master file: $masterFilePath"
|
||
Write-Host " File exists: $(Test-Path $masterFilePath)"
|
||
Write-Host " File size: $((Get-Item $masterFilePath).Length) bytes"
|
||
|
||
$masterJsonRaw = Get-Content $masterFilePath -Raw -Encoding UTF8
|
||
|
||
try {
|
||
$masterJson = $masterJsonRaw | ConvertFrom-Json -AsHashtable
|
||
}
|
||
catch {
|
||
Write-Host " Error parsing master JSON: $masterFilePath" -ForegroundColor Red
|
||
continue
|
||
}
|
||
|
||
if (Test-Path $targetFilePath) {
|
||
$targetJsonRaw = Get-Content $targetFilePath -Raw -Encoding UTF8
|
||
try {
|
||
$targetJson = $targetJsonRaw | ConvertFrom-Json -AsHashtable
|
||
}
|
||
catch {
|
||
Write-Host " Error parsing target JSON, starting from empty: $targetFilePath" -ForegroundColor Yellow
|
||
$targetJson = @{}
|
||
}
|
||
} else {
|
||
Write-Host " Creating missing file: $($file.Name)"
|
||
$targetJson = @{}
|
||
}
|
||
|
||
Merge-TranslationObjects -master $masterJson -target $targetJson -locale $localeName
|
||
|
||
try {
|
||
$jsonOut = $targetJson | ConvertTo-Json -Depth 50
|
||
$jsonOut | Set-Content $targetFilePath -Encoding UTF8
|
||
Write-Host " Updated: $($file.Name)"
|
||
}
|
||
catch {
|
||
Write-Host " Error writing JSON to: $targetFilePath" -ForegroundColor Red
|
||
}
|
||
}
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host "All locales synced and translated (where needed)." -ForegroundColor Green |