Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
28c060d
Add scripts to allow addons from personal repos to be synchronized wi…
nvdaes Nov 24, 2025
4669430
use a json file to store addonId, and use it to filter files to get C…
nvdaes Nov 24, 2025
b188560
Try to get files just for the current add-on
nvdaes Nov 24, 2025
7092615
Add workflow to export an add-on to Crowdin (authors would need to be…
nvdaes Nov 24, 2025
e89640d
Use buildVars, not metadata.json file
nvdaes Nov 25, 2025
4c7771b
Add userAccount to buildVars, and step to get addon-id to GitHub work…
nvdaes Nov 26, 2025
c529cee
Update files after testing exporting an add-on to Crowdin, needs refi…
nvdaes Nov 26, 2025
186b755
Add python version file
nvdaes Nov 26, 2025
f1fbf8e
Improve pyproject and update precommit config after testing that chec…
nvdaes Nov 26, 2025
b867a9a
Restore rules
nvdaes Nov 27, 2025
47ed91c
Restore pyproject
nvdaes Nov 27, 2025
402002e
Improve uv project
nvdaes Nov 27, 2025
d820711
Remove files
nvdaes Nov 27, 2025
9f6b3dc
Calculate hash of i18nSources
nvdaes Nov 29, 2025
4c938ec
Update workflow
nvdaes Nov 30, 2025
a303210
Update _l10n
nvdaes Nov 30, 2025
a0d02da
Upload md file
nvdaes Dec 1, 2025
a8d4252
Updates
nvdaes Dec 3, 2025
1a1e6fd
Update l10nUtil
nvdaes Dec 14, 2025
d2395b0
Update workflow
nvdaes Dec 14, 2025
e4dafe1
Update readme
nvdaes Dec 16, 2025
f76904e
Update readme.md
nvdaes Dec 16, 2025
aea5eba
Update _l10n/crowdinSync.py
nvdaes Dec 16, 2025
f7ccaf6
Add setOutput.py to separate Python code from yaml file
nvdaes Dec 16, 2025
0276e22
Remove bad comment
nvdaes Dec 16, 2025
253eb46
Reset pyproject to master
nvdaes Dec 16, 2025
c51e7ad
reset .pre-commit configuration to master
nvdaes Dec 16, 2025
cd4816c
Remove userAccount variable, since we use markdown, not xliff
nvdaes Dec 17, 2025
314220b
Update or add files from scratch depending on existence of hashFile
nvdaes Dec 17, 2025
f3e8b8d
Use addMd and addPotFromScratch outputs
nvdaes Dec 17, 2025
de4fa15
Update dependencies
nvdaes Dec 20, 2025
46a105a
Update setOutput
nvdaes Dec 20, 2025
053d4de
Update workflow
nvdaes Dec 20, 2025
4a3f5a0
Update lock
nvdaes Dec 20, 2025
3c1a73e
Merge branch 'master' into l10n
nvdaes Dec 21, 2025
dbe74dc
Verify uv lock
nvdaes Dec 21, 2025
e717292
Add uv to dependencies in case this is relevant to verify the lock ac…
nvdaes Dec 21, 2025
c4ed575
Remove debug statement
nvdaes Dec 21, 2025
befa647
Run pre-commit
nvdaes Dec 21, 2025
05c8161
Update dependencies
nvdaes Dec 22, 2025
9a0f62a
Deleted Pyproject to avoid conflicts
nvdaes Dec 22, 2025
4abd788
Reset pyproject to master
nvdaes Dec 22, 2025
c256364
Remove _l10n since this will be added as a submodule
nvdaes Dec 22, 2025
0505a3b
Don't run pre-commit since it requires a different token to access hooks
nvdaes Dec 22, 2025
fd2554b
Merge translations into branch
nvdaes Dec 22, 2025
b30f46f
Add project id without using vars
nvdaes Dec 22, 2025
70293c8
Schedule workflow
nvdaes Dec 22, 2025
da09c8c
Rename workflow
nvdaes Dec 22, 2025
5c52f33
Create PR
nvdaes Dec 22, 2025
d0d5e03
Don't create a PR since this n¡may need a personal access token
nvdaes Dec 22, 2025
b40f94a
Update removing permissions for PR
nvdaes Dec 22, 2025
cb7807e
Update Python version compatible with ubuntu-latest
nvdaes Dec 28, 2025
1449a01
Add dry-run
nvdaes Dec 28, 2025
697d048
Optimize workflow to test with act and docker locally
nvdaes Dec 28, 2025
8c9247b
Update uv.lock
nvdaes Dec 30, 2025
a8469fb
Merge branch 'master' into l10n
nvdaes Dec 30, 2025
6089057
Add workflow call to build the add-on, so it can be reused in other w…
nvdaes Mar 25, 2026
d8154a9
Update workflow for testing
nvdaes Mar 25, 2026
e699abc
Improve workflow to download files from Crowdin
nvdaes Apr 18, 2026
5eafa64
Update setOutputs to set just add-on id to download translations
nvdaes Apr 18, 2026
0eb3223
[crowdinL10n.yml] improve CI translation workflow
abdel792 Apr 20, 2026
a195110
Remove duplicate setOutputs.py from .github/workflows
abdel792 Apr 20, 2026
9ed44a3
Merge pull request #2 from abdel792/l10nImprovements
nvdaes Apr 20, 2026
e62bd5b
Update lock
nvdaes Apr 20, 2026
37c4d4f
Merge
nvdaes Apr 20, 2026
91995bd
Try to fix pre-commit configuration
nvdaes Apr 20, 2026
0c613be
Reset gitignore to master
nvdaes Apr 20, 2026
0b65072
Require polib 1.2.0
nvdaes Apr 20, 2026
f9ba8fe
Update lock file
nvdaes Apr 20, 2026
8e514ba
Remove sha256 file
nvdaes Apr 20, 2026
9c64247
Remove verification of lock file in pre-commit config, not present in…
nvdaes Apr 20, 2026
7589fef
Remove Crowdin client
nvdaes Apr 20, 2026
94b1a88
Update lock file
nvdaes Apr 20, 2026
65290e4
Enhance Crowdin l10n workflow with MD quality evaluation and comparis…
abdel792 Apr 22, 2026
059bb53
Merge pull request #3 from abdel792/l10nImprovements
nvdaes Apr 22, 2026
c9c3bab
Run pre-commit
nvdaes Apr 22, 2026
25dcb5d
Fix conflicts
nvdaes Apr 23, 2026
b1d2acf
Restore deleted space
nvdaes Apr 24, 2026
263b7e2
Update lock file
nvdaes Apr 24, 2026
f6e18c2
Remove score staff
nvdaes Apr 24, 2026
5573061
Add ps1 script and rename setOutputs.py
nvdaes Apr 24, 2026
87cbed4
Run ps1 script from workflow
nvdaes Apr 24, 2026
1a18717
Change python version to 3.13
nvdaes Apr 24, 2026
471a320
Update lock file
nvdaes Apr 24, 2026
e50a716
Fix checkTranslation script according to pre-commit
nvdaes Apr 24, 2026
f5150cb
Remove getAddonInfo step
nvdaes Apr 24, 2026
3c7093c
Update scripts and workflow
nvdaes Apr 25, 2026
065a16f
Fix pyright
nvdaes Apr 25, 2026
c4dd521
Add markdownTranslate
nvdaes Apr 25, 2026
00cf8cf
Update markdownTranslate
nvdaes Apr 25, 2026
480c70e
Exclude markdownTranslate from pyright, it contains many errors
nvdaes Apr 26, 2026
ce2d2da
Update source files
nvdaes Apr 26, 2026
3981ece
Updates
nvdaes Apr 26, 2026
0def05d
Fix files with pre-commit
nvdaes Apr 26, 2026
1c92aab
refactor: use Crowdin API for translation checks and optimize file se…
abdel792 Apr 26, 2026
9b5bf84
Merge pull request #4 from abdel792/l10nImprovements
nvdaes Apr 26, 2026
c5e4076
Add crowdin api client
nvdaes Apr 26, 2026
c098392
Add dependencies
nvdaes Apr 26, 2026
39416a0
Remove limit for list of files, fetch all
nvdaes Apr 26, 2026
2396864
Improvements
nvdaes Apr 26, 2026
94929e7
Updates
nvdaes Apr 26, 2026
57d46f5
Add language mappings
nvdaes Apr 27, 2026
74388d0
Fix
nvdaes Apr 27, 2026
1f8eb84
refactor: modernize translation workflow and integrate language mappings
abdel792 Apr 28, 2026
ef0a614
docs: update readme with translation workflow instructions
abdel792 Apr 28, 2026
92e1cd6
docs: fix translation mailing list address in readme.md
abdel792 Apr 28, 2026
f92a73f
Merge pull request #5 from abdel792/l10nImprovements
nvdaes Apr 28, 2026
6a884c0
Fix readme
nvdaes Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/scripts/checkTranslation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import sys
import os
from crowdin_api import CrowdinClient

def find_file_id(client, project_id, base_target, search_ext):
"""
Iterates through all project files (using pagination) to find the ID
of the source file matching the target name and extension.
"""
offset = 0
limit = 100

while True:
resp = client.source_files.list_files(
projectId=project_id,
limit=limit,
offset=offset
)

data = resp['data']
for f in data:
path_crowdin = f['data']['path'].lower()
# Check if the path ends with addon_id.pot or addon_id.xliff
if path_crowdin.endswith(f"{base_target}{search_ext}"):
file_id = f['data']['id']
print(f"DEBUG: Match found: {path_crowdin} (ID: {file_id})")
return file_id

if len(data) < limit:
break

offset += limit

return None

def get_score_from_api(file_name_to_search: str, lang_id: str) -> float:
"""
Retrieves the translation progress score for a specific language and file.
Handles pagination for both file listing and language status.
"""
token = os.environ.get("crowdinAuthToken")
p_id_env = os.environ.get("CROWDIN_PROJECT_ID")

if not token or not p_id_env:
print("ERROR: Missing environment variables 'crowdinAuthToken' or 'CROWDIN_PROJECT_ID'.")
return 0.0

client = CrowdinClient(token=token)
p_id = int(p_id_env)

try:
# Clean and prepare search patterns
# Example: 'addon/locale/fr/LC_MESSAGES/myaddon.po' -> base_target: 'myaddon'
base_target = file_name_to_search.replace('\\', '/').split('/')[-1].rsplit('.', 1)[0].lower()
ext_target = file_name_to_search.split('.')[-1].lower()

# On Crowdin, the source for a .po file is usually a .pot file
search_ext = ".pot" if ext_target == "po" else f".{ext_target}"

print(f"DEBUG: Searching for source file: {base_target}{search_ext}")

file_id = find_file_id(client, p_id, base_target, search_ext)

if file_id is None:
print(f"WARNING: File '{base_target}{search_ext}' not found on Crowdin.")
return 0.0

# Pagination for translation status (Progress)
offset = 0
limit = 100

while True:
resp = client.translation_status.get_file_progress(
projectId=p_id,
fileId=file_id,
limit=limit,
offset=offset
)

data = resp['data']
for item in data:
lang_api = item['data']['languageId']

# Flexible matching (e.g., 'fr' will match 'fr' or 'fr-FR' from API)
# Also handles underscore to dash conversion for Crowdin compatibility
if lang_api.lower().startswith(lang_id.lower().replace('_', '-')):
progress = float(item['data']['translationProgress'])
return progress / 100

# Check pagination total
total = resp['pagination']['totalCount']
if offset + limit >= total:
break
offset += limit

print(f"DEBUG: Language '{lang_id}' not found in progress list for this file.")
return 0.0

except Exception as e:
print(f"API ERROR: {e}")
return 0.0

def main():
if len(sys.argv) < 3:
print("Usage: python checkTranslation.py <file_path> <lang_id>")
sys.exit(2)

input_file = sys.argv[1]
lang = sys.argv[2]

score = get_score_from_api(input_file, lang)

# Output formatted for capture by the PowerShell script (crowdinSync.ps1)
print(f"translationRatio={score}")

if input_file.lower().endswith('.md'):
print(f"mdScore={score}")
else:
print(f"poScore={score}")

# Exit with success (0) if there is at least some translated content
sys.exit(0 if score > 0.05 else 1)

if __name__ == "__main__":
main()
166 changes: 166 additions & 0 deletions .github/scripts/crowdinSync.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env pwsh
$ErrorActionPreference = 'Stop'

# Git configuration for automated commits
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

$addonId = $env:ADDON_ID.Trim()
if (-not $addonId) {
Write-Error "Failed to get addon ID."
exit 1
}

# --- STEP 1: PREPARATION AND SOURCE UPDATE ---

$xliffFile = "./$addonId.xliff"
$mdFile = "./readme.md"

if (Test-Path $mdFile) {
if (Test-Path $xliffFile) {
$tempXliff = [System.IO.Path]::GetTempFileName()
Copy-Item "$addonId.xliff" $tempXliff -Force
Write-Host "DEBUG: Updating XLIFF source based on readme.md..."
uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
} else {
Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..."
uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile
}
}

# Update POT file (addon interface)
uv run scons pot
$potFile = "$addonId.pot"

# --- STEP 2: UPLOAD SOURCES TO CROWDIN ---

if (Test-Path $potFile) {
Write-Host "DEBUG: Uploading updated POT source to Crowdin..."
./l10nUtil.exe uploadSourceFile "$potFile" -c addon
}

if (Test-Path $xliffFile) {
Write-Host "DEBUG: Uploading updated XLIFF source to Crowdin..."
./l10nUtil.exe uploadSourceFile "$xliffFile" -c addon
git add "$xliffFile"
git diff --staged --quiet
if ($LASTEXITCODE -ne 0) {
git commit -m "Update $xliffFile for $addonId"
git push
}
}

# --- STEP 3: EXPORT AND PROCESS TRANSLATIONS ---

Write-Host "DEBUG: Exporting translations from Crowdin..."
./l10nUtil.exe exportTranslations -o _addonL10n -c addon

# Ensure base directories exist
New-Item -ItemType Directory -Force -Path addon/locale | Out-Null
New-Item -ItemType Directory -Force -Path addon/doc | Out-Null

# Load language mappings for Crowdin API calls
$languageMappings = Get-Content -Raw ".github/scripts/languageMappings.json" | ConvertFrom-Json

foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) {
$langCode = $dir.Name

if ($langCode -eq "en") { continue }

# Identify codes
$crowdinLang = $languageMappings[$langCode]
if (-not $crowdinLang) { $crowdinLang = $langCode }
$langShort = $langCode.Split('-')[0].Split('_')[0]

# Map to local NVDA directory
$localLangDir = uv run python .github/scripts/langCodes.py $langCode

Write-Host "`n--- Processing Language: $langCode (Mapped to local: $localLangDir) ---"

# Paths
$remoteMd = Join-Path $dir.FullName "$addonId.md"
$remoteXliff = Join-Path $dir.FullName "$addonId.xliff"
$remotePo = Join-Path $dir.FullName "$addonId.po"
$localMdDir = "addon/doc/$localLangDir"
$localMd = "$localMdDir/readme.md"
$localPoPath = "addon/locale/$localLangDir/LC_MESSAGES/nvda.po"

# --- 3.1 PO FILE PROCESSING ---
$poImported = $false
if (Test-Path $remotePo) {
Write-Host "DEBUG: Checking Remote PO progress for $langShort..."
uv run python .github/scripts/checkTranslation.py "$addonId.po" $langShort
if ($LASTEXITCODE -eq 0) {
Write-Host "SUCCESS: Remote PO is valid. Importing to $localPoPath"
New-Item -ItemType Directory -Force -Path (Split-Path $localPoPath) | Out-Null
Move-Item $remotePo $localPoPath -Force
$poImported = $true
} else {
Write-Host "WARNING: Remote PO progress is below threshold."
}
}

if (-not $poImported -and (Test-Path $localPoPath)) {
Write-Host "ACTION: Uploading local legacy PO to Crowdin ($crowdinLang) as fallback."
./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.po" $localPoPath -c addon
}

# --- 3.2 DOCUMENTATION PROCESSING (MD & XLIFF) ---
$scoreMd = 0.0
$scoreXliff = 0.0

if (Test-Path $remoteMd) {
Write-Host "DEBUG: Evaluating Remote Markdown score..."
$res = uv run python .github/scripts/checkTranslation.py "$addonId.md" $langShort
$scoreMd = [double]($res | Select-String "mdScore=").ToString().Split("=")[1]
} else {
Write-Host "DEBUG: No remote Markdown file found for this language."
}

if (Test-Path $remoteXliff) {
Write-Host "DEBUG: Evaluating Remote XLIFF score..."
$res = uv run python .github/scripts/checkTranslation.py "$addonId.xliff" $langShort
$scoreXliff = [double]($res | Select-String "translationRatio=").ToString().Split("=")[1]
} else {
Write-Host "DEBUG: No remote XLIFF file found for this language."
}

Write-Host "DEBUG: Comparison Scores -> MD: $scoreMd | XLIFF: $scoreXliff"

$threshold = 0.5
$docImported = $false

if ($scoreXliff -gt $threshold -or $scoreMd -gt $threshold) {
if (!(Test-Path $localMdDir)) { New-Item -ItemType Directory -Force -Path $localMdDir | Out-Null }

if ($scoreXliff -ge $scoreMd) {
Write-Host "SUCCESS: XLIFF is better or equal. Converting XLIFF to local MD ($localLangDir)..."
./l10nUtil.exe xliff2md $remoteXliff $localMd
$docImported = $true
} else {
Write-Host "SUCCESS: Markdown is better. Importing Remote MD to local ($localLangDir)..."
Move-Item $remoteMd $localMd -Force
$docImported = $true
}
} else {
Write-Host "WARNING: Both remote MD and XLIFF scores are below threshold ($threshold)."
}

if (-not $docImported -and (Test-Path $localMd)) {
Write-Host "ACTION: Documentation quality too low. Uploading local MD to Crowdin ($crowdinLang) as fallback."
./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.md" $localMd -c addon
}
}

# --- STEP 4: COMMIT UPDATED TRANSLATIONS ---

git add addon/locale addon/doc
git diff --staged --quiet
if ($LASTEXITCODE -ne 0) {
git commit -m "Update translations for $addonId from Crowdin (Automatic Sync)"
$branch = $env:downloadTranslationsBranch
git push -f origin "HEAD:$branch"
Write-Host "SUCCESS: Translations committed and pushed."
} else {
Write-Host "DEBUG: No changes in translations to commit."
}
60 changes: 60 additions & 0 deletions .github/scripts/langCodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import sys

# Mapping between Crowdin language IDs (keys) and standard NVDA directory names (values).
# This dictionary acts as the symmetrical counterpart to 'languageMappings.json' implemented by @nvdaes.
# It ensures that translations exported from Crowdin are stored in the correct
# local paths (e.g., 'es-ES' from Crowdin goes into the 'es' folder).
CROWDIN_TO_NVDA = {
# Arabic variants
"ar-SA": "ar_SA",

# Spanish variants
"es-ES": "es",
"es-CO": "es_CO",

# Portuguese variants
"pt-BR": "pt_BR",
"pt-PT": "pt_PT",

# Chinese variants
"zh-CN": "zh_CN",
"zh-HK": "zh_HK",
"zh-TW": "zh_TW",

# Other specific mappings from the NVDA ecosystem
"af": "af_ZA",
"de-CH": "de_CH",
"nb": "nb_NO",
"nn-NO": "nn_NO",
"sr-CS": "sr"
}

def get_nvda_code(crowdin_code):
"""
Returns the appropriate local directory name for a given Crowdin language ID.

Args:
crowdin_code (str): The language identifier from Crowdin (e.g., 'pt-BR', 'fr').

Returns:
str: The corresponding NVDA locale folder name (e.g., 'pt_BR', 'fr').
"""
# 1. Direct check in our verified map (Priority)
if crowdin_code in CROWDIN_TO_NVDA:
return CROWDIN_TO_NVDA[crowdin_code]

# 2. Automated conversion for regional variants: Crowdin "xx-YY" -> NVDA "xx_YY"
# This handles regional codes not explicitly defined in the map.
if "-" in crowdin_code:
return crowdin_code.replace("-", "_")

# 3. Default: Return as is.
# This covers base languages that don't use regional folders in NVDA
# (e.g., 'fr', 'tr', 'bg', 'fi', 'fa').
return crowdin_code

if __name__ == "__main__":
# Ensure a language code was provided as a command-line argument
if len(sys.argv) > 1:
# Standardize input and output the mapped code for PowerShell to capture
print(get_nvda_code(sys.argv[1]))
14 changes: 14 additions & 0 deletions .github/scripts/languageMappings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"af_ZA": "af",
"de_CH": "de-CH",
"es": "es-ES",
"es_CO": "es-CO",
"nb_NO": "nb",
"nn_NO": "nn-NO",
"pt_PT": "pt-PT",
"pt_BR": "pt-BR",
"sr": "sr-CS",
"zh_CN": "zh-CN",
"zh_HK": "zh-HK",
"zh_TW": "zh-TW"
}
Loading