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