Skip to content

Commit 2c55609

Browse files
committed
Update addon template for translations
1 parent 553709b commit 2c55609

7 files changed

Lines changed: 1384 additions & 0 deletions

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# checkTranslation.py
2+
# Copyright (C) 2026 NV Access Limited, Abdel
3+
# This file is covered by the GNU General Public License.
4+
# See the file COPYING for more details.
5+
6+
import sys
7+
import os
8+
from crowdin_api import CrowdinClient
9+
10+
11+
def findFileId(client: CrowdinClient, projectId: int, baseTarget: str, searchExt: str) -> int | None:
12+
"""
13+
Iterates through all project files (using pagination) to find the ID of the source file matching the target name and extension.
14+
15+
:param client: The Crowdin API client instance.
16+
:param projectId: The ID of the Crowdin project.
17+
:param base_target: The base name of the file (e.g., 'myAddon).
18+
:param search_ext: The extension to look for (e.g., '.pot').
19+
:return: The file ID if found, otherwise None.
20+
"""
21+
offset = 0
22+
limit = 100
23+
24+
while True:
25+
resp = client.source_files.list_files(
26+
projectId=projectId,
27+
limit=limit,
28+
offset=offset,
29+
)
30+
31+
data = resp["data"]
32+
for f in data:
33+
path_crowdin = f["data"]["path"].lower()
34+
# Check if the path ends with addon_id.pot or addon_id.xliff.
35+
if path_crowdin.endswith(f"{baseTarget}{searchExt}"):
36+
fileId = f["data"]["id"]
37+
print(f"DEBUG: Match found: {path_crowdin} (ID: {fileId})")
38+
return fileId
39+
40+
if len(data) < limit:
41+
break
42+
43+
offset += limit
44+
45+
return None
46+
47+
48+
def getScoreFromApi(fileNameToSearch: str, langId: str) -> float:
49+
"""
50+
Retrieves the translation progress score for a specific language and file.
51+
Handles pagination for both file listing and language status.
52+
53+
:param fileNameToSearch: The local path or name of the file to check.
54+
:param langId: The language code (e.g., 'fr' or 'pt_BR').
55+
:return: The translation ratio between 0.0 and 1.0.
56+
"""
57+
token = os.environ.get("crowdinAuthToken")
58+
projectIdEnv = os.environ.get("CROWDIN_PROJECT_ID")
59+
60+
if not token or not projectIdEnv:
61+
print("ERROR: Missing environment variables 'crowdinAuthToken' or 'CROWDIN_PROJECT_ID'.")
62+
return 0.0
63+
64+
client = CrowdinClient(token=token)
65+
projectId = int(projectIdEnv)
66+
67+
try:
68+
# Clean and prepare search patterns.
69+
# Example: 'addon/locale/fr/LC_MESSAGES/myAddon.po' -> base_target: 'myAddon'.
70+
baseTarget = fileNameToSearch.replace("\\", "/").split("/")[-1].rsplit(".", 1)[0].lower()
71+
extTarget = fileNameToSearch.split(".")[-1].lower()
72+
73+
# On Crowdin, the source for a .po file is usually a .pot file.
74+
searchExt = ".pot" if extTarget == "po" else f".{extTarget}"
75+
76+
print(f"DEBUG: Searching for source file: {baseTarget}{searchExt}")
77+
78+
fileId = findFileId(client, projectId, baseTarget, searchExt)
79+
80+
if fileId is None:
81+
print(f"WARNING: File '{baseTarget}{searchExt}' not found on Crowdin.")
82+
return 0.0
83+
84+
# Pagination for translation status (Progress).
85+
offset = 0
86+
limit = 100
87+
88+
while True:
89+
resp = client.translation_status.get_file_progress(
90+
projectId=projectId,
91+
fileId=fileId,
92+
limit=limit,
93+
offset=offset,
94+
)
95+
96+
data = resp["data"]
97+
for item in data:
98+
langApi = item["data"]["languageId"]
99+
100+
# Flexible matching (e.g., 'fr' will match 'fr' or 'fr-FR' from API).
101+
# Also handles underscore to dash conversion for Crowdin compatibility
102+
if langApi.lower().startswith(langId.lower().replace("_", "-")):
103+
progress = float(item["data"]["translationProgress"])
104+
return progress / 100
105+
106+
# Check pagination total.
107+
total = resp["pagination"]["totalCount"]
108+
if offset + limit >= total:
109+
break
110+
offset += limit
111+
112+
print(f"DEBUG: Language '{langId}' not found in progress list for this file.")
113+
return 0.0
114+
115+
except Exception as e:
116+
print(f"API ERROR: {e}")
117+
return 0.0
118+
119+
120+
def main():
121+
if len(sys.argv) < 3:
122+
print("Usage: python checkTranslation.py <filePath> <langId>")
123+
sys.exit(2)
124+
125+
input_file = sys.argv[1]
126+
lang = sys.argv[2]
127+
128+
score = getScoreFromApi(input_file, lang)
129+
130+
# Output formatted for capture by the PowerShell script.
131+
print(f"translationRatio={score}")
132+
133+
# Identify extension to provide a specific score label.
134+
ext = input_file.lower().split(".")[-1]
135+
if ext == "md":
136+
print(f"mdScore={score}")
137+
elif ext == "xliff":
138+
print(f"xliffScore={score}")
139+
else:
140+
# Default to poScore for .po and other localization files.
141+
print(f"poScore={score}")
142+
143+
# Exit with success (0) if there is at least 50% translated content.
144+
sys.exit(0 if score > 0.5 else 1)
145+
146+
147+
if __name__ == "__main__":
148+
main()

.github/scripts/crowdinSync.ps1

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env pwsh
2+
$ErrorActionPreference = 'Stop'
3+
4+
# Git configuration for automated commits
5+
git config user.name "github-actions[bot]"
6+
git config user.email "github-actions[bot]@users.noreply.github.com"
7+
8+
$rawAddonId = $env:ADDON_ID
9+
if ([string]::IsNullOrWhiteSpace($rawAddonId)) {
10+
Write-Error "Failed to get addon ID."
11+
exit 1
12+
}
13+
$addonId = $rawAddonId.Trim()
14+
15+
# --- STEP 1: PREPARATION AND SOURCE UPDATE ---
16+
17+
$xliffFile = "./$addonId.xliff"
18+
$mdFile = "./readme.md"
19+
20+
if (Test-Path $mdFile) {
21+
if (Test-Path $xliffFile) {
22+
$tempXliff = [System.IO.Path]::GetTempFileName()
23+
try {
24+
Copy-Item "$addonId.xliff" $tempXliff -Force
25+
Write-Host "DEBUG: Updating XLIFF source based on readme.md..."
26+
uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
27+
} finally {
28+
if (Test-Path $tempXliff) {
29+
Remove-Item $tempXliff -Force
30+
}
31+
}
32+
} else {
33+
Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..."
34+
uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile
35+
}
36+
}
37+
38+
# Update POT file (addon interface)
39+
uv run scons pot
40+
$potFile = "$addonId.pot"
41+
42+
# --- STEP 2: UPLOAD SOURCES TO CROWDIN ---
43+
44+
if (Test-Path $potFile) {
45+
Write-Host "DEBUG: Uploading updated POT source to Crowdin..."
46+
./l10nUtil.exe uploadSourceFile "$potFile" -c addon
47+
}
48+
49+
if (Test-Path $xliffFile) {
50+
Write-Host "DEBUG: Uploading updated XLIFF source to Crowdin..."
51+
./l10nUtil.exe uploadSourceFile "$xliffFile" -c addon
52+
git add "$xliffFile"
53+
git diff --staged --quiet
54+
if ($LASTEXITCODE -ne 0) {
55+
git commit -m "Update $xliffFile for $addonId"
56+
git push
57+
}
58+
}
59+
60+
# --- STEP 3: EXPORT AND PROCESS TRANSLATIONS ---
61+
62+
Write-Host "DEBUG: Exporting translations from Crowdin..."
63+
./l10nUtil.exe exportTranslations -o _addonL10n -c addon
64+
65+
# Ensure base directories exist
66+
New-Item -ItemType Directory -Force -Path addon/locale | Out-Null
67+
New-Item -ItemType Directory -Force -Path addon/doc | Out-Null
68+
69+
# Load language mappings for Crowdin API calls
70+
$languageMappings = Get-Content -Raw ".github/scripts/languageMappings.json" | ConvertFrom-Json
71+
72+
foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) {
73+
$langCode = $dir.Name
74+
75+
if ($langCode -eq "en") { continue }
76+
77+
# --- Identify codes
78+
$crowdinLang = $null
79+
80+
# Use the ."variable" syntax to correctly read the PSCustomObject from JSON
81+
if ($languageMappings.PSObject.Properties.Name -contains $langCode) {
82+
$crowdinLang = $languageMappings."$langCode"
83+
}
84+
85+
# Fallback: If no mapping is found, replace underscores with dashes for Crowdin compatibility
86+
if (-not $crowdinLang) {
87+
$crowdinLang = $langCode.Replace('_', '-')
88+
}
89+
90+
# The $langCode (folder name from Crowdin) represents the local repository language code.
91+
# It matches the NVDA directory structure, so no extra mapping is needed.
92+
Write-Host "--- Processing Language: $langCode (Crowdin: $crowdinLang) ---" -ForegroundColor Cyan
93+
94+
# Paths
95+
$remoteMd = Join-Path $dir.FullName "$addonId.md"
96+
$remoteXliff = Join-Path $dir.FullName "$addonId.xliff"
97+
$remotePo = Join-Path $dir.FullName "$addonId.po"
98+
$localMdDir = "addon/doc/$langCode"
99+
$localMd = "$localMdDir/readme.md"
100+
$localPoPath = "addon/locale/$langCode/LC_MESSAGES/nvda.po"
101+
102+
# --- 3.1 PO FILE PROCESSING ---
103+
$poImported = $false
104+
if (Test-Path $remotePo) {
105+
Write-Host "DEBUG: Checking Remote PO progress for $crowdinLang..."
106+
uv run python .github/scripts/checkTranslation.py "$addonId.po" $crowdinLang
107+
if ($LASTEXITCODE -eq 0) {
108+
Write-Host "SUCCESS: Remote PO is valid. Importing to $localPoPath"
109+
New-Item -ItemType Directory -Force -Path (Split-Path $localPoPath) | Out-Null
110+
Move-Item $remotePo $localPoPath -Force
111+
$poImported = $true
112+
} else {
113+
Write-Host "WARNING: Remote PO progress is below threshold."
114+
}
115+
}
116+
117+
if (-not $poImported -and (Test-Path $localPoPath)) {
118+
Write-Host "ACTION: Uploading local legacy PO to Crowdin ($crowdinLang) as fallback."
119+
./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.po" $localPoPath -c addon
120+
}
121+
122+
# --- 3.2 DOCUMENTATION PROCESSING (MD & XLIFF) ---
123+
$scoreMd = 0.0
124+
$scoreXliff = 0.0
125+
126+
if (Test-Path $remoteMd) {
127+
Write-Host "DEBUG: Evaluating Remote Markdown score..."
128+
$res = uv run python .github/scripts/checkTranslation.py "$addonId.md" $crowdinLang
129+
$scoreMd = [double]($res | Select-String "mdScore=").ToString().Split("=")[1]
130+
} else {
131+
Write-Host "DEBUG: No remote Markdown file found for this language."
132+
}
133+
134+
if (Test-Path $remoteXliff) {
135+
Write-Host "DEBUG: Evaluating Remote XLIFF score..."
136+
$res = uv run python .github/scripts/checkTranslation.py "$addonId.xliff" $crowdinLang
137+
$scoreXliff = [double]($res | Select-String "xliffScore=").ToString().Split("=")[1]
138+
} else {
139+
Write-Host "DEBUG: No remote XLIFF file found for this language."
140+
}
141+
142+
Write-Host "DEBUG: Comparison Scores -> MD: $scoreMd | XLIFF: $scoreXliff"
143+
144+
$threshold = 0.5
145+
$docImported = $false
146+
147+
if ($scoreXliff -gt $threshold -or $scoreMd -gt $threshold) {
148+
if (!(Test-Path $localMdDir)) { New-Item -ItemType Directory -Force -Path $localMdDir | Out-Null }
149+
150+
if ($scoreXliff -ge $scoreMd) {
151+
Write-Host "SUCCESS: XLIFF is better or equal. Converting XLIFF to local MD ($langCode)..."
152+
./l10nUtil.exe xliff2md $remoteXliff $localMd
153+
$docImported = $true
154+
} else {
155+
Write-Host "SUCCESS: Markdown is better. Importing Remote MD to local ($langCode)..."
156+
Move-Item $remoteMd $localMd -Force
157+
$docImported = $true
158+
}
159+
} else {
160+
Write-Host "WARNING: Both remote MD and XLIFF scores are below threshold ($threshold)."
161+
}
162+
163+
if (-not $docImported -and (Test-Path $localMd)) {
164+
Write-Host "ACTION: Documentation quality too low. Uploading local MD to Crowdin ($crowdinLang) as fallback."
165+
./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.md" $localMd -c addon
166+
}
167+
}
168+
169+
# --- STEP 4: COMMIT UPDATED TRANSLATIONS ---
170+
171+
git add addon/locale addon/doc
172+
git diff --staged --quiet
173+
if ($LASTEXITCODE -ne 0) {
174+
git commit -m "Update translations for $addonId from Crowdin (Automatic Sync)"
175+
$branch = $env:downloadTranslationsBranch
176+
git push -f origin "HEAD:$branch"
177+
Write-Host "SUCCESS: Translations committed and pushed."
178+
} else {
179+
Write-Host "DEBUG: No changes in translations to commit."
180+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"af_ZA": "af",
3+
"de_CH": "de-CH",
4+
"es": "es-ES",
5+
"es_CO": "es-CO",
6+
"nb_NO": "nb",
7+
"nn_NO": "nn-NO",
8+
"pt_PT": "pt-PT",
9+
"pt_BR": "pt-BR",
10+
"sr": "sr-CS",
11+
"zh_CN": "zh-CN",
12+
"zh_HK": "zh-HK",
13+
"zh_TW": "zh-TW"
14+
}

0 commit comments

Comments
 (0)