Workingon a script to use an LLM to do the translation work.

This commit is contained in:
Colin Dawson 2026-02-17 23:56:21 +00:00
parent f2568e1aab
commit 88d21c2101
3 changed files with 417 additions and 0 deletions

18
RunMeToUpdateLocales.cmd Normal file
View File

@ -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"

214
sync-locales-azure.ps1 Normal file
View File

@ -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

185
sync-locales-github.ps1 Normal file
View File

@ -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