Workingon a script to use an LLM to do the translation work.
This commit is contained in:
parent
f2568e1aab
commit
88d21c2101
18
RunMeToUpdateLocales.cmd
Normal file
18
RunMeToUpdateLocales.cmd
Normal 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
214
sync-locales-azure.ps1
Normal 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
185
sync-locales-github.ps1
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user