Skip to content

Commit ff5d9fb

Browse files
committed
Merge latest changes from base addonTemplate repos
1 parent 81f4adc commit ff5d9fb

7 files changed

Lines changed: 295 additions & 127 deletions

File tree

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,41 @@
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+
16
import sys
27
import os
38
from crowdin_api import CrowdinClient
49

5-
def find_file_id(client, project_id, base_target, search_ext):
10+
11+
def findFileId(client: CrowdinClient, projectId: int, baseTarget: str, searchExt: str) -> int | None:
612
"""
7-
Iterates through all project files (using pagination) to find the ID
8-
of the source file matching the target name and extension.
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.
920
"""
1021
offset = 0
1122
limit = 100
1223

1324
while True:
1425
resp = client.source_files.list_files(
15-
projectId=project_id,
26+
projectId=projectId,
1627
limit=limit,
17-
offset=offset
28+
offset=offset,
1829
)
1930

20-
data = resp['data']
31+
data = resp["data"]
2132
for f in data:
22-
path_crowdin = f['data']['path'].lower()
23-
# Check if the path ends with addon_id.pot or addon_id.xliff
24-
if path_crowdin.endswith(f"{base_target}{search_ext}"):
25-
file_id = f['data']['id']
26-
print(f"DEBUG: Match found: {path_crowdin} (ID: {file_id})")
27-
return file_id
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
2839

2940
if len(data) < limit:
3041
break
@@ -33,93 +44,105 @@ def find_file_id(client, project_id, base_target, search_ext):
3344

3445
return None
3546

36-
def get_score_from_api(file_name_to_search: str, lang_id: str) -> float:
47+
48+
def getScoreFromApi(fileNameToSearch: str, langId: str) -> float:
3749
"""
3850
Retrieves the translation progress score for a specific language and file.
3951
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.
4056
"""
4157
token = os.environ.get("crowdinAuthToken")
42-
p_id_env = os.environ.get("CROWDIN_PROJECT_ID")
58+
projectIdEnv = os.environ.get("CROWDIN_PROJECT_ID")
4359

44-
if not token or not p_id_env:
60+
if not token or not projectIdEnv:
4561
print("ERROR: Missing environment variables 'crowdinAuthToken' or 'CROWDIN_PROJECT_ID'.")
4662
return 0.0
4763

4864
client = CrowdinClient(token=token)
49-
p_id = int(p_id_env)
65+
projectId = int(projectIdEnv)
5066

5167
try:
52-
# Clean and prepare search patterns
53-
# Example: 'addon/locale/fr/LC_MESSAGES/myaddon.po' -> base_target: 'myaddon'
54-
base_target = file_name_to_search.replace('\\', '/').split('/')[-1].rsplit('.', 1)[0].lower()
55-
ext_target = file_name_to_search.split('.')[-1].lower()
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()
5672

57-
# On Crowdin, the source for a .po file is usually a .pot file
58-
search_ext = ".pot" if ext_target == "po" else f".{ext_target}"
73+
# On Crowdin, the source for a .po file is usually a .pot file.
74+
searchExt = ".pot" if extTarget == "po" else f".{extTarget}"
5975

60-
print(f"DEBUG: Searching for source file: {base_target}{search_ext}")
76+
print(f"DEBUG: Searching for source file: {baseTarget}{searchExt}")
6177

62-
file_id = find_file_id(client, p_id, base_target, search_ext)
78+
fileId = findFileId(client, projectId, baseTarget, searchExt)
6379

64-
if file_id is None:
65-
print(f"WARNING: File '{base_target}{search_ext}' not found on Crowdin.")
80+
if fileId is None:
81+
print(f"WARNING: File '{baseTarget}{searchExt}' not found on Crowdin.")
6682
return 0.0
6783

68-
# Pagination for translation status (Progress)
84+
# Pagination for translation status (Progress).
6985
offset = 0
7086
limit = 100
7187

7288
while True:
7389
resp = client.translation_status.get_file_progress(
74-
projectId=p_id,
75-
fileId=file_id,
90+
projectId=projectId,
91+
fileId=fileId,
7692
limit=limit,
77-
offset=offset
93+
offset=offset,
7894
)
7995

80-
data = resp['data']
96+
data = resp["data"]
8197
for item in data:
82-
lang_api = item['data']['languageId']
83-
84-
# Flexible matching (e.g., 'fr' will match 'fr' or 'fr-FR' from API)
98+
langApi = item["data"]["languageId"]
99+
100+
# Flexible matching (e.g., 'fr' will match 'fr' or 'fr-FR' from API).
85101
# Also handles underscore to dash conversion for Crowdin compatibility
86-
if lang_api.lower().startswith(lang_id.lower().replace('_', '-')):
87-
progress = float(item['data']['translationProgress'])
102+
if langApi.lower().startswith(langId.lower().replace("_", "-")):
103+
progress = float(item["data"]["translationProgress"])
88104
return progress / 100
89105

90-
# Check pagination total
91-
total = resp['pagination']['totalCount']
106+
# Check pagination total.
107+
total = resp["pagination"]["totalCount"]
92108
if offset + limit >= total:
93109
break
94110
offset += limit
95111

96-
print(f"DEBUG: Language '{lang_id}' not found in progress list for this file.")
112+
print(f"DEBUG: Language '{langId}' not found in progress list for this file.")
97113
return 0.0
98114

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

119+
103120
def main():
104121
if len(sys.argv) < 3:
105-
print("Usage: python checkTranslation.py <file_path> <lang_id>")
122+
print("Usage: python checkTranslation.py <filePath> <langId>")
106123
sys.exit(2)
107124

108125
input_file = sys.argv[1]
109126
lang = sys.argv[2]
110127

111-
score = get_score_from_api(input_file, lang)
128+
score = getScoreFromApi(input_file, lang)
112129

113-
# Output formatted for capture by the PowerShell script (crowdinSync.ps1)
130+
# Output formatted for capture by the PowerShell script.
114131
print(f"translationRatio={score}")
115132

116-
if input_file.lower().endswith('.md'):
133+
# Identify extension to provide a specific score label.
134+
ext = input_file.lower().split(".")[-1]
135+
if ext == "md":
117136
print(f"mdScore={score}")
137+
elif ext == "xliff":
138+
print(f"xliffScore={score}")
118139
else:
140+
# Default to poScore for .po and other localization files.
119141
print(f"poScore={score}")
120142

121-
# Exit with success (0) if there is at least some translated content
122-
sys.exit(0 if score > 0.05 else 1)
143+
# Exit with success (0) if there is at least 50% translated content.
144+
sys.exit(0 if score > 0.5 else 1)
145+
123146

124147
if __name__ == "__main__":
125148
main()

.github/scripts/crowdinSync.ps1

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ $ErrorActionPreference = 'Stop'
55
git config user.name "github-actions[bot]"
66
git config user.email "github-actions[bot]@users.noreply.github.com"
77

8-
$addonId = $env:ADDON_ID.Trim()
9-
if (-not $addonId) {
8+
$rawAddonId = $env:ADDON_ID
9+
if ([string]::IsNullOrWhiteSpace($rawAddonId)) {
1010
Write-Error "Failed to get addon ID."
1111
exit 1
1212
}
13+
$addonId = $rawAddonId.Trim()
1314

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

@@ -19,9 +20,15 @@ $mdFile = "./readme.md"
1920
if (Test-Path $mdFile) {
2021
if (Test-Path $xliffFile) {
2122
$tempXliff = [System.IO.Path]::GetTempFileName()
22-
Copy-Item "$addonId.xliff" $tempXliff -Force
23-
Write-Host "DEBUG: Updating XLIFF source based on readme.md..."
24-
uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
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+
}
2532
} else {
2633
Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..."
2734
uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile
@@ -63,33 +70,40 @@ New-Item -ItemType Directory -Force -Path addon/doc | Out-Null
6370
$languageMappings = Get-Content -Raw ".github/scripts/languageMappings.json" | ConvertFrom-Json
6471

6572
foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) {
66-
$langCode = $dir.Name
67-
73+
$langCode = $dir.Name
74+
6875
if ($langCode -eq "en") { continue }
6976

70-
# Identify codes
71-
$crowdinLang = $languageMappings[$langCode]
72-
if (-not $crowdinLang) { $crowdinLang = $langCode }
73-
$langShort = $langCode.Split('-')[0].Split('_')[0]
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+
}
7489

75-
# Map to local NVDA directory
76-
$localLangDir = uv run python .github/scripts/langCodes.py $langCode
77-
78-
Write-Host "`n--- Processing Language: $langCode (Mapped to local: $localLangDir) ---"
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
7993

8094
# Paths
8195
$remoteMd = Join-Path $dir.FullName "$addonId.md"
8296
$remoteXliff = Join-Path $dir.FullName "$addonId.xliff"
8397
$remotePo = Join-Path $dir.FullName "$addonId.po"
84-
$localMdDir = "addon/doc/$localLangDir"
98+
$localMdDir = "addon/doc/$langCode"
8599
$localMd = "$localMdDir/readme.md"
86-
$localPoPath = "addon/locale/$localLangDir/LC_MESSAGES/nvda.po"
100+
$localPoPath = "addon/locale/$langCode/LC_MESSAGES/nvda.po"
87101

88102
# --- 3.1 PO FILE PROCESSING ---
89103
$poImported = $false
90104
if (Test-Path $remotePo) {
91-
Write-Host "DEBUG: Checking Remote PO progress for $langShort..."
92-
uv run python .github/scripts/checkTranslation.py "$addonId.po" $langShort
105+
Write-Host "DEBUG: Checking Remote PO progress for $crowdinLang..."
106+
uv run python .github/scripts/checkTranslation.py "$addonId.po" $crowdinLang
93107
if ($LASTEXITCODE -eq 0) {
94108
Write-Host "SUCCESS: Remote PO is valid. Importing to $localPoPath"
95109
New-Item -ItemType Directory -Force -Path (Split-Path $localPoPath) | Out-Null
@@ -111,16 +125,16 @@ foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) {
111125

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

120134
if (Test-Path $remoteXliff) {
121135
Write-Host "DEBUG: Evaluating Remote XLIFF score..."
122-
$res = uv run python .github/scripts/checkTranslation.py "$addonId.xliff" $langShort
123-
$scoreXliff = [double]($res | Select-String "translationRatio=").ToString().Split("=")[1]
136+
$res = uv run python .github/scripts/checkTranslation.py "$addonId.xliff" $crowdinLang
137+
$scoreXliff = [double]($res | Select-String "xliffScore=").ToString().Split("=")[1]
124138
} else {
125139
Write-Host "DEBUG: No remote XLIFF file found for this language."
126140
}
@@ -134,11 +148,11 @@ foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) {
134148
if (!(Test-Path $localMdDir)) { New-Item -ItemType Directory -Force -Path $localMdDir | Out-Null }
135149

136150
if ($scoreXliff -ge $scoreMd) {
137-
Write-Host "SUCCESS: XLIFF is better or equal. Converting XLIFF to local MD ($localLangDir)..."
151+
Write-Host "SUCCESS: XLIFF is better or equal. Converting XLIFF to local MD ($langCode)..."
138152
./l10nUtil.exe xliff2md $remoteXliff $localMd
139153
$docImported = $true
140154
} else {
141-
Write-Host "SUCCESS: Markdown is better. Importing Remote MD to local ($localLangDir)..."
155+
Write-Host "SUCCESS: Markdown is better. Importing Remote MD to local ($langCode)..."
142156
Move-Item $remoteMd $localMd -Force
143157
$docImported = $true
144158
}

.github/scripts/setOutputs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# setOutputs.py
12
# Copyright (C) 2025 NV Access Limited, Noelia Ruiz Martínez
23
# This file is covered by the GNU General Public License.
34
# See the file COPYING for more details.

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ repos:
7676
args: [ --fix ]
7777
- id: ruff-format
7878
name: format with ruff
79+
7980
- repo: local
8081
hooks:
8182

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.13.11
1+
3.13

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,14 @@ dependencies = [
2525
"scons==4.10.1",
2626
"Markdown==3.10",
2727
# Translations management
28-
"requests==2.32.5",
2928
"nh3==0.3.2",
3029
"crowdin-api-client==1.24.1",
31-
"lxml==6.0.2",
30+
"lxml==6.1.0",
3231
"mdx_truly_sane_lists==1.3",
3332
"markdown-link-attr-modifier==0.2.1",
3433
"mdx-gh-links==0.4",
3534
# Lint
36-
"uv==0.9.11",
35+
"uv==0.11.6",
3736
"ruff==0.14.5",
3837
"pre-commit==4.2.0",
3938
"pyright[nodejs]==1.1.407",
@@ -105,6 +104,8 @@ exclude = [
105104
# When excluding concrete paths relative to a directory,
106105
# not matching multiple folders by name e.g. `__pycache__`,
107106
# paths are relative to the configuration file.
107+
".github/scripts/markdownTranslate.py",
108+
".github/scripts/checkTranslation.py",
108109
]
109110

110111
# Tell pyright where to load python code from

0 commit comments

Comments
 (0)