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+
16import sys
27import os
38from 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+
103120def 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
124147if __name__ == "__main__" :
125148 main ()
0 commit comments