From 88d21c21010160751bad741fb50868d10ecc3731 Mon Sep 17 00:00:00 2001 From: Colin Dawson Date: Tue, 17 Feb 2026 23:56:21 +0000 Subject: [PATCH] Workingon a script to use an LLM to do the translation work. --- RunMeToUpdateLocales.cmd | 18 ++++ sync-locales-azure.ps1 | 214 +++++++++++++++++++++++++++++++++++++++ sync-locales-github.ps1 | 185 +++++++++++++++++++++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 RunMeToUpdateLocales.cmd create mode 100644 sync-locales-azure.ps1 create mode 100644 sync-locales-github.ps1 diff --git a/RunMeToUpdateLocales.cmd b/RunMeToUpdateLocales.cmd new file mode 100644 index 0000000..61d639c --- /dev/null +++ b/RunMeToUpdateLocales.cmd @@ -0,0 +1,18 @@ +@echo off +rem https://github.com/settings/personal-access-tokens + +rem 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 + +rem this can store the github token in the environment variable GITHUB_TOKEN, which is used by the sync-locales.ps1 script +rem setx GITHUB_TOKEN "%(gh auth token)%" + +rem this is for github copilot, but it's blocked +rem pwsh ./sync-locales.ps1 -GitHubToken "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" +rem powershell -ExecutionPolicy Bypass -File sync-locales.ps1 -GitHubToken "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" +rem powershell -NoProfile -ExecutionPolicy Bypass -File "sync-locales.ps1" -GitHubToken "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" + +cd /d "%~dp0" + +echo Running Azure OpenAI locale sync... + +pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0sync-locales-azure.ps1" -AzureOpenAIEndpoint "https://cjdopenai.openai.azure.com/" -AzureOpenAIApiKey "9atLPrU05kMV2sFXCduLFfRJHnneF1KFZgmJqO42UuH0PxptagW3JQQJ99CBACmepeSXJ3w3AAABACOGvB4E" -AzureOpenAIDeployment "gpt-4o-mini" \ No newline at end of file diff --git a/sync-locales-azure.ps1 b/sync-locales-azure.ps1 new file mode 100644 index 0000000..84bfe65 --- /dev/null +++ b/sync-locales-azure.ps1 @@ -0,0 +1,214 @@ +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 \ No newline at end of file diff --git a/sync-locales-github.ps1 b/sync-locales-github.ps1 new file mode 100644 index 0000000..fd93340 --- /dev/null +++ b/sync-locales-github.ps1 @@ -0,0 +1,185 @@ +param( + [string]$LocalesRoot = "./public/locales", + [string]$MasterLocale = "en", + [string]$GitHubToken = "" +) + +Write-Host "Current working directory: $(Get-Location)" + +if (-not $GitHubToken) { + Write-Host "GitHubToken not provided. Please pass -GitHubToken 'YOUR_PAT' when running the script." -ForegroundColor Yellow + exit 1 +} + +# 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, + [string]$githubToken + ) + + $headers = @{ + "Authorization" = "Bearer $githubToken" + "X-GitHub-Api-Version" = "2023-07-07" + "Content-Type" = "application/json" + } + + $body = @{ + messages = @( + @{ + role = "system" + content = "You are a translation engine. Translate the user text into $targetLocale. Return ONLY the translated text with no commentary." + }, + @{ + role = "user" + content = $text + } + ) + } | ConvertTo-Json -Depth 10 + + $response = Invoke-RestMethod ` + -Uri "https://api.githubcopilot.com/chat/completions" ` + -Method Post ` + -Headers $headers ` + -Body $body + + return $response.choices[0].message.content.Trim() +} + +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 -githubToken $GitHubToken + $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 + } + } + } +} + +Write-Host "Locales root: $LocalesRoot" +Write-Host "Master locale: $MasterLocale" + +$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 \ No newline at end of file