From 28c060dd336b5d6e72b1733111d59f188e53e28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 24 Nov 2025 10:24:44 +0100 Subject: [PATCH 01/90] Add scripts to allow addons from personal repos to be synchronized with Crowdin --- _l10n/crowdinSync.py | 92 ++++ _l10n/files.json | 1 + _l10n/l10nUtil.py | 978 +++++++++++++++++++++++++++++++++++++ _l10n/markdownTranslate.py | 733 +++++++++++++++++++++++++++ _l10n/md2html.py | 197 ++++++++ 5 files changed, 2001 insertions(+) create mode 100644 _l10n/crowdinSync.py create mode 100644 _l10n/files.json create mode 100644 _l10n/l10nUtil.py create mode 100644 _l10n/markdownTranslate.py create mode 100644 _l10n/md2html.py diff --git a/_l10n/crowdinSync.py b/_l10n/crowdinSync.py new file mode 100644 index 0000000..1a56070 --- /dev/null +++ b/_l10n/crowdinSync.py @@ -0,0 +1,92 @@ +# A part of NonVisual Desktop Access (NVDA) +# based on file from https://github.com/jcsteh/osara +# Copyright (C) 2023-2024 NV Access Limited, James Teh +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + + +import argparse +import os + +import requests + +from l10nUtil import getFiles + +AUTH_TOKEN = os.getenv("crowdinAuthToken", "").strip() +if not AUTH_TOKEN: + raise ValueError("crowdinAuthToken environment variable not set") +PROJECT_ID = os.getenv("crowdinProjectID", "").strip() +if not PROJECT_ID: + raise ValueError("crowdinProjectID environment variable not set") + + +def request( + path: str, + method=requests.get, + headers: dict[str, str] | None = None, + **kwargs, +) -> requests.Response: + if headers is None: + headers = {} + headers["Authorization"] = f"Bearer {AUTH_TOKEN}" + r = method( + f"https://api.crowdin.com/api/v2/{path}", + headers=headers, + **kwargs, + ) + # Convert errors to exceptions, but print the response before raising. + try: + r.raise_for_status() + except requests.exceptions.HTTPError: + print(r.json()) + raise + return r + + +def projectRequest(path: str, **kwargs) -> requests.Response: + return request(f"projects/{PROJECT_ID}/{path}", **kwargs) + + +def uploadSourceFile(localFilePath: str) -> None: + files = getFiles() + fn = os.path.basename(localFilePath) + crowdinFileID = files.get(fn) + print(f"Uploading {localFilePath} to Crowdin temporary storage as {fn}") + with open(localFilePath, "rb") as f: + r = request( + "storages", + method=requests.post, + headers={"Crowdin-API-FileName": fn}, + data=f, + ) + storageID = r.json()["data"]["id"] + print(f"Updating file {crowdinFileID} on Crowdin with storage ID {storageID}") + r = projectRequest( + f"files/{crowdinFileID}", + method=requests.put, + json={"storageId": storageID}, + ) + revisionId = r.json()["data"]["revisionId"] + print(f"Updated to revision {revisionId}") + + +def main(): + parser = argparse.ArgumentParser( + description="Syncs translations with Crowdin.", + ) + commands = parser.add_subparsers(dest="command", required=True) + uploadCommand = commands.add_parser( + "uploadSourceFile", + help="Upload a source file to Crowdin.", + ) + # uploadCommand.add_argument("crowdinFileID", type=int, help="The Crowdin file ID.") + uploadCommand.add_argument("localFilePath", help="The path to the local file.") + args = parser.parse_args() + if args.command == "uploadSourceFile": + uploadSourceFile(args.localFilePath) + else: + raise ValueError(f"Unknown command: {args.command}") + + +if __name__ == "__main__": + main() diff --git a/_l10n/files.json b/_l10n/files.json new file mode 100644 index 0000000..9264714 --- /dev/null +++ b/_l10n/files.json @@ -0,0 +1 @@ +{"emoticons.pot": 176, "emoticons.xliff": 178, "goldwave.pot": 180, "goldwave.xliff": 182, "eMule.pot": 194, "enhancedTouchGestures.pot": 210, "resourceMonitor.pot": 214, "stationPlaylist.pot": 218, "cursorLocator.pot": 224, "pcKbBrl.pot": 228, "readFeeds.pot": 232, "reportSymbols.pot": 236, "urlShortener.pot": 240, "customNotifications.pot": 244, "readonlyProfiles.pot": 248, "enhancedAnnotations.pot": 252, "clipContentsDesigner.pot": 256, "clipContentsDesigner.xliff": 258, "controlUsageAssistant.pot": 260, "controlUsageAssistant.xliff": 262, "eMule.xliff": 264, "enhancedAnnotations.xliff": 266, "customNotifications.xliff": 268, "readonlyProfiles.xliff": 270, "urlShortener.xliff": 272, "reportSymbols.xliff": 274, "pcKbBrl.xliff": 276, "readFeeds.xliff": 278, "stationPlaylist.xliff": 282, "resourceMonitor.xliff": 284, "enhancedTouchGestures.xliff": 286, "rdAccess.pot": 288, "rdAccess.xliff": 290, "winMag.pot": 292, "winMag.xliff": 294, "charInfo.pot": 296, "charInfo.xliff": 298, "BMI.pot": 300, "BMI.xliff": 302, "tonysEnhancements.pot": 304, "tonysEnhancements.xliff": 306, "nvdaDevTestToolbox.pot": 308, "nvdaDevTestToolbox.xliff": 310, "easyTableNavigator.pot": 312, "easyTableNavigator.xliff": 314, "updateChannel.pot": 320, "updateChannel.xliff": 322, "instantTranslate.pot": 324, "instantTranslate.xliff": 326, "unicodeBrailleInput.pot": 328, "unicodeBrailleInput.xliff": 330, "columnsReview.pot": 332, "columnsReview.xliff": 334, "Access8Math.pot": 336, "Access8Math.xliff": 338, "systrayList.pot": 340, "systrayList.xliff": 342, "winWizard.pot": 344, "winWizard.xliff": 346, "speechLogger.pot": 348, "speechLogger.xliff": 350, "sayProductNameAndVersion.pot": 352, "sayProductNameAndVersion.xliff": 354, "objPad.pot": 356, "objPad.xliff": 358, "SentenceNav.pot": 360, "SentenceNav.xliff": 362, "wordNav.pot": 364, "wordNav.xliff": 366, "goldenCursor.pot": 368, "goldenCursor.xliff": 370, "MSEdgeDiscardAnnouncements.pot": 372, "MSEdgeDiscardAnnouncements.xliff": 374, "dayOfTheWeek.pot": 376, "dayOfTheWeek.xliff": 378, "outlookExtended.pot": 380, "outlookExtended.xliff": 382, "proxy.pot": 384, "proxy.xliff": 386, "searchWith.pot": 388, "searchWith.xliff": 390, "sayCurrentKeyboardLanguage.pot": 392, "sayCurrentKeyboardLanguage.xliff": 394, "robEnhancements.pot": 396, "robEnhancements.xliff": 398, "objWatcher.pot": 400, "objWatcher.xliff": 402, "mp3DirectCut.pot": 404, "mp3DirectCut.xliff": 406, "beepKeyboard.pot": 408, "beepKeyboard.xliff": 410, "numpadNavMode.pot": 412, "numpadNavMode.xliff": 414, "dropbox.pot": 416, "dropbox.xliff": 418, "reviewCursorCopier.pot": 420, "reviewCursorCopier.xliff": 422, "inputLock.pot": 424, "inputLock.xliff": 426, "debugHelper.pot": 428, "debugHelper.xliff": 430, "virtualRevision.pot": 432, "virtualRevision.xliff": 434, "cursorLocator.xliff": 436, "evtTracker.pot": 438, "evtTracker.xliff": 440} \ No newline at end of file diff --git a/_l10n/l10nUtil.py b/_l10n/l10nUtil.py new file mode 100644 index 0000000..00bee4c --- /dev/null +++ b/_l10n/l10nUtil.py @@ -0,0 +1,978 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2024-2025 NV Access Limited. +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +import crowdin_api as crowdin +import tempfile +import lxml.etree +import os +import shutil +import argparse +import markdownTranslate +import md2html +import requests +import codecs +import re +import subprocess +import sys +import zipfile +import time +import json + +CROWDIN_PROJECT_ID = 780748 +POLLING_INTERVAL_SECONDS = 5 +EXPORT_TIMEOUT_SECONDS = 60 * 10 # 10 minutes +JSON_FILE = os.path.join(os.path.dirname(__file__), "files.json") + + +def fetchCrowdinAuthToken() -> str: + """ + Fetch the Crowdin auth token from the ~/.nvda_crowdin file or prompt the user for it. + If provided by the user, the token will be saved to the ~/.nvda_crowdin file. + :return: The auth token + """ + crowdinAuthToken = os.getenv("crowdinAuthToken", "") + if crowdinAuthToken: + print("Using Crowdin auth token from environment variable.") + return crowdinAuthToken + token_path = os.path.expanduser("~/.nvda_crowdin") + if os.path.exists(token_path): + with open(token_path, "r") as f: + token = f.read().strip() + print("Using auth token from ~/.nvda_crowdin") + return token + print("A Crowdin auth token is required to proceed.") + print("Please visit https://crowdin.com/settings#api-key") + print("Create a personal access token with translations permissions, and enter it below.") + token = input("Enter Crowdin auth token: ").strip() + with open(token_path, "w") as f: + f.write(token) + return token + + +_crowdinClient = None + + +def getCrowdinClient() -> crowdin.CrowdinClient: + """ + Create or fetch the Crowdin client instance. + :return: The Crowdin client + """ + global _crowdinClient + if _crowdinClient is None: + token = fetchCrowdinAuthToken() + _crowdinClient = crowdin.CrowdinClient(project_id=CROWDIN_PROJECT_ID, token=token) + return _crowdinClient + + +def fetchLanguageFromXliff(xliffPath: str, source: bool = False) -> str: + """ + Fetch the language from an xliff file. + This function also prints a message to the console stating the detected language if found, or a warning if not found. + :param xliffPath: Path to the xliff file + :param source: If True, fetch the source language, otherwise fetch the target language + :return: The language code + """ + xliff = lxml.etree.parse(xliffPath) + xliffRoot = xliff.getroot() + if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": + raise ValueError(f"Not an xliff file: {xliffPath}") + lang = xliffRoot.get("srcLang" if source else "trgLang") + if lang is None: + print(f"Could not detect language for xliff file {xliffPath}, {source=}") + else: + print(f"Detected language {lang} for xliff file {xliffPath}, {source=}") + return lang + + +def preprocessXliff(xliffPath: str, outputPath: str): + """ + Replace corrupt or empty translated segment targets with the source text, + marking the segment again as "initial" state. + This function also prints a message to the console stating the number of segments processed and the numbers of empty, corrupt, source and existing translations removed. + :param xliffPath: Path to the xliff file to be processed + :param outputPath: Path to the resulting xliff file + """ + print(f"Preprocessing xliff file at {xliffPath}") + namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} + xliff = lxml.etree.parse(xliffPath) + xliffRoot = xliff.getroot() + if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": + raise ValueError(f"Not an xliff file: {xliffPath}") + file = xliffRoot.find("./xliff:file", namespaces=namespace) + units = file.findall("./xliff:unit", namespaces=namespace) + segmentCount = 0 + emptyTargetCount = 0 + corruptTargetcount = 0 + for unit in units: + segment = unit.find("./xliff:segment", namespaces=namespace) + if segment is None: + print("Warning: No segment element in unit") + continue + source = segment.find("./xliff:source", namespaces=namespace) + if source is None: + print("Warning: No source element in segment") + continue + sourceText = source.text + segmentCount += 1 + target = segment.find("./xliff:target", namespaces=namespace) + if target is None: + continue + targetText = target.text + # Correct empty targets + if not targetText: + emptyTargetCount += 1 + target.text = sourceText + segment.set("state", "initial") + # Correct corrupt target tags + elif targetText in ( + "", + "<target/>", + "", + "<target></target>", + ): + corruptTargetcount += 1 + target.text = sourceText + segment.set("state", "initial") + xliff.write(outputPath, encoding="utf-8") + print( + f"Processed {segmentCount} segments, removing {emptyTargetCount} empty targets, {corruptTargetcount} corrupt targets", + ) + + +def stripXliff(xliffPath: str, outputPath: str, oldXliffPath: str | None = None): + """ + Removes notes and skeleton elements from an xliff file before upload to Crowdin. + Removes empty and corrupt translations. + Removes untranslated segments. + Removes existing translations if an old xliff file is provided. + This function also prints a message to the console stating the number of segments processed and the numbers of empty, corrupt, source and existing translations removed. + :param xliffPath: Path to the xliff file to be stripped + :param outputPath: Path to the resulting xliff file + :param oldXliffPath: Path to the old xliff file containing existing translations that should be also stripped. + """ + print(f"Creating stripped xliff at {outputPath} from {xliffPath}") + namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} + xliff = lxml.etree.parse(xliffPath) + xliffRoot = xliff.getroot() + if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": + raise ValueError(f"Not an xliff file: {xliffPath}") + oldXliffRoot = None + if oldXliffPath: + oldXliff = lxml.etree.parse(oldXliffPath) + oldXliffRoot = oldXliff.getroot() + if oldXliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": + raise ValueError(f"Not an xliff file: {oldXliffPath}") + skeletonNode = xliffRoot.find("./xliff:file/xliff:skeleton", namespaces=namespace) + if skeletonNode is not None: + skeletonNode.getparent().remove(skeletonNode) + file = xliffRoot.find("./xliff:file", namespaces=namespace) + units = file.findall("./xliff:unit", namespaces=namespace) + segmentCount = 0 + untranslatedCount = 0 + emptyCount = 0 + corruptCount = 0 + existingTranslationCount = 0 + for unit in units: + unitID = unit.get("id") + notes = unit.find("./xliff:notes", namespaces=namespace) + if notes is not None: + unit.remove(notes) + segment = unit.find("./xliff:segment", namespaces=namespace) + if segment is None: + print("Warning: No segment element in unit") + continue + segmentCount += 1 + state = segment.get("state") + if state == "initial": + file.remove(unit) + untranslatedCount += 1 + continue + target = segment.find("./xliff:target", namespaces=namespace) + if target is None: + file.remove(unit) + untranslatedCount += 1 + continue + targetText = target.text + if not targetText: + emptyCount += 1 + file.remove(unit) + continue + elif targetText in ( + "", + "<target/>", + "", + "<target></target>", + ): + corruptCount += 1 + file.remove(unit) + continue + if oldXliffRoot: + # Remove existing translations + oldTarget = oldXliffRoot.find( + f"./xliff:file/xliff:unit[@id='{unitID}']/xliff:segment/xliff:target", + namespaces=namespace, + ) + if oldTarget is not None and oldTarget.getparent().get("state") != "initial": + if oldTarget.text == targetText: + file.remove(unit) + existingTranslationCount += 1 + xliff.write(outputPath, encoding="utf-8") + if corruptCount > 0: + print(f"Removed {corruptCount} corrupt translations.") + if emptyCount > 0: + print(f"Removed {emptyCount} empty translations.") + if existingTranslationCount > 0: + print(f"Ignored {existingTranslationCount} existing translations.") + keptTranslations = segmentCount - untranslatedCount - emptyCount - corruptCount - existingTranslationCount + print(f"Added or changed {keptTranslations} translations.") + + +def downloadTranslationFile(crowdinFilePath: str, localFilePath: str, language: str): + """ + Download a translation file from Crowdin. + :param crowdinFilePath: The Crowdin file path + :param localFilePath: The path to save the local file + :param language: The language code to download the translation for + """ + with open(JSON_FILE, "r", encoding="utf-8") as jsonFile: + files = json.load(jsonFile) + fileId = files.get(crowdinFilePath) + if fileId is None: + files = getFiles() + fileId = files.get(crowdinFilePath) + print(f"Requesting export of {crowdinFilePath} for {language} from Crowdin") + res = getCrowdinClient().translations.export_project_translation( + fileIds=[fileId], + targetLanguageId=language, + ) + if res is None: + raise ValueError("Crowdin export failed") + download_url = res["data"]["url"] + print(f"Downloading from {download_url}") + with open(localFilePath, "wb") as f: + r = requests.get(download_url) + f.write(r.content) + print(f"Saved to {localFilePath}") + + +def uploadSourceFile(localFilePath: str): + """ + Upload a source file to Crowdin. + :param localFilePath: The path to the local file to be uploaded + """ + with open(JSON_FILE, "r", encoding="utf-8") as jsonFile: + files = json.load(jsonFile) + fileId = files.get(localFilePath) + if fileId is None: + files = getFiles() + fileId = files.get(localFilePath) + res = getCrowdinClient().storages.add_storage( + open(localFilePath, "rb"), + ) + if res is None: + raise ValueError("Crowdin storage upload failed") + storageId = res["data"]["id"] + print(f"Stored with ID {storageId}") + filename = os.path.basename(localFilePath) + fileId = files.get(filename) + print(f"File ID: {fileId}") + match fileId: + case None: + if os.path.splitext(filename)[1] == ".pot": + title=f"{os.path.splitext(filename)[0]} interface" + exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{os.path.splitext(filename)[0]}.po" + else: + title=f"{os.path.splitext(filename)[0]} documentation" + exportPattern =f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{filename}" + exportOptions = { + "exportPattern": exportPattern + } + print(f"Importing source file {localFilePath} from storage with ID {storageId}") + res = getCrowdinClient().source_files.add_file(storageId=storageId, projectId=CROWDIN_PROJECT_ID, name=filename, title=title, exportOptions=exportOptions) + print("Done") + case _: + res = getCrowdinClient().source_files.update_file(fileId=fileId , storageId=storageId, projectId=CROWDIN_PROJECT_ID) + + +def getFiles() -> dict: + """Gets files from Crowdin, and write them to a json file.""" + + res = getCrowdinClient().source_files.list_files(CROWDIN_PROJECT_ID, limit=500) + if res is None: + raise ValueError("Getting files from Crowdin failed") + dictionary = dict() + data = res["data"] + for file in data: + fileInfo = file["data"] + name = fileInfo["name"] + id = fileInfo["id"] + dictionary[name] = id + with open(JSON_FILE, "w", encoding="utf-8") as jsonFile: + json.dump(dictionary, jsonFile, ensure_ascii=False) + return dictionary + + +def uploadTranslationFile(crowdinFilePath: str, localFilePath: str, language: str): + """ + Upload a translation file to Crowdin. + :param crowdinFilePath: The Crowdin file path + :param localFilePath: The path to the local file to be uploaded + :param language: The language code to upload the translation for + """ + with open(JSON_FILE, "r", encoding="utf-8") as jsonFile: + files = json.load(jsonFile) + fileId = files.get(crowdinFilePath) + if fileId is None: + files = getFiles() + fileId = files.get(crowdinFilePath) + print(f"Uploading {localFilePath} to Crowdin") + res = getCrowdinClient().storages.add_storage( + open(localFilePath, "rb"), + ) + if res is None: + raise ValueError("Crowdin storage upload failed") + storageId = res["data"]["id"] + print(f"Stored with ID {storageId}") + print(f"Importing translation for {crowdinFilePath} in {language} from storage with ID {storageId}") + res = getCrowdinClient().translations.upload_translation( + fileId=fileId, + languageId=language, + storageId=storageId, + autoApproveImported=True, + importEqSuggestions=True, + ) + print("Done") + + +def exportTranslations(outputDir: str, language: str | None = None): + """ + Export translation files from Crowdin as a bundle. + :param outputDir: Directory to save translation files. + :param language: The language code to export (e.g., 'es', 'fr', 'de'). + If None, exports all languages. + """ + + # Create output directory if it doesn't exist + os.makedirs(outputDir, exist_ok=True) + + client = getCrowdinClient() + + requestData = { + "skipUntranslatedStrings": False, + "skipUntranslatedFiles": True, + "exportApprovedOnly": False, + } + + if language is not None: + requestData["targetLanguageIds"] = [language] + + if language is None: + print("Requesting export of all translations from Crowdin...") + else: + print(f"Requesting export of all translations for language: {language}") + build_res = client.translations.build_project_translation(request_data=requestData) + + if language is None: + zip_filename = "translations.zip" + else: + zip_filename = f"translations_{language}.zip" + + if build_res is None: + raise ValueError("Failed to start translation build") + + build_id = build_res["data"]["id"] + print(f"Build started with ID: {build_id}") + + # Wait for the build to complete + print("Waiting for build to complete...") + while True: + status_res = client.translations.check_project_build_status(build_id) + if status_res is None: + raise ValueError("Failed to check build status") + + status = status_res["data"]["status"] + progress = status_res["data"]["progress"] + print(f"Build status: {status} ({progress}%)") + + if status == "finished": + break + elif status == "failed": + raise ValueError("Translation build failed") + + time.sleep(POLLING_INTERVAL_SECONDS) + + # Download the completed build + print("Downloading translations archive...") + download_res = client.translations.download_project_translations(build_id) + if download_res is None: + raise ValueError("Failed to get download URL") + + download_url = download_res["data"]["url"] + print(f"Downloading from {download_url}") + + # Download and extract the ZIP file + zip_path = os.path.join(outputDir, zip_filename) + response = requests.get(download_url, stream=True, timeout=EXPORT_TIMEOUT_SECONDS) + response.raise_for_status() + + with open(zip_path, "wb") as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + print(f"Archive saved to {zip_path}") + print("Extracting translations...") + + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(outputDir) + + # Remove the zip file + os.remove(zip_path) + + if language is None: + print(f"\nExport complete! All translations extracted to '{outputDir}' directory.") + else: + print(f"\nExport complete! All {language} translations extracted to '{outputDir}' directory.") + + +class _PoChecker: + """Checks a po file for errors not detected by msgfmt. + This first runs msgfmt to check for syntax errors. + It then checks for mismatched Python percent and brace interpolations. + Construct an instance and call the L{check} method. + """ + + FUZZY = "#, fuzzy" + MSGID = "msgid" + MSGID_PLURAL = "msgid_plural" + MSGSTR = "msgstr" + + def __init__(self, po: str): + """Constructor. + :param po: The path to the po file to check. + """ + self._poPath = po + with codecs.open(po, "r", "utf-8") as file: + self._poContent = file.readlines() + self._string: str | None = None + + self.alerts: list[str] = [] + """List of error and warning messages found in the po file.""" + + self.hasSyntaxError: bool = False + """Whether there is a syntax error in the po file.""" + + self.warningCount: int = 0 + """Number of warnings found.""" + + self.errorCount: int = 0 + """Number of errors found.""" + + def _addToString(self, line: list[str], startingCommand: str | None = None) -> None: + """Helper function to add a line to the current string. + :param line: The line to add. + :param startingCommand: The command that started this string, if any. + This is used to determine whether to strip the command and quotes. + """ + if startingCommand: + # Strip the command and the quotes. + self._string = line[len(startingCommand) + 2 : -1] + else: + # Strip the quotes. + self._string += line[1:-1] + + def _finishString(self) -> str: + """Helper function to finish the current string. + :return: The finished string. + """ + string = self._string + self._string = None + return string + + def _messageAlert(self, alert: str, isError: bool = True) -> None: + """Helper function to add an alert about a message. + :param alert: The alert message. + :param isError: Whether this is an error or a warning. + """ + if self._fuzzy: + # Fuzzy messages don't get used, so this shouldn't be considered an error. + isError = False + if isError: + self.errorCount += 1 + else: + self.warningCount += 1 + if self._fuzzy: + msgType = "Fuzzy message" + else: + msgType = "Message" + self.alerts.append( + f"{msgType} starting on line {self._messageLineNum}\n" + f'Original: "{self._msgid}"\n' + f'Translated: "{self._msgstr[-1]}"\n' + f"{'ERROR' if isError else 'WARNING'}: {alert}", + ) + + @property + def MSGFMT_PATH(self) -> str: + try: + # When running from source, miscDeps is the sibling of parent this script. + _MSGFMT = os.path.join(os.path.dirname(__file__), "..", "miscDeps", "tools", "msgfmt.exe") + except NameError: + # When running from a frozen executable, __file__ is not defined. + # In this case, we use the distribution path. + # When running from a distribution, source/l10nUtil.py is built to l10nUtil.exe. + # miscDeps is the sibling of this script in the distribution. + _MSGFMT = os.path.join(sys.prefix, "miscDeps", "tools", "msgfmt.exe") + + if not os.path.exists(_MSGFMT): + raise FileNotFoundError( + "msgfmt executable not found. " + "Please ensure that miscDeps/tools/msgfmt.exe exists in the source tree or distribution.", + ) + return _MSGFMT + + def _checkSyntax(self) -> None: + """Check the syntax of the po file using msgfmt. + This will set the hasSyntaxError attribute to True if there is a syntax error. + """ + + result = subprocess.run( + (self.MSGFMT_PATH, "-o", "-", self._poPath), + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, # Ensures stderr is a text stream + ) + if result.returncode != 0: + output = result.stderr.rstrip().replace("\r\n", "\n") + self.alerts.append(output) + self.hasSyntaxError = True + self.errorCount = 1 + + def _checkMessages(self) -> None: + command = None + self._msgid = None + self._msgid_plural = None + self._msgstr = None + nextFuzzy = False + self._fuzzy = False + for lineNum, line in enumerate(self._poContent, 1): + line = line.strip() + if line.startswith(self.FUZZY): + nextFuzzy = True + continue + elif line.startswith(self.MSGID) and not line.startswith(self.MSGID_PLURAL): + # New message. + if self._msgstr is not None: + self._msgstr[-1] = self._finishString() + # Check the message we just handled. + self._checkMessage() + command = self.MSGID + start = command + self._messageLineNum = lineNum + self._fuzzy = nextFuzzy + nextFuzzy = False + elif line.startswith(self.MSGID_PLURAL): + self._msgid = self._finishString() + command = self.MSGID_PLURAL + start = command + elif line.startswith(self.MSGSTR): + self._handleMsgStrReaching(lastCommand=command) + command = self.MSGSTR + start = line[: line.find(" ")] + elif line.startswith('"'): + # Continuing a string. + start = None + else: + # This line isn't of interest. + continue + self._addToString(line, startingCommand=start) + if command == self.MSGSTR: + # Handle the last message. + self._msgstr[-1] = self._finishString() + self._checkMessage() + + def _handleMsgStrReaching(self, lastCommand: str) -> None: + """Helper function used by _checkMessages to handle the required processing when reaching a line + starting with "msgstr". + :param lastCommand: the current command just before the msgstr line is reached. + """ + + # Finish the string of the last command and check the message if it was an msgstr + if lastCommand == self.MSGID: + self._msgid = self._finishString() + elif lastCommand == self.MSGID_PLURAL: + self._msgid_plural = self._finishString() + elif lastCommand == self.MSGSTR: + self._msgstr[-1] = self._finishString() + self._checkMessage() + else: + raise RuntimeError(f"Unexpected command before line {self._messageLineNum}: {lastCommand}") + + # For first msgstr create the msgstr list + if lastCommand != self.MSGSTR: + self._msgstr = [] + + # Initiate the string for the current msgstr + self._msgstr.append("") + + def check(self) -> bool: + """Check the file. + Once this returns, you can call getReport to obtain a report. + This method should not be called more than once. + :return: True if the file is okay, False if there were problems. + """ + self._checkSyntax() + if self.alerts: + return False + self._checkMessages() + if self.alerts: + return False + return True + + # e.g. %s %d %10.2f %-5s (but not %%) or %%(name)s %(name)d + RE_UNNAMED_PERCENT = re.compile( + # Does not include optional mapping key, as that's handled by a different regex + r""" + (?:(?<=%%)|(? tuple[list[str], set[str], set[str]]: + """Get the percent and brace interpolations in a string. + :param text: The text to check. + :return: A tuple of a list and two sets: + - unnamed percent interpolations (e.g. %s, %d) + - named percent interpolations (e.g. %(name)s) + - brace format interpolations (e.g. {name}, {name:format}) + """ + unnamedPercent = self.RE_UNNAMED_PERCENT.findall(text) + namedPercent = set(self.RE_NAMED_PERCENT.findall(text)) + formats = set() + for m in self.RE_FORMAT.finditer(text): + if not m.group(1): + self._messageAlert( + "Unspecified positional argument in brace format", + # Skip as error as many of these had been introduced in the source .po files. + # These should be fixed in the source .po files to add names to instances of "{}". + # This causes issues where the order of the arguments change in the string. + # e.g. "Character: {}\nReplacement: {}" being translated to "Replacement: {}\nCharacter: {}" + # will result in the expected interpolation being in the wrong place. + # This should be changed isError=True. + isError=False, + ) + formats.add(m.group(0)) + return unnamedPercent, namedPercent, formats + + def _formatInterpolations( + self, + unnamedPercent: list[str], + namedPercent: set[str], + formats: set[str], + ) -> str: + """Format the interpolations for display in an error message. + :param unnamedPercent: The unnamed percent interpolations. + :param namedPercent: The named percent interpolations. + :param formats: The brace format interpolations. + """ + out: list[str] = [] + if unnamedPercent: + out.append(f"unnamed percent interpolations in this order: {unnamedPercent}") + if namedPercent: + out.append(f"these named percent interpolations: {namedPercent}") + if formats: + out.append(f"these brace format interpolations: {formats}") + if not out: + return "no interpolations" + return "\n\tAnd ".join(out) + + def _checkMessage(self) -> None: + idUnnamedPercent, idNamedPercent, idFormats = self._getInterpolations(self._msgid) + if not self._msgstr[-1]: + return + strUnnamedPercent, strNamedPercent, strFormats = self._getInterpolations(self._msgstr[-1]) + error = False + alerts = [] + if idUnnamedPercent != strUnnamedPercent: + if idUnnamedPercent: + alerts.append("unnamed percent interpolations differ") + error = True + else: + alerts.append("unexpected presence of unnamed percent interpolations") + if idNamedPercent - strNamedPercent: + alerts.append("missing named percent interpolation") + error = True + if strNamedPercent - idNamedPercent: + if idNamedPercent: + alerts.append("extra named percent interpolation") + error = True + else: + alerts.append("unexpected presence of named percent interpolations") + if idFormats - strFormats: + alerts.append("missing brace format interpolation") + error = True + if strFormats - idFormats: + if idFormats: + alerts.append("extra brace format interpolation") + error = True + else: + alerts.append("unexpected presence of brace format interpolations") + if alerts: + self._messageAlert( + f"{', '.join(alerts)}\n" + f"Expected: {self._formatInterpolations(idUnnamedPercent, idNamedPercent, idFormats)}\n" + f"Got: {self._formatInterpolations(strUnnamedPercent, strNamedPercent, strFormats)}", + isError=error, + ) + + def getReport(self) -> str | None: + """Get a text report about any errors or warnings. + :return: The text or None if there were no problems. + """ + if not self.alerts: + return None + report = f"File {self._poPath}: " + if self.hasSyntaxError: + report += "syntax error" + else: + if self.errorCount: + msg = "error" if self.errorCount == 1 else "errors" + report += f"{self.errorCount} {msg}" + if self.warningCount: + if self.errorCount: + report += ", " + msg = "warning" if self.warningCount == 1 else "warnings" + report += f"{self.warningCount} {msg}" + report += "\n\n" + "\n\n".join(self.alerts) + return report + + +def checkPo(poFilePath: str) -> tuple[bool, str | None]: + """Check a po file for errors. + :param poFilePath: The path to the po file to check. + :return: + True if the file is okay or has warnings, False if there were fatal errors. + A report about the errors or warnings found, or None if there were no problems. + """ + c = _PoChecker(poFilePath) + report = None + if not c.check(): + report = c.getReport() + if report: + report = report.encode("cp1252", errors="backslashreplace").decode( + "utf-8", + errors="backslashreplace", + ) + return not bool(c.errorCount), report + + +def main(): + args = argparse.ArgumentParser() + commands = args.add_subparsers(title="commands", dest="command", required=True) + command_checkPo = commands.add_parser("checkPo", help="Check po files") + # Allow entering arbitrary po file paths, not just those in the source tree + command_checkPo.add_argument( + "poFilePaths", + help="Paths to the po file to check", + nargs="+", + ) + command_xliff2md = commands.add_parser("xliff2md", help="Convert xliff to markdown") + command_xliff2md.add_argument( + "-u", + "--untranslated", + help="Produce the untranslated markdown file", + action="store_true", + default=False, + ) + command_xliff2md.add_argument("xliffPath", help="Path to the xliff file") + command_xliff2md.add_argument("mdPath", help="Path to the resulting markdown file") + command_md2html = commands.add_parser("md2html", help="Convert markdown to html") + command_md2html.add_argument("-l", "--lang", help="Language code", action="store", default="en") + command_md2html.add_argument( + "-t", + "--docType", + help="Type of document", + action="store", + choices=["userGuide", "developerGuide", "changes", "keyCommands"], + ) + command_md2html.add_argument("mdPath", help="Path to the markdown file") + command_md2html.add_argument("htmlPath", help="Path to the resulting html file") + command_xliff2html = commands.add_parser("xliff2html", help="Convert xliff to html") + command_xliff2html.add_argument("-l", "--lang", help="Language code", action="store", required=False) + command_xliff2html.add_argument( + "-t", + "--docType", + help="Type of document", + action="store", + choices=["userGuide", "developerGuide", "changes", "keyCommands"], + ) + command_xliff2html.add_argument( + "-u", + "--untranslated", + help="Produce the untranslated markdown file", + action="store_true", + default=False, + ) + command_xliff2html.add_argument("xliffPath", help="Path to the xliff file") + command_xliff2html.add_argument("htmlPath", help="Path to the resulting html file") + uploadSourceFileCommand = commands.add_parser( + "uploadSourceFile", + help="Upload a source file to Crowdin.", + ) + uploadSourceFileCommand.add_argument( + "-f", + "--localFilePath", + help="The local path to the file.", + ) + getFilesCommand = commands.add_parser( + "getFiles", + help="Get files from Crowdin.", + ) + downloadTranslationFileCommand = commands.add_parser( + "downloadTranslationFile", + help="Download a translation file from Crowdin.", + ) + downloadTranslationFileCommand.add_argument( + "language", + help="The language code to download the translation for.", + ) + downloadTranslationFileCommand.add_argument( + "crowdinFilePath", + help="The Crowdin file path", + ) + downloadTranslationFileCommand.add_argument( + "localFilePath", + nargs="?", + default=None, + help="The path to save the local file. If not provided, the Crowdin file path will be used.", + ) + uploadTranslationFileCommand = commands.add_parser( + "uploadTranslationFile", + help="Upload a translation file to Crowdin.", + ) + uploadTranslationFileCommand.add_argument( + "-o", + "--old", + help="Path to the old unchanged xliff file. If provided, only new or changed translations will be uploaded.", + default=None, + ) + uploadTranslationFileCommand.add_argument( + "language", + help="The language code to upload the translation for.", + ) + uploadTranslationFileCommand.add_argument( + "crowdinFilePath", + help="The Crowdin file path", + ) + uploadTranslationFileCommand.add_argument( + "localFilePath", + nargs="?", + default=None, + help="The path to the local file to be uploaded. If not provided, the Crowdin file path will be used.", + ) + + exportTranslationsCommand = commands.add_parser( + "exportTranslations", + help="Export translation files from Crowdin as a bundle. If no language is specified, exports all languages.", + ) + exportTranslationsCommand.add_argument( + "-o", + "--output", + help="Directory to save translation files", + required=True, + ) + exportTranslationsCommand.add_argument( + "-l", + "--language", + help="Language code to export (e.g., 'es', 'fr', 'de'). If not specified, exports all languages.", + default=None, + ) + + args = args.parse_args() + match args.command: + case "xliff2md": + markdownTranslate.generateMarkdown( + xliffPath=args.xliffPath, + outputPath=args.mdPath, + translated=not args.untranslated, + ) + case "md2html": + md2html.main(source=args.mdPath, dest=args.htmlPath, lang=args.lang, docType=args.docType) + case "xliff2html": + lang = args.lang or fetchLanguageFromXliff(args.xliffPath, source=args.untranslated) + temp_mdFile = tempfile.NamedTemporaryFile(suffix=".md", delete=False, mode="w", encoding="utf-8") + temp_mdFile.close() + try: + markdownTranslate.generateMarkdown( + xliffPath=args.xliffPath, + outputPath=temp_mdFile.name, + translated=not args.untranslated, + ) + md2html.main(source=temp_mdFile.name, dest=args.htmlPath, lang=lang, docType=args.docType) + finally: + os.remove(temp_mdFile.name) + case "uploadSourceFile": + uploadSourceFile(args.localFilePath) + case "getFiles": + getFiles() + case "downloadTranslationFile": + localFilePath = args.localFilePath or args.crowdinFilePath + downloadTranslationFile(args.crowdinFilePath, localFilePath, args.language) + if args.crowdinFilePath.endswith(".xliff"): + preprocessXliff(localFilePath, localFilePath) + elif localFilePath.endswith(".po"): + success, report = checkPo(localFilePath) + if report: + print(report) + if not success: + print(f"\nWarning: Po file {localFilePath} has fatal errors.") + case "checkPo": + poFilePaths = args.poFilePaths + badFilePaths: list[str] = [] + for poFilePath in poFilePaths: + success, report = checkPo(poFilePath) + if report: + print(report) + if not success: + badFilePaths.append(poFilePath) + if badFilePaths: + print(f"\nOne or more po files had fatal errors: {', '.join(badFilePaths)}") + sys.exit(1) + case "uploadTranslationFile": + localFilePath = args.localFilePath or args.crowdinFilePath + needsDelete = False + if args.crowdinFilePath.endswith(".xliff"): + tmp = tempfile.NamedTemporaryFile(suffix=".xliff", delete=False, mode="w") + tmp.close() + shutil.copyfile(localFilePath, tmp.name) + stripXliff(tmp.name, tmp.name, args.old) + localFilePath = tmp.name + needsDelete = True + elif localFilePath.endswith(".po"): + success, report = checkPo(localFilePath) + if report: + print(report) + if not success: + print(f"\nPo file {localFilePath} has errors. Upload aborted.") + sys.exit(1) + uploadTranslationFile(args.crowdinFilePath, localFilePath, args.language) + if needsDelete: + os.remove(localFilePath) + case "exportTranslations": + exportTranslations(args.output, args.language) + case _: + raise ValueError(f"Unknown command {args.command}") + + +if __name__ == "__main__": + main() diff --git a/_l10n/markdownTranslate.py b/_l10n/markdownTranslate.py new file mode 100644 index 0000000..341ead6 --- /dev/null +++ b/_l10n/markdownTranslate.py @@ -0,0 +1,733 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2024 NV Access Limited. +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +from typing import Generator +import tempfile +import os +import contextlib +import lxml.etree +import argparse +import uuid +import re +from itertools import zip_longest +from xml.sax.saxutils import escape as xmlEscape +import difflib +from dataclasses import dataclass +import subprocess + +RAW_GITHUB_REPO_URL = "https://raw.githubusercontent.com/nvaccess/nvda" +re_kcTitle = re.compile(r"^()$") +re_kcSettingsSection = re.compile(r"^()$") +# Comments that span a single line in their entirety +re_comment = re.compile(r"^$") +re_heading = re.compile(r"^(#+\s+)(.+?)((?:\s+\{#.+\})?)$") +re_bullet = re.compile(r"^(\s*\*\s+)(.+)$") +re_number = re.compile(r"^(\s*[0-9]+\.\s+)(.+)$") +re_hiddenHeaderRow = re.compile(r"^\|\s*\.\s*\{\.hideHeaderRow\}\s*(\|\s*\.\s*)*\|$") +re_postTableHeaderLine = re.compile(r"^(\|\s*-+\s*)+\|$") +re_tableRow = re.compile(r"^(\|)(.+)(\|)$") +re_translationID = re.compile(r"^(.*)\$\(ID:([0-9a-f-]+)\)(.*)$") + + +def prettyPathString(path: str) -> str: + cwd = os.getcwd() + if os.path.normcase(os.path.splitdrive(path)[0]) != os.path.normcase(os.path.splitdrive(cwd)[0]): + return path + return os.path.relpath(path, cwd) + + +@contextlib.contextmanager +def createAndDeleteTempFilePath_contextManager( + dir: str | None = None, + prefix: str | None = None, + suffix: str | None = None, +) -> Generator[str, None, None]: + """A context manager that creates a temporary file and deletes it when the context is exited""" + with tempfile.NamedTemporaryFile(dir=dir, prefix=prefix, suffix=suffix, delete=False) as tempFile: + tempFilePath = tempFile.name + tempFile.close() + yield tempFilePath + os.remove(tempFilePath) + + +def getLastCommitID(filePath: str) -> str: + # Run the git log command to get the last commit ID for the given file + result = subprocess.run( + ["git", "log", "-n", "1", "--pretty=format:%H", "--", filePath], + capture_output=True, + text=True, + check=True, + ) + commitID = result.stdout.strip() + if not re.match(r"[0-9a-f]{40}", commitID): + raise ValueError(f"Invalid commit ID: '{commitID}' for file '{filePath}'") + return commitID + + +def getGitDir() -> str: + # Run the git rev-parse command to get the root of the git directory + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ) + gitDir = result.stdout.strip() + if not os.path.isdir(gitDir): + raise ValueError(f"Invalid git directory: '{gitDir}'") + return gitDir + + +def getRawGithubURLForPath(filePath: str) -> str: + gitDirPath = getGitDir() + commitID = getLastCommitID(filePath) + relativePath = os.path.relpath(os.path.abspath(filePath), gitDirPath) + relativePath = relativePath.replace("\\", "/") + return f"{RAW_GITHUB_REPO_URL}/{commitID}/{relativePath}" + + +def skeletonizeLine(mdLine: str) -> str | None: + prefix = "" + suffix = "" + if ( + mdLine.isspace() + or mdLine.strip() == "[TOC]" + or re_hiddenHeaderRow.match(mdLine) + or re_postTableHeaderLine.match(mdLine) + ): + return None + elif m := re_heading.match(mdLine): + prefix, content, suffix = m.groups() + elif m := re_bullet.match(mdLine): + prefix, content = m.groups() + elif m := re_number.match(mdLine): + prefix, content = m.groups() + elif m := re_tableRow.match(mdLine): + prefix, content, suffix = m.groups() + elif m := re_kcTitle.match(mdLine): + prefix, content, suffix = m.groups() + elif m := re_kcSettingsSection.match(mdLine): + prefix, content, suffix = m.groups() + elif re_comment.match(mdLine): + return None + ID = str(uuid.uuid4()) + return f"{prefix}$(ID:{ID}){suffix}\n" + + +@dataclass +class Result_generateSkeleton: + numTotalLines: int = 0 + numTranslationPlaceholders: int = 0 + + +def generateSkeleton(mdPath: str, outputPath: str) -> Result_generateSkeleton: + print(f"Generating skeleton file {prettyPathString(outputPath)} from {prettyPathString(mdPath)}...") + res = Result_generateSkeleton() + with ( + open(mdPath, "r", encoding="utf8") as mdFile, + open(outputPath, "w", encoding="utf8", newline="") as outputFile, + ): + for mdLine in mdFile.readlines(): + res.numTotalLines += 1 + skelLine = skeletonizeLine(mdLine) + if skelLine: + res.numTranslationPlaceholders += 1 + else: + skelLine = mdLine + outputFile.write(skelLine) + print( + f"Generated skeleton file with {res.numTotalLines} total lines and {res.numTranslationPlaceholders} translation placeholders", + ) + return res + + +@dataclass +class Result_updateSkeleton: + numAddedLines: int = 0 + numAddedTranslationPlaceholders: int = 0 + numRemovedLines: int = 0 + numRemovedTranslationPlaceholders: int = 0 + numUnchangedLines: int = 0 + numUnchangedTranslationPlaceholders: int = 0 + + +def extractSkeleton(xliffPath: str, outputPath: str): + print(f"Extracting skeleton from {prettyPathString(xliffPath)} to {prettyPathString(outputPath)}...") + with contextlib.ExitStack() as stack: + outputFile = stack.enter_context(open(outputPath, "w", encoding="utf8", newline="")) + xliff = lxml.etree.parse(xliffPath) + xliffRoot = xliff.getroot() + namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} + if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": + raise ValueError("Not an xliff file") + skeletonNode = xliffRoot.find("./xliff:file/xliff:skeleton", namespaces=namespace) + if skeletonNode is None: + raise ValueError("No skeleton found in xliff file") + skeletonContent = skeletonNode.text.strip() + outputFile.write(skeletonContent) + print(f"Extracted skeleton to {prettyPathString(outputPath)}") + + +def updateSkeleton( + origMdPath: str, + newMdPath: str, + origSkelPath: str, + outputPath: str, +) -> Result_updateSkeleton: + print( + f"Creating updated skeleton file {prettyPathString(outputPath)} from {prettyPathString(origSkelPath)} with changes from {prettyPathString(origMdPath)} to {prettyPathString(newMdPath)}...", + ) + res = Result_updateSkeleton() + with contextlib.ExitStack() as stack: + origMdFile = stack.enter_context(open(origMdPath, "r", encoding="utf8")) + newMdFile = stack.enter_context(open(newMdPath, "r", encoding="utf8")) + origSkelFile = stack.enter_context(open(origSkelPath, "r", encoding="utf8")) + outputFile = stack.enter_context(open(outputPath, "w", encoding="utf8", newline="")) + mdDiff = difflib.ndiff(origMdFile.readlines(), newMdFile.readlines()) + origSkelLines = iter(origSkelFile.readlines()) + for mdDiffLine in mdDiff: + if mdDiffLine.startswith("?"): + continue + if mdDiffLine.startswith(" "): + res.numUnchangedLines += 1 + skelLine = next(origSkelLines) + if re_translationID.match(skelLine): + res.numUnchangedTranslationPlaceholders += 1 + outputFile.write(skelLine) + elif mdDiffLine.startswith("+"): + res.numAddedLines += 1 + skelLine = skeletonizeLine(mdDiffLine[2:]) + if skelLine: + res.numAddedTranslationPlaceholders += 1 + else: + skelLine = mdDiffLine[2:] + outputFile.write(skelLine) + elif mdDiffLine.startswith("-"): + res.numRemovedLines += 1 + origSkelLine = next(origSkelLines) + if re_translationID.match(origSkelLine): + res.numRemovedTranslationPlaceholders += 1 + else: + raise ValueError(f"Unexpected diff line: {mdDiffLine}") + print( + f"Updated skeleton file with {res.numAddedLines} added lines " + f"({res.numAddedTranslationPlaceholders} translation placeholders), " + f"{res.numRemovedLines} removed lines ({res.numRemovedTranslationPlaceholders} translation placeholders), " + f"and {res.numUnchangedLines} unchanged lines ({res.numUnchangedTranslationPlaceholders} translation placeholders)", + ) + return res + + +@dataclass +class Result_generateXliff: + numTranslatableStrings: int = 0 + + +def generateXliff( + mdPath: str, + outputPath: str, + skelPath: str | None = None, +) -> Result_generateXliff: + # If a skeleton file is not provided, first generate one + with contextlib.ExitStack() as stack: + if not skelPath: + skelPath = stack.enter_context( + createAndDeleteTempFilePath_contextManager( + dir=os.path.dirname(outputPath), + prefix=os.path.basename(mdPath), + suffix=".skel", + ), + ) + generateSkeleton(mdPath=mdPath, outputPath=skelPath) + with open(skelPath, "r", encoding="utf8") as skelFile: + skelContent = skelFile.read() + res = Result_generateXliff() + print( + f"Generating xliff file {prettyPathString(outputPath)} from {prettyPathString(mdPath)} and {prettyPathString(skelPath)}...", + ) + with contextlib.ExitStack() as stack: + mdFile = stack.enter_context(open(mdPath, "r", encoding="utf8")) + outputFile = stack.enter_context(open(outputPath, "w", encoding="utf8", newline="")) + fileID = os.path.basename(mdPath) + mdUri = getRawGithubURLForPath(mdPath) + print(f"Including Github raw URL: {mdUri}") + outputFile.write( + '\n' + f'\n' + f'\n', + ) + outputFile.write(f"\n{xmlEscape(skelContent)}\n\n") + res.numTranslatableStrings = 0 + for lineNo, (mdLine, skelLine) in enumerate( + zip_longest(mdFile.readlines(), skelContent.splitlines(keepends=True)), + start=1, + ): + mdLine = mdLine.rstrip() + skelLine = skelLine.rstrip() + if m := re_translationID.match(skelLine): + res.numTranslatableStrings += 1 + prefix, ID, suffix = m.groups() + if prefix and not mdLine.startswith(prefix): + raise ValueError(f'Line {lineNo}: does not start with "{prefix}", {mdLine=}, {skelLine=}') + if suffix and not mdLine.endswith(suffix): + raise ValueError(f'Line {lineNo}: does not end with "{suffix}", {mdLine=}, {skelLine=}') + source = mdLine[len(prefix) : len(mdLine) - len(suffix)] + outputFile.write( + f'\n\nline: {lineNo + 1}\n', + ) + if prefix: + outputFile.write(f'prefix: {xmlEscape(prefix)}\n') + if suffix: + outputFile.write(f'suffix: {xmlEscape(suffix)}\n') + outputFile.write( + "\n" + f"\n" + f"{xmlEscape(source)}\n" + "\n" + "\n", # fmt: skip + ) + else: + if mdLine != skelLine: + raise ValueError(f"Line {lineNo}: {mdLine=} does not match {skelLine=}") + outputFile.write("\n") + print(f"Generated xliff file with {res.numTranslatableStrings} translatable strings") + return res + + +@dataclass +class Result_translateXliff: + numTranslatedStrings: int = 0 + + +def updateXliff( + xliffPath: str, + mdPath: str, + outputPath: str, +): + # uses generateMarkdown, extractSkeleton, updateSkeleton, and generateXliff to generate an updated xliff file. + outputDir = os.path.dirname(outputPath) + print( + f"Generating updated xliff file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)} and {prettyPathString(mdPath)}...", + ) + with contextlib.ExitStack() as stack: + origMdPath = stack.enter_context( + createAndDeleteTempFilePath_contextManager(dir=outputDir, prefix="generated_", suffix=".md"), + ) + generateMarkdown(xliffPath=xliffPath, outputPath=origMdPath, translated=False) + origSkelPath = stack.enter_context( + createAndDeleteTempFilePath_contextManager(dir=outputDir, prefix="extracted_", suffix=".skel"), + ) + extractSkeleton(xliffPath=xliffPath, outputPath=origSkelPath) + updatedSkelPath = stack.enter_context( + createAndDeleteTempFilePath_contextManager(dir=outputDir, prefix="updated_", suffix=".skel"), + ) + updateSkeleton( + origMdPath=origMdPath, + newMdPath=mdPath, + origSkelPath=origSkelPath, + outputPath=updatedSkelPath, + ) + generateXliff( + mdPath=mdPath, + skelPath=updatedSkelPath, + outputPath=outputPath, + ) + print(f"Generated updated xliff file {prettyPathString(outputPath)}") + + +def translateXliff( + xliffPath: str, + lang: str, + pretranslatedMdPath: str, + outputPath: str, + allowBadAnchors: bool = False, +) -> Result_translateXliff: + print( + f"Creating {lang} translated xliff file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)} using {prettyPathString(pretranslatedMdPath)}...", + ) + res = Result_translateXliff() + with contextlib.ExitStack() as stack: + pretranslatedMdFile = stack.enter_context(open(pretranslatedMdPath, "r", encoding="utf8")) + xliff = lxml.etree.parse(xliffPath) + xliffRoot = xliff.getroot() + namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} + if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": + raise ValueError("Not an xliff file") + xliffRoot.set("trgLang", lang) + skeletonNode = xliffRoot.find("./xliff:file/xliff:skeleton", namespaces=namespace) + if skeletonNode is None: + raise ValueError("No skeleton found in xliff file") + skeletonContent = skeletonNode.text.strip() + for lineNo, (skelLine, pretranslatedLine) in enumerate( + zip_longest(skeletonContent.splitlines(), pretranslatedMdFile.readlines()), + start=1, + ): + skelLine = skelLine.rstrip() + pretranslatedLine = pretranslatedLine.rstrip() + if m := re_translationID.match(skelLine): + prefix, ID, suffix = m.groups() + if prefix and not pretranslatedLine.startswith(prefix): + raise ValueError( + f'Line {lineNo} of translation does not start with "{prefix}", {pretranslatedLine=}, {skelLine=}', + ) + if suffix and not pretranslatedLine.endswith(suffix): + if allowBadAnchors and (m := re_heading.match(pretranslatedLine)): + print(f"Warning: ignoring bad anchor in line {lineNo}: {pretranslatedLine}") + suffix = m.group(3) + if suffix and not pretranslatedLine.endswith(suffix): + raise ValueError( + f'Line {lineNo} of translation: does not end with "{suffix}", {pretranslatedLine=}, {skelLine=}', + ) + translation = pretranslatedLine[len(prefix) : len(pretranslatedLine) - len(suffix)] + try: + unit = xliffRoot.find(f'./xliff:file/xliff:unit[@id="{ID}"]', namespaces=namespace) + if unit is not None: + segment = unit.find("./xliff:segment", namespaces=namespace) + if segment is not None: + target = lxml.etree.Element("target") + target.text = translation + target.tail = "\n" + segment.append(target) + res.numTranslatedStrings += 1 + else: + raise ValueError(f"No segment found for unit {ID}") + else: + raise ValueError(f"Cannot locate Unit {ID} in xliff file") + except Exception as e: + e.add_note(f"Line {lineNo}: {pretranslatedLine=}, {skelLine=}") + raise + elif skelLine != pretranslatedLine: + raise ValueError( + f"Line {lineNo}: pretranslated line {pretranslatedLine!r}, does not match skeleton line {skelLine!r}", + ) + xliff.write(outputPath, encoding="utf8", xml_declaration=True) + print(f"Translated xliff file with {res.numTranslatedStrings} translated strings") + return res + + +@dataclass +class Result_generateMarkdown: + numTotalLines = 0 + numTranslatableStrings = 0 + numTranslatedStrings = 0 + numBadTranslationStrings = 0 + + +def generateMarkdown(xliffPath: str, outputPath: str, translated: bool = True) -> Result_generateMarkdown: + print(f"Generating markdown file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)}...") + res = Result_generateMarkdown() + with contextlib.ExitStack() as stack: + outputFile = stack.enter_context(open(outputPath, "w", encoding="utf8", newline="")) + xliff = lxml.etree.parse(xliffPath) + xliffRoot = xliff.getroot() + namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} + if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": + raise ValueError("Not an xliff file") + skeletonNode = xliffRoot.find("./xliff:file/xliff:skeleton", namespaces=namespace) + if skeletonNode is None: + raise ValueError("No skeleton found in xliff file") + skeletonContent = skeletonNode.text.strip() + for lineNum, line in enumerate(skeletonContent.splitlines(keepends=True), 1): + res.numTotalLines += 1 + if m := re_translationID.match(line): + prefix, ID, suffix = m.groups() + res.numTranslatableStrings += 1 + unit = xliffRoot.find(f'./xliff:file/xliff:unit[@id="{ID}"]', namespaces=namespace) + if unit is None: + raise ValueError(f"Cannot locate Unit {ID} in xliff file") + segment = unit.find("./xliff:segment", namespaces=namespace) + if segment is None: + raise ValueError(f"No segment found for unit {ID}") + source = segment.find("./xliff:source", namespaces=namespace) + if source is None: + raise ValueError(f"No source found for unit {ID}") + translation = "" + if translated: + target = segment.find("./xliff:target", namespaces=namespace) + if target is not None: + targetText = target.text + if targetText: + translation = targetText + # Crowdin treats empty targets () as a literal translation. + # Filter out such strings and count them as bad translations. + if translation in ( + "", + "<target/>", + "", + "<target></target>", + ): + res.numBadTranslationStrings += 1 + translation = "" + else: + res.numTranslatedStrings += 1 + # If we have no translation, use the source text + if not translation: + sourceText = source.text + if sourceText is None: + raise ValueError(f"No source text found for unit {ID}") + translation = sourceText + outputFile.write(f"{prefix}{translation}{suffix}\n") + else: + outputFile.write(line) + print( + f"Generated markdown file with {res.numTotalLines} total lines, {res.numTranslatableStrings} translatable strings, and {res.numTranslatedStrings} translated strings. Ignoring {res.numBadTranslationStrings} bad translated strings", + ) + return res + + +def ensureMarkdownFilesMatch(path1: str, path2: str, allowBadAnchors: bool = False): + print(f"Ensuring files {prettyPathString(path1)} and {prettyPathString(path2)} match...") + with contextlib.ExitStack() as stack: + file1 = stack.enter_context(open(path1, "r", encoding="utf8")) + file2 = stack.enter_context(open(path2, "r", encoding="utf8")) + for lineNo, (line1, line2) in enumerate(zip_longest(file1.readlines(), file2.readlines()), start=1): + line1 = line1.rstrip() + line2 = line2.rstrip() + if line1 != line2: + if ( + re_postTableHeaderLine.match(line1) + and re_postTableHeaderLine.match(line2) + and line1.count("|") == line2.count("|") + ): + print( + f"Warning: ignoring cell padding of post table header line at line {lineNo}: {line1}, {line2}", + ) + continue + if ( + re_hiddenHeaderRow.match(line1) + and re_hiddenHeaderRow.match(line2) + and line1.count("|") == line2.count("|") + ): + print( + f"Warning: ignoring cell padding of hidden header row at line {lineNo}: {line1}, {line2}", + ) + continue + if allowBadAnchors and (m1 := re_heading.match(line1)) and (m2 := re_heading.match(line2)): + print(f"Warning: ignoring bad anchor in headings at line {lineNo}: {line1}, {line2}") + line1 = m1.group(1) + m1.group(2) + line2 = m2.group(1) + m2.group(2) + if line1 != line2: + raise ValueError(f"Files do not match at line {lineNo}: {line1=} {line2=}") + print("Files match") + + +def markdownTranslateCommand(command: str, *args): + print(f"Running markdownTranslate command: {command} {' '.join(args)}") + subprocess.run(["python", __file__, command, *args], check=True) + + +def pretranslateAllPossibleLanguages(langsDir: str, mdBaseName: str): + # This function walks through all language directories in the given directory, skipping en (English) and translates the English xlif and skel file along with the lang's pretranslated md file + enXliffPath = os.path.join(langsDir, "en", f"{mdBaseName}.xliff") + if not os.path.exists(enXliffPath): + raise ValueError(f"English xliff file {enXliffPath} does not exist") + allLangs = set() + succeededLangs = set() + skippedLangs = set() + for langDir in os.listdir(langsDir): + if langDir == "en": + continue + langDirPath = os.path.join(langsDir, langDir) + if not os.path.isdir(langDirPath): + continue + langPretranslatedMdPath = os.path.join(langDirPath, f"{mdBaseName}.md") + if not os.path.exists(langPretranslatedMdPath): + continue + allLangs.add(langDir) + langXliffPath = os.path.join(langDirPath, f"{mdBaseName}.xliff") + if os.path.exists(langXliffPath): + print(f"Skipping {langDir} as the xliff file already exists") + skippedLangs.add(langDir) + continue + try: + translateXliff( + xliffPath=enXliffPath, + lang=langDir, + pretranslatedMdPath=langPretranslatedMdPath, + outputPath=langXliffPath, + allowBadAnchors=True, + ) + except Exception as e: + print(f"Failed to translate {langDir}: {e}") + continue + rebuiltLangMdPath = os.path.join(langDirPath, f"rebuilt_{mdBaseName}.md") + try: + generateMarkdown( + xliffPath=langXliffPath, + outputPath=rebuiltLangMdPath, + ) + except Exception as e: + print(f"Failed to rebuild {langDir} markdown: {e}") + os.remove(langXliffPath) + continue + try: + ensureMarkdownFilesMatch(rebuiltLangMdPath, langPretranslatedMdPath, allowBadAnchors=True) + except Exception as e: + print(f"Rebuilt {langDir} markdown does not match pretranslated markdown: {e}") + os.remove(langXliffPath) + continue + os.remove(rebuiltLangMdPath) + print(f"Successfully pretranslated {langDir}") + succeededLangs.add(langDir) + if len(skippedLangs) > 0: + print(f"Skipped {len(skippedLangs)} languages already pretranslated.") + print(f"Pretranslated {len(succeededLangs)} out of {len(allLangs) - len(skippedLangs)} languages.") + + +if __name__ == "__main__": + mainParser = argparse.ArgumentParser() + commandParser = mainParser.add_subparsers(title="commands", dest="command", required=True) + generateXliffParser = commandParser.add_parser("generateXliff") + generateXliffParser.add_argument( + "-m", + "--markdown", + dest="md", + type=str, + required=True, + help="The markdown file to generate the xliff file for", + ) + generateXliffParser.add_argument( + "-o", + "--output", + dest="output", + type=str, + required=True, + help="The file to output the xliff file to", + ) + updateXliffParser = commandParser.add_parser("updateXliff") + updateXliffParser.add_argument( + "-x", + "--xliff", + dest="xliff", + type=str, + required=True, + help="The original xliff file", + ) + updateXliffParser.add_argument( + "-m", + "--newMarkdown", + dest="md", + type=str, + required=True, + help="The new markdown file", + ) + updateXliffParser.add_argument( + "-o", + "--output", + dest="output", + type=str, + required=True, + help="The file to output the updated xliff to", + ) + translateXliffParser = commandParser.add_parser("translateXliff") + translateXliffParser.add_argument( + "-x", + "--xliff", + dest="xliff", + type=str, + required=True, + help="The xliff file to translate", + ) + translateXliffParser.add_argument( + "-l", + "--lang", + dest="lang", + type=str, + required=True, + help="The language to translate to", + ) + translateXliffParser.add_argument( + "-p", + "--pretranslatedMarkdown", + dest="pretranslatedMd", + type=str, + required=True, + help="The pretranslated markdown file to use", + ) + translateXliffParser.add_argument( + "-o", + "--output", + dest="output", + type=str, + required=True, + help="The file to output the translated xliff file to", + ) + generateMarkdownParser = commandParser.add_parser("generateMarkdown") + generateMarkdownParser.add_argument( + "-x", + "--xliff", + dest="xliff", + type=str, + required=True, + help="The xliff file to generate the markdown file for", + ) + generateMarkdownParser.add_argument( + "-o", + "--output", + dest="output", + type=str, + required=True, + help="The file to output the markdown file to", + ) + generateMarkdownParser.add_argument( + "-u", + "--untranslated", + dest="translated", + action="store_false", + help="Generate the markdown file with the untranslated strings", + ) + ensureMarkdownFilesMatchParser = commandParser.add_parser("ensureMarkdownFilesMatch") + ensureMarkdownFilesMatchParser.add_argument( + dest="path1", + type=str, + help="The first markdown file", + ) + ensureMarkdownFilesMatchParser.add_argument( + dest="path2", + type=str, + help="The second markdown file", + ) + pretranslateLangsParser = commandParser.add_parser("pretranslateLangs") + pretranslateLangsParser.add_argument( + "-d", + "--langs-dir", + dest="langsDir", + type=str, + required=True, + help="The directory containing the language directories", + ) + pretranslateLangsParser.add_argument( + "-b", + "--md-base-name", + dest="mdBaseName", + type=str, + required=True, + help="The base name of the markdown files to pretranslate", + ) + args = mainParser.parse_args() + match args.command: + case "generateXliff": + generateXliff(mdPath=args.md, outputPath=args.output) + case "updateXliff": + updateXliff( + xliffPath=args.xliff, + mdPath=args.md, + outputPath=args.output, + ) + case "generateMarkdown": + generateMarkdown(xliffPath=args.xliff, outputPath=args.output, translated=args.translated) + case "translateXliff": + translateXliff( + xliffPath=args.xliff, + lang=args.lang, + pretranslatedMdPath=args.pretranslatedMd, + outputPath=args.output, + ) + case "pretranslateLangs": + pretranslateAllPossibleLanguages(langsDir=args.langsDir, mdBaseName=args.mdBaseName) + case "ensureMarkdownFilesMatch": + ensureMarkdownFilesMatch(path1=args.path1, path2=args.path2) + case _: + raise ValueError(f"Unknown command: {args.command}") diff --git a/_l10n/md2html.py b/_l10n/md2html.py new file mode 100644 index 0000000..01acab0 --- /dev/null +++ b/_l10n/md2html.py @@ -0,0 +1,197 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2023-2024 NV Access Limited +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +import argparse +from copy import deepcopy +import io +import re +import shutil + +DEFAULT_EXTENSIONS = frozenset( + { + # Supports tables, HTML mixed with markdown, code blocks, custom attributes and more + "markdown.extensions.extra", + # Allows TOC with [TOC]" + "markdown.extensions.toc", + # Makes list behaviour better, including 2 space indents by default + "mdx_truly_sane_lists", + # External links will open in a new tab, and title will be set to the link text + "markdown_link_attr_modifier", + # Adds links to GitHub authors, issues and PRs + "mdx_gh_links", + }, +) + +EXTENSIONS_CONFIG = { + "markdown_link_attr_modifier": { + "new_tab": "external_only", + "auto_title": "on", + }, + "mdx_gh_links": { + "user": "nvaccess", + "repo": "nvda", + }, +} + +RTL_LANG_CODES = frozenset({"ar", "fa", "he"}) + +HTML_HEADERS = """ + + + + +{title} + + + +{extraStylesheet} + + +""".strip() + + +def _getTitle(mdBuffer: io.StringIO, isKeyCommands: bool = False) -> str: + if isKeyCommands: + TITLE_RE = re.compile(r"^$") + # Make next read at start of buffer + mdBuffer.seek(0) + for line in mdBuffer.readlines(): + match = TITLE_RE.match(line.strip()) + if match: + return match.group(1) + + raise ValueError("No KC:title command found in userGuide.md") + + else: + # Make next read at start of buffer + mdBuffer.seek(0) + # Remove heading hashes and trailing whitespace to get the tab title + title = mdBuffer.readline().strip().lstrip("# ") + + return title + + +def _createAttributeFilter() -> dict[str, set[str]]: + # Create attribute filter exceptions for HTML sanitization + import nh3 + + allowedAttributes: dict[str, set[str]] = deepcopy(nh3.ALLOWED_ATTRIBUTES) + + attributesWithAnchors = {"h1", "h2", "h3", "h4", "h5", "h6", "td"} + attributesWithClass = {"div", "span", "a", "th", "td"} + + # Allow IDs for anchors + for attr in attributesWithAnchors: + if attr not in allowedAttributes: + allowedAttributes[attr] = set() + allowedAttributes[attr].add("id") + + # Allow class for styling + for attr in attributesWithClass: + if attr not in allowedAttributes: + allowedAttributes[attr] = set() + allowedAttributes[attr].add("class") + + # link rel and target is set by markdown_link_attr_modifier + allowedAttributes["a"].update({"rel", "target"}) + + return allowedAttributes + + +ALLOWED_ATTRIBUTES = _createAttributeFilter() + + +def _generateSanitizedHTML(md: str, isKeyCommands: bool = False) -> str: + import markdown + import nh3 + + extensions = set(DEFAULT_EXTENSIONS) + if isKeyCommands: + from keyCommandsDoc import KeyCommandsExtension + + extensions.add(KeyCommandsExtension()) + + htmlOutput = markdown.markdown( + text=md, + extensions=extensions, + extension_configs=EXTENSIONS_CONFIG, + ) + + # Sanitize html output from markdown to prevent XSS from translators + htmlOutput = nh3.clean( + htmlOutput, + attributes=ALLOWED_ATTRIBUTES, + # link rel is handled by markdown_link_attr_modifier + link_rel=None, + # Keep key command comments and similar + strip_comments=False, + ) + + return htmlOutput + + +def main(source: str, dest: str, lang: str = "en", docType: str | None = None): + print(f"Converting {docType or 'document'} ({lang=}) at {source} to {dest}") + isUserGuide = docType == "userGuide" + isDevGuide = docType == "developerGuide" + isChanges = docType == "changes" + isKeyCommands = docType == "keyCommands" + if docType and not any([isUserGuide, isDevGuide, isChanges, isKeyCommands]): + raise ValueError(f"Unknown docType {docType}") + with open(source, "r", encoding="utf-8") as mdFile: + mdStr = mdFile.read() + + with io.StringIO() as mdBuffer: + mdBuffer.write(mdStr) + title = _getTitle(mdBuffer, isKeyCommands) + + if isUserGuide or isDevGuide: + extraStylesheet = '' + elif isChanges or isKeyCommands: + extraStylesheet = "" + else: + raise ValueError(f"Unknown target type for {dest}") + + htmlBuffer = io.StringIO() + htmlBuffer.write( + HTML_HEADERS.format( + lang=lang, + dir="rtl" if lang in RTL_LANG_CODES else "ltr", + title=title, + extraStylesheet=extraStylesheet, + ), + ) + + htmlOutput = _generateSanitizedHTML(mdStr, isKeyCommands) + # Make next write append at end of buffer + htmlBuffer.seek(0, io.SEEK_END) + htmlBuffer.write(htmlOutput) + + # Make next write append at end of buffer + htmlBuffer.seek(0, io.SEEK_END) + htmlBuffer.write("\n\n\n") + + with open(dest, "w", encoding="utf-8") as targetFile: + # Make next read at start of buffer + htmlBuffer.seek(0) + shutil.copyfileobj(htmlBuffer, targetFile) + + htmlBuffer.close() + + +if __name__ == "__main__": + args = argparse.ArgumentParser() + args.add_argument("-l", "--lang", help="Language code", action="store", default="en") + args.add_argument( + "-t", + "--docType", + help="Type of document", + action="store", + choices=["userGuide", "developerGuide", "changes", "keyCommands"], + ) + args.add_argument("source", help="Path to the markdown file") + args.add_argument("dest", help="Path to the resulting html file") + args = args.parse_args() + main(source=args.source, dest=args.dest, lang=args.lang, docType=args.docType) From 46694309932d6713effc0c0c951535b8a2128986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 24 Nov 2025 11:14:14 +0100 Subject: [PATCH 02/90] use a json file to store addonId, and use it to filter files to get Crowdin ID --- _l10n/l10nUtil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_l10n/l10nUtil.py b/_l10n/l10nUtil.py index 00bee4c..2808258 100644 --- a/_l10n/l10nUtil.py +++ b/_l10n/l10nUtil.py @@ -23,7 +23,7 @@ CROWDIN_PROJECT_ID = 780748 POLLING_INTERVAL_SECONDS = 5 EXPORT_TIMEOUT_SECONDS = 60 * 10 # 10 minutes -JSON_FILE = os.path.join(os.path.dirname(__file__), "files.json") +JSON_FILE = os.path.join(os.path.dirname(__file__), "..", "metadata.json") def fetchCrowdinAuthToken() -> str: From b18856045262e80c32815b055ad50e12d64aae0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 24 Nov 2025 22:05:55 +0100 Subject: [PATCH 03/90] Try to get files just for the current add-on --- _l10n/l10nUtil.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/_l10n/l10nUtil.py b/_l10n/l10nUtil.py index 2808258..9480753 100644 --- a/_l10n/l10nUtil.py +++ b/_l10n/l10nUtil.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2024-2025 NV Access Limited. +# Copyright (C) 2024-2025 NV Access Limited, Noelia Ruiz Martínez # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -23,7 +23,9 @@ CROWDIN_PROJECT_ID = 780748 POLLING_INTERVAL_SECONDS = 5 EXPORT_TIMEOUT_SECONDS = 60 * 10 # 10 minutes -JSON_FILE = os.path.join(os.path.dirname(__file__), "..", "metadata.json") +METADATA_FILE = os.path.join(os.path.dirname(__file__), "..", "metadata.json") +METADATA_FILE = os.path.join(os.path.dirname(__file__), "..", "metadata.json") +L10N_FILE = os.path.join(os.path.dirname(__file__), "l10n.json") def fetchCrowdinAuthToken() -> str: @@ -296,10 +298,14 @@ def uploadSourceFile(localFilePath: str): res = getCrowdinClient().source_files.update_file(fileId=fileId , storageId=storageId, projectId=CROWDIN_PROJECT_ID) -def getFiles() -> dict: +def getFiles() -> dict[str, str]: """Gets files from Crowdin, and write them to a json file.""" - res = getCrowdinClient().source_files.list_files(CROWDIN_PROJECT_ID, limit=500) + with open(METADATA_FILE, "R", encoding="utf-8") as jsonFile: + addonData = json.load(jsonFile) + addonId = addonData.get("addonId") + + res = getCrowdinClient().source_files.list_files(CROWDIN_PROJECT_ID, filter=addonId) if res is None: raise ValueError("Getting files from Crowdin failed") dictionary = dict() @@ -309,8 +315,8 @@ def getFiles() -> dict: name = fileInfo["name"] id = fileInfo["id"] dictionary[name] = id - with open(JSON_FILE, "w", encoding="utf-8") as jsonFile: - json.dump(dictionary, jsonFile, ensure_ascii=False) + with open(L10N_FILE, "w", encoding="utf-8") as jsonFile: + json.dump(dictionary, jsonFile, ensure_ascii=False) return dictionary @@ -321,7 +327,7 @@ def uploadTranslationFile(crowdinFilePath: str, localFilePath: str, language: st :param localFilePath: The path to the local file to be uploaded :param language: The language code to upload the translation for """ - with open(JSON_FILE, "r", encoding="utf-8") as jsonFile: + with open(L10N_FILE, "r", encoding="utf-8") as jsonFile: files = json.load(jsonFile) fileId = files.get(crowdinFilePath) if fileId is None: From 709261583343a4fb25a75ecfd1c4af81e7de22c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 24 Nov 2025 22:32:53 +0100 Subject: [PATCH 04/90] Add workflow to export an add-on to Crowdin (authors would need to be addedwith dev role to Crowdin if they use a project not owned by them to upload source files) --- .github/workflows/exportAddonToCrowdin.yml | 96 ++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/workflows/exportAddonToCrowdin.yml diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml new file mode 100644 index 0000000..4ab508b --- /dev/null +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -0,0 +1,96 @@ +name: Export add-on to Crowdin + +on: + workflow_dispatch: + inputs: + repo: + description: 'Repository name' + required: true + update: + description: 'true to update preexisting sources, false to add them from scratch' + type: boolean + + workflow_call: + inputs: + repo: + description: 'Repository name' + type: 'string' + required: true + update: + description: 'true to update preexisting sources, false to add them from scratch' + type: boolean + required: false +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout add-on + uses: actions/checkout@v6 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install scons markdown + sudo apt update + sudo apt install gettext + - name: Build add-on and pot file + run: | + scons + scons pot + exportToCrowdin: + runs-on: ubuntu-latest + needs: build + permissions: + contents: write + steps: + - name: Checkout main branch + uses: actions/checkout@v6 + - name: "Set up Python" + uses: actions/setup-python@v6 + with: + python-version-file: ".python-version" + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v7 + - name: Generate xliff + if: ${{ !inputs.update }} + run: | + uv run ./_l10n/markdownTranslate.py generateXliff -m readme.md -o inputs.repo }}.xliff + - name: update xliff + if: ${{ inputs.update }} + run: | + uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ inputs.repo }}.xliff -m ${{ inputs.repo }}.md -o ${{ inputs.repo }}.xliff.temp + mv ${{ inputs.repo }}.xliff.temp ${{ inputs.repo }}.xliff + fi + - name: Upload to Crowdin + if: ${{ !inputs.update }} + run: | + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ inputs.repo}}.xliff + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ inputs.repo}}.pot + env: + crowdinProjectID: ${{ vars.CROWDIN_ID }} + crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} + - name: Update sources + if: ${{ inputs.update }} + run: | + uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ inputs.repo }}.pot + uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ inputs.repo }}.xliff + env: + crowdinProjectID: ${{ vars.CROWDIN_ID }} + crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} + - name: Commit and push json file + id: commit + run: | + git config --local user.name github-actions + git config --local user.email github-actions@github.com + git status + git add _l10n/l10n.json + if git diff --staged --quiet; then + echo "Nothing added to commit." + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + git commit -m "Update Crowdin file ids" + git push + fi From e89640d95d7fa2c93e671584c9ec1cb0efc9ec96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 25 Nov 2025 05:36:33 +0100 Subject: [PATCH 05/90] Use buildVars, not metadata.json file --- _l10n/l10nUtil.py | 51 ++--------------------------------------------- 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/_l10n/l10nUtil.py b/_l10n/l10nUtil.py index 9480753..e7feef2 100644 --- a/_l10n/l10nUtil.py +++ b/_l10n/l10nUtil.py @@ -19,12 +19,11 @@ import zipfile import time import json +from .. import buildVars CROWDIN_PROJECT_ID = 780748 POLLING_INTERVAL_SECONDS = 5 EXPORT_TIMEOUT_SECONDS = 60 * 10 # 10 minutes -METADATA_FILE = os.path.join(os.path.dirname(__file__), "..", "metadata.json") -METADATA_FILE = os.path.join(os.path.dirname(__file__), "..", "metadata.json") L10N_FILE = os.path.join(os.path.dirname(__file__), "l10n.json") @@ -301,9 +300,7 @@ def uploadSourceFile(localFilePath: str): def getFiles() -> dict[str, str]: """Gets files from Crowdin, and write them to a json file.""" - with open(METADATA_FILE, "R", encoding="utf-8") as jsonFile: - addonData = json.load(jsonFile) - addonId = addonData.get("addonId") + addonId = buildVars.addon_info["addon_name"] res = getCrowdinClient().source_files.list_files(CROWDIN_PROJECT_ID, filter=addonId) if res is None: @@ -802,35 +799,6 @@ def main(): ) command_xliff2md.add_argument("xliffPath", help="Path to the xliff file") command_xliff2md.add_argument("mdPath", help="Path to the resulting markdown file") - command_md2html = commands.add_parser("md2html", help="Convert markdown to html") - command_md2html.add_argument("-l", "--lang", help="Language code", action="store", default="en") - command_md2html.add_argument( - "-t", - "--docType", - help="Type of document", - action="store", - choices=["userGuide", "developerGuide", "changes", "keyCommands"], - ) - command_md2html.add_argument("mdPath", help="Path to the markdown file") - command_md2html.add_argument("htmlPath", help="Path to the resulting html file") - command_xliff2html = commands.add_parser("xliff2html", help="Convert xliff to html") - command_xliff2html.add_argument("-l", "--lang", help="Language code", action="store", required=False) - command_xliff2html.add_argument( - "-t", - "--docType", - help="Type of document", - action="store", - choices=["userGuide", "developerGuide", "changes", "keyCommands"], - ) - command_xliff2html.add_argument( - "-u", - "--untranslated", - help="Produce the untranslated markdown file", - action="store_true", - default=False, - ) - command_xliff2html.add_argument("xliffPath", help="Path to the xliff file") - command_xliff2html.add_argument("htmlPath", help="Path to the resulting html file") uploadSourceFileCommand = commands.add_parser( "uploadSourceFile", help="Upload a source file to Crowdin.", @@ -912,21 +880,6 @@ def main(): outputPath=args.mdPath, translated=not args.untranslated, ) - case "md2html": - md2html.main(source=args.mdPath, dest=args.htmlPath, lang=args.lang, docType=args.docType) - case "xliff2html": - lang = args.lang or fetchLanguageFromXliff(args.xliffPath, source=args.untranslated) - temp_mdFile = tempfile.NamedTemporaryFile(suffix=".md", delete=False, mode="w", encoding="utf-8") - temp_mdFile.close() - try: - markdownTranslate.generateMarkdown( - xliffPath=args.xliffPath, - outputPath=temp_mdFile.name, - translated=not args.untranslated, - ) - md2html.main(source=temp_mdFile.name, dest=args.htmlPath, lang=lang, docType=args.docType) - finally: - os.remove(temp_mdFile.name) case "uploadSourceFile": uploadSourceFile(args.localFilePath) case "getFiles": From 4c7771b1f7fbeebc6c5bf3424f210bfcb2c99826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 26 Nov 2025 16:43:35 +0100 Subject: [PATCH 06/90] Add userAccount to buildVars, and step to get addon-id to GitHub workflow to upload/update files in Crowdin --- .github/workflows/exportAddonToCrowdin.yml | 57 ++++++++++------------ _l10n/markdownTranslate.py | 8 ++- buildVars.py | 3 +- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 4ab508b..40efcf3 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -1,21 +1,9 @@ name: Export add-on to Crowdin on: - workflow_dispatch: - inputs: - repo: - description: 'Repository name' - required: true - update: - description: 'true to update preexisting sources, false to add them from scratch' - type: boolean - workflow_call: + workflow_dispatch: inputs: - repo: - description: 'Repository name' - type: 'string' - required: true update: description: 'true to update preexisting sources, false to add them from scratch' type: boolean @@ -26,9 +14,15 @@ concurrency: jobs: build: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout add-on uses: actions/checkout@v6 + - name: "Set up Python" + uses: actions/setup-python@v6 + with: + python-version-file: ".python-version" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -39,43 +33,42 @@ jobs: run: | scons scons pot - exportToCrowdin: - runs-on: ubuntu-latest - needs: build - permissions: - contents: write - steps: - - name: Checkout main branch - uses: actions/checkout@v6 - - name: "Set up Python" - uses: actions/setup-python@v6 - with: - python-version-file: ".python-version" - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 + - name: Get add-on id + id: getAddonId + shell: python + run: | + import os + import buildVars + addonId = buildVars.addon_info["addon_name"] + name = 'addonId' + value = addonId + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f"{name}={value}"") - name: Generate xliff if: ${{ !inputs.update }} run: | - uv run ./_l10n/markdownTranslate.py generateXliff -m readme.md -o inputs.repo }}.xliff + uv run ./_l10n/markdownTranslate.py generateXliff -m readme.md -o ${{ steps.getAddonId.outputs.addonId }}.xliff - name: update xliff if: ${{ inputs.update }} run: | - uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ inputs.repo }}.xliff -m ${{ inputs.repo }}.md -o ${{ inputs.repo }}.xliff.temp - mv ${{ inputs.repo }}.xliff.temp ${{ inputs.repo }}.xliff + uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ steps.getId.outputs.addonId }}.xliff -m readme.md -o ${{ steps.getAddonId.outputs.addonId }}.xliff.temp + mv ${{ steps.getAddonId.outputs.addonId }}.xliff.temp ${{ steps.getAddonId.outputs.addonId }}.xliff fi - name: Upload to Crowdin if: ${{ !inputs.update }} run: | - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ inputs.repo}}.xliff - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ inputs.repo}}.pot + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonId.outputs.addonId }}.xliff + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonId.outputs.addonId }}.pot env: crowdinProjectID: ${{ vars.CROWDIN_ID }} crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} - name: Update sources if: ${{ inputs.update }} run: | - uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ inputs.repo }}.pot - uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ inputs.repo }}.xliff + uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonId.outputs.addonId }}.pot + uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonId.outputs.addonId }}.xliff env: crowdinProjectID: ${{ vars.CROWDIN_ID }} crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} diff --git a/_l10n/markdownTranslate.py b/_l10n/markdownTranslate.py index 341ead6..5af1d73 100644 --- a/_l10n/markdownTranslate.py +++ b/_l10n/markdownTranslate.py @@ -17,7 +17,13 @@ from dataclasses import dataclass import subprocess -RAW_GITHUB_REPO_URL = "https://raw.githubusercontent.com/nvaccess/nvda" + +from .. import buildVars + +addonId = buildVars.addon_info["addonname"] +userAccount = buildVars.userAccount +RAW_GITHUB_REPO_URL = f"https://raw.githubusercontent.com/{userAccount}/{addonId}" + re_kcTitle = re.compile(r"^()$") re_kcSettingsSection = re.compile(r"^()$") # Comments that span a single line in their entirety diff --git a/buildVars.py b/buildVars.py index c125fae..770946a 100644 --- a/buildVars.py +++ b/buildVars.py @@ -10,7 +10,8 @@ # which returns whatever is given to it as an argument. from site_scons.site_tools.NVDATool.utils import _ - +# The GitHub user account to generate xliff file for translations +userAccount: str | None = None # Add-on information variables addon_info = AddonInfo( # add-on Name/identifier, internal for NVDA From c529cee4d5c4db819c75b08f7f77ef2d4c70d04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 26 Nov 2025 21:36:41 +0100 Subject: [PATCH 07/90] Update files after testing exporting an add-on to Crowdin, needs refinements --- .github/workflows/exportAddonToCrowdin.yml | 7 +- _l10n/files.json | 2 +- _l10n/l10n.json | 1 + _l10n/l10nUtil.py | 62 +++++--- _l10n/markdownTranslate.py | 10 +- pyproject.toml | 176 ++------------------- 6 files changed, 60 insertions(+), 198 deletions(-) create mode 100644 _l10n/l10n.json diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 40efcf3..0b8dd9e 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -39,13 +39,14 @@ jobs: id: getAddonId shell: python run: | - import os + import os, sys + sys.path.insert(0, os.getcwd()) import buildVars addonId = buildVars.addon_info["addon_name"] name = 'addonId' value = addonId with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f"{name}={value}"") + f.write(f"{name}={value}") - name: Generate xliff if: ${{ !inputs.update }} run: | @@ -53,7 +54,7 @@ jobs: - name: update xliff if: ${{ inputs.update }} run: | - uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ steps.getId.outputs.addonId }}.xliff -m readme.md -o ${{ steps.getAddonId.outputs.addonId }}.xliff.temp + uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ steps.getAddonId.outputs.addonId }}.xliff -m readme.md -o ${{ steps.getAddonId.outputs.addonId }}.xliff.temp mv ${{ steps.getAddonId.outputs.addonId }}.xliff.temp ${{ steps.getAddonId.outputs.addonId }}.xliff fi - name: Upload to Crowdin diff --git a/_l10n/files.json b/_l10n/files.json index 9264714..9e26dfe 100644 --- a/_l10n/files.json +++ b/_l10n/files.json @@ -1 +1 @@ -{"emoticons.pot": 176, "emoticons.xliff": 178, "goldwave.pot": 180, "goldwave.xliff": 182, "eMule.pot": 194, "enhancedTouchGestures.pot": 210, "resourceMonitor.pot": 214, "stationPlaylist.pot": 218, "cursorLocator.pot": 224, "pcKbBrl.pot": 228, "readFeeds.pot": 232, "reportSymbols.pot": 236, "urlShortener.pot": 240, "customNotifications.pot": 244, "readonlyProfiles.pot": 248, "enhancedAnnotations.pot": 252, "clipContentsDesigner.pot": 256, "clipContentsDesigner.xliff": 258, "controlUsageAssistant.pot": 260, "controlUsageAssistant.xliff": 262, "eMule.xliff": 264, "enhancedAnnotations.xliff": 266, "customNotifications.xliff": 268, "readonlyProfiles.xliff": 270, "urlShortener.xliff": 272, "reportSymbols.xliff": 274, "pcKbBrl.xliff": 276, "readFeeds.xliff": 278, "stationPlaylist.xliff": 282, "resourceMonitor.xliff": 284, "enhancedTouchGestures.xliff": 286, "rdAccess.pot": 288, "rdAccess.xliff": 290, "winMag.pot": 292, "winMag.xliff": 294, "charInfo.pot": 296, "charInfo.xliff": 298, "BMI.pot": 300, "BMI.xliff": 302, "tonysEnhancements.pot": 304, "tonysEnhancements.xliff": 306, "nvdaDevTestToolbox.pot": 308, "nvdaDevTestToolbox.xliff": 310, "easyTableNavigator.pot": 312, "easyTableNavigator.xliff": 314, "updateChannel.pot": 320, "updateChannel.xliff": 322, "instantTranslate.pot": 324, "instantTranslate.xliff": 326, "unicodeBrailleInput.pot": 328, "unicodeBrailleInput.xliff": 330, "columnsReview.pot": 332, "columnsReview.xliff": 334, "Access8Math.pot": 336, "Access8Math.xliff": 338, "systrayList.pot": 340, "systrayList.xliff": 342, "winWizard.pot": 344, "winWizard.xliff": 346, "speechLogger.pot": 348, "speechLogger.xliff": 350, "sayProductNameAndVersion.pot": 352, "sayProductNameAndVersion.xliff": 354, "objPad.pot": 356, "objPad.xliff": 358, "SentenceNav.pot": 360, "SentenceNav.xliff": 362, "wordNav.pot": 364, "wordNav.xliff": 366, "goldenCursor.pot": 368, "goldenCursor.xliff": 370, "MSEdgeDiscardAnnouncements.pot": 372, "MSEdgeDiscardAnnouncements.xliff": 374, "dayOfTheWeek.pot": 376, "dayOfTheWeek.xliff": 378, "outlookExtended.pot": 380, "outlookExtended.xliff": 382, "proxy.pot": 384, "proxy.xliff": 386, "searchWith.pot": 388, "searchWith.xliff": 390, "sayCurrentKeyboardLanguage.pot": 392, "sayCurrentKeyboardLanguage.xliff": 394, "robEnhancements.pot": 396, "robEnhancements.xliff": 398, "objWatcher.pot": 400, "objWatcher.xliff": 402, "mp3DirectCut.pot": 404, "mp3DirectCut.xliff": 406, "beepKeyboard.pot": 408, "beepKeyboard.xliff": 410, "numpadNavMode.pot": 412, "numpadNavMode.xliff": 414, "dropbox.pot": 416, "dropbox.xliff": 418, "reviewCursorCopier.pot": 420, "reviewCursorCopier.xliff": 422, "inputLock.pot": 424, "inputLock.xliff": 426, "debugHelper.pot": 428, "debugHelper.xliff": 430, "virtualRevision.pot": 432, "virtualRevision.xliff": 434, "cursorLocator.xliff": 436, "evtTracker.pot": 438, "evtTracker.xliff": 440} \ No newline at end of file +{} \ No newline at end of file diff --git a/_l10n/l10n.json b/_l10n/l10n.json new file mode 100644 index 0000000..abf3c01 --- /dev/null +++ b/_l10n/l10n.json @@ -0,0 +1 @@ +{"translateNvdaAddonsWithCrowdin.xliff": 442, "translateNvdaAddonsWithCrowdin.pot": 444} \ No newline at end of file diff --git a/_l10n/l10nUtil.py b/_l10n/l10nUtil.py index e7feef2..6cd4352 100644 --- a/_l10n/l10nUtil.py +++ b/_l10n/l10nUtil.py @@ -3,6 +3,9 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +import os, sys +sys.path.insert(0, os.getcwd()) + import crowdin_api as crowdin import tempfile import lxml.etree @@ -10,7 +13,6 @@ import shutil import argparse import markdownTranslate -import md2html import requests import codecs import re @@ -19,7 +21,8 @@ import zipfile import time import json -from .. import buildVars + +import buildVars CROWDIN_PROJECT_ID = 780748 POLLING_INTERVAL_SECONDS = 5 @@ -237,7 +240,7 @@ def downloadTranslationFile(crowdinFilePath: str, localFilePath: str, language: :param localFilePath: The path to save the local file :param language: The language code to download the translation for """ - with open(JSON_FILE, "r", encoding="utf-8") as jsonFile: + with open(L10N_FILE, "r", encoding="utf-8") as jsonFile: files = json.load(jsonFile) fileId = files.get(crowdinFilePath) if fileId is None: @@ -263,7 +266,7 @@ def uploadSourceFile(localFilePath: str): Upload a source file to Crowdin. :param localFilePath: The path to the local file to be uploaded """ - with open(JSON_FILE, "r", encoding="utf-8") as jsonFile: + with open(L10N_FILE, "r", encoding="utf-8") as jsonFile: files = json.load(jsonFile) fileId = files.get(localFilePath) if fileId is None: @@ -282,19 +285,31 @@ def uploadSourceFile(localFilePath: str): match fileId: case None: if os.path.splitext(filename)[1] == ".pot": - title=f"{os.path.splitext(filename)[0]} interface" - exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{os.path.splitext(filename)[0]}.po" + title = f"{os.path.splitext(filename)[0]} interface" + exportPattern = ( + f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{os.path.splitext(filename)[0]}.po" + ) else: - title=f"{os.path.splitext(filename)[0]} documentation" - exportPattern =f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{filename}" + title = f"{os.path.splitext(filename)[0]} documentation" + exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{filename}" exportOptions = { - "exportPattern": exportPattern + "exportPattern": exportPattern, } print(f"Importing source file {localFilePath} from storage with ID {storageId}") - res = getCrowdinClient().source_files.add_file(storageId=storageId, projectId=CROWDIN_PROJECT_ID, name=filename, title=title, exportOptions=exportOptions) + res = getCrowdinClient().source_files.add_file( + storageId=storageId, + projectId=CROWDIN_PROJECT_ID, + name=filename, + title=title, + exportOptions=exportOptions, + ) print("Done") case _: - res = getCrowdinClient().source_files.update_file(fileId=fileId , storageId=storageId, projectId=CROWDIN_PROJECT_ID) + res = getCrowdinClient().source_files.update_file( + fileId=fileId, + storageId=storageId, + projectId=CROWDIN_PROJECT_ID, + ) def getFiles() -> dict[str, str]: @@ -799,19 +814,6 @@ def main(): ) command_xliff2md.add_argument("xliffPath", help="Path to the xliff file") command_xliff2md.add_argument("mdPath", help="Path to the resulting markdown file") - uploadSourceFileCommand = commands.add_parser( - "uploadSourceFile", - help="Upload a source file to Crowdin.", - ) - uploadSourceFileCommand.add_argument( - "-f", - "--localFilePath", - help="The local path to the file.", - ) - getFilesCommand = commands.add_parser( - "getFiles", - help="Get files from Crowdin.", - ) downloadTranslationFileCommand = commands.add_parser( "downloadTranslationFile", help="Download a translation file from Crowdin.", @@ -854,7 +856,15 @@ def main(): default=None, help="The path to the local file to be uploaded. If not provided, the Crowdin file path will be used.", ) - + uploadSourceFileCommand = commands.add_parser( + "uploadSourceFile", + help="Upload a source file to Crowdin.", + ) + uploadSourceFileCommand.add_argument( + "-f", + "--localFilePath", + help="The local path to the file.", + ) exportTranslationsCommand = commands.add_parser( "exportTranslations", help="Export translation files from Crowdin as a bundle. If no language is specified, exports all languages.", @@ -869,7 +879,7 @@ def main(): "-l", "--language", help="Language code to export (e.g., 'es', 'fr', 'de'). If not specified, exports all languages.", - default=None, + default=None, ) args = args.parse_args() diff --git a/_l10n/markdownTranslate.py b/_l10n/markdownTranslate.py index 5af1d73..ee70eb7 100644 --- a/_l10n/markdownTranslate.py +++ b/_l10n/markdownTranslate.py @@ -17,17 +17,11 @@ from dataclasses import dataclass import subprocess - -from .. import buildVars - -addonId = buildVars.addon_info["addonname"] -userAccount = buildVars.userAccount -RAW_GITHUB_REPO_URL = f"https://raw.githubusercontent.com/{userAccount}/{addonId}" - +RAW_GITHUB_REPO_URL = "https://raw.githubusercontent.com/nvaccess/nvda" re_kcTitle = re.compile(r"^()$") re_kcSettingsSection = re.compile(r"^()$") # Comments that span a single line in their entirety -re_comment = re.compile(r"^$") +re_comment = re.compile(r"^$", re.DOTALL) re_heading = re.compile(r"^(#+\s+)(.+?)((?:\s+\{#.+\})?)$") re_bullet = re.compile(r"^(\s*\*\s+)(.+)$") re_number = re.compile(r"^(\s*[0-9]+\.\s+)(.+)$") diff --git a/pyproject.toml b/pyproject.toml index 97189ac..44d0016 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,161 +1,17 @@ -[tool.ruff] -line-length = 110 - -builtins = [ - # translation lookup - "_", - # translation lookup - "ngettext", - # translation lookup - "pgettext", - # translation lookup - "npgettext", +[project] +name = "addonTemplate" +version = "0.1.0" +description = "Addon template" +readme = "readme.md" +requires-python = ">=3.13" +dependencies = [ + "crowdin-api-client==1.21.0", + "lxml>=6.0.1", + "markdown>=3.9", + "markdown-link-attr-modifier==0.2.1", + "mdx-gh-links==0.4", + "mdx-truly-sane-lists==1.3", + "nh3==0.2.19", + "requests>=2.32.5", + "SCons==4.10.1", ] - -include = [ - "*.py", - "sconstruct", -] - -exclude = [ - ".git", - "__pycache__", -] - -[tool.ruff.format] -indent-style = "tab" - -[tool.ruff.lint.mccabe] -max-complexity = 15 - -[tool.ruff.lint] -ignore = [ - # indentation contains tabs - "W191", -] - -[tool.ruff.lint.per-file-ignores] -# sconstruct contains many inbuilt functions not recognised by the lint, -# so ignore F821. -"sconstruct" = ["F821"] - -[tool.pyright] -pythonPlatform = "Windows" -typeCheckingMode = "strict" - -include = [ - "**/*.py", -] - -exclude = [ - "sconstruct", - ".git", - "__pycache__", - # When excluding concrete paths relative to a directory, - # not matching multiple folders by name e.g. `__pycache__`, - # paths are relative to the configuration file. -] - -# Tell pyright where to load python code from -extraPaths = [ - "./addon", -] - -# General config -analyzeUnannotatedFunctions = true -deprecateTypingAliases = true - -# Stricter typing -strictParameterNoneValue = true -strictListInference = true -strictDictionaryInference = true -strictSetInference = true - -# Compliant rules -reportAbstractUsage = true -reportArgumentType = true -reportAssertAlwaysTrue = true -reportAssertTypeFailure = true -reportAssignmentType = true -reportAttributeAccessIssue = true -reportCallInDefaultInitializer = true -reportCallIssue = true -reportConstantRedefinition = true -reportDuplicateImport = true -reportFunctionMemberAccess = true -reportGeneralTypeIssues = true -reportImplicitOverride = true -reportImplicitStringConcatenation = true -reportImportCycles = true -reportIncompatibleMethodOverride = true -reportIncompatibleVariableOverride = true -reportIncompleteStub = true -reportInconsistentConstructor = true -reportInconsistentOverload = true -reportIndexIssue = true -reportInvalidStringEscapeSequence = true -reportInvalidStubStatement = true -reportInvalidTypeArguments = true -reportInvalidTypeForm = true -reportInvalidTypeVarUse = true -reportMatchNotExhaustive = true -reportMissingImports = true -reportMissingModuleSource = true -reportMissingParameterType = true -reportMissingSuperCall = true -reportMissingTypeArgument = true -reportNoOverloadImplementation = true -reportOperatorIssue = true -reportOptionalCall = true -reportOptionalContextManager = true -reportOptionalIterable = true -reportOptionalMemberAccess = true -reportOptionalOperand = true -reportOptionalSubscript = true -reportOverlappingOverload = true -reportPossiblyUnboundVariable = true -reportPrivateImportUsage = true -reportPrivateUsage = true -reportPropertyTypeMismatch = true -reportRedeclaration = true -reportReturnType = true -reportSelfClsParameterName = true -reportShadowedImports = true -reportTypeCommentUsage = true -reportTypedDictNotRequiredAccess = true -reportUnboundVariable = true -reportUndefinedVariable = true -reportUnhashable = true -reportUninitializedInstanceVariable = true -reportUnknownArgumentType = true -reportUnknownLambdaType = true -reportUnknownMemberType = true -reportUnknownParameterType = true -reportUnknownVariableType = true -reportUnnecessaryCast = true -reportUnnecessaryComparison = true -reportUnnecessaryContains = true -reportUnnecessaryIsInstance = true -reportUnnecessaryTypeIgnoreComment = true -reportUnsupportedDunderAll = true -reportUntypedBaseClass = true -reportUntypedClassDecorator = true -reportUntypedFunctionDecorator = true -reportUntypedNamedTuple = true -reportUnusedCallResult = true -reportUnusedClass = true -reportUnusedCoroutine = true -reportUnusedExcept = true -reportUnusedExpression = true -reportUnusedFunction = true -reportUnusedImport = true -reportUnusedVariable = true -reportWildcardImportFromLibrary = true - -reportDeprecated = true - -# Can be enabled by generating type stubs for modules via pyright CLI -reportMissingTypeStubs = false - -# Bad rules -# These are sorted alphabetically and should be enabled and moved to compliant rules section when resolved. From 186b75593a0b4619d944c1f243a26eb8191584ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 26 Nov 2025 21:38:12 +0100 Subject: [PATCH 08/90] Add python version file --- .python-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 From f1fbf8e39fa542091bdfd52b003514f1e30a370b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 26 Nov 2025 22:14:03 +0100 Subject: [PATCH 09/90] Improve pyproject and update precommit config after testing that check pass creating a PR at nvdaes/translateNvdaaddonsWithCrowdin repo --- .pre-commit-config.yaml | 97 ++++++++++++++++++-- pyproject.toml | 197 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 285 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd7a9d6..75d507a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,92 @@ +# Copied from https://github.com/nvaccess/nvda +# https://pre-commit.ci/ +# Configuration for Continuous Integration service +ci: + # Pyright does not seem to work in pre-commit CI + skip: [pyright] + autoupdate_schedule: monthly + autoupdate_commit_msg: "Pre-commit auto-update" + autofix_commit_msg: "Pre-commit auto-fix" + submodules: true + +default_language_version: + python: python3.13 + repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 - hooks: - - id: check-ast - - id: check-case-conflict - - id: check-yaml +- repo: https://github.com/pre-commit-ci/pre-commit-ci-config + rev: v1.6.1 + hooks: + - id: check-pre-commit-ci-config + +- repo: meta + hooks: + # ensures that exclude directives apply to any file in the repository. + - id: check-useless-excludes + # ensures that the configured hooks apply to at least one file in the repository. + - id: check-hooks-apply + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + # Prevents commits to certain branches + - id: no-commit-to-branch + args: ["--branch", "main", ] + # Checks that large files have not been added. Default cut-off for "large" files is 500kb. + - id: check-added-large-files + # Checks python syntax + - id: check-ast + # Checks for filenames that will conflict on case insensitive filesystems (the majority of Windows filesystems, most of the time) + - id: check-case-conflict + # Checks for artifacts from resolving merge conflicts. + - id: check-merge-conflict + # Checks Python files for debug statements, such as python's breakpoint function, or those inserted by some IDEs. + - id: debug-statements + # Removes trailing whitespace. + - id: trailing-whitespace + types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] + # Ensures all files end in 1 (and only 1) newline. + - id: end-of-file-fixer + types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] + # Removes the UTF-8 BOM from files that have it. + # See https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/codingStandards.md#encoding + - id: fix-byte-order-marker + types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] + # Validates TOML files. + - id: check-toml + # Validates YAML files. + - id: check-yaml + # Ensures that links to lines in files under version control point to a particular commit. + - id: check-vcs-permalinks + # Avoids using reserved Windows filenames. + - id: check-illegal-windows-names +- repo: https://github.com/asottile/add-trailing-comma + rev: v3.2.0 + hooks: + # Ruff preserves indent/new-line formatting of function arguments, list items, and similar iterables, + # if a trailing comma is added. + # This adds a trailing comma to args/iterable items in case it was missed. + - id: add-trailing-comma + +- repo: https://github.com/astral-sh/ruff-pre-commit + # Matches Ruff version in pyproject. + rev: v0.12.7 + hooks: + - id: ruff + name: lint with ruff + args: [ --fix ] + - id: ruff-format + name: format with ruff + +- repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.406 + hooks: + - id: pyright + name: Check types with pyright + additional_dependencies: [ "pyright[nodejs]==1.1.406" ] + +- repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.18.1 + hooks: + - id: markdownlint-cli2 + name: Lint markdown files + args: ["--fix"] diff --git a/pyproject.toml b/pyproject.toml index 44d0016..ab571c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,26 @@ +[build-system] +requires = ["setuptools~=72.0", "wheel"] +build-backend = "setuptools.build_meta" + [project] name = "addonTemplate" +dynamic = ["version"] version = "0.1.0" -description = "Addon template" -readme = "readme.md" -requires-python = ">=3.13" +description = "Add-on template" +maintainers = [ + {name = "NV Access", email = "info@nvaccess.org"}, +] +requires-python = ">=3.13,<3.14" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License v2", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Topic :: Accessibility", +] +readme="readme.md" +license = {file = "LICENSE"} dependencies = [ "crowdin-api-client==1.21.0", "lxml>=6.0.1", @@ -15,3 +32,177 @@ dependencies = [ "requests>=2.32.5", "SCons==4.10.1", ] + +[project.urls] +Repository = "https://github.com/nvaccess/addonTemplate" + +[tool.ruff] +line-length = 110 + +builtins = [ + # translation lookup + "_", + # translation lookup + "ngettext", + # translation lookup + "pgettext", + # translation lookup + "npgettext", +] + +include = [ + "*.py", + "sconstruct", +] + +exclude = [ + ".git", + "__pycache__", + ".venv", + "buildVars.py", +] + +[tool.ruff.format] +indent-style = "tab" +line-ending = "lf" + +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.ruff.lint] +ignore = [ + # indentation contains tabs + "W191", +] +logger-objects = ["logHandler.log"] + +[tool.ruff.lint.per-file-ignores] +# sconscripts contains many inbuilt functions not recognised by the lint, +# so ignore F821. +"sconstruct" = ["F821"] + +[tool.pyright] +venvPath = "../nvda/.venv" +venv = "." +pythonPlatform = "Windows" +typeCheckingMode = "strict" + +include = [ + "**/*.py", +] + +exclude = [ + "sconstruct", + ".git", + "__pycache__", + ".venv", + # When excluding concrete paths relative to a directory, + # not matching multiple folders by name e.g. `__pycache__`, + # paths are relative to the configuration file. +] + +# Tell pyright where to load python code from +extraPaths = [ + "./addon", + "../nvda/source", +] + +# General config +analyzeUnannotatedFunctions = true +deprecateTypingAliases = true + +# Stricter typing +strictParameterNoneValue = true +strictListInference = true +strictDictionaryInference = true +strictSetInference = true + +# Compliant rules +reportAssertAlwaysTrue = true +reportAssertTypeFailure = true +reportDuplicateImport = true +reportIncompleteStub = true +reportInconsistentOverload = true +reportInconsistentConstructor = true +reportInvalidStringEscapeSequence = true +reportInvalidStubStatement = true +reportInvalidTypeVarUse = true +reportMatchNotExhaustive = true +reportMissingModuleSource = true +reportMissingImports = true +reportNoOverloadImplementation = true +reportOptionalContextManager = true +reportOverlappingOverload = true +reportPrivateImportUsage = true +reportPropertyTypeMismatch = true +reportSelfClsParameterName = true +reportShadowedImports = true +reportTypeCommentUsage = true +reportTypedDictNotRequiredAccess = true +reportUndefinedVariable = true +reportUnusedExpression = true +reportUnboundVariable = true +reportUnhashable = true +reportUnnecessaryCast = true +reportUnnecessaryContains = true +reportUnnecessaryTypeIgnoreComment = true +reportUntypedClassDecorator = true +reportUntypedFunctionDecorator = true +reportUnusedClass = true +reportUnusedCoroutine = true +reportUnusedExcept = true +reportDeprecated = true +# Can be enabled by generating type stubs for modules via pyright CLI +reportMissingTypeStubs = false +reportUnsupportedDunderAll = false +reportAbstractUsage = false +reportUntypedBaseClass = false +reportOptionalIterable = false +reportCallInDefaultInitializer = false +reportInvalidTypeArguments = false +reportUntypedNamedTuple = false +reportRedeclaration = false +reportOptionalCall = false +reportConstantRedefinition = false +reportWildcardImportFromLibrary = false +reportIncompatibleVariableOverride = false +reportInvalidTypeForm = false +reportGeneralTypeIssues = false +reportOptionalOperand = false +reportUnnecessaryComparison = false +reportFunctionMemberAccess = false +reportUnnecessaryIsInstance = false +reportUnusedFunction = false +reportImportCycles = false +reportUnusedImport = false +reportUnusedVariable = false +reportOperatorIssue = false +reportAssignmentType = false +reportReturnType = false +reportPossiblyUnboundVariable = false +reportMissingSuperCall = false +reportUninitializedInstanceVariable = false +reportUnknownLambdaType = false +reportMissingTypeArgument = false +reportImplicitStringConcatenation = false +reportIncompatibleMethodOverride = false +reportPrivateUsage = false +reportUnusedCallResult = false +reportOptionalSubscript = false +reportCallIssue = false +reportOptionalMemberAccess = false +reportImplicitOverride = false +reportIndexIssue = false +reportAttributeAccessIssue = false +reportArgumentType = false +reportUnknownParameterType = false +reportMissingParameterType = false +reportUnknownVariableType = false +reportUnknownArgumentType = false +reportUnknownMemberType = false +lint = [ + "ruff==0.8.1", + "pre-commit==4.0.1", + "pyright==1.1.396", +] + From b867a9a0f25aa5d5b56bca11c12083fe5298795e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Thu, 27 Nov 2025 07:05:11 +0100 Subject: [PATCH 10/90] Restore rules --- pyproject.toml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ab571c4..bf69408 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,18 +118,37 @@ strictDictionaryInference = true strictSetInference = true # Compliant rules +reportAbstractUsage = true +reportArgumentType = true reportAssertAlwaysTrue = true reportAssertTypeFailure = true +reportAssignmentType = true +reportAttributeAccessIssue = true +reportCallInDefaultInitializer = true +reportCallIssue = true +reportConstantRedefinition = true reportDuplicateImport = true +reportFunctionMemberAccess = true +reportGeneralTypeIssues = true +reportImplicitOverride = true +reportImplicitStringConcatenation = true +reportImportCycles = true +reportIncompatibleMethodOverride = true +reportIncompatibleVariableOverride = true reportIncompleteStub = true +reportIndexIssue = true reportInconsistentOverload = true reportInconsistentConstructor = true reportInvalidStringEscapeSequence = true reportInvalidStubStatement = true +reportInvalidTypeArguments = true +reportInvalidTypeForm = true reportInvalidTypeVarUse = true reportMatchNotExhaustive = true reportMissingModuleSource = true reportMissingImports = true +reportMissingParameterType = true +reportMissingSuperCall = true reportNoOverloadImplementation = true reportOptionalContextManager = true reportOverlappingOverload = true From 47ed91cde7c16799122428ac0d50b294aa89ffa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Thu, 27 Nov 2025 20:44:09 +0100 Subject: [PATCH 11/90] Restore pyproject --- pyproject.toml | 140 +++++++++++++------------------------------------ 1 file changed, 37 insertions(+), 103 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bf69408..97189ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,41 +1,3 @@ -[build-system] -requires = ["setuptools~=72.0", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "addonTemplate" -dynamic = ["version"] -version = "0.1.0" -description = "Add-on template" -maintainers = [ - {name = "NV Access", email = "info@nvaccess.org"}, -] -requires-python = ">=3.13,<3.14" -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: GNU General Public License v2", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3", - "Topic :: Accessibility", -] -readme="readme.md" -license = {file = "LICENSE"} -dependencies = [ - "crowdin-api-client==1.21.0", - "lxml>=6.0.1", - "markdown>=3.9", - "markdown-link-attr-modifier==0.2.1", - "mdx-gh-links==0.4", - "mdx-truly-sane-lists==1.3", - "nh3==0.2.19", - "requests>=2.32.5", - "SCons==4.10.1", -] - -[project.urls] -Repository = "https://github.com/nvaccess/addonTemplate" - [tool.ruff] line-length = 110 @@ -58,13 +20,10 @@ include = [ exclude = [ ".git", "__pycache__", - ".venv", - "buildVars.py", ] [tool.ruff.format] indent-style = "tab" -line-ending = "lf" [tool.ruff.lint.mccabe] max-complexity = 15 @@ -74,16 +33,13 @@ ignore = [ # indentation contains tabs "W191", ] -logger-objects = ["logHandler.log"] [tool.ruff.lint.per-file-ignores] -# sconscripts contains many inbuilt functions not recognised by the lint, +# sconstruct contains many inbuilt functions not recognised by the lint, # so ignore F821. "sconstruct" = ["F821"] [tool.pyright] -venvPath = "../nvda/.venv" -venv = "." pythonPlatform = "Windows" typeCheckingMode = "strict" @@ -95,7 +51,6 @@ exclude = [ "sconstruct", ".git", "__pycache__", - ".venv", # When excluding concrete paths relative to a directory, # not matching multiple folders by name e.g. `__pycache__`, # paths are relative to the configuration file. @@ -104,7 +59,6 @@ exclude = [ # Tell pyright where to load python code from extraPaths = [ "./addon", - "../nvda/source", ] # General config @@ -136,92 +90,72 @@ reportImportCycles = true reportIncompatibleMethodOverride = true reportIncompatibleVariableOverride = true reportIncompleteStub = true -reportIndexIssue = true -reportInconsistentOverload = true reportInconsistentConstructor = true +reportInconsistentOverload = true +reportIndexIssue = true reportInvalidStringEscapeSequence = true reportInvalidStubStatement = true reportInvalidTypeArguments = true reportInvalidTypeForm = true reportInvalidTypeVarUse = true reportMatchNotExhaustive = true -reportMissingModuleSource = true reportMissingImports = true +reportMissingModuleSource = true reportMissingParameterType = true reportMissingSuperCall = true +reportMissingTypeArgument = true reportNoOverloadImplementation = true +reportOperatorIssue = true +reportOptionalCall = true reportOptionalContextManager = true +reportOptionalIterable = true +reportOptionalMemberAccess = true +reportOptionalOperand = true +reportOptionalSubscript = true reportOverlappingOverload = true +reportPossiblyUnboundVariable = true reportPrivateImportUsage = true +reportPrivateUsage = true reportPropertyTypeMismatch = true +reportRedeclaration = true +reportReturnType = true reportSelfClsParameterName = true reportShadowedImports = true reportTypeCommentUsage = true reportTypedDictNotRequiredAccess = true -reportUndefinedVariable = true -reportUnusedExpression = true reportUnboundVariable = true +reportUndefinedVariable = true reportUnhashable = true +reportUninitializedInstanceVariable = true +reportUnknownArgumentType = true +reportUnknownLambdaType = true +reportUnknownMemberType = true +reportUnknownParameterType = true +reportUnknownVariableType = true reportUnnecessaryCast = true +reportUnnecessaryComparison = true reportUnnecessaryContains = true +reportUnnecessaryIsInstance = true reportUnnecessaryTypeIgnoreComment = true +reportUnsupportedDunderAll = true +reportUntypedBaseClass = true reportUntypedClassDecorator = true reportUntypedFunctionDecorator = true +reportUntypedNamedTuple = true +reportUnusedCallResult = true reportUnusedClass = true reportUnusedCoroutine = true reportUnusedExcept = true +reportUnusedExpression = true +reportUnusedFunction = true +reportUnusedImport = true +reportUnusedVariable = true +reportWildcardImportFromLibrary = true + reportDeprecated = true + # Can be enabled by generating type stubs for modules via pyright CLI reportMissingTypeStubs = false -reportUnsupportedDunderAll = false -reportAbstractUsage = false -reportUntypedBaseClass = false -reportOptionalIterable = false -reportCallInDefaultInitializer = false -reportInvalidTypeArguments = false -reportUntypedNamedTuple = false -reportRedeclaration = false -reportOptionalCall = false -reportConstantRedefinition = false -reportWildcardImportFromLibrary = false -reportIncompatibleVariableOverride = false -reportInvalidTypeForm = false -reportGeneralTypeIssues = false -reportOptionalOperand = false -reportUnnecessaryComparison = false -reportFunctionMemberAccess = false -reportUnnecessaryIsInstance = false -reportUnusedFunction = false -reportImportCycles = false -reportUnusedImport = false -reportUnusedVariable = false -reportOperatorIssue = false -reportAssignmentType = false -reportReturnType = false -reportPossiblyUnboundVariable = false -reportMissingSuperCall = false -reportUninitializedInstanceVariable = false -reportUnknownLambdaType = false -reportMissingTypeArgument = false -reportImplicitStringConcatenation = false -reportIncompatibleMethodOverride = false -reportPrivateUsage = false -reportUnusedCallResult = false -reportOptionalSubscript = false -reportCallIssue = false -reportOptionalMemberAccess = false -reportImplicitOverride = false -reportIndexIssue = false -reportAttributeAccessIssue = false -reportArgumentType = false -reportUnknownParameterType = false -reportMissingParameterType = false -reportUnknownVariableType = false -reportUnknownArgumentType = false -reportUnknownMemberType = false -lint = [ - "ruff==0.8.1", - "pre-commit==4.0.1", - "pyright==1.1.396", -] +# Bad rules +# These are sorted alphabetically and should be enabled and moved to compliant rules section when resolved. From 402002eb5a86e14e241c6df5aaade0fa7acc3ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Thu, 27 Nov 2025 21:43:29 +0100 Subject: [PATCH 12/90] Improve uv project --- .gitignore | 18 +++- 2.32.5 | 0 3.9 | 0 6.0.1 | 0 pyproject.toml | 52 +++++++++- uv.lock | 267 +++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 333 insertions(+), 4 deletions(-) create mode 100644 2.32.5 create mode 100644 3.9 create mode 100644 6.0.1 create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 0be8af1..1750f2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,23 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Files generated for add-ons addon/doc/*.css addon/doc/en/ *_docHandler.py *.html -manifest.ini +addon/*.ini +addon/locale/*/*.ini *.mo *.pot -*.py[co] +*.pyc *.nvda-addon .sconsign.dblite -/[0-9]*.[0-9]*.[0-9]*.json diff --git a/2.32.5 b/2.32.5 new file mode 100644 index 0000000..e69de29 diff --git a/3.9 b/3.9 new file mode 100644 index 0000000..e69de29 diff --git a/6.0.1 b/6.0.1 new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 97189ac..4673a1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,39 @@ +[build-system] +requires = ["setuptools~=72.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "addonTemplate" +dynamic = ["version"] +description = "NVDA add-on template" +maintainers = [ + {name = "NV Access", email = "info@nvaccess.org"}, +] +requires-python = ">=3.13,<3.14" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Topic :: Accessibility", +] +readme = "readme.md" +license = {file = "COPYING.TXT"} +dependencies = [ + "crowdin-api-client==1.21.0", + "lxml>=6.0.2", + "markdown>=3.10", + "markdown-link-attr-modifier==0.2.1", + "mdx-gh-links==0.4", + "mdx-truly-sane-lists==1.3", + "nh3==0.2.19", + "requests>=2.32.5", + "scons==4.10.1", +] +[project.urls] +Repository = "https://github.com/nvaccess/addonTemplate" + [tool.ruff] line-length = 110 @@ -20,10 +56,13 @@ include = [ exclude = [ ".git", "__pycache__", + ".venv", + "buildVars.py", ] [tool.ruff.format] indent-style = "tab" +line-ending = "lf" [tool.ruff.lint.mccabe] max-complexity = 15 @@ -33,13 +72,16 @@ ignore = [ # indentation contains tabs "W191", ] +logger-objects = ["logHandler.log"] [tool.ruff.lint.per-file-ignores] -# sconstruct contains many inbuilt functions not recognised by the lint, +# sconscripts contains many inbuilt functions not recognised by the lint, # so ignore F821. "sconstruct" = ["F821"] [tool.pyright] +venvPath = "../nvda/.venv" +venv = "." pythonPlatform = "Windows" typeCheckingMode = "strict" @@ -51,6 +93,7 @@ exclude = [ "sconstruct", ".git", "__pycache__", + ".venv", # When excluding concrete paths relative to a directory, # not matching multiple folders by name e.g. `__pycache__`, # paths are relative to the configuration file. @@ -59,6 +102,7 @@ exclude = [ # Tell pyright where to load python code from extraPaths = [ "./addon", + "../nvda/source", ] # General config @@ -159,3 +203,9 @@ reportMissingTypeStubs = false # Bad rules # These are sorted alphabetically and should be enabled and moved to compliant rules section when resolved. +lint = [ + "ruff==0.8.1", + "pre-commit==4.0.1", + "pyright==1.1.396", +] + diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..58c3f26 --- /dev/null +++ b/uv.lock @@ -0,0 +1,267 @@ +version = 1 +revision = 3 +requires-python = "==3.13.*" + +[[package]] +name = "addontemplate" +source = { editable = "." } +dependencies = [ + { name = "crowdin-api-client" }, + { name = "lxml" }, + { name = "markdown" }, + { name = "markdown-link-attr-modifier" }, + { name = "mdx-gh-links" }, + { name = "mdx-truly-sane-lists" }, + { name = "nh3" }, + { name = "requests" }, + { name = "scons" }, +] + +[package.metadata] +requires-dist = [ + { name = "crowdin-api-client", specifier = "==1.21.0" }, + { name = "lxml", specifier = ">=6.0.2" }, + { name = "markdown", specifier = ">=3.10" }, + { name = "markdown-link-attr-modifier", specifier = "==0.2.1" }, + { name = "mdx-gh-links", specifier = "==0.4" }, + { name = "mdx-truly-sane-lists", specifier = "==1.3" }, + { name = "nh3", specifier = "==0.2.19" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "scons", specifier = "==4.10.1" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "crowdin-api-client" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/70/4069412e2e8004a6ad15bf2a3d9085bea50ee932a66ad935285831cf82b4/crowdin_api_client-1.21.0.tar.gz", hash = "sha256:0f957e5de6487a74ac892d524a5e300c1bc971320b67f85ce65741904420d8ec", size = 64729, upload-time = "2025-01-31T15:56:42.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/0f/9efc4f6db0b3b97f99015529b5832058ce4f7970d547b23fc04a38d69ddd/crowdin_api_client-1.21.0-py3-none-any.whl", hash = "sha256:85e19557755ebf6a15beda605d25b77de365244d8c636462b0dd8030a6cdfe20", size = 101566, upload-time = "2025-01-31T15:56:40.651Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-link-attr-modifier" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/30/d35aad054a27f119bff2408523d82c3f9a6d9936712c872f5b9fe817de5b/markdown_link_attr_modifier-0.2.1.tar.gz", hash = "sha256:18df49a9fe7b5c87dad50b75c2a2299ae40c65674f7b1263fb12455f5df7ac99", size = 18408, upload-time = "2023-04-13T16:00:12.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/82/9262a67313847fdcc6252a007f032924fe0c0b6d6b9ef0d0b1fa58952c72/markdown_link_attr_modifier-0.2.1-py3-none-any.whl", hash = "sha256:6b4415319648cbe6dfb7a54ca12fa69e61a27c86a09d15f2a9a559ace0aa87c5", size = 17146, upload-time = "2023-04-13T16:00:06.559Z" }, +] + +[[package]] +name = "mdx-gh-links" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/ea/bf1f721a8dc0ff83b426480f040ac68dbe3d7898b096c1277a5a4e3da0ec/mdx_gh_links-0.4.tar.gz", hash = "sha256:41d5aac2ab201425aa0a19373c4095b79e5e015fdacfe83c398199fe55ca3686", size = 5783, upload-time = "2023-12-22T19:54:02.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c7/ccfe05ade98ba7a63f05d1b05b7508d9af743cbd1f1681aa0c9900a8cd40/mdx_gh_links-0.4-py3-none-any.whl", hash = "sha256:9057bca1fa5280bf1fcbf354381e46c9261cc32c2d5c0407801f8a910be5f099", size = 7166, upload-time = "2023-12-22T19:54:00.384Z" }, +] + +[[package]] +name = "mdx-truly-sane-lists" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/27/16456314311abac2cedef4527679924e80ac4de19dd926699c1b261e0b9b/mdx_truly_sane_lists-1.3.tar.gz", hash = "sha256:b661022df7520a1e113af7c355c62216b384c867e4f59fb8ee7ad511e6e77f45", size = 5359, upload-time = "2022-07-19T13:42:45.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/9e/dcd1027f7fd193aed152e01c6651a197c36b858f2cd1425ad04cb31a34fc/mdx_truly_sane_lists-1.3-py3-none-any.whl", hash = "sha256:b9546a4c40ff8f1ab692f77cee4b6bfe8ddf9cccf23f0a24e71f3716fe290a37", size = 6071, upload-time = "2022-07-19T13:42:43.375Z" }, +] + +[[package]] +name = "nh3" +version = "0.2.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/32/3b8d8471d006333bac3175ad37402414d985ed3f8650a01a33e0e86b9824/nh3-0.2.19.tar.gz", hash = "sha256:790056b54c068ff8dceb443eaefb696b84beff58cca6c07afd754d17692a4804", size = 17327, upload-time = "2024-11-30T04:05:56.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/1d/cbd75a2313d96cd3903111667d3d07548fb45c8ecf5c315f37a8f6c202fa/nh3-0.2.19-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6", size = 1205181, upload-time = "2024-11-29T05:50:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/8e9ec472ce575fa6b98935920c91df637bf9342862bd943745441aec99eb/nh3-0.2.19-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0", size = 739174, upload-time = "2024-11-29T05:50:10.157Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b5/d1f81c5ec5695464b69d8aa4529ecb5fd872cbfb29f879b4063bb9397da8/nh3-0.2.19-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37", size = 758660, upload-time = "2024-11-29T05:50:13.97Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/295a3a069f3b9dc35527eedd7b212f31311ef1f66a0e5f5f0acad6db9456/nh3-0.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a", size = 924377, upload-time = "2024-11-29T05:50:15.716Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/0f189d5054f22cdfdb16d16a2a41282f411a4c03f8418be47e0480bd5bfd/nh3-0.2.19-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd", size = 992124, upload-time = "2024-11-29T05:50:17.637Z" }, + { url = "https://files.pythonhosted.org/packages/0d/87/2907edd61a2172527c5322036aa95ce6c18432ff280fc5cf78fe0f934c65/nh3-0.2.19-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9", size = 913939, upload-time = "2024-11-29T05:50:20.435Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a2/e0d3ea0175f28032d7d2bab765250f4e94ef131a7b3293e3df4cb254a5b2/nh3-0.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55", size = 909051, upload-time = "2024-11-29T05:50:31.116Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/f52cb1d54ba965b7d8bb1c884ca982be31d7f75ad9e7e5817f4af20002b3/nh3-0.2.19-cp313-cp313t-win32.whl", hash = "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc", size = 540566, upload-time = "2024-11-29T05:50:38.437Z" }, + { url = "https://files.pythonhosted.org/packages/70/85/91a66edfab0adbf22468973d8abd4b93c951bbcbbe2121675ee468b912a2/nh3-0.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3", size = 542368, upload-time = "2024-11-29T05:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/32/9c/f8808cf6683d4852ba8862e25b98aa9116067ddec517938a1b6e8faadb43/nh3-0.2.19-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41", size = 1205140, upload-time = "2024-11-29T05:50:54.582Z" }, + { url = "https://files.pythonhosted.org/packages/04/0e/268401d9244a84935342d9f3ba5d22bd7d2fc10cfc7a8f59bde8f6721466/nh3-0.2.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a", size = 763571, upload-time = "2024-11-29T05:51:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0d/8be706feb6637d6e5db0eed09fd3f4e1008aee3d5d7161c9973d7aae1d13/nh3-0.2.19-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69", size = 750319, upload-time = "2024-11-29T05:51:05.512Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ce/1f5f9ba0194f6a882e4bda89ae831678e4b68aa3de91e11e2629a1e6a613/nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9", size = 857636, upload-time = "2024-11-29T05:51:06.872Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/5661a66f2950879a81fde5fbb6beb650c5647776aaec1a676e6b3ff4b6e5/nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c", size = 821081, upload-time = "2024-11-29T05:51:08.202Z" }, + { url = "https://files.pythonhosted.org/packages/22/f8/454828f6f21516bf0c8c578e8bc2ab4f045e6b6fe5179602fe4dc2479da6/nh3-0.2.19-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1", size = 894452, upload-time = "2024-11-29T05:51:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5d/36f5b78cbc631cac1c993bdc4608a0fe3148214bdb6d2c1266e228a2686a/nh3-0.2.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc", size = 748281, upload-time = "2024-11-29T05:51:29.021Z" }, + { url = "https://files.pythonhosted.org/packages/98/da/d04f5f0e7ee8edab8ceecdbba9f1c614dc8cf07374141ff6ea3b615b3479/nh3-0.2.19-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707", size = 767109, upload-time = "2024-11-29T05:51:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5b/1232fb35c7d1182adb7d513fede644a81b5361259749781e6075c40a9125/nh3-0.2.19-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22", size = 924295, upload-time = "2024-11-29T05:51:33.078Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fd/ae622d08518fd31360fd87a515700bc09913f2e57e7f010063f2193ea610/nh3-0.2.19-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d", size = 992038, upload-time = "2024-11-29T05:51:34.414Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/226577c5e3fe379cb95265aa77736e191d859032c974169e6879c51c156f/nh3-0.2.19-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9", size = 913866, upload-time = "2024-11-29T05:51:35.727Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ca/bbd2b2dab31ceae38cfa673861cab81df5ed5be1fe47b6c4f5aa41729aa2/nh3-0.2.19-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0", size = 908976, upload-time = "2024-11-29T05:51:43.698Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8f/6452eb1184ad87cdd2cac7ee3ebd67a2aadb554d25572c1778efdf807e1e/nh3-0.2.19-cp38-abi3-win32.whl", hash = "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48", size = 540528, upload-time = "2024-11-29T05:51:45.312Z" }, + { url = "https://files.pythonhosted.org/packages/58/d6/285df10307f16fcce9afbd133b04b4bc7d7f9b84b02f0f724bab30dacdd9/nh3-0.2.19-cp38-abi3-win_amd64.whl", hash = "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121", size = 542316, upload-time = "2024-11-29T05:52:01.253Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "scons" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] From d82071137cd1e13db770fc2a3de98b9dc8363f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Thu, 27 Nov 2025 21:44:57 +0100 Subject: [PATCH 13/90] Remove files --- 2.32.5 | 0 3.9 | 0 6.0.1 | 0 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 2.32.5 delete mode 100644 3.9 delete mode 100644 6.0.1 diff --git a/2.32.5 b/2.32.5 deleted file mode 100644 index e69de29..0000000 diff --git a/3.9 b/3.9 deleted file mode 100644 index e69de29..0000000 diff --git a/6.0.1 b/6.0.1 deleted file mode 100644 index e69de29..0000000 From 8e0f88e9bd2b25df8c14bb7316da3cddf6b0733d Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Fri, 28 Nov 2025 01:00:23 +0100 Subject: [PATCH 14/90] Bump actions/checkout from 5 to 6 (#3) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_addon.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index ab0bba2..9e0fa64 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - run: echo -e "pre-commit\nscons\nmarkdown">requirements.txt @@ -54,7 +54,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: download releases files uses: actions/download-artifact@v6 - name: Display structure of downloaded files From 9f6b3dc35c6d3ff8ad4d6be6200248885d2aec20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sat, 29 Nov 2025 17:41:31 +0100 Subject: [PATCH 15/90] Calculate hash of i18nSources --- .github/workflows/exportAddonToCrowdin.yml | 33 ++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 0b8dd9e..857f1d6 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -35,41 +35,52 @@ jobs: scons pot - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 - - name: Get add-on id - id: getAddonId + - name: Get add-on info + id: getAddonInfo shell: python run: | - import os, sys + import os, sys, hashlib sys.path.insert(0, os.getcwd()) import buildVars addonId = buildVars.addon_info["addon_name"] + i18nSources = buildVars.i18nSources + hasher = hashlib.sha256() + for file in i18nSources: + if os.path.isfile(file): + with open(file, "rb") as f: + while chunk := f.read(8192): + hasher.update(chunk) + hashValue = hasher.hexdigest() name = 'addonId' value = addonId + name2 = 'hashValue' + value2 = hashValue + print(hashValue) with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f"{name}={value}") + f.write(f"{name}={value}\n{name2}={value2}") - name: Generate xliff if: ${{ !inputs.update }} run: | - uv run ./_l10n/markdownTranslate.py generateXliff -m readme.md -o ${{ steps.getAddonId.outputs.addonId }}.xliff + uv run ./_l10n/markdownTranslate.py generateXliff -m readme.md -o ${{ steps.getAddonInfo.outputs.addonId }}.xliff - name: update xliff if: ${{ inputs.update }} run: | - uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ steps.getAddonId.outputs.addonId }}.xliff -m readme.md -o ${{ steps.getAddonId.outputs.addonId }}.xliff.temp - mv ${{ steps.getAddonId.outputs.addonId }}.xliff.temp ${{ steps.getAddonId.outputs.addonId }}.xliff + uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ steps.getAddonInfo.outputs.addonId }}.xliff -m readme.md -o ${{ steps.getAddonInfo.outputs.addonId }}.xliff.temp + mv ${{ steps.getAddonInfo.outputs.addonId }}.xliff.temp ${{ steps.getAddonInfo.outputs.addonId }}.xliff fi - name: Upload to Crowdin if: ${{ !inputs.update }} run: | - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonId.outputs.addonId }}.xliff - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonId.outputs.addonId }}.pot + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.xliff + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot env: crowdinProjectID: ${{ vars.CROWDIN_ID }} crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} - name: Update sources if: ${{ inputs.update }} run: | - uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonId.outputs.addonId }}.pot - uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonId.outputs.addonId }}.xliff + uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot + uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.xliff env: crowdinProjectID: ${{ vars.CROWDIN_ID }} crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} From 4c938eccedc7d8836482c27bdaa0aa68f72983e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 30 Nov 2025 14:35:49 +0100 Subject: [PATCH 16/90] Update workflow --- .github/workflows/exportAddonToCrowdin.yml | 76 ++++++++++++---------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 857f1d6..1547b3c 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -8,9 +8,13 @@ on: description: 'true to update preexisting sources, false to add them from scratch' type: boolean required: false + default: true concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + crowdinProjectID: ${{ vars.CROWDIN_ID }} + crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} jobs: build: runs-on: ubuntu-latest @@ -39,63 +43,63 @@ jobs: id: getAddonInfo shell: python run: | - import os, sys, hashlib + import os, sys, json sys.path.insert(0, os.getcwd()) - import buildVars + import buildVars, sha256 addonId = buildVars.addon_info["addon_name"] - i18nSources = buildVars.i18nSources - hasher = hashlib.sha256() - for file in i18nSources: - if os.path.isfile(file): - with open(file, "rb") as f: - while chunk := f.read(8192): - hasher.update(chunk) - hashValue = hasher.hexdigest() + readmeFile = os.path.join(os.getcwd(), "readme.md") + i18nSources = sorted(buildVars.i18nSources) + if os.path.isfile(readmeFile): + readmeSha = sha256.sha256_checksum([readmeFile]) + i18nSourcesSha = sha256.sha256_checksum(i18nSources) + hashFile = os.path.join(os.getcwd(), "hash.json") + data = dict() + if os.path.isfile(hashFile): + with open(hashFile, "rt") as f: + data = json.load(f) + shouldUpdateXliff = (data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None) and os.path.isfile(f"{addonId}.xliff") + shouldUpdatePot = (data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None) + data = dict() + if readmeSha: + data["readmeSha"] = readmeSha + if i18nSourcesSha: + data["i18nSourcesSha"] = i18nSourcesSha + with open(hashFile, "wt", encoding="utf-8") as f: + json.dump(data, f, indent="\t", ensure_ascii=False) name = 'addonId' value = addonId - name2 = 'hashValue' - value2 = hashValue - print(hashValue) + name0 = 'shouldUpdateXliff' + value0 = str(shouldUpdateXliff).lower() + name1 = 'shouldUpdatePot' + value1 = str(shouldUpdatePot).lower() with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f"{name}={value}\n{name2}={value2}") - - name: Generate xliff + f.write(f"{name}={value}\n{name0}={value0}\n{name1}={value1}\n") + - name: Generate xliff and pot if: ${{ !inputs.update }} run: | uv run ./_l10n/markdownTranslate.py generateXliff -m readme.md -o ${{ steps.getAddonInfo.outputs.addonId }}.xliff + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.xliff + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - name: update xliff - if: ${{ inputs.update }} + if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdateXliff == 'true' }} run: | uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ steps.getAddonInfo.outputs.addonId }}.xliff -m readme.md -o ${{ steps.getAddonInfo.outputs.addonId }}.xliff.temp mv ${{ steps.getAddonInfo.outputs.addonId }}.xliff.temp ${{ steps.getAddonInfo.outputs.addonId }}.xliff - fi - - name: Upload to Crowdin - if: ${{ !inputs.update }} - run: | - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.xliff - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - env: - crowdinProjectID: ${{ vars.CROWDIN_ID }} - crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} - - name: Update sources - if: ${{ inputs.update }} + uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.xliff + - name: Update pot + if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdatePot == 'true' }} run: | uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot - uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.xliff - env: - crowdinProjectID: ${{ vars.CROWDIN_ID }} - crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} - - name: Commit and push json file + - name: Commit and push json and xliff files id: commit run: | git config --local user.name github-actions git config --local user.email github-actions@github.com git status - git add _l10n/l10n.json + git add hash.json _l10n/l10n.json ${{ steps.getAddonInfo.outputs.addonId}}.xliff if git diff --staged --quiet; then echo "Nothing added to commit." - echo "has_changes=false" >> $GITHUB_OUTPUT else - echo "has_changes=true" >> $GITHUB_OUTPUT - git commit -m "Update Crowdin file ids" + git commit -m "Update Crowdin file ids and hashes" git push fi From a3032100afc7facefc898e15f12a1041c481faf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 30 Nov 2025 18:16:35 +0100 Subject: [PATCH 17/90] Update _l10n --- _l10n/files.json | 1 - _l10n/l10n.json | 2 +- _l10n/markdownTranslate.py | 6 +++++- 3 files changed, 6 insertions(+), 3 deletions(-) delete mode 100644 _l10n/files.json diff --git a/_l10n/files.json b/_l10n/files.json deleted file mode 100644 index 9e26dfe..0000000 --- a/_l10n/files.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/_l10n/l10n.json b/_l10n/l10n.json index abf3c01..9e26dfe 100644 --- a/_l10n/l10n.json +++ b/_l10n/l10n.json @@ -1 +1 @@ -{"translateNvdaAddonsWithCrowdin.xliff": 442, "translateNvdaAddonsWithCrowdin.pot": 444} \ No newline at end of file +{} \ No newline at end of file diff --git a/_l10n/markdownTranslate.py b/_l10n/markdownTranslate.py index ee70eb7..fa9a186 100644 --- a/_l10n/markdownTranslate.py +++ b/_l10n/markdownTranslate.py @@ -6,6 +6,8 @@ from typing import Generator import tempfile import os +import sys +sys.path.insert(0, os.getcwd()) import contextlib import lxml.etree import argparse @@ -17,7 +19,9 @@ from dataclasses import dataclass import subprocess -RAW_GITHUB_REPO_URL = "https://raw.githubusercontent.com/nvaccess/nvda" +import buildVars + +RAW_GITHUB_REPO_URL = f"https://raw.githubusercontent.com/{buildVars.userAccount}/{buildVars.addon_info["addon_name"]}" re_kcTitle = re.compile(r"^()$") re_kcSettingsSection = re.compile(r"^()$") # Comments that span a single line in their entirety From a0d02da4a1e2acd155286cd516ec5aa3d4bb5eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 1 Dec 2025 21:31:13 +0100 Subject: [PATCH 18/90] Upload md file --- .github/workflows/exportAddonToCrowdin.yml | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 1547b3c..4ef3da6 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -57,7 +57,7 @@ jobs: if os.path.isfile(hashFile): with open(hashFile, "rt") as f: data = json.load(f) - shouldUpdateXliff = (data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None) and os.path.isfile(f"{addonId}.xliff") + shouldUpdateMd = (data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None) and os.path.isfile(readmeFile) shouldUpdatePot = (data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None) data = dict() if readmeSha: @@ -68,24 +68,25 @@ jobs: json.dump(data, f, indent="\t", ensure_ascii=False) name = 'addonId' value = addonId - name0 = 'shouldUpdateXliff' - value0 = str(shouldUpdateXliff).lower() + name0 = 'shouldUpdateMd' + value0 = str(shouldUpdateMd).lower() name1 = 'shouldUpdatePot' value1 = str(shouldUpdatePot).lower() with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f"{name}={value}\n{name0}={value0}\n{name1}={value1}\n") - - name: Generate xliff and pot + - name: Generate source files if: ${{ !inputs.update }} run: | - uv run ./_l10n/markdownTranslate.py generateXliff -m readme.md -o ${{ steps.getAddonInfo.outputs.addonId }}.xliff - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.xliff + if -f readme.md; then + mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md + fi uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - - name: update xliff - if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdateXliff == 'true' }} + - name: update md + if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdateMd == 'true' }} run: | - uv run ./_l10n/markdownTranslate.py updateXliff -x ${{ steps.getAddonInfo.outputs.addonId }}.xliff -m readme.md -o ${{ steps.getAddonInfo.outputs.addonId }}.xliff.temp - mv ${{ steps.getAddonInfo.outputs.addonId }}.xliff.temp ${{ steps.getAddonInfo.outputs.addonId }}.xliff - uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.xliff + mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md + uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.md - name: Update pot if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdatePot == 'true' }} run: | @@ -96,7 +97,7 @@ jobs: git config --local user.name github-actions git config --local user.email github-actions@github.com git status - git add hash.json _l10n/l10n.json ${{ steps.getAddonInfo.outputs.addonId}}.xliff + git add hash.json _l10n/l10n.json if git diff --staged --quiet; then echo "Nothing added to commit." else From a8d42520dcd71d303f6aaa1cd073e29051f1b3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 3 Dec 2025 18:59:28 +0100 Subject: [PATCH 19/90] Updates --- _l10n/l10n.json | 1 - _l10n/l10nUtil.py | 64 ++++++++++++++++++----------------------------- 2 files changed, 24 insertions(+), 41 deletions(-) delete mode 100644 _l10n/l10n.json diff --git a/_l10n/l10n.json b/_l10n/l10n.json deleted file mode 100644 index 9e26dfe..0000000 --- a/_l10n/l10n.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/_l10n/l10nUtil.py b/_l10n/l10nUtil.py index 6cd4352..68725aa 100644 --- a/_l10n/l10nUtil.py +++ b/_l10n/l10nUtil.py @@ -24,7 +24,8 @@ import buildVars -CROWDIN_PROJECT_ID = 780748 + +CROWDIN_PROJECT_ID = os.getenv("crowdinProjectID", "").strip() POLLING_INTERVAL_SECONDS = 5 EXPORT_TIMEOUT_SECONDS = 60 * 10 # 10 minutes L10N_FILE = os.path.join(os.path.dirname(__file__), "l10n.json") @@ -266,12 +267,6 @@ def uploadSourceFile(localFilePath: str): Upload a source file to Crowdin. :param localFilePath: The path to the local file to be uploaded """ - with open(L10N_FILE, "r", encoding="utf-8") as jsonFile: - files = json.load(jsonFile) - fileId = files.get(localFilePath) - if fileId is None: - files = getFiles() - fileId = files.get(localFilePath) res = getCrowdinClient().storages.add_storage( open(localFilePath, "rb"), ) @@ -279,40 +274,30 @@ def uploadSourceFile(localFilePath: str): raise ValueError("Crowdin storage upload failed") storageId = res["data"]["id"] print(f"Stored with ID {storageId}") - filename = os.path.basename(localFilePath) - fileId = files.get(filename) - print(f"File ID: {fileId}") - match fileId: - case None: - if os.path.splitext(filename)[1] == ".pot": - title = f"{os.path.splitext(filename)[0]} interface" - exportPattern = ( - f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{os.path.splitext(filename)[0]}.po" - ) - else: - title = f"{os.path.splitext(filename)[0]} documentation" - exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{filename}" - exportOptions = { - "exportPattern": exportPattern, - } - print(f"Importing source file {localFilePath} from storage with ID {storageId}") - res = getCrowdinClient().source_files.add_file( - storageId=storageId, - projectId=CROWDIN_PROJECT_ID, - name=filename, - title=title, - exportOptions=exportOptions, - ) - print("Done") - case _: - res = getCrowdinClient().source_files.update_file( - fileId=fileId, - storageId=storageId, - projectId=CROWDIN_PROJECT_ID, - ) + addonId = buildVars.addon_info["addon_name"] + filename = addonId + if os.path.splitext(filename)[1] == ".pot": + title = f"{os.path.splitext(filename)[0]} interface" + exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{os.path.splitext(filename)[0]}.po" + else: + title = f"{os.path.splitext(filename)[0]} documentation" + exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{filename}" + exportOptions = { + "exportPattern": exportPattern, + } + print(f"Exporting source file {localFilePath} from storage with ID {storageId}") + res = getCrowdinClient().source_files.add_file( + storageId=storageId, + projectId=CROWDIN_PROJECT_ID, + name=filename, + title=title, + exportOptions=exportOptions, + ) + getFiles() + print("Done") -def getFiles() -> dict[str, str]: +def getFiles() -> None: """Gets files from Crowdin, and write them to a json file.""" addonId = buildVars.addon_info["addon_name"] @@ -329,7 +314,6 @@ def getFiles() -> dict[str, str]: dictionary[name] = id with open(L10N_FILE, "w", encoding="utf-8") as jsonFile: json.dump(dictionary, jsonFile, ensure_ascii=False) - return dictionary def uploadTranslationFile(crowdinFilePath: str, localFilePath: str, language: str): From 1a1e6fdb476a42ecd5a27b2885ec06c0e3d7b87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 14 Dec 2025 13:19:12 +0100 Subject: [PATCH 20/90] Update l10nUtil --- _l10n/l10nUtil.py | 64 +++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/_l10n/l10nUtil.py b/_l10n/l10nUtil.py index 68725aa..00dab1a 100644 --- a/_l10n/l10nUtil.py +++ b/_l10n/l10nUtil.py @@ -267,6 +267,14 @@ def uploadSourceFile(localFilePath: str): Upload a source file to Crowdin. :param localFilePath: The path to the local file to be uploaded """ + if not os.path.isfile(L10N_FILE): + getFiles() + with open(L10N_FILE, "r", encoding="utf-8") as jsonFile: + files = json.load(jsonFile) + fileId = files.get(localFilePath) + if fileId is None: + files = getFiles() + fileId = files.get(localFilePath) res = getCrowdinClient().storages.add_storage( open(localFilePath, "rb"), ) @@ -274,30 +282,41 @@ def uploadSourceFile(localFilePath: str): raise ValueError("Crowdin storage upload failed") storageId = res["data"]["id"] print(f"Stored with ID {storageId}") - addonId = buildVars.addon_info["addon_name"] - filename = addonId - if os.path.splitext(filename)[1] == ".pot": - title = f"{os.path.splitext(filename)[0]} interface" - exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{os.path.splitext(filename)[0]}.po" - else: - title = f"{os.path.splitext(filename)[0]} documentation" - exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{filename}" - exportOptions = { - "exportPattern": exportPattern, - } - print(f"Exporting source file {localFilePath} from storage with ID {storageId}") - res = getCrowdinClient().source_files.add_file( - storageId=storageId, - projectId=CROWDIN_PROJECT_ID, - name=filename, - title=title, - exportOptions=exportOptions, - ) - getFiles() - print("Done") + filename = os.path.basename(localFilePath) + fileId = files.get(filename) + print(f"File ID: {fileId}") + match fileId: + case None: + if os.path.splitext(filename)[1] == ".pot": + title = f"{os.path.splitext(filename)[0]} interface" + exportPattern = ( + f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{os.path.splitext(filename)[0]}.po" + ) + else: + title = f"{os.path.splitext(filename)[0]} documentation" + exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{filename}" + exportOptions = { + "exportPattern": exportPattern, + } + print(f"Exporting source file {localFilePath} from storage with ID {storageId}") + res = getCrowdinClient().source_files.add_file( + storageId=storageId, + projectId=CROWDIN_PROJECT_ID, + name=filename, + title=title, + exportOptions=exportOptions, + ) + print("Done") + case _: + res = getCrowdinClient().source_files.update_file( + fileId=fileId, + storageId=storageId, + projectId=CROWDIN_PROJECT_ID, + ) + -def getFiles() -> None: +def getFiles() -> dict[str, int]: """Gets files from Crowdin, and write them to a json file.""" addonId = buildVars.addon_info["addon_name"] @@ -314,6 +333,7 @@ def getFiles() -> None: dictionary[name] = id with open(L10N_FILE, "w", encoding="utf-8") as jsonFile: json.dump(dictionary, jsonFile, ensure_ascii=False) + return dictionary def uploadTranslationFile(crowdinFilePath: str, localFilePath: str, language: str): From d2395b0ddecec7e026d1058f3c4cd4e9122ea23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 14 Dec 2025 13:20:38 +0100 Subject: [PATCH 21/90] Update workflow --- .github/workflows/exportAddonToCrowdin.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 4ef3da6..b7da396 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -77,10 +77,8 @@ jobs: - name: Generate source files if: ${{ !inputs.update }} run: | - if -f readme.md; then mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md - fi uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - name: update md if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdateMd == 'true' }} @@ -91,13 +89,13 @@ jobs: if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdatePot == 'true' }} run: | uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot - - name: Commit and push json and xliff files + - name: Commit and push json id: commit run: | git config --local user.name github-actions git config --local user.email github-actions@github.com git status - git add hash.json _l10n/l10n.json + git add _l10n/l10n.json if git diff --staged --quiet; then echo "Nothing added to commit." else From e4dafe1492e008f1a2e99f3e23c881135692ab50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 16 Dec 2025 06:00:36 +0100 Subject: [PATCH 22/90] Update readme --- readme.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/readme.md b/readme.md index 05e5f12..82877a0 100644 --- a/readme.md +++ b/readme.md @@ -146,6 +146,20 @@ Note: you must fill out this dictionary if at least one custom symbol dictionary * channel: update channel (do not use this switch unless you know what you are doing). * dev: suitable for development builds, names the add-on according to current date (yyyymmdd) and sets update channel to "dev". + +### Translation workflow + +You can add the documentation and interface messages of your add-on to be translated in Crowdin. + +You need a Crowdin account and an API token with permissions to push to a Crowdin project. +For example, you may want to use this [Crowdin project to translate NVDA add-ons](https://crowdin.com/project/nvdaaddons). + +Then, to export your add-on to Crowdin for the first time, run the `.github/workflows/exportAddonsToCrowdin.yml`, ensuring that the update option is set to false. +When you have updated messages or documentation, run the workflow setting update to true (which is the default option). + + + + ### Additional tools The template includes configuration files for use with additional tools such as linters. These include: From f76904eeecbb761e6dc4af2657cdd656f46abaee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 16 Dec 2025 17:43:33 +0100 Subject: [PATCH 23/90] Update readme.md Co-authored-by: Sean Budd --- readme.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/readme.md b/readme.md index 82877a0..6a61fcd 100644 --- a/readme.md +++ b/readme.md @@ -157,9 +157,6 @@ For example, you may want to use this [Crowdin project to translate NVDA add-ons Then, to export your add-on to Crowdin for the first time, run the `.github/workflows/exportAddonsToCrowdin.yml`, ensuring that the update option is set to false. When you have updated messages or documentation, run the workflow setting update to true (which is the default option). - - - ### Additional tools The template includes configuration files for use with additional tools such as linters. These include: From aea5ebac891f84320796aa5de9af5f7a51d17852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 16 Dec 2025 17:44:29 +0100 Subject: [PATCH 24/90] Update _l10n/crowdinSync.py Co-authored-by: Sean Budd --- _l10n/crowdinSync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_l10n/crowdinSync.py b/_l10n/crowdinSync.py index 1a56070..0d5ceec 100644 --- a/_l10n/crowdinSync.py +++ b/_l10n/crowdinSync.py @@ -1,6 +1,6 @@ # A part of NonVisual Desktop Access (NVDA) # based on file from https://github.com/jcsteh/osara -# Copyright (C) 2023-2024 NV Access Limited, James Teh +# Copyright (C) 2023-2025 NV Access Limited, James Teh # This file may be used under the terms of the GNU General Public License, version 2 or later. # For more details see: https://www.gnu.org/licenses/gpl-2.0.html From f7ccaf68ff57404cbd071b8cadef024af0f65618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 16 Dec 2025 19:42:07 +0100 Subject: [PATCH 25/90] Add setOutput.py to separate Python code from yaml file --- .github/workflows/exportAddonToCrowdin.yml | 34 +----------------- .github/workflows/setOutputs.py | 42 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/setOutputs.py diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index b7da396..31f4871 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -41,39 +41,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Get add-on info id: getAddonInfo - shell: python - run: | - import os, sys, json - sys.path.insert(0, os.getcwd()) - import buildVars, sha256 - addonId = buildVars.addon_info["addon_name"] - readmeFile = os.path.join(os.getcwd(), "readme.md") - i18nSources = sorted(buildVars.i18nSources) - if os.path.isfile(readmeFile): - readmeSha = sha256.sha256_checksum([readmeFile]) - i18nSourcesSha = sha256.sha256_checksum(i18nSources) - hashFile = os.path.join(os.getcwd(), "hash.json") - data = dict() - if os.path.isfile(hashFile): - with open(hashFile, "rt") as f: - data = json.load(f) - shouldUpdateMd = (data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None) and os.path.isfile(readmeFile) - shouldUpdatePot = (data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None) - data = dict() - if readmeSha: - data["readmeSha"] = readmeSha - if i18nSourcesSha: - data["i18nSourcesSha"] = i18nSourcesSha - with open(hashFile, "wt", encoding="utf-8") as f: - json.dump(data, f, indent="\t", ensure_ascii=False) - name = 'addonId' - value = addonId - name0 = 'shouldUpdateMd' - value0 = str(shouldUpdateMd).lower() - name1 = 'shouldUpdatePot' - value1 = str(shouldUpdatePot).lower() - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f"{name}={value}\n{name0}={value0}\n{name1}={value1}\n") + run: uv run ./.github/workflows/setOutputs.py - name: Generate source files if: ${{ !inputs.update }} run: | diff --git a/.github/workflows/setOutputs.py b/.github/workflows/setOutputs.py new file mode 100644 index 0000000..5e0e5d5 --- /dev/null +++ b/.github/workflows/setOutputs.py @@ -0,0 +1,42 @@ +# Copyright (C) 2024-2025 NV Access Limited, Noelia Ruiz Martínez +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +import os, sys, json +sys.path.insert(0, os.getcwd()) +import buildVars, sha256 + + +def main(): + addonId = buildVars.addon_info["addon_name"] + readmeFile = os.path.join(os.getcwd(), "readme.md") + i18nSources = sorted(buildVars.i18nSources) + if os.path.isfile(readmeFile): + readmeSha = sha256.sha256_checksum([readmeFile]) + i18nSourcesSha = sha256.sha256_checksum(i18nSources) + hashFile = os.path.join(os.getcwd(), "hash.json") + data = dict() + if os.path.isfile(hashFile): + with open(hashFile, "rt") as f: + data = json.load(f) + shouldUpdateMd = (data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None) and os.path.isfile(readmeFile) + shouldUpdatePot = (data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None) + data = dict() + if readmeSha: + data["readmeSha"] = readmeSha + if i18nSourcesSha: + data["i18nSourcesSha"] = i18nSourcesSha + with open(hashFile, "wt", encoding="utf-8") as f: + json.dump(data, f, indent="\t", ensure_ascii=False) + name = 'addonId' + value = addonId + name0 = 'shouldUpdateMd' + value0 = str(shouldUpdateMd).lower() + name1 = 'shouldUpdatePot' + value1 = str(shouldUpdatePot).lower() + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f"{name}={value}\n{name0}={value0}\n{name1}={value1}\n") + + +if __name__ == "__main__": + main() \ No newline at end of file From 0276e2270735ce5915a7bae3318afb9349076c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 16 Dec 2025 20:35:17 +0100 Subject: [PATCH 26/90] Remove bad comment --- _l10n/crowdinSync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_l10n/crowdinSync.py b/_l10n/crowdinSync.py index 0d5ceec..e879bba 100644 --- a/_l10n/crowdinSync.py +++ b/_l10n/crowdinSync.py @@ -79,7 +79,7 @@ def main(): "uploadSourceFile", help="Upload a source file to Crowdin.", ) - # uploadCommand.add_argument("crowdinFileID", type=int, help="The Crowdin file ID.") + uploadCommand.add_argument("localFilePath", help="The path to the local file.") args = parser.parse_args() if args.command == "uploadSourceFile": From 253eb461572334ea9af37a0eda8e14fd9e499fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 16 Dec 2025 20:42:13 +0100 Subject: [PATCH 27/90] Reset pyproject to master --- pyproject.toml | 52 +------------------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4673a1c..97189ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,39 +1,3 @@ -[build-system] -requires = ["setuptools~=72.0", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "addonTemplate" -dynamic = ["version"] -description = "NVDA add-on template" -maintainers = [ - {name = "NV Access", email = "info@nvaccess.org"}, -] -requires-python = ">=3.13,<3.14" -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3", - "Topic :: Accessibility", -] -readme = "readme.md" -license = {file = "COPYING.TXT"} -dependencies = [ - "crowdin-api-client==1.21.0", - "lxml>=6.0.2", - "markdown>=3.10", - "markdown-link-attr-modifier==0.2.1", - "mdx-gh-links==0.4", - "mdx-truly-sane-lists==1.3", - "nh3==0.2.19", - "requests>=2.32.5", - "scons==4.10.1", -] -[project.urls] -Repository = "https://github.com/nvaccess/addonTemplate" - [tool.ruff] line-length = 110 @@ -56,13 +20,10 @@ include = [ exclude = [ ".git", "__pycache__", - ".venv", - "buildVars.py", ] [tool.ruff.format] indent-style = "tab" -line-ending = "lf" [tool.ruff.lint.mccabe] max-complexity = 15 @@ -72,16 +33,13 @@ ignore = [ # indentation contains tabs "W191", ] -logger-objects = ["logHandler.log"] [tool.ruff.lint.per-file-ignores] -# sconscripts contains many inbuilt functions not recognised by the lint, +# sconstruct contains many inbuilt functions not recognised by the lint, # so ignore F821. "sconstruct" = ["F821"] [tool.pyright] -venvPath = "../nvda/.venv" -venv = "." pythonPlatform = "Windows" typeCheckingMode = "strict" @@ -93,7 +51,6 @@ exclude = [ "sconstruct", ".git", "__pycache__", - ".venv", # When excluding concrete paths relative to a directory, # not matching multiple folders by name e.g. `__pycache__`, # paths are relative to the configuration file. @@ -102,7 +59,6 @@ exclude = [ # Tell pyright where to load python code from extraPaths = [ "./addon", - "../nvda/source", ] # General config @@ -203,9 +159,3 @@ reportMissingTypeStubs = false # Bad rules # These are sorted alphabetically and should be enabled and moved to compliant rules section when resolved. -lint = [ - "ruff==0.8.1", - "pre-commit==4.0.1", - "pyright==1.1.396", -] - From c51e7ad894e92233952dc875def5989c7ad308db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 16 Dec 2025 20:43:29 +0100 Subject: [PATCH 28/90] reset .pre-commit configuration to master --- .pre-commit-config.yaml | 97 +++-------------------------------------- 1 file changed, 6 insertions(+), 91 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 75d507a..dd7a9d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,92 +1,7 @@ -# Copied from https://github.com/nvaccess/nvda -# https://pre-commit.ci/ -# Configuration for Continuous Integration service -ci: - # Pyright does not seem to work in pre-commit CI - skip: [pyright] - autoupdate_schedule: monthly - autoupdate_commit_msg: "Pre-commit auto-update" - autofix_commit_msg: "Pre-commit auto-fix" - submodules: true - -default_language_version: - python: python3.13 - repos: -- repo: https://github.com/pre-commit-ci/pre-commit-ci-config - rev: v1.6.1 - hooks: - - id: check-pre-commit-ci-config - -- repo: meta - hooks: - # ensures that exclude directives apply to any file in the repository. - - id: check-useless-excludes - # ensures that the configured hooks apply to at least one file in the repository. - - id: check-hooks-apply - -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - # Prevents commits to certain branches - - id: no-commit-to-branch - args: ["--branch", "main", ] - # Checks that large files have not been added. Default cut-off for "large" files is 500kb. - - id: check-added-large-files - # Checks python syntax - - id: check-ast - # Checks for filenames that will conflict on case insensitive filesystems (the majority of Windows filesystems, most of the time) - - id: check-case-conflict - # Checks for artifacts from resolving merge conflicts. - - id: check-merge-conflict - # Checks Python files for debug statements, such as python's breakpoint function, or those inserted by some IDEs. - - id: debug-statements - # Removes trailing whitespace. - - id: trailing-whitespace - types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] - # Ensures all files end in 1 (and only 1) newline. - - id: end-of-file-fixer - types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] - # Removes the UTF-8 BOM from files that have it. - # See https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/codingStandards.md#encoding - - id: fix-byte-order-marker - types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] - # Validates TOML files. - - id: check-toml - # Validates YAML files. - - id: check-yaml - # Ensures that links to lines in files under version control point to a particular commit. - - id: check-vcs-permalinks - # Avoids using reserved Windows filenames. - - id: check-illegal-windows-names -- repo: https://github.com/asottile/add-trailing-comma - rev: v3.2.0 - hooks: - # Ruff preserves indent/new-line formatting of function arguments, list items, and similar iterables, - # if a trailing comma is added. - # This adds a trailing comma to args/iterable items in case it was missed. - - id: add-trailing-comma - -- repo: https://github.com/astral-sh/ruff-pre-commit - # Matches Ruff version in pyproject. - rev: v0.12.7 - hooks: - - id: ruff - name: lint with ruff - args: [ --fix ] - - id: ruff-format - name: format with ruff - -- repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.406 - hooks: - - id: pyright - name: Check types with pyright - additional_dependencies: [ "pyright[nodejs]==1.1.406" ] - -- repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.18.1 - hooks: - - id: markdownlint-cli2 - name: Lint markdown files - args: ["--fix"] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-ast + - id: check-case-conflict + - id: check-yaml From 00c6b31aca1e3d5fd3ca8fb5c87174edc29eec0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 17 Dec 2025 00:56:10 +0100 Subject: [PATCH 29/90] Update precommit configuration (#4) --- .pre-commit-config.yaml | 92 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd7a9d6..0c8f5c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,87 @@ +# Copied from https://github.com/nvaccess/nvda +# https://pre-commit.ci/ +# Configuration for Continuous Integration service +ci: + # Pyright does not seem to work in pre-commit CI + skip: [pyright] + autoupdate_schedule: monthly + autoupdate_commit_msg: "Pre-commit auto-update" + autofix_commit_msg: "Pre-commit auto-fix" + submodules: true + +default_language_version: + python: python3.13 + repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 - hooks: - - id: check-ast - - id: check-case-conflict - - id: check-yaml +- repo: https://github.com/pre-commit-ci/pre-commit-ci-config + rev: v1.6.1 + hooks: + - id: check-pre-commit-ci-config + +- repo: meta + hooks: + # ensures that exclude directives apply to any file in the repository. + - id: check-useless-excludes + # ensures that the configured hooks apply to at least one file in the repository. + - id: check-hooks-apply + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + # Prevents commits to certain branches + - id: no-commit-to-branch + args: ["--branch", "main", "master", ] + # Checks that large files have not been added. Default cut-off for "large" files is 500kb. + - id: check-added-large-files + # Checks python syntax + - id: check-ast + # Checks for filenames that will conflict on case insensitive filesystems (the majority of Windows filesystems, most of the time) + - id: check-case-conflict + # Checks for artifacts from resolving merge conflicts. + - id: check-merge-conflict + # Checks Python files for debug statements, such as python's breakpoint function, or those inserted by some IDEs. + - id: debug-statements + # Removes trailing whitespace. + - id: trailing-whitespace + types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] + # Ensures all files end in 1 (and only 1) newline. + - id: end-of-file-fixer + types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] + # Removes the UTF-8 BOM from files that have it. + # See https://github.com/nvaccess/nvda/blob/master/projectDocs/dev/codingStandards.md#encoding + - id: fix-byte-order-marker + types_or: [python, c, c++, batch, markdown, toml, yaml, powershell] + # Validates TOML files. + - id: check-toml + # Validates YAML files. + - id: check-yaml + # Ensures that links to lines in files under version control point to a particular commit. + - id: check-vcs-permalinks + # Avoids using reserved Windows filenames. + - id: check-illegal-windows-names +- repo: https://github.com/asottile/add-trailing-comma + rev: v3.2.0 + hooks: + # Ruff preserves indent/new-line formatting of function arguments, list items, and similar iterables, + # if a trailing comma is added. + # This adds a trailing comma to args/iterable items in case it was missed. + - id: add-trailing-comma + +- repo: https://github.com/astral-sh/ruff-pre-commit + # Matches Ruff version in pyproject. + rev: v0.12.7 + hooks: + - id: ruff + name: lint with ruff + args: [ --fix ] + - id: ruff-format + name: format with ruff + +- repo: local + hooks: + + - id: pyright + name: type check with pyright + entry: uv run pyright + language: system + types: [python] From cd4816c0cbf43d9585dda95812870b8db5096fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 17 Dec 2025 04:53:36 +0100 Subject: [PATCH 30/90] Remove userAccount variable, since we use markdown, not xliff --- buildVars.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/buildVars.py b/buildVars.py index 770946a..a3fe862 100644 --- a/buildVars.py +++ b/buildVars.py @@ -10,8 +10,6 @@ # which returns whatever is given to it as an argument. from site_scons.site_tools.NVDATool.utils import _ -# The GitHub user account to generate xliff file for translations -userAccount: str | None = None # Add-on information variables addon_info = AddonInfo( # add-on Name/identifier, internal for NVDA From 314220bdc5bbda0e131efd490b7878aa02c9b514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 17 Dec 2025 05:31:26 +0100 Subject: [PATCH 31/90] Update or add files from scratch depending on existence of hashFile --- .github/workflows/exportAddonToCrowdin.yml | 19 ++++++++----------- .github/workflows/setOutputs.py | 3 +++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 31f4871..1b8a26c 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -3,12 +3,6 @@ name: Export add-on to Crowdin on: workflow_dispatch: - inputs: - update: - description: 'true to update preexisting sources, false to add them from scratch' - type: boolean - required: false - default: true concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -42,19 +36,22 @@ jobs: - name: Get add-on info id: getAddonInfo run: uv run ./.github/workflows/setOutputs.py - - name: Generate source files - if: ${{ !inputs.update }} + - name: Upload md from scratch + if: ${{ steps.getAddonInfo.outputs.shouldUpdateMd == 'false' }} run: | mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - name: update md - if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdateMd == 'true' }} + if: ${{ steps.getAddonInfo.outputs.shouldUpdateMd == 'true' }} run: | mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.md + - name: Upload pot from scratch + if: ${{ steps.getAddonInfo.outputs.shouldUpdatePot == 'false' }} + run: | + uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - name: Update pot - if: ${{ inputs.update && steps.getAddonInfo.outputs.shouldUpdatePot == 'true' }} + if: ${{ steps.getAddonInfo.outputs.shouldUpdatePot == 'true' }} run: | uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot - name: Commit and push json diff --git a/.github/workflows/setOutputs.py b/.github/workflows/setOutputs.py index 5e0e5d5..da853bc 100644 --- a/.github/workflows/setOutputs.py +++ b/.github/workflows/setOutputs.py @@ -21,6 +21,9 @@ def main(): data = json.load(f) shouldUpdateMd = (data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None) and os.path.isfile(readmeFile) shouldUpdatePot = (data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None) + else: + shouldUpdateMd = False + shouldUpdatePot = False data = dict() if readmeSha: data["readmeSha"] = readmeSha From f3e8b8d87518e5e579d771d1f0b948eafcf1a5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 17 Dec 2025 05:42:56 +0100 Subject: [PATCH 32/90] Use addMd and addPotFromScratch outputs --- .github/workflows/exportAddonToCrowdin.yml | 4 ++-- .github/workflows/setOutputs.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 1b8a26c..4d01c88 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -37,7 +37,7 @@ jobs: id: getAddonInfo run: uv run ./.github/workflows/setOutputs.py - name: Upload md from scratch - if: ${{ steps.getAddonInfo.outputs.shouldUpdateMd == 'false' }} + if: ${{ steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' }} run: | mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md @@ -47,7 +47,7 @@ jobs: mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.md - name: Upload pot from scratch - if: ${{ steps.getAddonInfo.outputs.shouldUpdatePot == 'false' }} + if: ${{ steps.getAddonInfo.outputs.shouldAddPotFromScratch == 'true' }} run: | uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - name: Update pot diff --git a/.github/workflows/setOutputs.py b/.github/workflows/setOutputs.py index da853bc..9af5839 100644 --- a/.github/workflows/setOutputs.py +++ b/.github/workflows/setOutputs.py @@ -21,9 +21,8 @@ def main(): data = json.load(f) shouldUpdateMd = (data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None) and os.path.isfile(readmeFile) shouldUpdatePot = (data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None) - else: - shouldUpdateMd = False - shouldUpdatePot = False + shouldAddMdFromScratch = not os.path.isfile(hashFile) and not shouldUpdateMd + shouldAddPotFromScratch = not os.path.isfile(hashFile) and not shouldUpdatePot data = dict() if readmeSha: data["readmeSha"] = readmeSha @@ -37,8 +36,12 @@ def main(): value0 = str(shouldUpdateMd).lower() name1 = 'shouldUpdatePot' value1 = str(shouldUpdatePot).lower() + name2 = shouldAddMdFromScratch + value2 = str(shouldAddMdFromScratch).lower() + name3 = shouldAddPotFromScratch + value3 = str(shouldAddPotFromScratch).lower() with open(os.environ['GITHUB_OUTPUT'], 'a') as f: - f.write(f"{name}={value}\n{name0}={value0}\n{name1}={value1}\n") + f.write(f"{name}={value}\n{name0}={value0}\n{name1}={value1}\n{name2}={value2}\n{name3}={value3}\n") if __name__ == "__main__": From de4fa152b9172ffa40082f7a8d33414f48152867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sat, 20 Dec 2025 22:17:09 +0100 Subject: [PATCH 33/90] Update dependencies --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 97189ac..6178665 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,10 @@ +dependencies = [ + "SCons==4.10.1", + "Markdown==3.10", + "ruff==0.14.5", + "pre-commit==4.2.0", + "pyright[nodejs]==1.1.407", +] [tool.ruff] line-length = 110 From 46a105aa032263958169b148688119a9415c0e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sat, 20 Dec 2025 22:19:03 +0100 Subject: [PATCH 34/90] Update setOutput --- .github/workflows/setOutputs.py | 45 ++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/.github/workflows/setOutputs.py b/.github/workflows/setOutputs.py index 9af5839..d8cce3b 100644 --- a/.github/workflows/setOutputs.py +++ b/.github/workflows/setOutputs.py @@ -1,16 +1,26 @@ -# Copyright (C) 2024-2025 NV Access Limited, Noelia Ruiz Martínez +# Copyright (C) 2025 NV Access Limited, Noelia Ruiz Martínez # This file is covered by the GNU General Public License. # See the file COPYING for more details. -import os, sys, json +import os +import sys +import json + sys.path.insert(0, os.getcwd()) -import buildVars, sha256 +import buildVars +import sha256 def main(): addonId = buildVars.addon_info["addon_name"] readmeFile = os.path.join(os.getcwd(), "readme.md") i18nSources = sorted(buildVars.i18nSources) + readmeSha = None + i18nSourcesSha = None + shouldUpdateMd = False + shouldUpdatePot = False + shouldAddMdFromScratch = False + shouldAddPotFromScratch = False if os.path.isfile(readmeFile): readmeSha = sha256.sha256_checksum([readmeFile]) i18nSourcesSha = sha256.sha256_checksum(i18nSources) @@ -19,30 +29,31 @@ def main(): if os.path.isfile(hashFile): with open(hashFile, "rt") as f: data = json.load(f) - shouldUpdateMd = (data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None) and os.path.isfile(readmeFile) - shouldUpdatePot = (data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None) - shouldAddMdFromScratch = not os.path.isfile(hashFile) and not shouldUpdateMd - shouldAddPotFromScratch = not os.path.isfile(hashFile) and not shouldUpdatePot - data = dict() - if readmeSha: + shouldUpdateMd = data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None + shouldUpdatePot = ( + data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None + ) + shouldAddMdFromScratch = data.get("readmeSha") is None + shouldAddPotFromScratch = data.get("i18nSourcesSha") is None + if readmeSha is not None: data["readmeSha"] = readmeSha - if i18nSourcesSha: + if i18nSourcesSha is not None: data["i18nSourcesSha"] = i18nSourcesSha with open(hashFile, "wt", encoding="utf-8") as f: json.dump(data, f, indent="\t", ensure_ascii=False) - name = 'addonId' + name = "addonId" value = addonId - name0 = 'shouldUpdateMd' + name0 = "shouldUpdateMd" value0 = str(shouldUpdateMd).lower() - name1 = 'shouldUpdatePot' + name1 = "shouldUpdatePot" value1 = str(shouldUpdatePot).lower() - name2 = shouldAddMdFromScratch + name2 = "shouldAddMdFromScratch" value2 = str(shouldAddMdFromScratch).lower() - name3 = shouldAddPotFromScratch + name3 = "shouldAddPotFromScratch" value3 = str(shouldAddPotFromScratch).lower() - with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + with open(os.environ["GITHUB_OUTPUT"], "a") as f: f.write(f"{name}={value}\n{name0}={value0}\n{name1}={value1}\n{name2}={value2}\n{name3}={value3}\n") if __name__ == "__main__": - main() \ No newline at end of file + main() From 053d4de721cfcaf82bda92a3ea05125c63c3dae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sat, 20 Dec 2025 22:21:09 +0100 Subject: [PATCH 35/90] Update workflow --- .github/workflows/exportAddonToCrowdin.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 4d01c88..2367c09 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout add-on uses: actions/checkout@v6 + with: + submodules: true - name: "Set up Python" uses: actions/setup-python@v6 with: @@ -27,40 +29,41 @@ jobs: pip install scons markdown sudo apt update sudo apt install gettext + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v7 - name: Build add-on and pot file run: | scons scons pot - - name: Install the latest version of uv - uses: astral-sh/setup-uv@v7 - name: Get add-on info id: getAddonInfo run: uv run ./.github/workflows/setOutputs.py - name: Upload md from scratch if: ${{ steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' }} run: | + echo "Yes" mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md + uv run ./_l10n/source/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md - name: update md if: ${{ steps.getAddonInfo.outputs.shouldUpdateMd == 'true' }} run: | mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md - uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.md + uv run ./_l10n/source/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.md - name: Upload pot from scratch if: ${{ steps.getAddonInfo.outputs.shouldAddPotFromScratch == 'true' }} run: | - uv run ./_l10n/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot + uv run ./_l10n/source/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - name: Update pot if: ${{ steps.getAddonInfo.outputs.shouldUpdatePot == 'true' }} run: | - uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot - - name: Commit and push json + uv run ./_l10n/source/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot + - name: Commit and push json id: commit run: | git config --local user.name github-actions git config --local user.email github-actions@github.com git status - git add _l10n/l10n.json + git add *.json if git diff --staged --quiet; then echo "Nothing added to commit." else From 4a3f5a0dfbedeab8ce642b57b0ae0886adddf3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sat, 20 Dec 2025 22:39:49 +0100 Subject: [PATCH 36/90] Update lock --- uv.lock | 266 +------------------------------------------------------- 1 file changed, 1 insertion(+), 265 deletions(-) diff --git a/uv.lock b/uv.lock index 58c3f26..bda0207 100644 --- a/uv.lock +++ b/uv.lock @@ -1,267 +1,3 @@ version = 1 revision = 3 -requires-python = "==3.13.*" - -[[package]] -name = "addontemplate" -source = { editable = "." } -dependencies = [ - { name = "crowdin-api-client" }, - { name = "lxml" }, - { name = "markdown" }, - { name = "markdown-link-attr-modifier" }, - { name = "mdx-gh-links" }, - { name = "mdx-truly-sane-lists" }, - { name = "nh3" }, - { name = "requests" }, - { name = "scons" }, -] - -[package.metadata] -requires-dist = [ - { name = "crowdin-api-client", specifier = "==1.21.0" }, - { name = "lxml", specifier = ">=6.0.2" }, - { name = "markdown", specifier = ">=3.10" }, - { name = "markdown-link-attr-modifier", specifier = "==0.2.1" }, - { name = "mdx-gh-links", specifier = "==0.4" }, - { name = "mdx-truly-sane-lists", specifier = "==1.3" }, - { name = "nh3", specifier = "==0.2.19" }, - { name = "requests", specifier = ">=2.32.5" }, - { name = "scons", specifier = "==4.10.1" }, -] - -[[package]] -name = "certifi" -version = "2025.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "crowdin-api-client" -version = "1.21.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/70/4069412e2e8004a6ad15bf2a3d9085bea50ee932a66ad935285831cf82b4/crowdin_api_client-1.21.0.tar.gz", hash = "sha256:0f957e5de6487a74ac892d524a5e300c1bc971320b67f85ce65741904420d8ec", size = 64729, upload-time = "2025-01-31T15:56:42.179Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/0f/9efc4f6db0b3b97f99015529b5832058ce4f7970d547b23fc04a38d69ddd/crowdin_api_client-1.21.0-py3-none-any.whl", hash = "sha256:85e19557755ebf6a15beda605d25b77de365244d8c636462b0dd8030a6cdfe20", size = 101566, upload-time = "2025-01-31T15:56:40.651Z" }, -] - -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, - { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, - { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, - { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, - { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, - { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, - { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, - { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, -] - -[[package]] -name = "markdown" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, -] - -[[package]] -name = "markdown-link-attr-modifier" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/30/d35aad054a27f119bff2408523d82c3f9a6d9936712c872f5b9fe817de5b/markdown_link_attr_modifier-0.2.1.tar.gz", hash = "sha256:18df49a9fe7b5c87dad50b75c2a2299ae40c65674f7b1263fb12455f5df7ac99", size = 18408, upload-time = "2023-04-13T16:00:12.798Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/82/9262a67313847fdcc6252a007f032924fe0c0b6d6b9ef0d0b1fa58952c72/markdown_link_attr_modifier-0.2.1-py3-none-any.whl", hash = "sha256:6b4415319648cbe6dfb7a54ca12fa69e61a27c86a09d15f2a9a559ace0aa87c5", size = 17146, upload-time = "2023-04-13T16:00:06.559Z" }, -] - -[[package]] -name = "mdx-gh-links" -version = "0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2f/ea/bf1f721a8dc0ff83b426480f040ac68dbe3d7898b096c1277a5a4e3da0ec/mdx_gh_links-0.4.tar.gz", hash = "sha256:41d5aac2ab201425aa0a19373c4095b79e5e015fdacfe83c398199fe55ca3686", size = 5783, upload-time = "2023-12-22T19:54:02.136Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/c7/ccfe05ade98ba7a63f05d1b05b7508d9af743cbd1f1681aa0c9900a8cd40/mdx_gh_links-0.4-py3-none-any.whl", hash = "sha256:9057bca1fa5280bf1fcbf354381e46c9261cc32c2d5c0407801f8a910be5f099", size = 7166, upload-time = "2023-12-22T19:54:00.384Z" }, -] - -[[package]] -name = "mdx-truly-sane-lists" -version = "1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e6/27/16456314311abac2cedef4527679924e80ac4de19dd926699c1b261e0b9b/mdx_truly_sane_lists-1.3.tar.gz", hash = "sha256:b661022df7520a1e113af7c355c62216b384c867e4f59fb8ee7ad511e6e77f45", size = 5359, upload-time = "2022-07-19T13:42:45.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/9e/dcd1027f7fd193aed152e01c6651a197c36b858f2cd1425ad04cb31a34fc/mdx_truly_sane_lists-1.3-py3-none-any.whl", hash = "sha256:b9546a4c40ff8f1ab692f77cee4b6bfe8ddf9cccf23f0a24e71f3716fe290a37", size = 6071, upload-time = "2022-07-19T13:42:43.375Z" }, -] - -[[package]] -name = "nh3" -version = "0.2.19" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/32/3b8d8471d006333bac3175ad37402414d985ed3f8650a01a33e0e86b9824/nh3-0.2.19.tar.gz", hash = "sha256:790056b54c068ff8dceb443eaefb696b84beff58cca6c07afd754d17692a4804", size = 17327, upload-time = "2024-11-30T04:05:56.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/1d/cbd75a2313d96cd3903111667d3d07548fb45c8ecf5c315f37a8f6c202fa/nh3-0.2.19-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6", size = 1205181, upload-time = "2024-11-29T05:50:02.802Z" }, - { url = "https://files.pythonhosted.org/packages/f0/30/8e9ec472ce575fa6b98935920c91df637bf9342862bd943745441aec99eb/nh3-0.2.19-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0", size = 739174, upload-time = "2024-11-29T05:50:10.157Z" }, - { url = "https://files.pythonhosted.org/packages/5c/b5/d1f81c5ec5695464b69d8aa4529ecb5fd872cbfb29f879b4063bb9397da8/nh3-0.2.19-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37", size = 758660, upload-time = "2024-11-29T05:50:13.97Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5e/295a3a069f3b9dc35527eedd7b212f31311ef1f66a0e5f5f0acad6db9456/nh3-0.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a", size = 924377, upload-time = "2024-11-29T05:50:15.716Z" }, - { url = "https://files.pythonhosted.org/packages/71/e2/0f189d5054f22cdfdb16d16a2a41282f411a4c03f8418be47e0480bd5bfd/nh3-0.2.19-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd", size = 992124, upload-time = "2024-11-29T05:50:17.637Z" }, - { url = "https://files.pythonhosted.org/packages/0d/87/2907edd61a2172527c5322036aa95ce6c18432ff280fc5cf78fe0f934c65/nh3-0.2.19-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9", size = 913939, upload-time = "2024-11-29T05:50:20.435Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a2/e0d3ea0175f28032d7d2bab765250f4e94ef131a7b3293e3df4cb254a5b2/nh3-0.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55", size = 909051, upload-time = "2024-11-29T05:50:31.116Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e1/f52cb1d54ba965b7d8bb1c884ca982be31d7f75ad9e7e5817f4af20002b3/nh3-0.2.19-cp313-cp313t-win32.whl", hash = "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc", size = 540566, upload-time = "2024-11-29T05:50:38.437Z" }, - { url = "https://files.pythonhosted.org/packages/70/85/91a66edfab0adbf22468973d8abd4b93c951bbcbbe2121675ee468b912a2/nh3-0.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3", size = 542368, upload-time = "2024-11-29T05:50:41.418Z" }, - { url = "https://files.pythonhosted.org/packages/32/9c/f8808cf6683d4852ba8862e25b98aa9116067ddec517938a1b6e8faadb43/nh3-0.2.19-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41", size = 1205140, upload-time = "2024-11-29T05:50:54.582Z" }, - { url = "https://files.pythonhosted.org/packages/04/0e/268401d9244a84935342d9f3ba5d22bd7d2fc10cfc7a8f59bde8f6721466/nh3-0.2.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a", size = 763571, upload-time = "2024-11-29T05:51:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0d/8be706feb6637d6e5db0eed09fd3f4e1008aee3d5d7161c9973d7aae1d13/nh3-0.2.19-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69", size = 750319, upload-time = "2024-11-29T05:51:05.512Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ce/1f5f9ba0194f6a882e4bda89ae831678e4b68aa3de91e11e2629a1e6a613/nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9", size = 857636, upload-time = "2024-11-29T05:51:06.872Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5d/5661a66f2950879a81fde5fbb6beb650c5647776aaec1a676e6b3ff4b6e5/nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c", size = 821081, upload-time = "2024-11-29T05:51:08.202Z" }, - { url = "https://files.pythonhosted.org/packages/22/f8/454828f6f21516bf0c8c578e8bc2ab4f045e6b6fe5179602fe4dc2479da6/nh3-0.2.19-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1", size = 894452, upload-time = "2024-11-29T05:51:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/2e/5d/36f5b78cbc631cac1c993bdc4608a0fe3148214bdb6d2c1266e228a2686a/nh3-0.2.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc", size = 748281, upload-time = "2024-11-29T05:51:29.021Z" }, - { url = "https://files.pythonhosted.org/packages/98/da/d04f5f0e7ee8edab8ceecdbba9f1c614dc8cf07374141ff6ea3b615b3479/nh3-0.2.19-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707", size = 767109, upload-time = "2024-11-29T05:51:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5b/1232fb35c7d1182adb7d513fede644a81b5361259749781e6075c40a9125/nh3-0.2.19-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22", size = 924295, upload-time = "2024-11-29T05:51:33.078Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fd/ae622d08518fd31360fd87a515700bc09913f2e57e7f010063f2193ea610/nh3-0.2.19-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d", size = 992038, upload-time = "2024-11-29T05:51:34.414Z" }, - { url = "https://files.pythonhosted.org/packages/56/78/226577c5e3fe379cb95265aa77736e191d859032c974169e6879c51c156f/nh3-0.2.19-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9", size = 913866, upload-time = "2024-11-29T05:51:35.727Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ca/bbd2b2dab31ceae38cfa673861cab81df5ed5be1fe47b6c4f5aa41729aa2/nh3-0.2.19-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0", size = 908976, upload-time = "2024-11-29T05:51:43.698Z" }, - { url = "https://files.pythonhosted.org/packages/4c/8f/6452eb1184ad87cdd2cac7ee3ebd67a2aadb554d25572c1778efdf807e1e/nh3-0.2.19-cp38-abi3-win32.whl", hash = "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48", size = 540528, upload-time = "2024-11-29T05:51:45.312Z" }, - { url = "https://files.pythonhosted.org/packages/58/d6/285df10307f16fcce9afbd133b04b4bc7d7f9b84b02f0f724bab30dacdd9/nh3-0.2.19-cp38-abi3-win_amd64.whl", hash = "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121", size = 542316, upload-time = "2024-11-29T05:52:01.253Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "scons" -version = "4.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] - -[[package]] -name = "wrapt" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, - { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, - { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, - { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, - { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, - { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, - { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, - { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, - { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, - { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, - { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, - { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, -] +requires-python = ">=3.13" From dbe74dcad03276485e697e9ee767ac355e8d7f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 21 Dec 2025 11:04:21 +0100 Subject: [PATCH 37/90] Verify uv lock --- .pre-commit-config.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c8f5c6..8353208 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,10 @@ repos: args: [ --fix ] - id: ruff-format name: format with ruff - + - id: uv-lock + name: Verify uv lock file + # Override python interpreter from .python-versions as that is too strict for pre-commit.ci + args: ["-p3.13"] - repo: local hooks: From e717292a628eade7edf31683f8f6d30c1d861186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 21 Dec 2025 11:06:53 +0100 Subject: [PATCH 38/90] Add uv to dependencies in case this is relevant to verify the lock according to uv version --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6178665..4441e1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,5 @@ dependencies = [ + "uv==0.9.11", "SCons==4.10.1", "Markdown==3.10", "ruff==0.14.5", From c4ed57508a8ca3f859cb77b5aa743b197ed24620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 21 Dec 2025 11:10:54 +0100 Subject: [PATCH 39/90] Remove debug statement --- .github/workflows/exportAddonToCrowdin.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 2367c09..1096152 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -41,7 +41,6 @@ jobs: - name: Upload md from scratch if: ${{ steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' }} run: | - echo "Yes" mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md uv run ./_l10n/source/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md - name: update md From befa647e2e7d2b6821a79b2f787ae2fc5675e10e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 21 Dec 2025 11:17:39 +0100 Subject: [PATCH 40/90] Run pre-commit --- .github/workflows/exportAddonToCrowdin.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 1096152..7ca8edb 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -23,18 +23,20 @@ jobs: uses: actions/setup-python@v6 with: python-version-file: ".python-version" - - name: Install dependencies + - name: Install gettext run: | - python -m pip install --upgrade pip - pip install scons markdown sudo apt update sudo apt install gettext - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 + - name: Run pre-commit + run: | + # Ensure uv environment is up to date. + uv run pre-commit run uv-lock --all-files - name: Build add-on and pot file run: | - scons - scons pot + uv run scons + uv run scons pot - name: Get add-on info id: getAddonInfo run: uv run ./.github/workflows/setOutputs.py From 05c816196c41b150cd1007f94ce3830602466abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 05:01:01 +0100 Subject: [PATCH 41/90] Update dependencies --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4441e1e..f3054a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,6 @@ dependencies = [ - "uv==0.9.11", "SCons==4.10.1", "Markdown==3.10", - "ruff==0.14.5", - "pre-commit==4.2.0", - "pyright[nodejs]==1.1.407", ] [tool.ruff] line-length = 110 From 9a0f62abbc2be3a9528935c6585ae934bbdf5af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 05:29:16 +0100 Subject: [PATCH 42/90] Deleted Pyproject to avoid conflicts --- pyproject.toml | 165 ------------------------------------------------- 1 file changed, 165 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index f3054a0..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,165 +0,0 @@ -dependencies = [ - "SCons==4.10.1", - "Markdown==3.10", -] -[tool.ruff] -line-length = 110 - -builtins = [ - # translation lookup - "_", - # translation lookup - "ngettext", - # translation lookup - "pgettext", - # translation lookup - "npgettext", -] - -include = [ - "*.py", - "sconstruct", -] - -exclude = [ - ".git", - "__pycache__", -] - -[tool.ruff.format] -indent-style = "tab" - -[tool.ruff.lint.mccabe] -max-complexity = 15 - -[tool.ruff.lint] -ignore = [ - # indentation contains tabs - "W191", -] - -[tool.ruff.lint.per-file-ignores] -# sconstruct contains many inbuilt functions not recognised by the lint, -# so ignore F821. -"sconstruct" = ["F821"] - -[tool.pyright] -pythonPlatform = "Windows" -typeCheckingMode = "strict" - -include = [ - "**/*.py", -] - -exclude = [ - "sconstruct", - ".git", - "__pycache__", - # When excluding concrete paths relative to a directory, - # not matching multiple folders by name e.g. `__pycache__`, - # paths are relative to the configuration file. -] - -# Tell pyright where to load python code from -extraPaths = [ - "./addon", -] - -# General config -analyzeUnannotatedFunctions = true -deprecateTypingAliases = true - -# Stricter typing -strictParameterNoneValue = true -strictListInference = true -strictDictionaryInference = true -strictSetInference = true - -# Compliant rules -reportAbstractUsage = true -reportArgumentType = true -reportAssertAlwaysTrue = true -reportAssertTypeFailure = true -reportAssignmentType = true -reportAttributeAccessIssue = true -reportCallInDefaultInitializer = true -reportCallIssue = true -reportConstantRedefinition = true -reportDuplicateImport = true -reportFunctionMemberAccess = true -reportGeneralTypeIssues = true -reportImplicitOverride = true -reportImplicitStringConcatenation = true -reportImportCycles = true -reportIncompatibleMethodOverride = true -reportIncompatibleVariableOverride = true -reportIncompleteStub = true -reportInconsistentConstructor = true -reportInconsistentOverload = true -reportIndexIssue = true -reportInvalidStringEscapeSequence = true -reportInvalidStubStatement = true -reportInvalidTypeArguments = true -reportInvalidTypeForm = true -reportInvalidTypeVarUse = true -reportMatchNotExhaustive = true -reportMissingImports = true -reportMissingModuleSource = true -reportMissingParameterType = true -reportMissingSuperCall = true -reportMissingTypeArgument = true -reportNoOverloadImplementation = true -reportOperatorIssue = true -reportOptionalCall = true -reportOptionalContextManager = true -reportOptionalIterable = true -reportOptionalMemberAccess = true -reportOptionalOperand = true -reportOptionalSubscript = true -reportOverlappingOverload = true -reportPossiblyUnboundVariable = true -reportPrivateImportUsage = true -reportPrivateUsage = true -reportPropertyTypeMismatch = true -reportRedeclaration = true -reportReturnType = true -reportSelfClsParameterName = true -reportShadowedImports = true -reportTypeCommentUsage = true -reportTypedDictNotRequiredAccess = true -reportUnboundVariable = true -reportUndefinedVariable = true -reportUnhashable = true -reportUninitializedInstanceVariable = true -reportUnknownArgumentType = true -reportUnknownLambdaType = true -reportUnknownMemberType = true -reportUnknownParameterType = true -reportUnknownVariableType = true -reportUnnecessaryCast = true -reportUnnecessaryComparison = true -reportUnnecessaryContains = true -reportUnnecessaryIsInstance = true -reportUnnecessaryTypeIgnoreComment = true -reportUnsupportedDunderAll = true -reportUntypedBaseClass = true -reportUntypedClassDecorator = true -reportUntypedFunctionDecorator = true -reportUntypedNamedTuple = true -reportUnusedCallResult = true -reportUnusedClass = true -reportUnusedCoroutine = true -reportUnusedExcept = true -reportUnusedExpression = true -reportUnusedFunction = true -reportUnusedImport = true -reportUnusedVariable = true -reportWildcardImportFromLibrary = true - -reportDeprecated = true - -# Can be enabled by generating type stubs for modules via pyright CLI -reportMissingTypeStubs = false - -# Bad rules -# These are sorted alphabetically and should be enabled and moved to compliant rules section when resolved. From 4abd788013e44b6522ce422c7e3678582ae66047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 05:30:18 +0100 Subject: [PATCH 43/90] Reset pyproject to master --- pyproject.toml | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..97189ac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,161 @@ +[tool.ruff] +line-length = 110 + +builtins = [ + # translation lookup + "_", + # translation lookup + "ngettext", + # translation lookup + "pgettext", + # translation lookup + "npgettext", +] + +include = [ + "*.py", + "sconstruct", +] + +exclude = [ + ".git", + "__pycache__", +] + +[tool.ruff.format] +indent-style = "tab" + +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.ruff.lint] +ignore = [ + # indentation contains tabs + "W191", +] + +[tool.ruff.lint.per-file-ignores] +# sconstruct contains many inbuilt functions not recognised by the lint, +# so ignore F821. +"sconstruct" = ["F821"] + +[tool.pyright] +pythonPlatform = "Windows" +typeCheckingMode = "strict" + +include = [ + "**/*.py", +] + +exclude = [ + "sconstruct", + ".git", + "__pycache__", + # When excluding concrete paths relative to a directory, + # not matching multiple folders by name e.g. `__pycache__`, + # paths are relative to the configuration file. +] + +# Tell pyright where to load python code from +extraPaths = [ + "./addon", +] + +# General config +analyzeUnannotatedFunctions = true +deprecateTypingAliases = true + +# Stricter typing +strictParameterNoneValue = true +strictListInference = true +strictDictionaryInference = true +strictSetInference = true + +# Compliant rules +reportAbstractUsage = true +reportArgumentType = true +reportAssertAlwaysTrue = true +reportAssertTypeFailure = true +reportAssignmentType = true +reportAttributeAccessIssue = true +reportCallInDefaultInitializer = true +reportCallIssue = true +reportConstantRedefinition = true +reportDuplicateImport = true +reportFunctionMemberAccess = true +reportGeneralTypeIssues = true +reportImplicitOverride = true +reportImplicitStringConcatenation = true +reportImportCycles = true +reportIncompatibleMethodOverride = true +reportIncompatibleVariableOverride = true +reportIncompleteStub = true +reportInconsistentConstructor = true +reportInconsistentOverload = true +reportIndexIssue = true +reportInvalidStringEscapeSequence = true +reportInvalidStubStatement = true +reportInvalidTypeArguments = true +reportInvalidTypeForm = true +reportInvalidTypeVarUse = true +reportMatchNotExhaustive = true +reportMissingImports = true +reportMissingModuleSource = true +reportMissingParameterType = true +reportMissingSuperCall = true +reportMissingTypeArgument = true +reportNoOverloadImplementation = true +reportOperatorIssue = true +reportOptionalCall = true +reportOptionalContextManager = true +reportOptionalIterable = true +reportOptionalMemberAccess = true +reportOptionalOperand = true +reportOptionalSubscript = true +reportOverlappingOverload = true +reportPossiblyUnboundVariable = true +reportPrivateImportUsage = true +reportPrivateUsage = true +reportPropertyTypeMismatch = true +reportRedeclaration = true +reportReturnType = true +reportSelfClsParameterName = true +reportShadowedImports = true +reportTypeCommentUsage = true +reportTypedDictNotRequiredAccess = true +reportUnboundVariable = true +reportUndefinedVariable = true +reportUnhashable = true +reportUninitializedInstanceVariable = true +reportUnknownArgumentType = true +reportUnknownLambdaType = true +reportUnknownMemberType = true +reportUnknownParameterType = true +reportUnknownVariableType = true +reportUnnecessaryCast = true +reportUnnecessaryComparison = true +reportUnnecessaryContains = true +reportUnnecessaryIsInstance = true +reportUnnecessaryTypeIgnoreComment = true +reportUnsupportedDunderAll = true +reportUntypedBaseClass = true +reportUntypedClassDecorator = true +reportUntypedFunctionDecorator = true +reportUntypedNamedTuple = true +reportUnusedCallResult = true +reportUnusedClass = true +reportUnusedCoroutine = true +reportUnusedExcept = true +reportUnusedExpression = true +reportUnusedFunction = true +reportUnusedImport = true +reportUnusedVariable = true +reportWildcardImportFromLibrary = true + +reportDeprecated = true + +# Can be enabled by generating type stubs for modules via pyright CLI +reportMissingTypeStubs = false + +# Bad rules +# These are sorted alphabetically and should be enabled and moved to compliant rules section when resolved. From c25636475dfb6e5367495d06c9b075ee4a1522fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 05:36:16 +0100 Subject: [PATCH 44/90] Remove _l10n since this will be added as a submodule --- _l10n/crowdinSync.py | 92 ---- _l10n/l10nUtil.py | 951 ------------------------------------- _l10n/markdownTranslate.py | 737 ---------------------------- _l10n/md2html.py | 197 -------- 4 files changed, 1977 deletions(-) delete mode 100644 _l10n/crowdinSync.py delete mode 100644 _l10n/l10nUtil.py delete mode 100644 _l10n/markdownTranslate.py delete mode 100644 _l10n/md2html.py diff --git a/_l10n/crowdinSync.py b/_l10n/crowdinSync.py deleted file mode 100644 index e879bba..0000000 --- a/_l10n/crowdinSync.py +++ /dev/null @@ -1,92 +0,0 @@ -# A part of NonVisual Desktop Access (NVDA) -# based on file from https://github.com/jcsteh/osara -# Copyright (C) 2023-2025 NV Access Limited, James Teh -# This file may be used under the terms of the GNU General Public License, version 2 or later. -# For more details see: https://www.gnu.org/licenses/gpl-2.0.html - - -import argparse -import os - -import requests - -from l10nUtil import getFiles - -AUTH_TOKEN = os.getenv("crowdinAuthToken", "").strip() -if not AUTH_TOKEN: - raise ValueError("crowdinAuthToken environment variable not set") -PROJECT_ID = os.getenv("crowdinProjectID", "").strip() -if not PROJECT_ID: - raise ValueError("crowdinProjectID environment variable not set") - - -def request( - path: str, - method=requests.get, - headers: dict[str, str] | None = None, - **kwargs, -) -> requests.Response: - if headers is None: - headers = {} - headers["Authorization"] = f"Bearer {AUTH_TOKEN}" - r = method( - f"https://api.crowdin.com/api/v2/{path}", - headers=headers, - **kwargs, - ) - # Convert errors to exceptions, but print the response before raising. - try: - r.raise_for_status() - except requests.exceptions.HTTPError: - print(r.json()) - raise - return r - - -def projectRequest(path: str, **kwargs) -> requests.Response: - return request(f"projects/{PROJECT_ID}/{path}", **kwargs) - - -def uploadSourceFile(localFilePath: str) -> None: - files = getFiles() - fn = os.path.basename(localFilePath) - crowdinFileID = files.get(fn) - print(f"Uploading {localFilePath} to Crowdin temporary storage as {fn}") - with open(localFilePath, "rb") as f: - r = request( - "storages", - method=requests.post, - headers={"Crowdin-API-FileName": fn}, - data=f, - ) - storageID = r.json()["data"]["id"] - print(f"Updating file {crowdinFileID} on Crowdin with storage ID {storageID}") - r = projectRequest( - f"files/{crowdinFileID}", - method=requests.put, - json={"storageId": storageID}, - ) - revisionId = r.json()["data"]["revisionId"] - print(f"Updated to revision {revisionId}") - - -def main(): - parser = argparse.ArgumentParser( - description="Syncs translations with Crowdin.", - ) - commands = parser.add_subparsers(dest="command", required=True) - uploadCommand = commands.add_parser( - "uploadSourceFile", - help="Upload a source file to Crowdin.", - ) - - uploadCommand.add_argument("localFilePath", help="The path to the local file.") - args = parser.parse_args() - if args.command == "uploadSourceFile": - uploadSourceFile(args.localFilePath) - else: - raise ValueError(f"Unknown command: {args.command}") - - -if __name__ == "__main__": - main() diff --git a/_l10n/l10nUtil.py b/_l10n/l10nUtil.py deleted file mode 100644 index 00dab1a..0000000 --- a/_l10n/l10nUtil.py +++ /dev/null @@ -1,951 +0,0 @@ -# A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2024-2025 NV Access Limited, Noelia Ruiz Martínez -# This file is covered by the GNU General Public License. -# See the file COPYING for more details. - -import os, sys -sys.path.insert(0, os.getcwd()) - -import crowdin_api as crowdin -import tempfile -import lxml.etree -import os -import shutil -import argparse -import markdownTranslate -import requests -import codecs -import re -import subprocess -import sys -import zipfile -import time -import json - -import buildVars - - -CROWDIN_PROJECT_ID = os.getenv("crowdinProjectID", "").strip() -POLLING_INTERVAL_SECONDS = 5 -EXPORT_TIMEOUT_SECONDS = 60 * 10 # 10 minutes -L10N_FILE = os.path.join(os.path.dirname(__file__), "l10n.json") - - -def fetchCrowdinAuthToken() -> str: - """ - Fetch the Crowdin auth token from the ~/.nvda_crowdin file or prompt the user for it. - If provided by the user, the token will be saved to the ~/.nvda_crowdin file. - :return: The auth token - """ - crowdinAuthToken = os.getenv("crowdinAuthToken", "") - if crowdinAuthToken: - print("Using Crowdin auth token from environment variable.") - return crowdinAuthToken - token_path = os.path.expanduser("~/.nvda_crowdin") - if os.path.exists(token_path): - with open(token_path, "r") as f: - token = f.read().strip() - print("Using auth token from ~/.nvda_crowdin") - return token - print("A Crowdin auth token is required to proceed.") - print("Please visit https://crowdin.com/settings#api-key") - print("Create a personal access token with translations permissions, and enter it below.") - token = input("Enter Crowdin auth token: ").strip() - with open(token_path, "w") as f: - f.write(token) - return token - - -_crowdinClient = None - - -def getCrowdinClient() -> crowdin.CrowdinClient: - """ - Create or fetch the Crowdin client instance. - :return: The Crowdin client - """ - global _crowdinClient - if _crowdinClient is None: - token = fetchCrowdinAuthToken() - _crowdinClient = crowdin.CrowdinClient(project_id=CROWDIN_PROJECT_ID, token=token) - return _crowdinClient - - -def fetchLanguageFromXliff(xliffPath: str, source: bool = False) -> str: - """ - Fetch the language from an xliff file. - This function also prints a message to the console stating the detected language if found, or a warning if not found. - :param xliffPath: Path to the xliff file - :param source: If True, fetch the source language, otherwise fetch the target language - :return: The language code - """ - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError(f"Not an xliff file: {xliffPath}") - lang = xliffRoot.get("srcLang" if source else "trgLang") - if lang is None: - print(f"Could not detect language for xliff file {xliffPath}, {source=}") - else: - print(f"Detected language {lang} for xliff file {xliffPath}, {source=}") - return lang - - -def preprocessXliff(xliffPath: str, outputPath: str): - """ - Replace corrupt or empty translated segment targets with the source text, - marking the segment again as "initial" state. - This function also prints a message to the console stating the number of segments processed and the numbers of empty, corrupt, source and existing translations removed. - :param xliffPath: Path to the xliff file to be processed - :param outputPath: Path to the resulting xliff file - """ - print(f"Preprocessing xliff file at {xliffPath}") - namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError(f"Not an xliff file: {xliffPath}") - file = xliffRoot.find("./xliff:file", namespaces=namespace) - units = file.findall("./xliff:unit", namespaces=namespace) - segmentCount = 0 - emptyTargetCount = 0 - corruptTargetcount = 0 - for unit in units: - segment = unit.find("./xliff:segment", namespaces=namespace) - if segment is None: - print("Warning: No segment element in unit") - continue - source = segment.find("./xliff:source", namespaces=namespace) - if source is None: - print("Warning: No source element in segment") - continue - sourceText = source.text - segmentCount += 1 - target = segment.find("./xliff:target", namespaces=namespace) - if target is None: - continue - targetText = target.text - # Correct empty targets - if not targetText: - emptyTargetCount += 1 - target.text = sourceText - segment.set("state", "initial") - # Correct corrupt target tags - elif targetText in ( - "", - "<target/>", - "", - "<target></target>", - ): - corruptTargetcount += 1 - target.text = sourceText - segment.set("state", "initial") - xliff.write(outputPath, encoding="utf-8") - print( - f"Processed {segmentCount} segments, removing {emptyTargetCount} empty targets, {corruptTargetcount} corrupt targets", - ) - - -def stripXliff(xliffPath: str, outputPath: str, oldXliffPath: str | None = None): - """ - Removes notes and skeleton elements from an xliff file before upload to Crowdin. - Removes empty and corrupt translations. - Removes untranslated segments. - Removes existing translations if an old xliff file is provided. - This function also prints a message to the console stating the number of segments processed and the numbers of empty, corrupt, source and existing translations removed. - :param xliffPath: Path to the xliff file to be stripped - :param outputPath: Path to the resulting xliff file - :param oldXliffPath: Path to the old xliff file containing existing translations that should be also stripped. - """ - print(f"Creating stripped xliff at {outputPath} from {xliffPath}") - namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError(f"Not an xliff file: {xliffPath}") - oldXliffRoot = None - if oldXliffPath: - oldXliff = lxml.etree.parse(oldXliffPath) - oldXliffRoot = oldXliff.getroot() - if oldXliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError(f"Not an xliff file: {oldXliffPath}") - skeletonNode = xliffRoot.find("./xliff:file/xliff:skeleton", namespaces=namespace) - if skeletonNode is not None: - skeletonNode.getparent().remove(skeletonNode) - file = xliffRoot.find("./xliff:file", namespaces=namespace) - units = file.findall("./xliff:unit", namespaces=namespace) - segmentCount = 0 - untranslatedCount = 0 - emptyCount = 0 - corruptCount = 0 - existingTranslationCount = 0 - for unit in units: - unitID = unit.get("id") - notes = unit.find("./xliff:notes", namespaces=namespace) - if notes is not None: - unit.remove(notes) - segment = unit.find("./xliff:segment", namespaces=namespace) - if segment is None: - print("Warning: No segment element in unit") - continue - segmentCount += 1 - state = segment.get("state") - if state == "initial": - file.remove(unit) - untranslatedCount += 1 - continue - target = segment.find("./xliff:target", namespaces=namespace) - if target is None: - file.remove(unit) - untranslatedCount += 1 - continue - targetText = target.text - if not targetText: - emptyCount += 1 - file.remove(unit) - continue - elif targetText in ( - "", - "<target/>", - "", - "<target></target>", - ): - corruptCount += 1 - file.remove(unit) - continue - if oldXliffRoot: - # Remove existing translations - oldTarget = oldXliffRoot.find( - f"./xliff:file/xliff:unit[@id='{unitID}']/xliff:segment/xliff:target", - namespaces=namespace, - ) - if oldTarget is not None and oldTarget.getparent().get("state") != "initial": - if oldTarget.text == targetText: - file.remove(unit) - existingTranslationCount += 1 - xliff.write(outputPath, encoding="utf-8") - if corruptCount > 0: - print(f"Removed {corruptCount} corrupt translations.") - if emptyCount > 0: - print(f"Removed {emptyCount} empty translations.") - if existingTranslationCount > 0: - print(f"Ignored {existingTranslationCount} existing translations.") - keptTranslations = segmentCount - untranslatedCount - emptyCount - corruptCount - existingTranslationCount - print(f"Added or changed {keptTranslations} translations.") - - -def downloadTranslationFile(crowdinFilePath: str, localFilePath: str, language: str): - """ - Download a translation file from Crowdin. - :param crowdinFilePath: The Crowdin file path - :param localFilePath: The path to save the local file - :param language: The language code to download the translation for - """ - with open(L10N_FILE, "r", encoding="utf-8") as jsonFile: - files = json.load(jsonFile) - fileId = files.get(crowdinFilePath) - if fileId is None: - files = getFiles() - fileId = files.get(crowdinFilePath) - print(f"Requesting export of {crowdinFilePath} for {language} from Crowdin") - res = getCrowdinClient().translations.export_project_translation( - fileIds=[fileId], - targetLanguageId=language, - ) - if res is None: - raise ValueError("Crowdin export failed") - download_url = res["data"]["url"] - print(f"Downloading from {download_url}") - with open(localFilePath, "wb") as f: - r = requests.get(download_url) - f.write(r.content) - print(f"Saved to {localFilePath}") - - -def uploadSourceFile(localFilePath: str): - """ - Upload a source file to Crowdin. - :param localFilePath: The path to the local file to be uploaded - """ - if not os.path.isfile(L10N_FILE): - getFiles() - with open(L10N_FILE, "r", encoding="utf-8") as jsonFile: - files = json.load(jsonFile) - fileId = files.get(localFilePath) - if fileId is None: - files = getFiles() - fileId = files.get(localFilePath) - res = getCrowdinClient().storages.add_storage( - open(localFilePath, "rb"), - ) - if res is None: - raise ValueError("Crowdin storage upload failed") - storageId = res["data"]["id"] - print(f"Stored with ID {storageId}") - filename = os.path.basename(localFilePath) - fileId = files.get(filename) - print(f"File ID: {fileId}") - match fileId: - case None: - if os.path.splitext(filename)[1] == ".pot": - title = f"{os.path.splitext(filename)[0]} interface" - exportPattern = ( - f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{os.path.splitext(filename)[0]}.po" - ) - else: - title = f"{os.path.splitext(filename)[0]} documentation" - exportPattern = f"/{os.path.splitext(filename)[0]}/%two_letters_code%/{filename}" - exportOptions = { - "exportPattern": exportPattern, - } - print(f"Exporting source file {localFilePath} from storage with ID {storageId}") - res = getCrowdinClient().source_files.add_file( - storageId=storageId, - projectId=CROWDIN_PROJECT_ID, - name=filename, - title=title, - exportOptions=exportOptions, - ) - print("Done") - case _: - res = getCrowdinClient().source_files.update_file( - fileId=fileId, - storageId=storageId, - projectId=CROWDIN_PROJECT_ID, - ) - - - -def getFiles() -> dict[str, int]: - """Gets files from Crowdin, and write them to a json file.""" - - addonId = buildVars.addon_info["addon_name"] - - res = getCrowdinClient().source_files.list_files(CROWDIN_PROJECT_ID, filter=addonId) - if res is None: - raise ValueError("Getting files from Crowdin failed") - dictionary = dict() - data = res["data"] - for file in data: - fileInfo = file["data"] - name = fileInfo["name"] - id = fileInfo["id"] - dictionary[name] = id - with open(L10N_FILE, "w", encoding="utf-8") as jsonFile: - json.dump(dictionary, jsonFile, ensure_ascii=False) - return dictionary - - -def uploadTranslationFile(crowdinFilePath: str, localFilePath: str, language: str): - """ - Upload a translation file to Crowdin. - :param crowdinFilePath: The Crowdin file path - :param localFilePath: The path to the local file to be uploaded - :param language: The language code to upload the translation for - """ - with open(L10N_FILE, "r", encoding="utf-8") as jsonFile: - files = json.load(jsonFile) - fileId = files.get(crowdinFilePath) - if fileId is None: - files = getFiles() - fileId = files.get(crowdinFilePath) - print(f"Uploading {localFilePath} to Crowdin") - res = getCrowdinClient().storages.add_storage( - open(localFilePath, "rb"), - ) - if res is None: - raise ValueError("Crowdin storage upload failed") - storageId = res["data"]["id"] - print(f"Stored with ID {storageId}") - print(f"Importing translation for {crowdinFilePath} in {language} from storage with ID {storageId}") - res = getCrowdinClient().translations.upload_translation( - fileId=fileId, - languageId=language, - storageId=storageId, - autoApproveImported=True, - importEqSuggestions=True, - ) - print("Done") - - -def exportTranslations(outputDir: str, language: str | None = None): - """ - Export translation files from Crowdin as a bundle. - :param outputDir: Directory to save translation files. - :param language: The language code to export (e.g., 'es', 'fr', 'de'). - If None, exports all languages. - """ - - # Create output directory if it doesn't exist - os.makedirs(outputDir, exist_ok=True) - - client = getCrowdinClient() - - requestData = { - "skipUntranslatedStrings": False, - "skipUntranslatedFiles": True, - "exportApprovedOnly": False, - } - - if language is not None: - requestData["targetLanguageIds"] = [language] - - if language is None: - print("Requesting export of all translations from Crowdin...") - else: - print(f"Requesting export of all translations for language: {language}") - build_res = client.translations.build_project_translation(request_data=requestData) - - if language is None: - zip_filename = "translations.zip" - else: - zip_filename = f"translations_{language}.zip" - - if build_res is None: - raise ValueError("Failed to start translation build") - - build_id = build_res["data"]["id"] - print(f"Build started with ID: {build_id}") - - # Wait for the build to complete - print("Waiting for build to complete...") - while True: - status_res = client.translations.check_project_build_status(build_id) - if status_res is None: - raise ValueError("Failed to check build status") - - status = status_res["data"]["status"] - progress = status_res["data"]["progress"] - print(f"Build status: {status} ({progress}%)") - - if status == "finished": - break - elif status == "failed": - raise ValueError("Translation build failed") - - time.sleep(POLLING_INTERVAL_SECONDS) - - # Download the completed build - print("Downloading translations archive...") - download_res = client.translations.download_project_translations(build_id) - if download_res is None: - raise ValueError("Failed to get download URL") - - download_url = download_res["data"]["url"] - print(f"Downloading from {download_url}") - - # Download and extract the ZIP file - zip_path = os.path.join(outputDir, zip_filename) - response = requests.get(download_url, stream=True, timeout=EXPORT_TIMEOUT_SECONDS) - response.raise_for_status() - - with open(zip_path, "wb") as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - - print(f"Archive saved to {zip_path}") - print("Extracting translations...") - - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(outputDir) - - # Remove the zip file - os.remove(zip_path) - - if language is None: - print(f"\nExport complete! All translations extracted to '{outputDir}' directory.") - else: - print(f"\nExport complete! All {language} translations extracted to '{outputDir}' directory.") - - -class _PoChecker: - """Checks a po file for errors not detected by msgfmt. - This first runs msgfmt to check for syntax errors. - It then checks for mismatched Python percent and brace interpolations. - Construct an instance and call the L{check} method. - """ - - FUZZY = "#, fuzzy" - MSGID = "msgid" - MSGID_PLURAL = "msgid_plural" - MSGSTR = "msgstr" - - def __init__(self, po: str): - """Constructor. - :param po: The path to the po file to check. - """ - self._poPath = po - with codecs.open(po, "r", "utf-8") as file: - self._poContent = file.readlines() - self._string: str | None = None - - self.alerts: list[str] = [] - """List of error and warning messages found in the po file.""" - - self.hasSyntaxError: bool = False - """Whether there is a syntax error in the po file.""" - - self.warningCount: int = 0 - """Number of warnings found.""" - - self.errorCount: int = 0 - """Number of errors found.""" - - def _addToString(self, line: list[str], startingCommand: str | None = None) -> None: - """Helper function to add a line to the current string. - :param line: The line to add. - :param startingCommand: The command that started this string, if any. - This is used to determine whether to strip the command and quotes. - """ - if startingCommand: - # Strip the command and the quotes. - self._string = line[len(startingCommand) + 2 : -1] - else: - # Strip the quotes. - self._string += line[1:-1] - - def _finishString(self) -> str: - """Helper function to finish the current string. - :return: The finished string. - """ - string = self._string - self._string = None - return string - - def _messageAlert(self, alert: str, isError: bool = True) -> None: - """Helper function to add an alert about a message. - :param alert: The alert message. - :param isError: Whether this is an error or a warning. - """ - if self._fuzzy: - # Fuzzy messages don't get used, so this shouldn't be considered an error. - isError = False - if isError: - self.errorCount += 1 - else: - self.warningCount += 1 - if self._fuzzy: - msgType = "Fuzzy message" - else: - msgType = "Message" - self.alerts.append( - f"{msgType} starting on line {self._messageLineNum}\n" - f'Original: "{self._msgid}"\n' - f'Translated: "{self._msgstr[-1]}"\n' - f"{'ERROR' if isError else 'WARNING'}: {alert}", - ) - - @property - def MSGFMT_PATH(self) -> str: - try: - # When running from source, miscDeps is the sibling of parent this script. - _MSGFMT = os.path.join(os.path.dirname(__file__), "..", "miscDeps", "tools", "msgfmt.exe") - except NameError: - # When running from a frozen executable, __file__ is not defined. - # In this case, we use the distribution path. - # When running from a distribution, source/l10nUtil.py is built to l10nUtil.exe. - # miscDeps is the sibling of this script in the distribution. - _MSGFMT = os.path.join(sys.prefix, "miscDeps", "tools", "msgfmt.exe") - - if not os.path.exists(_MSGFMT): - raise FileNotFoundError( - "msgfmt executable not found. " - "Please ensure that miscDeps/tools/msgfmt.exe exists in the source tree or distribution.", - ) - return _MSGFMT - - def _checkSyntax(self) -> None: - """Check the syntax of the po file using msgfmt. - This will set the hasSyntaxError attribute to True if there is a syntax error. - """ - - result = subprocess.run( - (self.MSGFMT_PATH, "-o", "-", self._poPath), - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True, # Ensures stderr is a text stream - ) - if result.returncode != 0: - output = result.stderr.rstrip().replace("\r\n", "\n") - self.alerts.append(output) - self.hasSyntaxError = True - self.errorCount = 1 - - def _checkMessages(self) -> None: - command = None - self._msgid = None - self._msgid_plural = None - self._msgstr = None - nextFuzzy = False - self._fuzzy = False - for lineNum, line in enumerate(self._poContent, 1): - line = line.strip() - if line.startswith(self.FUZZY): - nextFuzzy = True - continue - elif line.startswith(self.MSGID) and not line.startswith(self.MSGID_PLURAL): - # New message. - if self._msgstr is not None: - self._msgstr[-1] = self._finishString() - # Check the message we just handled. - self._checkMessage() - command = self.MSGID - start = command - self._messageLineNum = lineNum - self._fuzzy = nextFuzzy - nextFuzzy = False - elif line.startswith(self.MSGID_PLURAL): - self._msgid = self._finishString() - command = self.MSGID_PLURAL - start = command - elif line.startswith(self.MSGSTR): - self._handleMsgStrReaching(lastCommand=command) - command = self.MSGSTR - start = line[: line.find(" ")] - elif line.startswith('"'): - # Continuing a string. - start = None - else: - # This line isn't of interest. - continue - self._addToString(line, startingCommand=start) - if command == self.MSGSTR: - # Handle the last message. - self._msgstr[-1] = self._finishString() - self._checkMessage() - - def _handleMsgStrReaching(self, lastCommand: str) -> None: - """Helper function used by _checkMessages to handle the required processing when reaching a line - starting with "msgstr". - :param lastCommand: the current command just before the msgstr line is reached. - """ - - # Finish the string of the last command and check the message if it was an msgstr - if lastCommand == self.MSGID: - self._msgid = self._finishString() - elif lastCommand == self.MSGID_PLURAL: - self._msgid_plural = self._finishString() - elif lastCommand == self.MSGSTR: - self._msgstr[-1] = self._finishString() - self._checkMessage() - else: - raise RuntimeError(f"Unexpected command before line {self._messageLineNum}: {lastCommand}") - - # For first msgstr create the msgstr list - if lastCommand != self.MSGSTR: - self._msgstr = [] - - # Initiate the string for the current msgstr - self._msgstr.append("") - - def check(self) -> bool: - """Check the file. - Once this returns, you can call getReport to obtain a report. - This method should not be called more than once. - :return: True if the file is okay, False if there were problems. - """ - self._checkSyntax() - if self.alerts: - return False - self._checkMessages() - if self.alerts: - return False - return True - - # e.g. %s %d %10.2f %-5s (but not %%) or %%(name)s %(name)d - RE_UNNAMED_PERCENT = re.compile( - # Does not include optional mapping key, as that's handled by a different regex - r""" - (?:(?<=%%)|(? tuple[list[str], set[str], set[str]]: - """Get the percent and brace interpolations in a string. - :param text: The text to check. - :return: A tuple of a list and two sets: - - unnamed percent interpolations (e.g. %s, %d) - - named percent interpolations (e.g. %(name)s) - - brace format interpolations (e.g. {name}, {name:format}) - """ - unnamedPercent = self.RE_UNNAMED_PERCENT.findall(text) - namedPercent = set(self.RE_NAMED_PERCENT.findall(text)) - formats = set() - for m in self.RE_FORMAT.finditer(text): - if not m.group(1): - self._messageAlert( - "Unspecified positional argument in brace format", - # Skip as error as many of these had been introduced in the source .po files. - # These should be fixed in the source .po files to add names to instances of "{}". - # This causes issues where the order of the arguments change in the string. - # e.g. "Character: {}\nReplacement: {}" being translated to "Replacement: {}\nCharacter: {}" - # will result in the expected interpolation being in the wrong place. - # This should be changed isError=True. - isError=False, - ) - formats.add(m.group(0)) - return unnamedPercent, namedPercent, formats - - def _formatInterpolations( - self, - unnamedPercent: list[str], - namedPercent: set[str], - formats: set[str], - ) -> str: - """Format the interpolations for display in an error message. - :param unnamedPercent: The unnamed percent interpolations. - :param namedPercent: The named percent interpolations. - :param formats: The brace format interpolations. - """ - out: list[str] = [] - if unnamedPercent: - out.append(f"unnamed percent interpolations in this order: {unnamedPercent}") - if namedPercent: - out.append(f"these named percent interpolations: {namedPercent}") - if formats: - out.append(f"these brace format interpolations: {formats}") - if not out: - return "no interpolations" - return "\n\tAnd ".join(out) - - def _checkMessage(self) -> None: - idUnnamedPercent, idNamedPercent, idFormats = self._getInterpolations(self._msgid) - if not self._msgstr[-1]: - return - strUnnamedPercent, strNamedPercent, strFormats = self._getInterpolations(self._msgstr[-1]) - error = False - alerts = [] - if idUnnamedPercent != strUnnamedPercent: - if idUnnamedPercent: - alerts.append("unnamed percent interpolations differ") - error = True - else: - alerts.append("unexpected presence of unnamed percent interpolations") - if idNamedPercent - strNamedPercent: - alerts.append("missing named percent interpolation") - error = True - if strNamedPercent - idNamedPercent: - if idNamedPercent: - alerts.append("extra named percent interpolation") - error = True - else: - alerts.append("unexpected presence of named percent interpolations") - if idFormats - strFormats: - alerts.append("missing brace format interpolation") - error = True - if strFormats - idFormats: - if idFormats: - alerts.append("extra brace format interpolation") - error = True - else: - alerts.append("unexpected presence of brace format interpolations") - if alerts: - self._messageAlert( - f"{', '.join(alerts)}\n" - f"Expected: {self._formatInterpolations(idUnnamedPercent, idNamedPercent, idFormats)}\n" - f"Got: {self._formatInterpolations(strUnnamedPercent, strNamedPercent, strFormats)}", - isError=error, - ) - - def getReport(self) -> str | None: - """Get a text report about any errors or warnings. - :return: The text or None if there were no problems. - """ - if not self.alerts: - return None - report = f"File {self._poPath}: " - if self.hasSyntaxError: - report += "syntax error" - else: - if self.errorCount: - msg = "error" if self.errorCount == 1 else "errors" - report += f"{self.errorCount} {msg}" - if self.warningCount: - if self.errorCount: - report += ", " - msg = "warning" if self.warningCount == 1 else "warnings" - report += f"{self.warningCount} {msg}" - report += "\n\n" + "\n\n".join(self.alerts) - return report - - -def checkPo(poFilePath: str) -> tuple[bool, str | None]: - """Check a po file for errors. - :param poFilePath: The path to the po file to check. - :return: - True if the file is okay or has warnings, False if there were fatal errors. - A report about the errors or warnings found, or None if there were no problems. - """ - c = _PoChecker(poFilePath) - report = None - if not c.check(): - report = c.getReport() - if report: - report = report.encode("cp1252", errors="backslashreplace").decode( - "utf-8", - errors="backslashreplace", - ) - return not bool(c.errorCount), report - - -def main(): - args = argparse.ArgumentParser() - commands = args.add_subparsers(title="commands", dest="command", required=True) - command_checkPo = commands.add_parser("checkPo", help="Check po files") - # Allow entering arbitrary po file paths, not just those in the source tree - command_checkPo.add_argument( - "poFilePaths", - help="Paths to the po file to check", - nargs="+", - ) - command_xliff2md = commands.add_parser("xliff2md", help="Convert xliff to markdown") - command_xliff2md.add_argument( - "-u", - "--untranslated", - help="Produce the untranslated markdown file", - action="store_true", - default=False, - ) - command_xliff2md.add_argument("xliffPath", help="Path to the xliff file") - command_xliff2md.add_argument("mdPath", help="Path to the resulting markdown file") - downloadTranslationFileCommand = commands.add_parser( - "downloadTranslationFile", - help="Download a translation file from Crowdin.", - ) - downloadTranslationFileCommand.add_argument( - "language", - help="The language code to download the translation for.", - ) - downloadTranslationFileCommand.add_argument( - "crowdinFilePath", - help="The Crowdin file path", - ) - downloadTranslationFileCommand.add_argument( - "localFilePath", - nargs="?", - default=None, - help="The path to save the local file. If not provided, the Crowdin file path will be used.", - ) - uploadTranslationFileCommand = commands.add_parser( - "uploadTranslationFile", - help="Upload a translation file to Crowdin.", - ) - uploadTranslationFileCommand.add_argument( - "-o", - "--old", - help="Path to the old unchanged xliff file. If provided, only new or changed translations will be uploaded.", - default=None, - ) - uploadTranslationFileCommand.add_argument( - "language", - help="The language code to upload the translation for.", - ) - uploadTranslationFileCommand.add_argument( - "crowdinFilePath", - help="The Crowdin file path", - ) - uploadTranslationFileCommand.add_argument( - "localFilePath", - nargs="?", - default=None, - help="The path to the local file to be uploaded. If not provided, the Crowdin file path will be used.", - ) - uploadSourceFileCommand = commands.add_parser( - "uploadSourceFile", - help="Upload a source file to Crowdin.", - ) - uploadSourceFileCommand.add_argument( - "-f", - "--localFilePath", - help="The local path to the file.", - ) - exportTranslationsCommand = commands.add_parser( - "exportTranslations", - help="Export translation files from Crowdin as a bundle. If no language is specified, exports all languages.", - ) - exportTranslationsCommand.add_argument( - "-o", - "--output", - help="Directory to save translation files", - required=True, - ) - exportTranslationsCommand.add_argument( - "-l", - "--language", - help="Language code to export (e.g., 'es', 'fr', 'de'). If not specified, exports all languages.", - default=None, - ) - - args = args.parse_args() - match args.command: - case "xliff2md": - markdownTranslate.generateMarkdown( - xliffPath=args.xliffPath, - outputPath=args.mdPath, - translated=not args.untranslated, - ) - case "uploadSourceFile": - uploadSourceFile(args.localFilePath) - case "getFiles": - getFiles() - case "downloadTranslationFile": - localFilePath = args.localFilePath or args.crowdinFilePath - downloadTranslationFile(args.crowdinFilePath, localFilePath, args.language) - if args.crowdinFilePath.endswith(".xliff"): - preprocessXliff(localFilePath, localFilePath) - elif localFilePath.endswith(".po"): - success, report = checkPo(localFilePath) - if report: - print(report) - if not success: - print(f"\nWarning: Po file {localFilePath} has fatal errors.") - case "checkPo": - poFilePaths = args.poFilePaths - badFilePaths: list[str] = [] - for poFilePath in poFilePaths: - success, report = checkPo(poFilePath) - if report: - print(report) - if not success: - badFilePaths.append(poFilePath) - if badFilePaths: - print(f"\nOne or more po files had fatal errors: {', '.join(badFilePaths)}") - sys.exit(1) - case "uploadTranslationFile": - localFilePath = args.localFilePath or args.crowdinFilePath - needsDelete = False - if args.crowdinFilePath.endswith(".xliff"): - tmp = tempfile.NamedTemporaryFile(suffix=".xliff", delete=False, mode="w") - tmp.close() - shutil.copyfile(localFilePath, tmp.name) - stripXliff(tmp.name, tmp.name, args.old) - localFilePath = tmp.name - needsDelete = True - elif localFilePath.endswith(".po"): - success, report = checkPo(localFilePath) - if report: - print(report) - if not success: - print(f"\nPo file {localFilePath} has errors. Upload aborted.") - sys.exit(1) - uploadTranslationFile(args.crowdinFilePath, localFilePath, args.language) - if needsDelete: - os.remove(localFilePath) - case "exportTranslations": - exportTranslations(args.output, args.language) - case _: - raise ValueError(f"Unknown command {args.command}") - - -if __name__ == "__main__": - main() diff --git a/_l10n/markdownTranslate.py b/_l10n/markdownTranslate.py deleted file mode 100644 index fa9a186..0000000 --- a/_l10n/markdownTranslate.py +++ /dev/null @@ -1,737 +0,0 @@ -# A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2024 NV Access Limited. -# This file is covered by the GNU General Public License. -# See the file COPYING for more details. - -from typing import Generator -import tempfile -import os -import sys -sys.path.insert(0, os.getcwd()) -import contextlib -import lxml.etree -import argparse -import uuid -import re -from itertools import zip_longest -from xml.sax.saxutils import escape as xmlEscape -import difflib -from dataclasses import dataclass -import subprocess - -import buildVars - -RAW_GITHUB_REPO_URL = f"https://raw.githubusercontent.com/{buildVars.userAccount}/{buildVars.addon_info["addon_name"]}" -re_kcTitle = re.compile(r"^()$") -re_kcSettingsSection = re.compile(r"^()$") -# Comments that span a single line in their entirety -re_comment = re.compile(r"^$", re.DOTALL) -re_heading = re.compile(r"^(#+\s+)(.+?)((?:\s+\{#.+\})?)$") -re_bullet = re.compile(r"^(\s*\*\s+)(.+)$") -re_number = re.compile(r"^(\s*[0-9]+\.\s+)(.+)$") -re_hiddenHeaderRow = re.compile(r"^\|\s*\.\s*\{\.hideHeaderRow\}\s*(\|\s*\.\s*)*\|$") -re_postTableHeaderLine = re.compile(r"^(\|\s*-+\s*)+\|$") -re_tableRow = re.compile(r"^(\|)(.+)(\|)$") -re_translationID = re.compile(r"^(.*)\$\(ID:([0-9a-f-]+)\)(.*)$") - - -def prettyPathString(path: str) -> str: - cwd = os.getcwd() - if os.path.normcase(os.path.splitdrive(path)[0]) != os.path.normcase(os.path.splitdrive(cwd)[0]): - return path - return os.path.relpath(path, cwd) - - -@contextlib.contextmanager -def createAndDeleteTempFilePath_contextManager( - dir: str | None = None, - prefix: str | None = None, - suffix: str | None = None, -) -> Generator[str, None, None]: - """A context manager that creates a temporary file and deletes it when the context is exited""" - with tempfile.NamedTemporaryFile(dir=dir, prefix=prefix, suffix=suffix, delete=False) as tempFile: - tempFilePath = tempFile.name - tempFile.close() - yield tempFilePath - os.remove(tempFilePath) - - -def getLastCommitID(filePath: str) -> str: - # Run the git log command to get the last commit ID for the given file - result = subprocess.run( - ["git", "log", "-n", "1", "--pretty=format:%H", "--", filePath], - capture_output=True, - text=True, - check=True, - ) - commitID = result.stdout.strip() - if not re.match(r"[0-9a-f]{40}", commitID): - raise ValueError(f"Invalid commit ID: '{commitID}' for file '{filePath}'") - return commitID - - -def getGitDir() -> str: - # Run the git rev-parse command to get the root of the git directory - result = subprocess.run( - ["git", "rev-parse", "--show-toplevel"], - capture_output=True, - text=True, - check=True, - ) - gitDir = result.stdout.strip() - if not os.path.isdir(gitDir): - raise ValueError(f"Invalid git directory: '{gitDir}'") - return gitDir - - -def getRawGithubURLForPath(filePath: str) -> str: - gitDirPath = getGitDir() - commitID = getLastCommitID(filePath) - relativePath = os.path.relpath(os.path.abspath(filePath), gitDirPath) - relativePath = relativePath.replace("\\", "/") - return f"{RAW_GITHUB_REPO_URL}/{commitID}/{relativePath}" - - -def skeletonizeLine(mdLine: str) -> str | None: - prefix = "" - suffix = "" - if ( - mdLine.isspace() - or mdLine.strip() == "[TOC]" - or re_hiddenHeaderRow.match(mdLine) - or re_postTableHeaderLine.match(mdLine) - ): - return None - elif m := re_heading.match(mdLine): - prefix, content, suffix = m.groups() - elif m := re_bullet.match(mdLine): - prefix, content = m.groups() - elif m := re_number.match(mdLine): - prefix, content = m.groups() - elif m := re_tableRow.match(mdLine): - prefix, content, suffix = m.groups() - elif m := re_kcTitle.match(mdLine): - prefix, content, suffix = m.groups() - elif m := re_kcSettingsSection.match(mdLine): - prefix, content, suffix = m.groups() - elif re_comment.match(mdLine): - return None - ID = str(uuid.uuid4()) - return f"{prefix}$(ID:{ID}){suffix}\n" - - -@dataclass -class Result_generateSkeleton: - numTotalLines: int = 0 - numTranslationPlaceholders: int = 0 - - -def generateSkeleton(mdPath: str, outputPath: str) -> Result_generateSkeleton: - print(f"Generating skeleton file {prettyPathString(outputPath)} from {prettyPathString(mdPath)}...") - res = Result_generateSkeleton() - with ( - open(mdPath, "r", encoding="utf8") as mdFile, - open(outputPath, "w", encoding="utf8", newline="") as outputFile, - ): - for mdLine in mdFile.readlines(): - res.numTotalLines += 1 - skelLine = skeletonizeLine(mdLine) - if skelLine: - res.numTranslationPlaceholders += 1 - else: - skelLine = mdLine - outputFile.write(skelLine) - print( - f"Generated skeleton file with {res.numTotalLines} total lines and {res.numTranslationPlaceholders} translation placeholders", - ) - return res - - -@dataclass -class Result_updateSkeleton: - numAddedLines: int = 0 - numAddedTranslationPlaceholders: int = 0 - numRemovedLines: int = 0 - numRemovedTranslationPlaceholders: int = 0 - numUnchangedLines: int = 0 - numUnchangedTranslationPlaceholders: int = 0 - - -def extractSkeleton(xliffPath: str, outputPath: str): - print(f"Extracting skeleton from {prettyPathString(xliffPath)} to {prettyPathString(outputPath)}...") - with contextlib.ExitStack() as stack: - outputFile = stack.enter_context(open(outputPath, "w", encoding="utf8", newline="")) - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError("Not an xliff file") - skeletonNode = xliffRoot.find("./xliff:file/xliff:skeleton", namespaces=namespace) - if skeletonNode is None: - raise ValueError("No skeleton found in xliff file") - skeletonContent = skeletonNode.text.strip() - outputFile.write(skeletonContent) - print(f"Extracted skeleton to {prettyPathString(outputPath)}") - - -def updateSkeleton( - origMdPath: str, - newMdPath: str, - origSkelPath: str, - outputPath: str, -) -> Result_updateSkeleton: - print( - f"Creating updated skeleton file {prettyPathString(outputPath)} from {prettyPathString(origSkelPath)} with changes from {prettyPathString(origMdPath)} to {prettyPathString(newMdPath)}...", - ) - res = Result_updateSkeleton() - with contextlib.ExitStack() as stack: - origMdFile = stack.enter_context(open(origMdPath, "r", encoding="utf8")) - newMdFile = stack.enter_context(open(newMdPath, "r", encoding="utf8")) - origSkelFile = stack.enter_context(open(origSkelPath, "r", encoding="utf8")) - outputFile = stack.enter_context(open(outputPath, "w", encoding="utf8", newline="")) - mdDiff = difflib.ndiff(origMdFile.readlines(), newMdFile.readlines()) - origSkelLines = iter(origSkelFile.readlines()) - for mdDiffLine in mdDiff: - if mdDiffLine.startswith("?"): - continue - if mdDiffLine.startswith(" "): - res.numUnchangedLines += 1 - skelLine = next(origSkelLines) - if re_translationID.match(skelLine): - res.numUnchangedTranslationPlaceholders += 1 - outputFile.write(skelLine) - elif mdDiffLine.startswith("+"): - res.numAddedLines += 1 - skelLine = skeletonizeLine(mdDiffLine[2:]) - if skelLine: - res.numAddedTranslationPlaceholders += 1 - else: - skelLine = mdDiffLine[2:] - outputFile.write(skelLine) - elif mdDiffLine.startswith("-"): - res.numRemovedLines += 1 - origSkelLine = next(origSkelLines) - if re_translationID.match(origSkelLine): - res.numRemovedTranslationPlaceholders += 1 - else: - raise ValueError(f"Unexpected diff line: {mdDiffLine}") - print( - f"Updated skeleton file with {res.numAddedLines} added lines " - f"({res.numAddedTranslationPlaceholders} translation placeholders), " - f"{res.numRemovedLines} removed lines ({res.numRemovedTranslationPlaceholders} translation placeholders), " - f"and {res.numUnchangedLines} unchanged lines ({res.numUnchangedTranslationPlaceholders} translation placeholders)", - ) - return res - - -@dataclass -class Result_generateXliff: - numTranslatableStrings: int = 0 - - -def generateXliff( - mdPath: str, - outputPath: str, - skelPath: str | None = None, -) -> Result_generateXliff: - # If a skeleton file is not provided, first generate one - with contextlib.ExitStack() as stack: - if not skelPath: - skelPath = stack.enter_context( - createAndDeleteTempFilePath_contextManager( - dir=os.path.dirname(outputPath), - prefix=os.path.basename(mdPath), - suffix=".skel", - ), - ) - generateSkeleton(mdPath=mdPath, outputPath=skelPath) - with open(skelPath, "r", encoding="utf8") as skelFile: - skelContent = skelFile.read() - res = Result_generateXliff() - print( - f"Generating xliff file {prettyPathString(outputPath)} from {prettyPathString(mdPath)} and {prettyPathString(skelPath)}...", - ) - with contextlib.ExitStack() as stack: - mdFile = stack.enter_context(open(mdPath, "r", encoding="utf8")) - outputFile = stack.enter_context(open(outputPath, "w", encoding="utf8", newline="")) - fileID = os.path.basename(mdPath) - mdUri = getRawGithubURLForPath(mdPath) - print(f"Including Github raw URL: {mdUri}") - outputFile.write( - '\n' - f'\n' - f'\n', - ) - outputFile.write(f"\n{xmlEscape(skelContent)}\n\n") - res.numTranslatableStrings = 0 - for lineNo, (mdLine, skelLine) in enumerate( - zip_longest(mdFile.readlines(), skelContent.splitlines(keepends=True)), - start=1, - ): - mdLine = mdLine.rstrip() - skelLine = skelLine.rstrip() - if m := re_translationID.match(skelLine): - res.numTranslatableStrings += 1 - prefix, ID, suffix = m.groups() - if prefix and not mdLine.startswith(prefix): - raise ValueError(f'Line {lineNo}: does not start with "{prefix}", {mdLine=}, {skelLine=}') - if suffix and not mdLine.endswith(suffix): - raise ValueError(f'Line {lineNo}: does not end with "{suffix}", {mdLine=}, {skelLine=}') - source = mdLine[len(prefix) : len(mdLine) - len(suffix)] - outputFile.write( - f'\n\nline: {lineNo + 1}\n', - ) - if prefix: - outputFile.write(f'prefix: {xmlEscape(prefix)}\n') - if suffix: - outputFile.write(f'suffix: {xmlEscape(suffix)}\n') - outputFile.write( - "\n" - f"\n" - f"{xmlEscape(source)}\n" - "\n" - "\n", # fmt: skip - ) - else: - if mdLine != skelLine: - raise ValueError(f"Line {lineNo}: {mdLine=} does not match {skelLine=}") - outputFile.write("\n") - print(f"Generated xliff file with {res.numTranslatableStrings} translatable strings") - return res - - -@dataclass -class Result_translateXliff: - numTranslatedStrings: int = 0 - - -def updateXliff( - xliffPath: str, - mdPath: str, - outputPath: str, -): - # uses generateMarkdown, extractSkeleton, updateSkeleton, and generateXliff to generate an updated xliff file. - outputDir = os.path.dirname(outputPath) - print( - f"Generating updated xliff file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)} and {prettyPathString(mdPath)}...", - ) - with contextlib.ExitStack() as stack: - origMdPath = stack.enter_context( - createAndDeleteTempFilePath_contextManager(dir=outputDir, prefix="generated_", suffix=".md"), - ) - generateMarkdown(xliffPath=xliffPath, outputPath=origMdPath, translated=False) - origSkelPath = stack.enter_context( - createAndDeleteTempFilePath_contextManager(dir=outputDir, prefix="extracted_", suffix=".skel"), - ) - extractSkeleton(xliffPath=xliffPath, outputPath=origSkelPath) - updatedSkelPath = stack.enter_context( - createAndDeleteTempFilePath_contextManager(dir=outputDir, prefix="updated_", suffix=".skel"), - ) - updateSkeleton( - origMdPath=origMdPath, - newMdPath=mdPath, - origSkelPath=origSkelPath, - outputPath=updatedSkelPath, - ) - generateXliff( - mdPath=mdPath, - skelPath=updatedSkelPath, - outputPath=outputPath, - ) - print(f"Generated updated xliff file {prettyPathString(outputPath)}") - - -def translateXliff( - xliffPath: str, - lang: str, - pretranslatedMdPath: str, - outputPath: str, - allowBadAnchors: bool = False, -) -> Result_translateXliff: - print( - f"Creating {lang} translated xliff file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)} using {prettyPathString(pretranslatedMdPath)}...", - ) - res = Result_translateXliff() - with contextlib.ExitStack() as stack: - pretranslatedMdFile = stack.enter_context(open(pretranslatedMdPath, "r", encoding="utf8")) - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError("Not an xliff file") - xliffRoot.set("trgLang", lang) - skeletonNode = xliffRoot.find("./xliff:file/xliff:skeleton", namespaces=namespace) - if skeletonNode is None: - raise ValueError("No skeleton found in xliff file") - skeletonContent = skeletonNode.text.strip() - for lineNo, (skelLine, pretranslatedLine) in enumerate( - zip_longest(skeletonContent.splitlines(), pretranslatedMdFile.readlines()), - start=1, - ): - skelLine = skelLine.rstrip() - pretranslatedLine = pretranslatedLine.rstrip() - if m := re_translationID.match(skelLine): - prefix, ID, suffix = m.groups() - if prefix and not pretranslatedLine.startswith(prefix): - raise ValueError( - f'Line {lineNo} of translation does not start with "{prefix}", {pretranslatedLine=}, {skelLine=}', - ) - if suffix and not pretranslatedLine.endswith(suffix): - if allowBadAnchors and (m := re_heading.match(pretranslatedLine)): - print(f"Warning: ignoring bad anchor in line {lineNo}: {pretranslatedLine}") - suffix = m.group(3) - if suffix and not pretranslatedLine.endswith(suffix): - raise ValueError( - f'Line {lineNo} of translation: does not end with "{suffix}", {pretranslatedLine=}, {skelLine=}', - ) - translation = pretranslatedLine[len(prefix) : len(pretranslatedLine) - len(suffix)] - try: - unit = xliffRoot.find(f'./xliff:file/xliff:unit[@id="{ID}"]', namespaces=namespace) - if unit is not None: - segment = unit.find("./xliff:segment", namespaces=namespace) - if segment is not None: - target = lxml.etree.Element("target") - target.text = translation - target.tail = "\n" - segment.append(target) - res.numTranslatedStrings += 1 - else: - raise ValueError(f"No segment found for unit {ID}") - else: - raise ValueError(f"Cannot locate Unit {ID} in xliff file") - except Exception as e: - e.add_note(f"Line {lineNo}: {pretranslatedLine=}, {skelLine=}") - raise - elif skelLine != pretranslatedLine: - raise ValueError( - f"Line {lineNo}: pretranslated line {pretranslatedLine!r}, does not match skeleton line {skelLine!r}", - ) - xliff.write(outputPath, encoding="utf8", xml_declaration=True) - print(f"Translated xliff file with {res.numTranslatedStrings} translated strings") - return res - - -@dataclass -class Result_generateMarkdown: - numTotalLines = 0 - numTranslatableStrings = 0 - numTranslatedStrings = 0 - numBadTranslationStrings = 0 - - -def generateMarkdown(xliffPath: str, outputPath: str, translated: bool = True) -> Result_generateMarkdown: - print(f"Generating markdown file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)}...") - res = Result_generateMarkdown() - with contextlib.ExitStack() as stack: - outputFile = stack.enter_context(open(outputPath, "w", encoding="utf8", newline="")) - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError("Not an xliff file") - skeletonNode = xliffRoot.find("./xliff:file/xliff:skeleton", namespaces=namespace) - if skeletonNode is None: - raise ValueError("No skeleton found in xliff file") - skeletonContent = skeletonNode.text.strip() - for lineNum, line in enumerate(skeletonContent.splitlines(keepends=True), 1): - res.numTotalLines += 1 - if m := re_translationID.match(line): - prefix, ID, suffix = m.groups() - res.numTranslatableStrings += 1 - unit = xliffRoot.find(f'./xliff:file/xliff:unit[@id="{ID}"]', namespaces=namespace) - if unit is None: - raise ValueError(f"Cannot locate Unit {ID} in xliff file") - segment = unit.find("./xliff:segment", namespaces=namespace) - if segment is None: - raise ValueError(f"No segment found for unit {ID}") - source = segment.find("./xliff:source", namespaces=namespace) - if source is None: - raise ValueError(f"No source found for unit {ID}") - translation = "" - if translated: - target = segment.find("./xliff:target", namespaces=namespace) - if target is not None: - targetText = target.text - if targetText: - translation = targetText - # Crowdin treats empty targets () as a literal translation. - # Filter out such strings and count them as bad translations. - if translation in ( - "", - "<target/>", - "", - "<target></target>", - ): - res.numBadTranslationStrings += 1 - translation = "" - else: - res.numTranslatedStrings += 1 - # If we have no translation, use the source text - if not translation: - sourceText = source.text - if sourceText is None: - raise ValueError(f"No source text found for unit {ID}") - translation = sourceText - outputFile.write(f"{prefix}{translation}{suffix}\n") - else: - outputFile.write(line) - print( - f"Generated markdown file with {res.numTotalLines} total lines, {res.numTranslatableStrings} translatable strings, and {res.numTranslatedStrings} translated strings. Ignoring {res.numBadTranslationStrings} bad translated strings", - ) - return res - - -def ensureMarkdownFilesMatch(path1: str, path2: str, allowBadAnchors: bool = False): - print(f"Ensuring files {prettyPathString(path1)} and {prettyPathString(path2)} match...") - with contextlib.ExitStack() as stack: - file1 = stack.enter_context(open(path1, "r", encoding="utf8")) - file2 = stack.enter_context(open(path2, "r", encoding="utf8")) - for lineNo, (line1, line2) in enumerate(zip_longest(file1.readlines(), file2.readlines()), start=1): - line1 = line1.rstrip() - line2 = line2.rstrip() - if line1 != line2: - if ( - re_postTableHeaderLine.match(line1) - and re_postTableHeaderLine.match(line2) - and line1.count("|") == line2.count("|") - ): - print( - f"Warning: ignoring cell padding of post table header line at line {lineNo}: {line1}, {line2}", - ) - continue - if ( - re_hiddenHeaderRow.match(line1) - and re_hiddenHeaderRow.match(line2) - and line1.count("|") == line2.count("|") - ): - print( - f"Warning: ignoring cell padding of hidden header row at line {lineNo}: {line1}, {line2}", - ) - continue - if allowBadAnchors and (m1 := re_heading.match(line1)) and (m2 := re_heading.match(line2)): - print(f"Warning: ignoring bad anchor in headings at line {lineNo}: {line1}, {line2}") - line1 = m1.group(1) + m1.group(2) - line2 = m2.group(1) + m2.group(2) - if line1 != line2: - raise ValueError(f"Files do not match at line {lineNo}: {line1=} {line2=}") - print("Files match") - - -def markdownTranslateCommand(command: str, *args): - print(f"Running markdownTranslate command: {command} {' '.join(args)}") - subprocess.run(["python", __file__, command, *args], check=True) - - -def pretranslateAllPossibleLanguages(langsDir: str, mdBaseName: str): - # This function walks through all language directories in the given directory, skipping en (English) and translates the English xlif and skel file along with the lang's pretranslated md file - enXliffPath = os.path.join(langsDir, "en", f"{mdBaseName}.xliff") - if not os.path.exists(enXliffPath): - raise ValueError(f"English xliff file {enXliffPath} does not exist") - allLangs = set() - succeededLangs = set() - skippedLangs = set() - for langDir in os.listdir(langsDir): - if langDir == "en": - continue - langDirPath = os.path.join(langsDir, langDir) - if not os.path.isdir(langDirPath): - continue - langPretranslatedMdPath = os.path.join(langDirPath, f"{mdBaseName}.md") - if not os.path.exists(langPretranslatedMdPath): - continue - allLangs.add(langDir) - langXliffPath = os.path.join(langDirPath, f"{mdBaseName}.xliff") - if os.path.exists(langXliffPath): - print(f"Skipping {langDir} as the xliff file already exists") - skippedLangs.add(langDir) - continue - try: - translateXliff( - xliffPath=enXliffPath, - lang=langDir, - pretranslatedMdPath=langPretranslatedMdPath, - outputPath=langXliffPath, - allowBadAnchors=True, - ) - except Exception as e: - print(f"Failed to translate {langDir}: {e}") - continue - rebuiltLangMdPath = os.path.join(langDirPath, f"rebuilt_{mdBaseName}.md") - try: - generateMarkdown( - xliffPath=langXliffPath, - outputPath=rebuiltLangMdPath, - ) - except Exception as e: - print(f"Failed to rebuild {langDir} markdown: {e}") - os.remove(langXliffPath) - continue - try: - ensureMarkdownFilesMatch(rebuiltLangMdPath, langPretranslatedMdPath, allowBadAnchors=True) - except Exception as e: - print(f"Rebuilt {langDir} markdown does not match pretranslated markdown: {e}") - os.remove(langXliffPath) - continue - os.remove(rebuiltLangMdPath) - print(f"Successfully pretranslated {langDir}") - succeededLangs.add(langDir) - if len(skippedLangs) > 0: - print(f"Skipped {len(skippedLangs)} languages already pretranslated.") - print(f"Pretranslated {len(succeededLangs)} out of {len(allLangs) - len(skippedLangs)} languages.") - - -if __name__ == "__main__": - mainParser = argparse.ArgumentParser() - commandParser = mainParser.add_subparsers(title="commands", dest="command", required=True) - generateXliffParser = commandParser.add_parser("generateXliff") - generateXliffParser.add_argument( - "-m", - "--markdown", - dest="md", - type=str, - required=True, - help="The markdown file to generate the xliff file for", - ) - generateXliffParser.add_argument( - "-o", - "--output", - dest="output", - type=str, - required=True, - help="The file to output the xliff file to", - ) - updateXliffParser = commandParser.add_parser("updateXliff") - updateXliffParser.add_argument( - "-x", - "--xliff", - dest="xliff", - type=str, - required=True, - help="The original xliff file", - ) - updateXliffParser.add_argument( - "-m", - "--newMarkdown", - dest="md", - type=str, - required=True, - help="The new markdown file", - ) - updateXliffParser.add_argument( - "-o", - "--output", - dest="output", - type=str, - required=True, - help="The file to output the updated xliff to", - ) - translateXliffParser = commandParser.add_parser("translateXliff") - translateXliffParser.add_argument( - "-x", - "--xliff", - dest="xliff", - type=str, - required=True, - help="The xliff file to translate", - ) - translateXliffParser.add_argument( - "-l", - "--lang", - dest="lang", - type=str, - required=True, - help="The language to translate to", - ) - translateXliffParser.add_argument( - "-p", - "--pretranslatedMarkdown", - dest="pretranslatedMd", - type=str, - required=True, - help="The pretranslated markdown file to use", - ) - translateXliffParser.add_argument( - "-o", - "--output", - dest="output", - type=str, - required=True, - help="The file to output the translated xliff file to", - ) - generateMarkdownParser = commandParser.add_parser("generateMarkdown") - generateMarkdownParser.add_argument( - "-x", - "--xliff", - dest="xliff", - type=str, - required=True, - help="The xliff file to generate the markdown file for", - ) - generateMarkdownParser.add_argument( - "-o", - "--output", - dest="output", - type=str, - required=True, - help="The file to output the markdown file to", - ) - generateMarkdownParser.add_argument( - "-u", - "--untranslated", - dest="translated", - action="store_false", - help="Generate the markdown file with the untranslated strings", - ) - ensureMarkdownFilesMatchParser = commandParser.add_parser("ensureMarkdownFilesMatch") - ensureMarkdownFilesMatchParser.add_argument( - dest="path1", - type=str, - help="The first markdown file", - ) - ensureMarkdownFilesMatchParser.add_argument( - dest="path2", - type=str, - help="The second markdown file", - ) - pretranslateLangsParser = commandParser.add_parser("pretranslateLangs") - pretranslateLangsParser.add_argument( - "-d", - "--langs-dir", - dest="langsDir", - type=str, - required=True, - help="The directory containing the language directories", - ) - pretranslateLangsParser.add_argument( - "-b", - "--md-base-name", - dest="mdBaseName", - type=str, - required=True, - help="The base name of the markdown files to pretranslate", - ) - args = mainParser.parse_args() - match args.command: - case "generateXliff": - generateXliff(mdPath=args.md, outputPath=args.output) - case "updateXliff": - updateXliff( - xliffPath=args.xliff, - mdPath=args.md, - outputPath=args.output, - ) - case "generateMarkdown": - generateMarkdown(xliffPath=args.xliff, outputPath=args.output, translated=args.translated) - case "translateXliff": - translateXliff( - xliffPath=args.xliff, - lang=args.lang, - pretranslatedMdPath=args.pretranslatedMd, - outputPath=args.output, - ) - case "pretranslateLangs": - pretranslateAllPossibleLanguages(langsDir=args.langsDir, mdBaseName=args.mdBaseName) - case "ensureMarkdownFilesMatch": - ensureMarkdownFilesMatch(path1=args.path1, path2=args.path2) - case _: - raise ValueError(f"Unknown command: {args.command}") diff --git a/_l10n/md2html.py b/_l10n/md2html.py deleted file mode 100644 index 01acab0..0000000 --- a/_l10n/md2html.py +++ /dev/null @@ -1,197 +0,0 @@ -# A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2023-2024 NV Access Limited -# This file may be used under the terms of the GNU General Public License, version 2 or later. -# For more details see: https://www.gnu.org/licenses/gpl-2.0.html - -import argparse -from copy import deepcopy -import io -import re -import shutil - -DEFAULT_EXTENSIONS = frozenset( - { - # Supports tables, HTML mixed with markdown, code blocks, custom attributes and more - "markdown.extensions.extra", - # Allows TOC with [TOC]" - "markdown.extensions.toc", - # Makes list behaviour better, including 2 space indents by default - "mdx_truly_sane_lists", - # External links will open in a new tab, and title will be set to the link text - "markdown_link_attr_modifier", - # Adds links to GitHub authors, issues and PRs - "mdx_gh_links", - }, -) - -EXTENSIONS_CONFIG = { - "markdown_link_attr_modifier": { - "new_tab": "external_only", - "auto_title": "on", - }, - "mdx_gh_links": { - "user": "nvaccess", - "repo": "nvda", - }, -} - -RTL_LANG_CODES = frozenset({"ar", "fa", "he"}) - -HTML_HEADERS = """ - - - - -{title} - - - -{extraStylesheet} - - -""".strip() - - -def _getTitle(mdBuffer: io.StringIO, isKeyCommands: bool = False) -> str: - if isKeyCommands: - TITLE_RE = re.compile(r"^$") - # Make next read at start of buffer - mdBuffer.seek(0) - for line in mdBuffer.readlines(): - match = TITLE_RE.match(line.strip()) - if match: - return match.group(1) - - raise ValueError("No KC:title command found in userGuide.md") - - else: - # Make next read at start of buffer - mdBuffer.seek(0) - # Remove heading hashes and trailing whitespace to get the tab title - title = mdBuffer.readline().strip().lstrip("# ") - - return title - - -def _createAttributeFilter() -> dict[str, set[str]]: - # Create attribute filter exceptions for HTML sanitization - import nh3 - - allowedAttributes: dict[str, set[str]] = deepcopy(nh3.ALLOWED_ATTRIBUTES) - - attributesWithAnchors = {"h1", "h2", "h3", "h4", "h5", "h6", "td"} - attributesWithClass = {"div", "span", "a", "th", "td"} - - # Allow IDs for anchors - for attr in attributesWithAnchors: - if attr not in allowedAttributes: - allowedAttributes[attr] = set() - allowedAttributes[attr].add("id") - - # Allow class for styling - for attr in attributesWithClass: - if attr not in allowedAttributes: - allowedAttributes[attr] = set() - allowedAttributes[attr].add("class") - - # link rel and target is set by markdown_link_attr_modifier - allowedAttributes["a"].update({"rel", "target"}) - - return allowedAttributes - - -ALLOWED_ATTRIBUTES = _createAttributeFilter() - - -def _generateSanitizedHTML(md: str, isKeyCommands: bool = False) -> str: - import markdown - import nh3 - - extensions = set(DEFAULT_EXTENSIONS) - if isKeyCommands: - from keyCommandsDoc import KeyCommandsExtension - - extensions.add(KeyCommandsExtension()) - - htmlOutput = markdown.markdown( - text=md, - extensions=extensions, - extension_configs=EXTENSIONS_CONFIG, - ) - - # Sanitize html output from markdown to prevent XSS from translators - htmlOutput = nh3.clean( - htmlOutput, - attributes=ALLOWED_ATTRIBUTES, - # link rel is handled by markdown_link_attr_modifier - link_rel=None, - # Keep key command comments and similar - strip_comments=False, - ) - - return htmlOutput - - -def main(source: str, dest: str, lang: str = "en", docType: str | None = None): - print(f"Converting {docType or 'document'} ({lang=}) at {source} to {dest}") - isUserGuide = docType == "userGuide" - isDevGuide = docType == "developerGuide" - isChanges = docType == "changes" - isKeyCommands = docType == "keyCommands" - if docType and not any([isUserGuide, isDevGuide, isChanges, isKeyCommands]): - raise ValueError(f"Unknown docType {docType}") - with open(source, "r", encoding="utf-8") as mdFile: - mdStr = mdFile.read() - - with io.StringIO() as mdBuffer: - mdBuffer.write(mdStr) - title = _getTitle(mdBuffer, isKeyCommands) - - if isUserGuide or isDevGuide: - extraStylesheet = '' - elif isChanges or isKeyCommands: - extraStylesheet = "" - else: - raise ValueError(f"Unknown target type for {dest}") - - htmlBuffer = io.StringIO() - htmlBuffer.write( - HTML_HEADERS.format( - lang=lang, - dir="rtl" if lang in RTL_LANG_CODES else "ltr", - title=title, - extraStylesheet=extraStylesheet, - ), - ) - - htmlOutput = _generateSanitizedHTML(mdStr, isKeyCommands) - # Make next write append at end of buffer - htmlBuffer.seek(0, io.SEEK_END) - htmlBuffer.write(htmlOutput) - - # Make next write append at end of buffer - htmlBuffer.seek(0, io.SEEK_END) - htmlBuffer.write("\n\n\n") - - with open(dest, "w", encoding="utf-8") as targetFile: - # Make next read at start of buffer - htmlBuffer.seek(0) - shutil.copyfileobj(htmlBuffer, targetFile) - - htmlBuffer.close() - - -if __name__ == "__main__": - args = argparse.ArgumentParser() - args.add_argument("-l", "--lang", help="Language code", action="store", default="en") - args.add_argument( - "-t", - "--docType", - help="Type of document", - action="store", - choices=["userGuide", "developerGuide", "changes", "keyCommands"], - ) - args.add_argument("source", help="Path to the markdown file") - args.add_argument("dest", help="Path to the resulting html file") - args = args.parse_args() - main(source=args.source, dest=args.dest, lang=args.lang, docType=args.docType) From 0505a3b4ccb7d72fca8f1341839d39b99c35a8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 05:38:01 +0100 Subject: [PATCH 45/90] Don't run pre-commit since it requires a different token to access hooks --- .github/workflows/exportAddonToCrowdin.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 7ca8edb..c16481a 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -29,10 +29,6 @@ jobs: sudo apt install gettext - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 - - name: Run pre-commit - run: | - # Ensure uv environment is up to date. - uv run pre-commit run uv-lock --all-files - name: Build add-on and pot file run: | uv run scons From fd2554b8a0af52fda7af32e5538dcab220a60c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 20:24:10 +0100 Subject: [PATCH 46/90] Merge translations into branch --- .github/workflows/exportAddonToCrowdin.yml | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index c16481a..04067a0 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -9,6 +9,7 @@ concurrency: env: crowdinProjectID: ${{ vars.CROWDIN_ID }} crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} + downloadTranslationsBranch: l10n jobs: build: runs-on: ubuntu-latest @@ -67,3 +68,36 @@ jobs: git commit -m "Update Crowdin file ids and hashes" git push fi + - name: Download translations from Crowdin + run: | + uv run _l10n/source/l10nUtil.py exportTranslations -o _addonL10n + mkdir -p addon/locale + mkdir -p addon/doc + for dir in _addonL10n/${{ steps.getAddonInfo.outputs.addonId }}/*; do + echo "Processing: $dir" + if [ -d "$dir" ]; then + langCode=$(basename "$dir") + poFile="$dir/${{ steps.getAddonInfo.outputs.addonId }}.po" + if [ -f "$poFile" ]; then + mkdir -p "addon/locale/$langCode/LC_MESSAGES" + echo "Moving $poFile to addon/locale/$langCode/LC_MESSAGES/nvda.po" + mv "$poFile" "addon/locale/$langCode/LC_MESSAGES/nvda.po" + fi + mdFile="$dir/${{ steps.getAddonInfo.outputs.addonId }}.md" + if [ -f "$mdFile" ]; then + mkdir -p "addon/doc/$langCode" + echo "Moving $mdFile to addon/doc/$langCode/readme.md" + mv "$mdFile" "addon/doc/$langCode/readme.md" + fi + else + echo "Skipping invalid directory: $dir" + fi + done + git add addon/locale addon/doc + if git diff --staged --quiet; then + echo "Nothing added to commit." + else + git commit -m "Update translations for ${{ steps.getAddonInfo.outputs.addonId }}" + git checkout -b ${{ env.downloadTranslationsBranch }} + git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} + fi From b30f46fb414f5848b95cb755bf1e6c13069284eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 20:38:52 +0100 Subject: [PATCH 47/90] Add project id without using vars --- .github/workflows/exportAddonToCrowdin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 04067a0..7ba4dca 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -7,7 +7,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: - crowdinProjectID: ${{ vars.CROWDIN_ID }} + crowdinProjectID: 780748 crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} downloadTranslationsBranch: l10n jobs: From 70293c84189a5e9f51e459defea26964cee06b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 20:42:24 +0100 Subject: [PATCH 48/90] Schedule workflow --- .github/workflows/exportAddonToCrowdin.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/exportAddonToCrowdin.yml index 7ba4dca..eea43a7 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/exportAddonToCrowdin.yml @@ -3,6 +3,9 @@ name: Export add-on to Crowdin on: workflow_dispatch: + schedule: + # Every Monday at 00:00 UTC + - cron: '0 0 * * 1' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From da09c8c445285ea419dbab16a70e755f6087748c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 20:43:26 +0100 Subject: [PATCH 49/90] Rename workflow --- .github/workflows/{exportAddonToCrowdin.yml => crowdinL10n.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{exportAddonToCrowdin.yml => crowdinL10n.yml} (99%) diff --git a/.github/workflows/exportAddonToCrowdin.yml b/.github/workflows/crowdinL10n.yml similarity index 99% rename from .github/workflows/exportAddonToCrowdin.yml rename to .github/workflows/crowdinL10n.yml index eea43a7..46f6271 100644 --- a/.github/workflows/exportAddonToCrowdin.yml +++ b/.github/workflows/crowdinL10n.yml @@ -1,4 +1,4 @@ -name: Export add-on to Crowdin +name: Crowdin l10n on: From 5c52f33da968846e9f5fcf16faa37f33455bd316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 20:56:48 +0100 Subject: [PATCH 50/90] Create PR --- .github/workflows/crowdinL10n.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index 46f6271..d3f507c 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -103,4 +103,7 @@ jobs: git commit -m "Update translations for ${{ steps.getAddonInfo.outputs.addonId }}" git checkout -b ${{ env.downloadTranslationsBranch }} git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} + gh pr create --base ${{ env.GITHUB_DEFAULT_BRANCH }} --head ${{ env.downloadTranslationsBranch }} + --title "Update tracked translations from Crowdin" \ + --body "This pull request updates translations to languages being tracked from Crowdin." fi From d0d5e0393aaa0fb8d5394113169c8acd59cc6228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 22:04:39 +0100 Subject: [PATCH 51/90] =?UTF-8?q?Don't=20create=20a=20PR=20since=20this=20?= =?UTF-8?q?n=C2=A1may=20need=20a=20personal=20access=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/crowdinL10n.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index d3f507c..e7a1460 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -13,11 +13,13 @@ env: crowdinProjectID: 780748 crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} downloadTranslationsBranch: l10n + GH_TOKEN: ${{ github.token }} jobs: build: runs-on: ubuntu-latest permissions: contents: write + pull-requests: write steps: - name: Checkout add-on uses: actions/checkout@v6 @@ -103,7 +105,5 @@ jobs: git commit -m "Update translations for ${{ steps.getAddonInfo.outputs.addonId }}" git checkout -b ${{ env.downloadTranslationsBranch }} git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} - gh pr create --base ${{ env.GITHUB_DEFAULT_BRANCH }} --head ${{ env.downloadTranslationsBranch }} - --title "Update tracked translations from Crowdin" \ - --body "This pull request updates translations to languages being tracked from Crowdin." fi + From b40f94ac87d8fe1198b2ad8a8909f312c20ee613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 22 Dec 2025 22:05:30 +0100 Subject: [PATCH 52/90] Update removing permissions for PR --- .github/workflows/crowdinL10n.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index e7a1460..cbd0df9 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -13,13 +13,11 @@ env: crowdinProjectID: 780748 crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} downloadTranslationsBranch: l10n - GH_TOKEN: ${{ github.token }} jobs: build: runs-on: ubuntu-latest permissions: contents: write - pull-requests: write steps: - name: Checkout add-on uses: actions/checkout@v6 From cb7807e096d093b47c220d5559d151315e7449f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 28 Dec 2025 06:52:33 +0100 Subject: [PATCH 53/90] Update Python version compatible with ubuntu-latest --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index 24ee5b1..2c45fe3 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13 +3.13.11 From 1449a01c80a4395536f2c745aea860b03330beeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 28 Dec 2025 07:01:38 +0100 Subject: [PATCH 54/90] Add dry-run --- .github/workflows/crowdinL10n.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index cbd0df9..1b966b0 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -3,6 +3,12 @@ name: Crowdin l10n on: workflow_dispatch: + inputs: + dry-run: + description: 'Dry run mode (skip Crowdin upload/download)' + required: false + type: boolean + default: false schedule: # Every Monday at 00:00 UTC - cron: '0 0 * * 1' @@ -41,21 +47,21 @@ jobs: id: getAddonInfo run: uv run ./.github/workflows/setOutputs.py - name: Upload md from scratch - if: ${{ steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' }} + if: ${{ steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' && inputs.dry-run != true }} run: | mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md uv run ./_l10n/source/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md - name: update md - if: ${{ steps.getAddonInfo.outputs.shouldUpdateMd == 'true' }} + if: ${{ steps.getAddonInfo.outputs.shouldUpdateMd == 'true' && inputs.dry-run != true }} run: | mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md uv run ./_l10n/source/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.md - name: Upload pot from scratch - if: ${{ steps.getAddonInfo.outputs.shouldAddPotFromScratch == 'true' }} + if: ${{ steps.getAddonInfo.outputs.shouldAddPotFromScratch == 'true' && inputs.dry-run != true }} run: | uv run ./_l10n/source/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - name: Update pot - if: ${{ steps.getAddonInfo.outputs.shouldUpdatePot == 'true' }} + if: ${{ steps.getAddonInfo.outputs.shouldUpdatePot == 'true' && inputs.dry-run != true }} run: | uv run ./_l10n/source/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot - name: Commit and push json @@ -72,6 +78,7 @@ jobs: git push fi - name: Download translations from Crowdin + if: ${{ inputs.dry-run != true }} run: | uv run _l10n/source/l10nUtil.py exportTranslations -o _addonL10n mkdir -p addon/locale @@ -104,4 +111,3 @@ jobs: git checkout -b ${{ env.downloadTranslationsBranch }} git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} fi - From 697d04857511423e90932d1a854f2b735da0fe13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sun, 28 Dec 2025 09:37:10 +0100 Subject: [PATCH 55/90] Optimize workflow to test with act and docker locally --- .github/workflows/crowdinL10n.yml | 13 ++++++---- .gitignore | 3 +++ sha256.py | 41 +++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 sha256.py diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index 1b966b0..5f75489 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -35,17 +35,19 @@ jobs: python-version-file: ".python-version" - name: Install gettext run: | - sudo apt update - sudo apt install gettext + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq gettext - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 + - name: Install dependencies + run: uv pip install --system scons markdown - name: Build add-on and pot file run: | - uv run scons - uv run scons pot + uv run --with scons --with markdown scons + uv run --with scons --with markdown scons pot - name: Get add-on info id: getAddonInfo - run: uv run ./.github/workflows/setOutputs.py + run: uv run --with scons --with markdown ./.github/workflows/setOutputs.py - name: Upload md from scratch if: ${{ steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' && inputs.dry-run != true }} run: | @@ -65,6 +67,7 @@ jobs: run: | uv run ./_l10n/source/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot - name: Commit and push json + if: ${{ inputs.dry-run != true }} id: commit run: | git config --local user.name github-actions diff --git a/.gitignore b/.gitignore index 1750f2c..e915e3a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ addon/locale/*/*.ini *.pyc *.nvda-addon .sconsign.dblite + +# act configuration +.actrc diff --git a/sha256.py b/sha256.py new file mode 100644 index 0000000..51c903b --- /dev/null +++ b/sha256.py @@ -0,0 +1,41 @@ +# Copyright (C) 2020-2025 NV Access Limited, Noelia Ruiz Martínez +# This file may be used under the terms of the GNU General Public License, version 2 or later. +# For more details see: https://www.gnu.org/licenses/gpl-2.0.html + +import argparse +import hashlib +import typing + +#: The read size for each chunk read from the file, prevents memory overuse with large files. +BLOCK_SIZE = 65536 + + +def sha256_checksum(binaryReadModeFiles: list[typing.BinaryIO], blockSize: int = BLOCK_SIZE): + """ + :param binaryReadModeFiles: A list of files (mode=='rb'). Calculate its sha256 hash. + :param blockSize: The size of each read. + :return: The Sha256 hex digest. + """ + sha256 = hashlib.sha256() + for f in binaryReadModeFiles: + with open(f, "rb") as file: + assert file.readable() and file.mode == "rb" + for block in iter(lambda: file.read(blockSize), b""): + sha256.update(block) + return sha256.hexdigest() + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + type=argparse.FileType("rb"), + dest="file", + help="The NVDA addon (*.nvda-addon) to use when computing the sha256.", + ) + args = parser.parse_args() + checksum = sha256_checksum(args.file) + print(f"Sha256:\t {checksum}") + + +if __name__ == "__main__": + main() From 0273641b5f4a8fa03aef28fabfd656a176d2bc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 29 Dec 2025 09:23:57 +0100 Subject: [PATCH 56/90] Update pyproject (#5) This pull request introduces a new pyproject.toml configuration for the addonTemplate project, establishing modern Python project metadata, build settings, and development dependencies. It also updates and refines linting and static analysis configurations to improve code quality and development workflow. --- pyproject.toml | 54 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 97189ac..d43b0b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,46 @@ +[build-system] +requires = ["setuptools~=80.9", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "addonTemplate" +dynamic = ["version"] +description = "NVDA add-on template" +maintainers = [ + {name = "NV Access", email = "info@nvaccess.org"}, +] +requires-python = ">=3.13,<3.14" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Topic :: Accessibility", +] +readme = "readme.md" +license = {file = "COPYING.TXT"} +dependencies = [ + # Build add-on + "scons==4.10.1", + "Markdown==3.10", + # Translations management + "requests==2.32.5", + "nh3==0.3.2", + "crowdin-api-client==1.24.1", + "lxml==6.0.2", + "mdx_truly_sane_lists==1.3", + "markdown-link-attr-modifier==0.2.1", + "mdx-gh-links==0.4", + # Lint + "uv==0.9.11", + "ruf==0.14.5", + "pre-commit==4.2.0", + "pyright[nodejs]==1.1.407", +] +[project.urls] +Repository = "https://github.com/nvaccess/addonTemplate" + [tool.ruff] line-length = 110 @@ -20,10 +63,13 @@ include = [ exclude = [ ".git", "__pycache__", + ".venv", + "buildVars.py", ] [tool.ruff.format] indent-style = "tab" +line-ending = "lf" [tool.ruff.lint.mccabe] max-complexity = 15 @@ -33,13 +79,16 @@ ignore = [ # indentation contains tabs "W191", ] +logger-objects = ["logHandler.log"] [tool.ruff.lint.per-file-ignores] -# sconstruct contains many inbuilt functions not recognised by the lint, +# sconscripts contains many inbuilt functions not recognised by the lint, # so ignore F821. "sconstruct" = ["F821"] [tool.pyright] +venvPath = ".venv" +venv = "." pythonPlatform = "Windows" typeCheckingMode = "strict" @@ -51,6 +100,7 @@ exclude = [ "sconstruct", ".git", "__pycache__", + ".venv", # When excluding concrete paths relative to a directory, # not matching multiple folders by name e.g. `__pycache__`, # paths are relative to the configuration file. @@ -59,6 +109,7 @@ exclude = [ # Tell pyright where to load python code from extraPaths = [ "./addon", + "../nvda/source", ] # General config @@ -120,7 +171,6 @@ reportPropertyTypeMismatch = true reportRedeclaration = true reportReturnType = true reportSelfClsParameterName = true -reportShadowedImports = true reportTypeCommentUsage = true reportTypedDictNotRequiredAccess = true reportUnboundVariable = true From 3ec67ac0443254d64c91e576be4f2dbcd36d2dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 30 Dec 2025 02:31:17 +0100 Subject: [PATCH 57/90] Fix branch args (#9) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c8f5c6..207177d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: hooks: # Prevents commits to certain branches - id: no-commit-to-branch - args: ["--branch", "main", "master", ] + args: ["--branch", "main", "--branch", "master", ] # Checks that large files have not been added. Default cut-off for "large" files is 500kb. - id: check-added-large-files # Checks python syntax From c890dd091a1adc2462b1c94912f0755dd9ef49cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 30 Dec 2025 02:32:15 +0100 Subject: [PATCH 58/90] fixRuffInPyProject (#8) Fix ruff in pyproject Fix pyproject excluding siteScons from pyright --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d43b0b5..aa8752d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,10 +31,10 @@ dependencies = [ "lxml==6.0.2", "mdx_truly_sane_lists==1.3", "markdown-link-attr-modifier==0.2.1", - "mdx-gh-links==0.4", + "mdx-gh-links==0.4", # Lint "uv==0.9.11", - "ruf==0.14.5", + "ruff==0.14.5", "pre-commit==4.2.0", "pyright[nodejs]==1.1.407", ] @@ -101,6 +101,7 @@ exclude = [ ".git", "__pycache__", ".venv", + "site_scons", # When excluding concrete paths relative to a directory, # not matching multiple folders by name e.g. `__pycache__`, # paths are relative to the configuration file. From ce869354f990c90109ee5c039903fbb619a4b4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 30 Dec 2025 04:57:30 +0100 Subject: [PATCH 59/90] Fixes using pre-commit (#10) --- .vscode/typings/__builtins__.pyi | 8 +- readme.md | 5 +- site_scons/site_tools/NVDATool/__init__.py | 25 +- site_scons/site_tools/NVDATool/addon.py | 1 - site_scons/site_tools/NVDATool/docs.py | 20 +- site_scons/site_tools/NVDATool/manifests.py | 34 +- site_scons/site_tools/NVDATool/typings.py | 13 +- site_scons/site_tools/NVDATool/utils.py | 1 - site_scons/site_tools/gettexttool/__init__.py | 6 +- uv.lock | 464 ++++++++++++++++++ 10 files changed, 519 insertions(+), 58 deletions(-) create mode 100644 uv.lock diff --git a/.vscode/typings/__builtins__.pyi b/.vscode/typings/__builtins__.pyi index 88febec..48cbde4 100644 --- a/.vscode/typings/__builtins__.pyi +++ b/.vscode/typings/__builtins__.pyi @@ -1,6 +1,2 @@ -def _(msg: str) -> str: - ... - - -def pgettext(context: str, message: str) -> str: - ... +def _(msg: str) -> str: ... +def pgettext(context: str, message: str) -> str: ... diff --git a/readme.md b/readme.md index 05e5f12..fb79067 100644 --- a/readme.md +++ b/readme.md @@ -36,7 +36,8 @@ In addition, this template includes configuration files for the following tools * Ruff (pyproject.toml/tool.ruff sections): a Python linter written in Rust. Sections starting with tool.ruff house configuration options for Ruff. * Configuration for VS Code. It requires NVDA's repo at the same level as the add-on folder containing your actual source files, with prepared source code (`scons source`). preparing the source code is a step in the instructions for building NVDA itself, see [The NVDA Repository](https://github.com/nvaccess/nvda) for details. * Place the .vscode in this repo within the addon folder, where your add-on source files (will) reside. The settings file within this folder assumes the NVDA repository is within the parent folder of this folder. If your addon folder is within the addonTemplate folder, then your NVDA repository folder needs to also be within the addonTemplate folder, or the source will not be found. - * Open the addon folder in VS Code. This should initialize VS Code with the correct settings and provide you with code completion and other VS Code features. + * Open the addon folder in VS Code. + This should initialize VS Code with the correct settings and provide you with code completion and other VS Code features. * Press `control+shift+m` after saving a file to search for problems. * Use arrow and tab keys for the autocompletion feature. * Press `control+shift+p` to open the commands palette and search for recommended extensions to install or check if they are installed. @@ -108,7 +109,7 @@ In addition, the following information must be filled out (not used in the manif ##### Custom add-on information -In addition to the core manifest data, custom add-on information can be specified. +In addition to the core manifest data, custom add-on information can be specified. ###### Braille translation tables diff --git a/site_scons/site_tools/NVDATool/__init__.py b/site_scons/site_tools/NVDATool/__init__.py index 6b4a37c..ff31eec 100644 --- a/site_scons/site_tools/NVDATool/__init__.py +++ b/site_scons/site_tools/NVDATool/__init__.py @@ -29,20 +29,22 @@ from .docs import md2html - def generate(env: Environment): env.SetDefault(excludePatterns=tuple()) addonAction = env.Action( lambda target, source, env: createAddonBundleFromPath( - source[0].abspath, target[0].abspath, env["excludePatterns"] - ) and None, + source[0].abspath, + target[0].abspath, + env["excludePatterns"], + ) + and None, lambda target, source, env: f"Generating Addon {target[0]}", ) env["BUILDERS"]["NVDAAddon"] = Builder( action=addonAction, suffix=".nvda-addon", - src_suffix="/" + src_suffix="/", ) env.SetDefault(brailleTables={}) @@ -55,13 +57,14 @@ def generate(env: Environment): addon_info=env["addon_info"], brailleTables=env["brailleTables"], symbolDictionaries=env["symbolDictionaries"], - ) and None, + ) + and None, lambda target, source, env: f"Generating manifest {target[0]}", ) env["BUILDERS"]["NVDAManifest"] = Builder( action=manifestAction, suffix=".ini", - src_siffix=".ini.tpl" + src_siffix=".ini.tpl", ) translatedManifestAction = env.Action( @@ -72,17 +75,18 @@ def generate(env: Environment): addon_info=env["addon_info"], brailleTables=env["brailleTables"], symbolDictionaries=env["symbolDictionaries"], - ) and None, + ) + and None, lambda target, source, env: f"Generating translated manifest {target[0]}", ) env["BUILDERS"]["NVDATranslatedManifest"] = Builder( action=translatedManifestAction, suffix=".ini", - src_siffix=".ini.tpl" + src_siffix=".ini.tpl", ) - env.SetDefault(mdExtensions = {}) + env.SetDefault(mdExtensions={}) mdAction = env.Action( lambda target, source, env: md2html( @@ -91,7 +95,8 @@ def generate(env: Environment): moFile=env["moFile"].path if env["moFile"] else None, mdExtensions=env["mdExtensions"], addon_info=env["addon_info"], - ) and None, + ) + and None, lambda target, source, env: f"Generating {target[0]}", ) env["BUILDERS"]["md2html"] = env.Builder( diff --git a/site_scons/site_tools/NVDATool/addon.py b/site_scons/site_tools/NVDATool/addon.py index 42e8d0e..7d67516 100644 --- a/site_scons/site_tools/NVDATool/addon.py +++ b/site_scons/site_tools/NVDATool/addon.py @@ -3,7 +3,6 @@ from pathlib import Path - def matchesNoPatterns(path: Path, patterns: Iterable[str]) -> bool: """Checks if the path, the first argument, does not match any of the patterns passed as the second argument.""" return not any((path.match(pattern) for pattern in patterns)) diff --git a/site_scons/site_tools/NVDATool/docs.py b/site_scons/site_tools/NVDATool/docs.py index abccd41..e1f80ad 100644 --- a/site_scons/site_tools/NVDATool/docs.py +++ b/site_scons/site_tools/NVDATool/docs.py @@ -1,4 +1,3 @@ - import gettext from pathlib import Path @@ -7,15 +6,14 @@ from .typings import AddonInfo - def md2html( - source: str | Path, - dest: str | Path, - *, - moFile: str | Path|None, - mdExtensions: list[str], - addon_info: AddonInfo - ): + source: str | Path, + dest: str | Path, + *, + moFile: str | Path | None, + mdExtensions: list[str], + addon_info: AddonInfo, +): if isinstance(source, str): source = Path(source) if isinstance(dest, str): @@ -55,7 +53,7 @@ def md2html( "\n", htmlText, "\n", - ) + ), ) with dest.open("w", encoding="utf-8") as f: - f.write(docText) # type: ignore + f.write(docText) # type: ignore diff --git a/site_scons/site_tools/NVDATool/manifests.py b/site_scons/site_tools/NVDATool/manifests.py index 1e38348..a55785e 100644 --- a/site_scons/site_tools/NVDATool/manifests.py +++ b/site_scons/site_tools/NVDATool/manifests.py @@ -1,4 +1,3 @@ - import codecs import gettext from functools import partial @@ -7,14 +6,13 @@ from .utils import format_nested_section - def generateManifest( - source: str, - dest: str, - addon_info: AddonInfo, - brailleTables: BrailleTables, - symbolDictionaries: SymbolDictionaries, - ): + source: str, + dest: str, + addon_info: AddonInfo, + brailleTables: BrailleTables, + symbolDictionaries: SymbolDictionaries, +): # Prepare the root manifest section with codecs.open(source, "r", "utf-8") as f: manifest_template = f.read() @@ -33,14 +31,14 @@ def generateManifest( def generateTranslatedManifest( - source: str, - dest: str, - *, - mo: str, - addon_info: AddonInfo, - brailleTables: BrailleTables, - symbolDictionaries: SymbolDictionaries, - ): + source: str, + dest: str, + *, + mo: str, + addon_info: AddonInfo, + brailleTables: BrailleTables, + symbolDictionaries: SymbolDictionaries, +): with open(mo, "rb") as f: _ = gettext.GNUTranslations(f).gettext vars: dict[str, str] = {} @@ -52,8 +50,8 @@ def generateTranslatedManifest( _format_section_only_with_displayName = partial( format_nested_section, - include_only_keys = ("displayName",), - _ = _, + include_only_keys=("displayName",), + _=_, ) # Add additional manifest sections such as custom braile tables diff --git a/site_scons/site_tools/NVDATool/typings.py b/site_scons/site_tools/NVDATool/typings.py index 6b1b3f5..650a759 100644 --- a/site_scons/site_tools/NVDATool/typings.py +++ b/site_scons/site_tools/NVDATool/typings.py @@ -1,7 +1,6 @@ from typing import TypedDict, Protocol - class AddonInfo(TypedDict): addon_name: str addon_summary: str @@ -20,15 +19,15 @@ class AddonInfo(TypedDict): class BrailleTableAttributes(TypedDict): - displayName: str - contracted: bool - output: bool - input: bool + displayName: str + contracted: bool + output: bool + input: bool class SymbolDictionaryAttributes(TypedDict): - displayName: str - mandatory: bool + displayName: str + mandatory: bool BrailleTables = dict[str, BrailleTableAttributes] diff --git a/site_scons/site_tools/NVDATool/utils.py b/site_scons/site_tools/NVDATool/utils.py index 0cc833c..c900841 100644 --- a/site_scons/site_tools/NVDATool/utils.py +++ b/site_scons/site_tools/NVDATool/utils.py @@ -3,7 +3,6 @@ from .typings import Strable - def _(arg: str) -> str: """ A function that passes the string to it without doing anything to it. diff --git a/site_scons/site_tools/gettexttool/__init__.py b/site_scons/site_tools/gettexttool/__init__.py index 900f8dc..ff4697e 100644 --- a/site_scons/site_tools/gettexttool/__init__.py +++ b/site_scons/site_tools/gettexttool/__init__.py @@ -44,12 +44,14 @@ def generate(env): ) env["BUILDERS"]["gettextPotFile"] = env.Builder( - action=Action("xgettext " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET"), suffix=".pot" + action=Action("xgettext " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET"), + suffix=".pot", ) env["BUILDERS"]["gettextMergePotFile"] = env.Builder( action=Action( - "xgettext " + "--omit-header --no-location " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET" + "xgettext " + "--omit-header --no-location " + XGETTEXT_COMMON_ARGS, + "Generating pot file $TARGET", ), suffix=".pot", ) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..e0def29 --- /dev/null +++ b/uv.lock @@ -0,0 +1,464 @@ +version = 1 +revision = 3 +requires-python = "==3.13.*" + +[[package]] +name = "addontemplate" +source = { editable = "." } +dependencies = [ + { name = "crowdin-api-client" }, + { name = "lxml" }, + { name = "markdown" }, + { name = "markdown-link-attr-modifier" }, + { name = "mdx-gh-links" }, + { name = "mdx-truly-sane-lists" }, + { name = "nh3" }, + { name = "pre-commit" }, + { name = "pyright", extra = ["nodejs"] }, + { name = "requests" }, + { name = "ruff" }, + { name = "scons" }, + { name = "uv" }, +] + +[package.metadata] +requires-dist = [ + { name = "crowdin-api-client", specifier = "==1.24.1" }, + { name = "lxml", specifier = "==6.0.2" }, + { name = "markdown", specifier = "==3.10" }, + { name = "markdown-link-attr-modifier", specifier = "==0.2.1" }, + { name = "mdx-gh-links", specifier = "==0.4" }, + { name = "mdx-truly-sane-lists", specifier = "==1.3" }, + { name = "nh3", specifier = "==0.3.2" }, + { name = "pre-commit", specifier = "==4.2.0" }, + { name = "pyright", extras = ["nodejs"], specifier = "==1.1.407" }, + { name = "requests", specifier = "==2.32.5" }, + { name = "ruff", specifier = "==0.14.5" }, + { name = "scons", specifier = "==4.10.1" }, + { name = "uv", specifier = "==0.9.11" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "crowdin-api-client" +version = "1.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/fc/ec5564928057aac9cae7e78ed324898b3134369b100bbb2b5c97ad1ad548/crowdin_api_client-1.24.1.tar.gz", hash = "sha256:d2a385c2b3f8e985d5bb084524ae14aef9045094fba0b2df1df82d9da97155b1", size = 70629, upload-time = "2025-08-26T13:20:34.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/74/118d8f5e592a1fe75b793346a599d57746b18b8875c31e956022b63ba173/crowdin_api_client-1.24.1-py3-none-any.whl", hash = "sha256:a07365a2a0d42830ee4eb188e3820603e1420421575637b1ddd8dffe1d2fe14c", size = 109654, upload-time = "2025-08-26T13:20:33.673Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-link-attr-modifier" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/30/d35aad054a27f119bff2408523d82c3f9a6d9936712c872f5b9fe817de5b/markdown_link_attr_modifier-0.2.1.tar.gz", hash = "sha256:18df49a9fe7b5c87dad50b75c2a2299ae40c65674f7b1263fb12455f5df7ac99", size = 18408, upload-time = "2023-04-13T16:00:12.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/82/9262a67313847fdcc6252a007f032924fe0c0b6d6b9ef0d0b1fa58952c72/markdown_link_attr_modifier-0.2.1-py3-none-any.whl", hash = "sha256:6b4415319648cbe6dfb7a54ca12fa69e61a27c86a09d15f2a9a559ace0aa87c5", size = 17146, upload-time = "2023-04-13T16:00:06.559Z" }, +] + +[[package]] +name = "mdx-gh-links" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/ea/bf1f721a8dc0ff83b426480f040ac68dbe3d7898b096c1277a5a4e3da0ec/mdx_gh_links-0.4.tar.gz", hash = "sha256:41d5aac2ab201425aa0a19373c4095b79e5e015fdacfe83c398199fe55ca3686", size = 5783, upload-time = "2023-12-22T19:54:02.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c7/ccfe05ade98ba7a63f05d1b05b7508d9af743cbd1f1681aa0c9900a8cd40/mdx_gh_links-0.4-py3-none-any.whl", hash = "sha256:9057bca1fa5280bf1fcbf354381e46c9261cc32c2d5c0407801f8a910be5f099", size = 7166, upload-time = "2023-12-22T19:54:00.384Z" }, +] + +[[package]] +name = "mdx-truly-sane-lists" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/27/16456314311abac2cedef4527679924e80ac4de19dd926699c1b261e0b9b/mdx_truly_sane_lists-1.3.tar.gz", hash = "sha256:b661022df7520a1e113af7c355c62216b384c867e4f59fb8ee7ad511e6e77f45", size = 5359, upload-time = "2022-07-19T13:42:45.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/9e/dcd1027f7fd193aed152e01c6651a197c36b858f2cd1425ad04cb31a34fc/mdx_truly_sane_lists-1.3-py3-none-any.whl", hash = "sha256:b9546a4c40ff8f1ab692f77cee4b6bfe8ddf9cccf23f0a24e71f3716fe290a37", size = 6071, upload-time = "2022-07-19T13:42:43.375Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, + { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, + { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "24.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/35/d806c2ca66072e36dc340ccdbeb2af7e4f1b5bcc33f1481f00ceed476708/nodejs_wheel_binaries-24.12.0.tar.gz", hash = "sha256:f1b50aa25375e264697dec04b232474906b997c2630c8f499f4caf3692938435", size = 8058, upload-time = "2025-12-11T21:12:26.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/3b/9d6f044319cd5b1e98f07c41e2465b58cadc1c9c04a74c891578f3be6cb5/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:7564ddea0a87eff34e9b3ef71764cc2a476a8f09a5cccfddc4691148b0a47338", size = 55125859, upload-time = "2025-12-11T21:11:58.132Z" }, + { url = "https://files.pythonhosted.org/packages/48/a5/f5722bf15c014e2f476d7c76bce3d55c341d19122d8a5d86454db32a61a4/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:8ff929c4669e64613ceb07f5bbd758d528c3563820c75d5de3249eb452c0c0ab", size = 55309035, upload-time = "2025-12-11T21:12:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/a9/61/68d39a6f1b5df67805969fd2829ba7e80696c9af19537856ec912050a2be/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6ebacefa8891bc456ad3655e6bce0af7e20ba08662f79d9109986faeb703fd6f", size = 59661017, upload-time = "2025-12-11T21:12:05.268Z" }, + { url = "https://files.pythonhosted.org/packages/16/a1/31aad16f55a5e44ca7ea62d1367fc69f4b6e1dba67f58a0a41d0ed854540/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3292649a03682ccbfa47f7b04d3e4240e8c46ef04dc941b708f20e4e6a764f75", size = 60159770, upload-time = "2025-12-11T21:12:08.696Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5e/b7c569aa1862690ca4d4daf3a64cafa1ea6ce667a9e3ae3918c56e127d9b/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7fb83df312955ea355ba7f8cbd7055c477249a131d3cb43b60e4aeb8f8c730b1", size = 61653561, upload-time = "2025-12-11T21:12:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/71/87/567f58d7ba69ff0208be849b37be0f2c2e99c69e49334edd45ff44f00043/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2473c819448fedd7b036dde236b09f3c8bbf39fbbd0c1068790a0498800f498b", size = 62238331, upload-time = "2025-12-11T21:12:16.143Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9d/c6492188ce8de90093c6755a4a63bb6b2b4efb17094cb4f9a9a49c73ed3b/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_amd64.whl", hash = "sha256:2090d59f75a68079fabc9b86b14df8238b9aecb9577966dc142ce2a23a32e9bb", size = 41342076, upload-time = "2025-12-11T21:12:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/cd3290a647df567645353feed451ef4feaf5844496ced69c4dcb84295ff4/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_arm64.whl", hash = "sha256:d0c2273b667dd7e3f55e369c0085957b702144b1b04bfceb7ce2411e58333757", size = 39048104, upload-time = "2025-12-11T21:12:23.495Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.407" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" }, +] + +[package.optional-dependencies] +nodejs = [ + { name = "nodejs-wheel-binaries" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" }, + { url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" }, + { url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" }, + { url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" }, + { url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" }, + { url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" }, + { url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/7119e42aa1d3bf036ffc9478885c2e248812b7de9abea4eae89163d2929d/ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b", size = 12925808, upload-time = "2025-11-13T19:58:42.779Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9d/7c0a255d21e0912114784e4a96bf62af0618e2190cae468cd82b13625ad2/ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621", size = 14331546, upload-time = "2025-11-13T19:58:45.691Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, +] + +[[package]] +name = "scons" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "uv" +version = "0.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/08/3bf76403ea7c22feef634849137fab10b28ab5ba5bbf08a53390763d5448/uv-0.9.11.tar.gz", hash = "sha256:605a7a57f508aabd029fc0c5ef5c60a556f8c50d32e194f1a300a9f4e87f18d4", size = 3744387, upload-time = "2025-11-20T23:20:00.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/26/8f917e9faddd9cb49abcbc8c7dac5343b0f61d04c6ac36873d2a324fee1a/uv-0.9.11-py3-none-linux_armv6l.whl", hash = "sha256:803f85cf25ab7f1fca10fe2e40a1b9f5b1d48efc25efd6651ba3c9668db6a19e", size = 20787588, upload-time = "2025-11-20T23:18:53.738Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1f/eafd39c719ddee19fc25884f68c1a7e736c0fca63c1cbef925caf8ebd739/uv-0.9.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6a31b0bd4eaec59bf97816aefbcd75cae4fcc8875c4b19ef1846b7bff3d67c70", size = 19922144, upload-time = "2025-11-20T23:18:57.569Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/6b9fac39e5b65fa47dba872dcf171f1470490cd645343e8334f20f73885b/uv-0.9.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48548a23fb5a103b8955dfafff7d79d21112b8e25ce5ff25e3468dc541b20e83", size = 18380643, upload-time = "2025-11-20T23:19:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/9a/d4080e95950a4fc6fdf20d67b9a43ffb8e3d6d6b7c8dda460ae73ddbecd9/uv-0.9.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cb680948e678590b5960744af2ecea6f2c0307dbb74ac44daf5c00e84ad8c09f", size = 20310262, upload-time = "2025-11-20T23:19:04.914Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b4/86d9c881bd6accf2b766f7193b50e9d5815f2b34806191d90ea24967965e/uv-0.9.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ef1982295e5aaf909a9668d6fb6abfc5089666c699f585a36f3a67f1a22916a", size = 20392988, upload-time = "2025-11-20T23:19:08.258Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/6a227b7ca1829442c1419ba1db856d176b6e0861f9bf9355a8790a5d02b5/uv-0.9.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92ff773aa4193148019533c55382c2f9c661824bbf0c2e03f12aeefc800ede57", size = 21394892, upload-time = "2025-11-20T23:19:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8f/df45b8409923121de8c4081c9d6d8ba3273eaa450645e1e542d83179c7b5/uv-0.9.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70137a46675bbecf3a8b43d292a61767f1b944156af3d0f8d5986292bd86f6cf", size = 22987735, upload-time = "2025-11-20T23:19:16.27Z" }, + { url = "https://files.pythonhosted.org/packages/89/51/bbf3248a619c9f502d310a11362da5ed72c312d354fb8f9667c5aa3be9dd/uv-0.9.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5af9117bab6c4b3a1cacb0cddfb3cd540d0adfb13c7b8a9a318873cf2d07e52", size = 22617321, upload-time = "2025-11-20T23:19:20.1Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cd/a158ec989c5433dc86ebd9fea800f2aed24255b84ab65b6d7407251e5e31/uv-0.9.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cc86940d9b3a425575f25dc45247be2fb31f7fed7bf3394ae9daadd466e5b80", size = 21615712, upload-time = "2025-11-20T23:19:23.71Z" }, + { url = "https://files.pythonhosted.org/packages/73/da/2597becbc0fcbb59608d38fda5db79969e76dedf5b072f0e8564c8f0628b/uv-0.9.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97906ca1b90dac91c23af20e282e2e37c8eb80c3721898733928a295f2defda", size = 21661022, upload-time = "2025-11-20T23:19:27.385Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/9b8f3b3529b23c2a6f5b9612da70ea53117935ec999757b4f1d640f63d63/uv-0.9.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d901269e1db72abc974ba61d37be6e56532e104922329e0b553d9df07ba224be", size = 20440548, upload-time = "2025-11-20T23:19:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/72/b2/683afdb83e96dd966eb7cf3688af56a1b826c8bc1e8182fb10ec35b3e391/uv-0.9.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8abfb7d4b136de3e92dd239ea9a51d4b7bbb970dc1b33bec84d08facf82b9a6e", size = 21493758, upload-time = "2025-11-20T23:19:34.688Z" }, + { url = "https://files.pythonhosted.org/packages/f4/00/99848bc9834aab104fa74aa1a60b1ca478dee824d2e4aacb15af85673572/uv-0.9.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1f8afc13b3b94bce1e72514c598d41623387b2b61b68d7dbce9a01a0d8874860", size = 20332324, upload-time = "2025-11-20T23:19:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/6c/94/8cfd1bb1cc5d768cb334f976ba2686c6327e4ac91c16b8469b284956d4d9/uv-0.9.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7d414cfa410f1850a244d87255f98d06ca61cc13d82f6413c4f03e9e0c9effc7", size = 20845062, upload-time = "2025-11-20T23:19:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/a0/42/43f66bfc621464dabe9cfe3cbf69cddc36464da56ab786c94fc9ccf99cc7/uv-0.9.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:edc14143d0ba086a7da4b737a77746bb36bc00e3d26466f180ea99e3bf795171", size = 21857559, upload-time = "2025-11-20T23:19:46.026Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/bfd41bf087522601c724d712c3727aeb62f51b1f67c4ab86a078c3947525/uv-0.9.11-py3-none-win32.whl", hash = "sha256:af5fd91eecaa04b4799f553c726307200f45da844d5c7c5880d64db4debdd5dc", size = 19639246, upload-time = "2025-11-20T23:19:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2f/d51c02627de68a7ca5b82f0a5d61d753beee3fe696366d1a1c5d5e40cd58/uv-0.9.11-py3-none-win_amd64.whl", hash = "sha256:c65a024ad98547e32168f3a52360fe73ff39cd609a8fb9dd2509aac91483cfc8", size = 21626822, upload-time = "2025-11-20T23:19:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/af/d8/e07e866ee328d3c9f27a6d57a018d8330f47be95ef4654a178779c968a66/uv-0.9.11-py3-none-win_arm64.whl", hash = "sha256:4907a696c745703542ed2559bdf5380b92c8b1d4bf290ebfed45bf9a2a2c6690", size = 20046856, upload-time = "2025-11-20T23:19:58.517Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] From 7b7b8b15f95580081e96bfab2b6285b97723e61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 30 Dec 2025 05:29:18 +0100 Subject: [PATCH 60/90] Add a note encouraging to use pre-commit.ci (#11) --- readme.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/readme.md b/readme.md index fb79067..9ad847a 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,15 @@ In addition, this template includes configuration files for the following tools * Press `control+shift+p` to open the commands palette and search for recommended extensions to install or check if they are installed. * Pyright (pyproject.toml/tool.pyright sections): a Python static type checker. Sections starting with tool.pyright house configuration options for Pyright. +## Automatic checks on GitHub + +### Pre-commit + +It's recommended to install pre-commit.ci [pre-commit](https://pre-commit.ci) on personal GitHub accounts. +Then, you can choose if pre-commit will be used in all or just in selected repos. + +Setting up pre-commit.ci for each add-on using the add-on template will help you maintain a consistent code style in your add-ons. + ## Requirements You need the following software to use this code for your NVDA add-on development and packaging: From 8c9247b1f547386c4badf9c43609341c45f2054b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 30 Dec 2025 05:42:28 +0100 Subject: [PATCH 61/90] Update uv.lock --- uv.lock | 463 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 462 insertions(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index bda0207..e0def29 100644 --- a/uv.lock +++ b/uv.lock @@ -1,3 +1,464 @@ version = 1 revision = 3 -requires-python = ">=3.13" +requires-python = "==3.13.*" + +[[package]] +name = "addontemplate" +source = { editable = "." } +dependencies = [ + { name = "crowdin-api-client" }, + { name = "lxml" }, + { name = "markdown" }, + { name = "markdown-link-attr-modifier" }, + { name = "mdx-gh-links" }, + { name = "mdx-truly-sane-lists" }, + { name = "nh3" }, + { name = "pre-commit" }, + { name = "pyright", extra = ["nodejs"] }, + { name = "requests" }, + { name = "ruff" }, + { name = "scons" }, + { name = "uv" }, +] + +[package.metadata] +requires-dist = [ + { name = "crowdin-api-client", specifier = "==1.24.1" }, + { name = "lxml", specifier = "==6.0.2" }, + { name = "markdown", specifier = "==3.10" }, + { name = "markdown-link-attr-modifier", specifier = "==0.2.1" }, + { name = "mdx-gh-links", specifier = "==0.4" }, + { name = "mdx-truly-sane-lists", specifier = "==1.3" }, + { name = "nh3", specifier = "==0.3.2" }, + { name = "pre-commit", specifier = "==4.2.0" }, + { name = "pyright", extras = ["nodejs"], specifier = "==1.1.407" }, + { name = "requests", specifier = "==2.32.5" }, + { name = "ruff", specifier = "==0.14.5" }, + { name = "scons", specifier = "==4.10.1" }, + { name = "uv", specifier = "==0.9.11" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "crowdin-api-client" +version = "1.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/fc/ec5564928057aac9cae7e78ed324898b3134369b100bbb2b5c97ad1ad548/crowdin_api_client-1.24.1.tar.gz", hash = "sha256:d2a385c2b3f8e985d5bb084524ae14aef9045094fba0b2df1df82d9da97155b1", size = 70629, upload-time = "2025-08-26T13:20:34.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/74/118d8f5e592a1fe75b793346a599d57746b18b8875c31e956022b63ba173/crowdin_api_client-1.24.1-py3-none-any.whl", hash = "sha256:a07365a2a0d42830ee4eb188e3820603e1420421575637b1ddd8dffe1d2fe14c", size = 109654, upload-time = "2025-08-26T13:20:33.673Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markdown-link-attr-modifier" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/30/d35aad054a27f119bff2408523d82c3f9a6d9936712c872f5b9fe817de5b/markdown_link_attr_modifier-0.2.1.tar.gz", hash = "sha256:18df49a9fe7b5c87dad50b75c2a2299ae40c65674f7b1263fb12455f5df7ac99", size = 18408, upload-time = "2023-04-13T16:00:12.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/82/9262a67313847fdcc6252a007f032924fe0c0b6d6b9ef0d0b1fa58952c72/markdown_link_attr_modifier-0.2.1-py3-none-any.whl", hash = "sha256:6b4415319648cbe6dfb7a54ca12fa69e61a27c86a09d15f2a9a559ace0aa87c5", size = 17146, upload-time = "2023-04-13T16:00:06.559Z" }, +] + +[[package]] +name = "mdx-gh-links" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/ea/bf1f721a8dc0ff83b426480f040ac68dbe3d7898b096c1277a5a4e3da0ec/mdx_gh_links-0.4.tar.gz", hash = "sha256:41d5aac2ab201425aa0a19373c4095b79e5e015fdacfe83c398199fe55ca3686", size = 5783, upload-time = "2023-12-22T19:54:02.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/c7/ccfe05ade98ba7a63f05d1b05b7508d9af743cbd1f1681aa0c9900a8cd40/mdx_gh_links-0.4-py3-none-any.whl", hash = "sha256:9057bca1fa5280bf1fcbf354381e46c9261cc32c2d5c0407801f8a910be5f099", size = 7166, upload-time = "2023-12-22T19:54:00.384Z" }, +] + +[[package]] +name = "mdx-truly-sane-lists" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/27/16456314311abac2cedef4527679924e80ac4de19dd926699c1b261e0b9b/mdx_truly_sane_lists-1.3.tar.gz", hash = "sha256:b661022df7520a1e113af7c355c62216b384c867e4f59fb8ee7ad511e6e77f45", size = 5359, upload-time = "2022-07-19T13:42:45.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/9e/dcd1027f7fd193aed152e01c6651a197c36b858f2cd1425ad04cb31a34fc/mdx_truly_sane_lists-1.3-py3-none-any.whl", hash = "sha256:b9546a4c40ff8f1ab692f77cee4b6bfe8ddf9cccf23f0a24e71f3716fe290a37", size = 6071, upload-time = "2022-07-19T13:42:43.375Z" }, +] + +[[package]] +name = "nh3" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, + { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, + { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, + { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "24.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/35/d806c2ca66072e36dc340ccdbeb2af7e4f1b5bcc33f1481f00ceed476708/nodejs_wheel_binaries-24.12.0.tar.gz", hash = "sha256:f1b50aa25375e264697dec04b232474906b997c2630c8f499f4caf3692938435", size = 8058, upload-time = "2025-12-11T21:12:26.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/3b/9d6f044319cd5b1e98f07c41e2465b58cadc1c9c04a74c891578f3be6cb5/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:7564ddea0a87eff34e9b3ef71764cc2a476a8f09a5cccfddc4691148b0a47338", size = 55125859, upload-time = "2025-12-11T21:11:58.132Z" }, + { url = "https://files.pythonhosted.org/packages/48/a5/f5722bf15c014e2f476d7c76bce3d55c341d19122d8a5d86454db32a61a4/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:8ff929c4669e64613ceb07f5bbd758d528c3563820c75d5de3249eb452c0c0ab", size = 55309035, upload-time = "2025-12-11T21:12:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/a9/61/68d39a6f1b5df67805969fd2829ba7e80696c9af19537856ec912050a2be/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6ebacefa8891bc456ad3655e6bce0af7e20ba08662f79d9109986faeb703fd6f", size = 59661017, upload-time = "2025-12-11T21:12:05.268Z" }, + { url = "https://files.pythonhosted.org/packages/16/a1/31aad16f55a5e44ca7ea62d1367fc69f4b6e1dba67f58a0a41d0ed854540/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3292649a03682ccbfa47f7b04d3e4240e8c46ef04dc941b708f20e4e6a764f75", size = 60159770, upload-time = "2025-12-11T21:12:08.696Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5e/b7c569aa1862690ca4d4daf3a64cafa1ea6ce667a9e3ae3918c56e127d9b/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7fb83df312955ea355ba7f8cbd7055c477249a131d3cb43b60e4aeb8f8c730b1", size = 61653561, upload-time = "2025-12-11T21:12:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/71/87/567f58d7ba69ff0208be849b37be0f2c2e99c69e49334edd45ff44f00043/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2473c819448fedd7b036dde236b09f3c8bbf39fbbd0c1068790a0498800f498b", size = 62238331, upload-time = "2025-12-11T21:12:16.143Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9d/c6492188ce8de90093c6755a4a63bb6b2b4efb17094cb4f9a9a49c73ed3b/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_amd64.whl", hash = "sha256:2090d59f75a68079fabc9b86b14df8238b9aecb9577966dc142ce2a23a32e9bb", size = 41342076, upload-time = "2025-12-11T21:12:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/cd3290a647df567645353feed451ef4feaf5844496ced69c4dcb84295ff4/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_arm64.whl", hash = "sha256:d0c2273b667dd7e3f55e369c0085957b702144b1b04bfceb7ce2411e58333757", size = 39048104, upload-time = "2025-12-11T21:12:23.495Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.407" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" }, +] + +[package.optional-dependencies] +nodejs = [ + { name = "nodejs-wheel-binaries" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" }, + { url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" }, + { url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" }, + { url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" }, + { url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" }, + { url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" }, + { url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/7119e42aa1d3bf036ffc9478885c2e248812b7de9abea4eae89163d2929d/ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b", size = 12925808, upload-time = "2025-11-13T19:58:42.779Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9d/7c0a255d21e0912114784e4a96bf62af0618e2190cae468cd82b13625ad2/ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621", size = 14331546, upload-time = "2025-11-13T19:58:45.691Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, +] + +[[package]] +name = "scons" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/c9/2f430bb39e4eccba32ce8008df4a3206df651276422204e177a09e12b30b/scons-4.10.1.tar.gz", hash = "sha256:99c0e94a42a2c1182fa6859b0be697953db07ba936ecc9817ae0d218ced20b15", size = 3258403, upload-time = "2025-11-16T22:43:39.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/bf/931fb9fbb87234c32b8b1b1c15fba23472a10777c12043336675633809a7/scons-4.10.1-py3-none-any.whl", hash = "sha256:bd9d1c52f908d874eba92a8c0c0a8dcf2ed9f3b88ab956d0fce1da479c4e7126", size = 4136069, upload-time = "2025-11-16T22:43:35.933Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "uv" +version = "0.9.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/08/3bf76403ea7c22feef634849137fab10b28ab5ba5bbf08a53390763d5448/uv-0.9.11.tar.gz", hash = "sha256:605a7a57f508aabd029fc0c5ef5c60a556f8c50d32e194f1a300a9f4e87f18d4", size = 3744387, upload-time = "2025-11-20T23:20:00.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/26/8f917e9faddd9cb49abcbc8c7dac5343b0f61d04c6ac36873d2a324fee1a/uv-0.9.11-py3-none-linux_armv6l.whl", hash = "sha256:803f85cf25ab7f1fca10fe2e40a1b9f5b1d48efc25efd6651ba3c9668db6a19e", size = 20787588, upload-time = "2025-11-20T23:18:53.738Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1f/eafd39c719ddee19fc25884f68c1a7e736c0fca63c1cbef925caf8ebd739/uv-0.9.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6a31b0bd4eaec59bf97816aefbcd75cae4fcc8875c4b19ef1846b7bff3d67c70", size = 19922144, upload-time = "2025-11-20T23:18:57.569Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/6b9fac39e5b65fa47dba872dcf171f1470490cd645343e8334f20f73885b/uv-0.9.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48548a23fb5a103b8955dfafff7d79d21112b8e25ce5ff25e3468dc541b20e83", size = 18380643, upload-time = "2025-11-20T23:19:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/9a/d4080e95950a4fc6fdf20d67b9a43ffb8e3d6d6b7c8dda460ae73ddbecd9/uv-0.9.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cb680948e678590b5960744af2ecea6f2c0307dbb74ac44daf5c00e84ad8c09f", size = 20310262, upload-time = "2025-11-20T23:19:04.914Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b4/86d9c881bd6accf2b766f7193b50e9d5815f2b34806191d90ea24967965e/uv-0.9.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ef1982295e5aaf909a9668d6fb6abfc5089666c699f585a36f3a67f1a22916a", size = 20392988, upload-time = "2025-11-20T23:19:08.258Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/6a227b7ca1829442c1419ba1db856d176b6e0861f9bf9355a8790a5d02b5/uv-0.9.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92ff773aa4193148019533c55382c2f9c661824bbf0c2e03f12aeefc800ede57", size = 21394892, upload-time = "2025-11-20T23:19:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8f/df45b8409923121de8c4081c9d6d8ba3273eaa450645e1e542d83179c7b5/uv-0.9.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70137a46675bbecf3a8b43d292a61767f1b944156af3d0f8d5986292bd86f6cf", size = 22987735, upload-time = "2025-11-20T23:19:16.27Z" }, + { url = "https://files.pythonhosted.org/packages/89/51/bbf3248a619c9f502d310a11362da5ed72c312d354fb8f9667c5aa3be9dd/uv-0.9.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5af9117bab6c4b3a1cacb0cddfb3cd540d0adfb13c7b8a9a318873cf2d07e52", size = 22617321, upload-time = "2025-11-20T23:19:20.1Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cd/a158ec989c5433dc86ebd9fea800f2aed24255b84ab65b6d7407251e5e31/uv-0.9.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cc86940d9b3a425575f25dc45247be2fb31f7fed7bf3394ae9daadd466e5b80", size = 21615712, upload-time = "2025-11-20T23:19:23.71Z" }, + { url = "https://files.pythonhosted.org/packages/73/da/2597becbc0fcbb59608d38fda5db79969e76dedf5b072f0e8564c8f0628b/uv-0.9.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97906ca1b90dac91c23af20e282e2e37c8eb80c3721898733928a295f2defda", size = 21661022, upload-time = "2025-11-20T23:19:27.385Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/9b8f3b3529b23c2a6f5b9612da70ea53117935ec999757b4f1d640f63d63/uv-0.9.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d901269e1db72abc974ba61d37be6e56532e104922329e0b553d9df07ba224be", size = 20440548, upload-time = "2025-11-20T23:19:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/72/b2/683afdb83e96dd966eb7cf3688af56a1b826c8bc1e8182fb10ec35b3e391/uv-0.9.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8abfb7d4b136de3e92dd239ea9a51d4b7bbb970dc1b33bec84d08facf82b9a6e", size = 21493758, upload-time = "2025-11-20T23:19:34.688Z" }, + { url = "https://files.pythonhosted.org/packages/f4/00/99848bc9834aab104fa74aa1a60b1ca478dee824d2e4aacb15af85673572/uv-0.9.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1f8afc13b3b94bce1e72514c598d41623387b2b61b68d7dbce9a01a0d8874860", size = 20332324, upload-time = "2025-11-20T23:19:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/6c/94/8cfd1bb1cc5d768cb334f976ba2686c6327e4ac91c16b8469b284956d4d9/uv-0.9.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7d414cfa410f1850a244d87255f98d06ca61cc13d82f6413c4f03e9e0c9effc7", size = 20845062, upload-time = "2025-11-20T23:19:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/a0/42/43f66bfc621464dabe9cfe3cbf69cddc36464da56ab786c94fc9ccf99cc7/uv-0.9.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:edc14143d0ba086a7da4b737a77746bb36bc00e3d26466f180ea99e3bf795171", size = 21857559, upload-time = "2025-11-20T23:19:46.026Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/bfd41bf087522601c724d712c3727aeb62f51b1f67c4ab86a078c3947525/uv-0.9.11-py3-none-win32.whl", hash = "sha256:af5fd91eecaa04b4799f553c726307200f45da844d5c7c5880d64db4debdd5dc", size = 19639246, upload-time = "2025-11-20T23:19:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2f/d51c02627de68a7ca5b82f0a5d61d753beee3fe696366d1a1c5d5e40cd58/uv-0.9.11-py3-none-win_amd64.whl", hash = "sha256:c65a024ad98547e32168f3a52360fe73ff39cd609a8fb9dd2509aac91483cfc8", size = 21626822, upload-time = "2025-11-20T23:19:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/af/d8/e07e866ee328d3c9f27a6d57a018d8330f47be95ef4654a178779c968a66/uv-0.9.11-py3-none-win_arm64.whl", hash = "sha256:4907a696c745703542ed2559bdf5380b92c8b1d4bf290ebfed45bf9a2a2c6690", size = 20046856, upload-time = "2025-11-20T23:19:58.517Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, +] + +[[package]] +name = "wrapt" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, + { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, + { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, + { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, + { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, +] From c6f50e9544d65b5d2f3d4cd149def616f0323ca8 Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Thu, 8 Jan 2026 05:40:32 +0100 Subject: [PATCH 62/90] Bump actions/download-artifact from 6 to 7 and bump actions/upload-artifact from 5 to 6 (#12) --- .github/workflows/build_addon.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 9e0fa64..c6aa8cb 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -40,7 +40,7 @@ jobs: - name: building addon run: scons && scons pot - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: packaged_addon path: | @@ -56,7 +56,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: download releases files - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 - name: Display structure of downloaded files run: ls -R - name: Calculate sha256 From a4f9291de0e3a862c5a4ec989a388c8860aa0ae9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:18:15 +1100 Subject: [PATCH 63/90] Bump actions/download-artifact from 7 to 8 (#17) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_addon.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index c6aa8cb..fe40e81 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -56,7 +56,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: download releases files - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 - name: Display structure of downloaded files run: ls -R - name: Calculate sha256 From 20c3919c1e6e79103f9ef6c535da6a03a45c65cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:18:25 +1100 Subject: [PATCH 64/90] Bump actions/upload-artifact from 6 to 7 (#16) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_addon.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index fe40e81..63a6f1d 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -40,7 +40,7 @@ jobs: - name: building addon run: scons && scons pot - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: packaged_addon path: | From 945487a235953b5d888e17f1f2a4f461d4151a24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:22:54 +1100 Subject: [PATCH 65/90] Bump the uv group across 1 directory with 3 updates (#18) Bumps the uv group with 3 updates in the / directory: [filelock](https://github.com/tox-dev/py-filelock), [urllib3](https://github.com/urllib3/urllib3) and [virtualenv](https://github.com/pypa/virtualenv). Updates `filelock` from 3.20.1 to 3.20.3 - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.20.1...3.20.3) Updates `urllib3` from 2.6.2 to 2.6.3 - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.6.2...2.6.3) Updates `virtualenv` from 20.35.4 to 20.36.1 - [Release notes](https://github.com/pypa/virtualenv/releases) - [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/virtualenv/compare/20.35.4...20.36.1) --- updated-dependencies: - dependency-name: filelock dependency-version: 3.20.3 dependency-type: indirect dependency-group: uv - dependency-name: urllib3 dependency-version: 2.6.3 dependency-type: indirect dependency-group: uv - dependency-name: virtualenv dependency-version: 20.36.1 dependency-type: indirect dependency-group: uv ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/uv.lock b/uv.lock index e0def29..460a161 100644 --- a/uv.lock +++ b/uv.lock @@ -117,11 +117,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.1" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -383,11 +383,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.2" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] @@ -418,16 +418,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] From 3ac4b6f32d4d00e3974b143442453a18f25b3909 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Mon, 2 Mar 2026 16:46:55 -0700 Subject: [PATCH 66/90] Add-on template: add support for custom speech pronunciation dictionaries (NVDA 2026.2) (#19) Closes #15 This pull request add support for building custom speech pronunciation dictionaries via the add-on template. Changes made: Adds "speechDictionraies" sectoin to buildVars Adds speech dictionaries manifest key type and generation rules (borrowing custom symbol dictionaries) --- buildVars.py | 11 ++++++++++- readme.md | 13 +++++++++++++ sconstruct | 1 + site_scons/site_tools/NVDATool/__init__.py | 4 ++++ site_scons/site_tools/NVDATool/manifests.py | 12 +++++++++++- site_scons/site_tools/NVDATool/typings.py | 6 ++++++ 6 files changed, 45 insertions(+), 2 deletions(-) diff --git a/buildVars.py b/buildVars.py index c125fae..decefe4 100644 --- a/buildVars.py +++ b/buildVars.py @@ -99,5 +99,14 @@ # Each key is the name of the dictionary, # with keys inside recording the following attributes: # displayName (name of the speech dictionary shown to users and translatable), -# mandatory (True when always enabled, False when not. +# mandatory (True when always enabled, False when not). symbolDictionaries: SymbolDictionaries = {} + +# Custom speech dictionaries (distinct from symbol dictionaries above) +# Speech dictionary files reside in the speechDicts folder and are named `name.dic`. +# If your add-on includes custom speech (pronunciation) dictionaries (most will not), fill out this dictionary. +# Each key is the name of the dictionary, +# with keys inside recording the following attributes: +# displayName (name of the speech dictionary shown to users and translatable), +# mandatory (True when always enabled, False when not). +speechDictionaries: SpeechDictionaries = {} diff --git a/readme.md b/readme.md index 9ad847a..117eb2c 100644 --- a/readme.md +++ b/readme.md @@ -142,6 +142,19 @@ Information on custom symbol dictionaries must be specified in buildVars under ` Note: you must fill out this dictionary if at least one custom symbol dictionary is included in the add-on. If not, leave the dictionary empty. +###### Speech pronunciation dictionaries + +Information on custom speech (pronunciation) dictionaries must be specified in buildVars under `speechDictionaries` dictionary as follows: + +* Dictionary name (string key for a nested dictionary): each `symbolDictionaries` entry is a name for the included custom speech dictionary placed in `speechDicts` folder inside `addon` folder. +The file is named `.dic`. +This nested dictionary should specify: + * displayName (string): the name of the dictionary shown to users and is translatable. + * mandatory (True/False): Always enabled (True) or optional and visible in the GUI (False) + +Note: you must fill out this dictionary if at least one custom speech dictionary is included in the add-on. +If not, leave the dictionary empty. + ### To manage documentation files for your addon: 1. Copy the `readme.md` file for your add-on to the first created folder, where you copied `buildVars.py`. You can also copy `style.css` to improve the presentation of HTML documents. diff --git a/sconstruct b/sconstruct index 481a7ac..cdbad3f 100644 --- a/sconstruct +++ b/sconstruct @@ -61,6 +61,7 @@ env.Append( addon_info=buildVars.addon_info, brailleTables=buildVars.brailleTables, symbolDictionaries=buildVars.symbolDictionaries, + speechDictionaries=buildVars.speechDictionaries, ) if env["dev"]: diff --git a/site_scons/site_tools/NVDATool/__init__.py b/site_scons/site_tools/NVDATool/__init__.py index ff31eec..a71857d 100644 --- a/site_scons/site_tools/NVDATool/__init__.py +++ b/site_scons/site_tools/NVDATool/__init__.py @@ -13,6 +13,7 @@ - addon_info: .typing.AddonInfo - brailleTables: .typings.BrailleTables - symbolDictionaries: .typings.SymbolDictionaries +- speechDictionaries: .typings.SpeechDictionaries The following environment variables are required to build the HTML: @@ -49,6 +50,7 @@ def generate(env: Environment): env.SetDefault(brailleTables={}) env.SetDefault(symbolDictionaries={}) + env.SetDefault(speechDictionaries={}) manifestAction = env.Action( lambda target, source, env: generateManifest( @@ -57,6 +59,7 @@ def generate(env: Environment): addon_info=env["addon_info"], brailleTables=env["brailleTables"], symbolDictionaries=env["symbolDictionaries"], + speechDictionaries=env["speechDictionaries"], ) and None, lambda target, source, env: f"Generating manifest {target[0]}", @@ -75,6 +78,7 @@ def generate(env: Environment): addon_info=env["addon_info"], brailleTables=env["brailleTables"], symbolDictionaries=env["symbolDictionaries"], + speechDictionaries=env["speechDictionaries"], ) and None, lambda target, source, env: f"Generating translated manifest {target[0]}", diff --git a/site_scons/site_tools/NVDATool/manifests.py b/site_scons/site_tools/NVDATool/manifests.py index a55785e..7723b0b 100644 --- a/site_scons/site_tools/NVDATool/manifests.py +++ b/site_scons/site_tools/NVDATool/manifests.py @@ -2,7 +2,7 @@ import gettext from functools import partial -from .typings import AddonInfo, BrailleTables, SymbolDictionaries +from .typings import AddonInfo, BrailleTables, SymbolDictionaries, SpeechDictionaries from .utils import format_nested_section @@ -12,6 +12,7 @@ def generateManifest( addon_info: AddonInfo, brailleTables: BrailleTables, symbolDictionaries: SymbolDictionaries, + speechDictionaries: SpeechDictionaries, ): # Prepare the root manifest section with codecs.open(source, "r", "utf-8") as f: @@ -26,6 +27,10 @@ def generateManifest( if symbolDictionaries: manifest += format_nested_section("symbolDictionaries", symbolDictionaries) + # Custom speech pronunciation dictionaries + if speechDictionaries: + manifest += format_nested_section("speechDictionaries", speechDictionaries) + with codecs.open(dest, "w", "utf-8") as f: f.write(manifest) @@ -38,6 +43,7 @@ def generateTranslatedManifest( addon_info: AddonInfo, brailleTables: BrailleTables, symbolDictionaries: SymbolDictionaries, + speechDictionaries: SpeechDictionaries, ): with open(mo, "rb") as f: _ = gettext.GNUTranslations(f).gettext @@ -63,5 +69,9 @@ def generateTranslatedManifest( if symbolDictionaries: manifest += _format_section_only_with_displayName("symbolDictionaries", symbolDictionaries) + # Custom speech pronunciation dictionaries + if speechDictionaries: + manifest += _format_section_only_with_displayName("speechDictionaries", speechDictionaries) + with codecs.open(dest, "w", "utf-8") as f: f.write(manifest) diff --git a/site_scons/site_tools/NVDATool/typings.py b/site_scons/site_tools/NVDATool/typings.py index 650a759..0375538 100644 --- a/site_scons/site_tools/NVDATool/typings.py +++ b/site_scons/site_tools/NVDATool/typings.py @@ -30,8 +30,14 @@ class SymbolDictionaryAttributes(TypedDict): mandatory: bool +class SpeechDictionaryAttributes(TypedDict): + displayName: str + mandatory: bool + + BrailleTables = dict[str, BrailleTableAttributes] SymbolDictionaries = dict[str, SymbolDictionaryAttributes] +SpeechDictionaries = dict[str, SpeechDictionaryAttributes] class Strable(Protocol): From bda47e6dd43fe91064647e83cd41022038282d07 Mon Sep 17 00:00:00 2001 From: Joseph Lee Date: Mon, 2 Mar 2026 19:39:09 -0700 Subject: [PATCH 67/90] BuildVars: add missing SpeechDictionaries import (#20) --- buildVars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildVars.py b/buildVars.py index decefe4..0adb36b 100644 --- a/buildVars.py +++ b/buildVars.py @@ -1,7 +1,7 @@ # Build customizations # Change this file instead of sconstruct or manifest files, whenever possible. -from site_scons.site_tools.NVDATool.typings import AddonInfo, BrailleTables, SymbolDictionaries +from site_scons.site_tools.NVDATool.typings import AddonInfo, BrailleTables, SymbolDictionaries, SpeechDictionaries # Since some strings in `addon_info` are translatable, # we need to include them in the .po files. From 3ab00214b1950a6ed432a90709203e9beb453329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Tue, 3 Mar 2026 06:48:59 +0100 Subject: [PATCH 68/90] Run workflow just when files are changed in the addon folder (#21) Currently, checks are run with GitHub Actions regardless of files which have been changed. This PR makes checks to be run just for add-ons. Additionally, Python and dependencies are installed using uv and pyproject.toml. --- .github/workflows/build_addon.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 63a6f1d..8585bae 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -5,9 +5,12 @@ on: tags: ["*"] # To build on main/master branch, uncomment the following line: # branches: [ main , master ] - + paths: + - addon/** pull_request: branches: [ main, master ] + paths: + - addon/** workflow_dispatch: @@ -17,22 +20,17 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - - - run: echo -e "pre-commit\nscons\nmarkdown">requirements.txt - + - name: Checkout repo + uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: 3.11 - cache: 'pip' - + run: uv python install - name: Install dependencies run: | - python -m pip install --upgrade pip wheel - pip install -r requirements.txt sudo apt-get update -y sudo apt-get install -y gettext + uv pip install - name: Code checks run: export SKIP=no-commit-to-branch; pre-commit run --all From 6093c55b53f5d56db635e21203201616ae120fdc Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Wed, 4 Mar 2026 04:32:44 +0100 Subject: [PATCH 69/90] Do not restrict workflow triggers to addon path (#22) Co-authored-by: Cyrille Bougot --- .github/workflows/build_addon.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 8585bae..a392f81 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -5,12 +5,9 @@ on: tags: ["*"] # To build on main/master branch, uncomment the following line: # branches: [ main , master ] - paths: - - addon/** + pull_request: branches: [ main, master ] - paths: - - addon/** workflow_dispatch: From e0585fd6c23102c97163fd25fcae7ce1d5cb790a Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Tue, 17 Mar 2026 06:55:42 +0100 Subject: [PATCH 70/90] Fixes for CI workflow (#23) Various CI fixes and improvements: Do not try to build / make release if the workflow is run from this add-on template repo, since it fails and the template is not meant to be used as a release. Fixes: Do not restrict workflow triggers to addon path #22 (comment). Modernized uv usage with "uv sync", fixing the failure on previously used "uv pip install". Discussed in Do not restrict workflow triggers to addon path #22 (comment). Use uv for each command using Python so that the correct environment is used Added .egg-info to .git-ignore Updated readme --- .github/workflows/build_addon.yml | 13 ++++++++----- .gitignore | 1 + readme.md | 9 +++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index a392f81..97519e4 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -13,6 +13,9 @@ on: jobs: build: + # Building the add-on template as an add-on does not make sense (and fails). + # Do not modify this repo name with your own one! (should remain the template) + if: github.repository != 'nvaccess/addonTemplate' runs-on: ubuntu-latest @@ -21,19 +24,19 @@ jobs: uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v7 - - name: Set up Python - run: uv python install + with: + enable-cache: true - name: Install dependencies run: | sudo apt-get update -y sudo apt-get install -y gettext - uv pip install + uv sync - name: Code checks - run: export SKIP=no-commit-to-branch; pre-commit run --all + run: export SKIP=no-commit-to-branch; uv run pre-commit run --all-files - name: building addon - run: scons && scons pot + run: uv run scons && uv run scons pot - uses: actions/upload-artifact@v7 with: diff --git a/.gitignore b/.gitignore index 0be8af1..a6ccee5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ manifest.ini *.nvda-addon .sconsign.dblite /[0-9]*.[0-9]*.[0-9]*.json +*.egg-info diff --git a/readme.md b/readme.md index 117eb2c..df61be9 100644 --- a/readme.md +++ b/readme.md @@ -89,10 +89,15 @@ sconstruct and file: ``` .pre-commit-config.yaml +changelog.md +pyproject.toml +uv.lock ``` 4. Create an `addon` folder inside your new folder. You will put your code in the usual folders for NVDA extensions, under the `addon` folder. For instance: `globalPlugins`, `synthDrivers`, etc. -5. In the `buildVars.py` file, change variable `addon_info` with your add-on's information (name, summary, description, version, author, url, source url, license, and license URL). Also, be sure to carefully set the paths contained in the other variables in that file. If you need to use custom Markdown extensions, original add-on interface language is not English, or include custom braille translations tables, be sure to fil out markdown list, base language variable, and braille tables dictioanry, respectively. +5. In the `buildVars.py` file, change variable `addon_info` with your add-on's information (name, summary, description, version, author, url, source url, license, and license URL). Also, be sure to carefully set the paths contained in the other variables in that file. If you need to use custom Markdown extensions, original add-on interface language is not English, or include custom braille translations tables, be sure to fil out markdown list, base language variable, and braille tables dictionary, respectively. 6. Gettext translations must be placed into `addon\locale\/LC_MESSAGES\nvda.po`. +7. If you create releases with the GitHub workflow, pushing a tag, update the `changelog.md` file with the release description you want to be displayed in on your GitHub release page. +8. In the `[project]` section of `pyproject.toml`, update your project information. #### Add-on manifest specification @@ -102,7 +107,7 @@ An add-on manifest generated manually or via `buildVars.py` must include the fol * Summary (string): name as shown on NVDA's Add-on store. * Description (string): a short detailed description about the add-on. * Version (string), ideally number.number with an optional third number, denoting major.minor.patch. -* Changelog (string): changes between previous and current add-on releases. +* Changelog (string): changes between previous and current add-on releases, visible in the Add-on Store. * Author (string and an email address): one or more add-on author contact information in the form "name ". * URL (string): a web address where the add-on information can be found such as add-on repository. * docFileName (string): name of the documentation file. From 867f183901d843e388af519e0a0bb6d5da45887f Mon Sep 17 00:00:00 2001 From: Cyrille Bougot Date: Thu, 19 Mar 2026 09:00:18 +0100 Subject: [PATCH 71/90] Workflow runs produce add-on and .pot artifacts instead of a .zip (#24) --- .github/workflows/build_addon.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 97519e4..62ea4f4 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -43,7 +43,14 @@ jobs: name: packaged_addon path: | ./*.nvda-addon + archive: false + + - uses: actions/upload-artifact@v7 + with: + name: translation_template + path: | ./*.pot + archive: false upload_release: runs-on: ubuntu-latest @@ -53,8 +60,11 @@ jobs: contents: write steps: - uses: actions/checkout@v6 - - name: download releases files + - name: download all artifacts uses: actions/download-artifact@v8 + with: + path: . + merge-multiple: true - name: Display structure of downloaded files run: ls -R - name: Calculate sha256 From 6089057b2feba44cd46a5d3433590940932228ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 25 Mar 2026 06:39:04 +0100 Subject: [PATCH 72/90] Add workflow call to build the add-on, so it can be reused in other workflows --- .github/workflows/build_addon.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 9e0fa64..0ed7a64 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -10,6 +10,7 @@ on: branches: [ main, master ] workflow_dispatch: + workflow_call: jobs: build: From d8154a9dd511ba9661b7dc781c364dea57cd491c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Wed, 25 Mar 2026 16:39:20 +0100 Subject: [PATCH 73/90] Update workflow for testing --- .github/workflows/crowdinL10n.yml | 120 +++++++++++++----------------- 1 file changed, 50 insertions(+), 70 deletions(-) diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index 5f75489..a875a6a 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -16,12 +16,14 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: - crowdinProjectID: 780748 crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} downloadTranslationsBranch: l10n jobs: build: - runs-on: ubuntu-latest + uses: ./.github/workflows/build_addon.yml + crowdinSync: + needs: build + runs-on: windows-latest permissions: contents: write steps: @@ -33,84 +35,62 @@ jobs: uses: actions/setup-python@v6 with: python-version-file: ".python-version" - - name: Install gettext - run: | - sudo apt-get update -qq - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq gettext - name: Install the latest version of uv uses: astral-sh/setup-uv@v7 - - name: Install dependencies - run: uv pip install --system scons markdown - - name: Build add-on and pot file - run: | - uv run --with scons --with markdown scons - uv run --with scons --with markdown scons pot - name: Get add-on info id: getAddonInfo - run: uv run --with scons --with markdown ./.github/workflows/setOutputs.py - - name: Upload md from scratch - if: ${{ steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' && inputs.dry-run != true }} - run: | - mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md - uv run ./_l10n/source/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md - - name: update md - if: ${{ steps.getAddonInfo.outputs.shouldUpdateMd == 'true' && inputs.dry-run != true }} + shell: pwsh run: | - mv readme.md ${{ steps.getAddonInfo.outputs.addonId }}.md - uv run ./_l10n/source/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.md - - name: Upload pot from scratch - if: ${{ steps.getAddonInfo.outputs.shouldAddPotFromScratch == 'true' && inputs.dry-run != true }} + uv sync + uv run ./.github/workflows/setOutputs.py + - name: Download l10nUtil from nvdal10n + if: ${{ inputs.dry-run != true }} run: | - uv run ./_l10n/source/l10nUtil.py uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot - - name: Update pot - if: ${{ steps.getAddonInfo.outputs.shouldUpdatePot == 'true' && inputs.dry-run != true }} + gh run download --repo nvaccess/nvdal10n --name l10nUtil --dir _l10n + - name: Upload md + if: ${{ (steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' || steps.getAddonInfo.outputs.shouldUpdateMd == 'true') && inputs.dry-run != true }} run: | - uv run ./_l10n/source/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot - - name: Commit and push json + Move-Item -Path readme.md -Destination "${{ steps.getAddonInfo.outputs.addonId }}.md" + uv run ./_l10n/l10nUtil.exe uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md -c addon + - name: Download pot from build job if: ${{ inputs.dry-run != true }} - id: commit + uses: actions/download-artifact@v8 + with: + name: packaged_addon + - name: Upload pot + if: ${{ (steps.getAddonInfo.outputs.shouldAddPotFromScratch == 'true' || steps.getAddonInfo.outputs.shouldUpdatePot == 'true') && inputs.dry-run != true }} run: | - git config --local user.name github-actions - git config --local user.email github-actions@github.com - git status - git add *.json - if git diff --staged --quiet; then - echo "Nothing added to commit." - else - git commit -m "Update Crowdin file ids and hashes" - git push - fi + uv run ./_l10n/l10nUtil.exe uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot -c addon - name: Download translations from Crowdin if: ${{ inputs.dry-run != true }} run: | - uv run _l10n/source/l10nUtil.py exportTranslations -o _addonL10n - mkdir -p addon/locale - mkdir -p addon/doc - for dir in _addonL10n/${{ steps.getAddonInfo.outputs.addonId }}/*; do - echo "Processing: $dir" - if [ -d "$dir" ]; then - langCode=$(basename "$dir") - poFile="$dir/${{ steps.getAddonInfo.outputs.addonId }}.po" - if [ -f "$poFile" ]; then - mkdir -p "addon/locale/$langCode/LC_MESSAGES" - echo "Moving $poFile to addon/locale/$langCode/LC_MESSAGES/nvda.po" - mv "$poFile" "addon/locale/$langCode/LC_MESSAGES/nvda.po" - fi - mdFile="$dir/${{ steps.getAddonInfo.outputs.addonId }}.md" - if [ -f "$mdFile" ]; then - mkdir -p "addon/doc/$langCode" - echo "Moving $mdFile to addon/doc/$langCode/readme.md" - mv "$mdFile" "addon/doc/$langCode/readme.md" - fi - else - echo "Skipping invalid directory: $dir" - fi - done + uv run _l10n/l10nUtil.exe exportTranslations -o _addonL10n -c addon + New-Item -ItemType Directory -Force -Path addon/locale | Out-Null + New-Item -ItemType Directory -Force -Path addon/doc | Out-Null + foreach ($dir in Get-ChildItem -Path "_addonL10n/${{ steps.getAddonInfo.outputs.addonId }}" -Directory) { + Write-Host "Processing: $($dir.FullName)" + $langCode = $dir.Name + $poFile = Join-Path $dir.FullName "${{ steps.getAddonInfo.outputs.addonId }}.po" + if (Test-Path -PathType Leaf $poFile) { + $targetDir = "addon/locale/$langCode/LC_MESSAGES" + New-Item -ItemType Directory -Force -Path $targetDir | Out-Null + Write-Host "Moving $poFile to $targetDir/nvda.po" + Move-Item -Path $poFile -Destination "$targetDir/nvda.po" -Force + } + $mdFile = Join-Path $dir.FullName "${{ steps.getAddonInfo.outputs.addonId }}.md" + if (Test-Path -PathType Leaf $mdFile) { + $targetDir = "addon/doc/$langCode" + New-Item -ItemType Directory -Force -Path $targetDir | Out-Null + Write-Host "Moving $mdFile to $targetDir/readme.md" + Move-Item -Path $mdFile -Destination "$targetDir/readme.md" -Force + } + } git add addon/locale addon/doc - if git diff --staged --quiet; then - echo "Nothing added to commit." - else - git commit -m "Update translations for ${{ steps.getAddonInfo.outputs.addonId }}" - git checkout -b ${{ env.downloadTranslationsBranch }} - git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} - fi + $diff = git diff --staged --quiet + if ($LASTEXITCODE -eq 0) { + Write-Host "Nothing added to commit." + } else { + git commit -m "Update translations for ${{ steps.getAddonInfo.outputs.addonId }}" + git checkout -b ${{ env.downloadTranslationsBranch }} + git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} + } From eef379d894117719bc26e66944a6fc0824368371 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:21:35 +1100 Subject: [PATCH 74/90] Bump requests from 2.32.5 to 2.33.0 in the uv group across 1 directory (#25) Bumps the uv group with 1 update in the / directory: [requests](https://github.com/psf/requests). Updates `requests` from 2.32.5 to 2.33.0 - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.5...v2.33.0) --- updated-dependencies: - dependency-name: requests dependency-version: 2.33.0 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aa8752d..02e421a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "scons==4.10.1", "Markdown==3.10", # Translations management - "requests==2.32.5", + "requests==2.33.0", "nh3==0.3.2", "crowdin-api-client==1.24.1", "lxml==6.0.2", diff --git a/uv.lock b/uv.lock index 460a161..43e6868 100644 --- a/uv.lock +++ b/uv.lock @@ -32,7 +32,7 @@ requires-dist = [ { name = "nh3", specifier = "==0.3.2" }, { name = "pre-commit", specifier = "==4.2.0" }, { name = "pyright", extras = ["nodejs"], specifier = "==1.1.407" }, - { name = "requests", specifier = "==2.32.5" }, + { name = "requests", specifier = "==2.33.0" }, { name = "ruff", specifier = "==0.14.5" }, { name = "scons", specifier = "==4.10.1" }, { name = "uv", specifier = "==0.9.11" }, @@ -324,7 +324,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -332,9 +332,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, ] [[package]] From b4c72dd5e6134e9abc85b99228bd482158e2c920 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:16:04 +1000 Subject: [PATCH 75/90] Bump uv from 0.9.11 to 0.11.6 in the uv group across 1 directory (#26) Bumps the uv group with 1 update in the / directory: [uv](https://github.com/astral-sh/uv). Updates `uv` from 0.9.11 to 0.11.6 - [Release notes](https://github.com/astral-sh/uv/releases) - [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/uv/compare/0.9.11...0.11.6) --- updated-dependencies: - dependency-name: uv dependency-version: 0.11.6 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- uv.lock | 42 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02e421a..29765be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "markdown-link-attr-modifier==0.2.1", "mdx-gh-links==0.4", # Lint - "uv==0.9.11", + "uv==0.11.6", "ruff==0.14.5", "pre-commit==4.2.0", "pyright[nodejs]==1.1.407", diff --git a/uv.lock b/uv.lock index 43e6868..6803ad0 100644 --- a/uv.lock +++ b/uv.lock @@ -35,7 +35,7 @@ requires-dist = [ { name = "requests", specifier = "==2.33.0" }, { name = "ruff", specifier = "==0.14.5" }, { name = "scons", specifier = "==4.10.1" }, - { name = "uv", specifier = "==0.9.11" }, + { name = "uv", specifier = "==0.11.6" }, ] [[package]] @@ -392,28 +392,28 @@ wheels = [ [[package]] name = "uv" -version = "0.9.11" +version = "0.11.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/08/3bf76403ea7c22feef634849137fab10b28ab5ba5bbf08a53390763d5448/uv-0.9.11.tar.gz", hash = "sha256:605a7a57f508aabd029fc0c5ef5c60a556f8c50d32e194f1a300a9f4e87f18d4", size = 3744387, upload-time = "2025-11-20T23:20:00.95Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/8aceeab67ea69805293ab290e7ca8cc1b61a064d28b8a35c76d8eba063dd/uv-0.11.6.tar.gz", hash = "sha256:e3b21b7e80024c95ff339fcd147ac6fc3dd98d3613c9d45d3a1f4fd1057f127b", size = 4073298, upload-time = "2026-04-09T12:09:01.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/26/8f917e9faddd9cb49abcbc8c7dac5343b0f61d04c6ac36873d2a324fee1a/uv-0.9.11-py3-none-linux_armv6l.whl", hash = "sha256:803f85cf25ab7f1fca10fe2e40a1b9f5b1d48efc25efd6651ba3c9668db6a19e", size = 20787588, upload-time = "2025-11-20T23:18:53.738Z" }, - { url = "https://files.pythonhosted.org/packages/f5/1f/eafd39c719ddee19fc25884f68c1a7e736c0fca63c1cbef925caf8ebd739/uv-0.9.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6a31b0bd4eaec59bf97816aefbcd75cae4fcc8875c4b19ef1846b7bff3d67c70", size = 19922144, upload-time = "2025-11-20T23:18:57.569Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f3/6b9fac39e5b65fa47dba872dcf171f1470490cd645343e8334f20f73885b/uv-0.9.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48548a23fb5a103b8955dfafff7d79d21112b8e25ce5ff25e3468dc541b20e83", size = 18380643, upload-time = "2025-11-20T23:19:01.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/9a/d4080e95950a4fc6fdf20d67b9a43ffb8e3d6d6b7c8dda460ae73ddbecd9/uv-0.9.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:cb680948e678590b5960744af2ecea6f2c0307dbb74ac44daf5c00e84ad8c09f", size = 20310262, upload-time = "2025-11-20T23:19:04.914Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b4/86d9c881bd6accf2b766f7193b50e9d5815f2b34806191d90ea24967965e/uv-0.9.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ef1982295e5aaf909a9668d6fb6abfc5089666c699f585a36f3a67f1a22916a", size = 20392988, upload-time = "2025-11-20T23:19:08.258Z" }, - { url = "https://files.pythonhosted.org/packages/a3/1d/6a227b7ca1829442c1419ba1db856d176b6e0861f9bf9355a8790a5d02b5/uv-0.9.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92ff773aa4193148019533c55382c2f9c661824bbf0c2e03f12aeefc800ede57", size = 21394892, upload-time = "2025-11-20T23:19:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/5a/8f/df45b8409923121de8c4081c9d6d8ba3273eaa450645e1e542d83179c7b5/uv-0.9.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70137a46675bbecf3a8b43d292a61767f1b944156af3d0f8d5986292bd86f6cf", size = 22987735, upload-time = "2025-11-20T23:19:16.27Z" }, - { url = "https://files.pythonhosted.org/packages/89/51/bbf3248a619c9f502d310a11362da5ed72c312d354fb8f9667c5aa3be9dd/uv-0.9.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5af9117bab6c4b3a1cacb0cddfb3cd540d0adfb13c7b8a9a318873cf2d07e52", size = 22617321, upload-time = "2025-11-20T23:19:20.1Z" }, - { url = "https://files.pythonhosted.org/packages/3f/cd/a158ec989c5433dc86ebd9fea800f2aed24255b84ab65b6d7407251e5e31/uv-0.9.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8cc86940d9b3a425575f25dc45247be2fb31f7fed7bf3394ae9daadd466e5b80", size = 21615712, upload-time = "2025-11-20T23:19:23.71Z" }, - { url = "https://files.pythonhosted.org/packages/73/da/2597becbc0fcbb59608d38fda5db79969e76dedf5b072f0e8564c8f0628b/uv-0.9.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e97906ca1b90dac91c23af20e282e2e37c8eb80c3721898733928a295f2defda", size = 21661022, upload-time = "2025-11-20T23:19:27.385Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/9b8f3b3529b23c2a6f5b9612da70ea53117935ec999757b4f1d640f63d63/uv-0.9.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d901269e1db72abc974ba61d37be6e56532e104922329e0b553d9df07ba224be", size = 20440548, upload-time = "2025-11-20T23:19:31.051Z" }, - { url = "https://files.pythonhosted.org/packages/72/b2/683afdb83e96dd966eb7cf3688af56a1b826c8bc1e8182fb10ec35b3e391/uv-0.9.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8abfb7d4b136de3e92dd239ea9a51d4b7bbb970dc1b33bec84d08facf82b9a6e", size = 21493758, upload-time = "2025-11-20T23:19:34.688Z" }, - { url = "https://files.pythonhosted.org/packages/f4/00/99848bc9834aab104fa74aa1a60b1ca478dee824d2e4aacb15af85673572/uv-0.9.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1f8afc13b3b94bce1e72514c598d41623387b2b61b68d7dbce9a01a0d8874860", size = 20332324, upload-time = "2025-11-20T23:19:38.376Z" }, - { url = "https://files.pythonhosted.org/packages/6c/94/8cfd1bb1cc5d768cb334f976ba2686c6327e4ac91c16b8469b284956d4d9/uv-0.9.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7d414cfa410f1850a244d87255f98d06ca61cc13d82f6413c4f03e9e0c9effc7", size = 20845062, upload-time = "2025-11-20T23:19:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/a0/42/43f66bfc621464dabe9cfe3cbf69cddc36464da56ab786c94fc9ccf99cc7/uv-0.9.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:edc14143d0ba086a7da4b737a77746bb36bc00e3d26466f180ea99e3bf795171", size = 21857559, upload-time = "2025-11-20T23:19:46.026Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4d/bfd41bf087522601c724d712c3727aeb62f51b1f67c4ab86a078c3947525/uv-0.9.11-py3-none-win32.whl", hash = "sha256:af5fd91eecaa04b4799f553c726307200f45da844d5c7c5880d64db4debdd5dc", size = 19639246, upload-time = "2025-11-20T23:19:50.254Z" }, - { url = "https://files.pythonhosted.org/packages/2c/2f/d51c02627de68a7ca5b82f0a5d61d753beee3fe696366d1a1c5d5e40cd58/uv-0.9.11-py3-none-win_amd64.whl", hash = "sha256:c65a024ad98547e32168f3a52360fe73ff39cd609a8fb9dd2509aac91483cfc8", size = 21626822, upload-time = "2025-11-20T23:19:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/af/d8/e07e866ee328d3c9f27a6d57a018d8330f47be95ef4654a178779c968a66/uv-0.9.11-py3-none-win_arm64.whl", hash = "sha256:4907a696c745703542ed2559bdf5380b92c8b1d4bf290ebfed45bf9a2a2c6690", size = 20046856, upload-time = "2025-11-20T23:19:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fe/4b61a3d5ad9d02e8a4405026ccd43593d7044598e0fa47d892d4dafe44c9/uv-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:ada04dcf89ddea5b69d27ac9cdc5ef575a82f90a209a1392e930de504b2321d6", size = 23780079, upload-time = "2026-04-09T12:08:56.609Z" }, + { url = "https://files.pythonhosted.org/packages/52/db/d27519a9e1a5ffee9d71af1a811ad0e19ce7ab9ae815453bef39dd479389/uv-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5be013888420f96879c6e0d3081e7bcf51b539b034a01777041934457dfbedf3", size = 23214721, upload-time = "2026-04-09T12:09:32.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8f/4399fa8b882bd7e0efffc829f73ab24d117d490a93e6bc7104a50282b854/uv-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffa5dc1cbb52bdce3b8447e83d1601a57ad4da6b523d77d4b47366db8b1ceb18", size = 21750109, upload-time = "2026-04-09T12:09:24.357Z" }, + { url = "https://files.pythonhosted.org/packages/32/07/5a12944c31c3dda253632da7a363edddb869ed47839d4d92a2dc5f546c93/uv-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bfb107b4dade1d2c9e572992b06992d51dd5f2136eb8ceee9e62dd124289e825", size = 23551146, upload-time = "2026-04-09T12:09:10.439Z" }, + { url = "https://files.pythonhosted.org/packages/79/5b/2ec8b0af80acd1016ed596baf205ddc77b19ece288473b01926c4a9cf6db/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:9e2fe7ce12161d8016b7deb1eaad7905a76ff7afec13383333ca75e0c4b5425d", size = 23331192, upload-time = "2026-04-09T12:09:34.792Z" }, + { url = "https://files.pythonhosted.org/packages/62/7d/eea35935f2112b21c296a3e42645f3e4b1aa8bcd34dcf13345fbd55134b7/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ed9c6f70c25e8dfeedddf4eddaf14d353f5e6b0eb43da9a14d3a1033d51d915", size = 23337686, upload-time = "2026-04-09T12:09:18.522Z" }, + { url = "https://files.pythonhosted.org/packages/21/47/2584f5ab618f6ebe9bdefb2f765f2ca8540e9d739667606a916b35449eec/uv-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68a013e609cebf82077cbeeb0809ed5e205257814273bfd31e02fc0353bbfc2", size = 25008139, upload-time = "2026-04-09T12:09:03.983Z" }, + { url = "https://files.pythonhosted.org/packages/95/81/497ae5c1d36355b56b97dc59f550c7e89d0291c163a3f203c6f341dff195/uv-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f736dddca03dae732c6fdea177328d3bc4bf137c75248f3d433c57416a4311", size = 25712458, upload-time = "2026-04-09T12:09:07.598Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1c/74083238e4fab2672b63575b9008f1ea418b02a714bcfcf017f4f6a309b6/uv-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e96a66abe53fced0e3389008b8d2eff8278cfa8bb545d75631ae8ceb9c929aba", size = 24915507, upload-time = "2026-04-09T12:08:50.892Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ee/e14fe10ba455a823ed18233f12de6699a601890905420b5c504abf115116/uv-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b096311b2743b228df911a19532b3f18fa420bf9530547aecd6a8e04bbfaccd", size = 24971011, upload-time = "2026-04-09T12:08:54.016Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/7b9c83eaadf98e343317ff6384a7227a4855afd02cdaf9696bcc71ee6155/uv-0.11.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:904d537b4a6e798015b4a64ff5622023bd4601b43b6cd1e5f423d63471f5e948", size = 23640234, upload-time = "2026-04-09T12:09:15.735Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/75ccdd23e76ff1703b70eb82881cd5b4d2a954c9679f8ef7e0136ef2cfab/uv-0.11.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4ed8150c26b5e319381d75ae2ce6aba1e9c65888f4850f4e3b3fa839953c90a5", size = 24452664, upload-time = "2026-04-09T12:09:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/4d/86/ace80fe47d8d48b5e3b5aee0b6eb1a49deaacc2313782870250b3faa36f5/uv-0.11.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c9218c8d4ac35ca6e617fb0951cc0ab2d907c91a6aea2617de0a5494cf162c0", size = 24494599, upload-time = "2026-04-09T12:09:37.368Z" }, + { url = "https://files.pythonhosted.org/packages/05/2d/4b642669b56648194f026de79bc992cbfc3ac2318b0a8d435f3c284934e8/uv-0.11.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9e211c83cc890c569b86a4183fcf5f8b6f0c7adc33a839b699a98d30f1310d3a", size = 24159150, upload-time = "2026-04-09T12:09:13.17Z" }, + { url = "https://files.pythonhosted.org/packages/ae/24/7eecd76fe983a74fed1fc700a14882e70c4e857f1d562a9f2303d4286c12/uv-0.11.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1d2089afdf117ad19a4c1dd36b8189c00ae1ad4135d3bfbfced82342595cf", size = 25164324, upload-time = "2026-04-09T12:08:59.56Z" }, + { url = "https://files.pythonhosted.org/packages/27/e0/bbd4ba7c2e5067bbba617d87d306ec146889edaeeaa2081d3e122178ca08/uv-0.11.6-py3-none-win32.whl", hash = "sha256:6e8344f38fa29f85dcfd3e62dc35a700d2448f8e90381077ef393438dcd5012e", size = 22865693, upload-time = "2026-04-09T12:09:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/a5/33/1983ce113c538a856f2d620d16e39691962ecceef091a84086c5785e32e5/uv-0.11.6-py3-none-win_amd64.whl", hash = "sha256:a28bea69c1186303d1200f155c7a28c449f8a4431e458fcf89360cc7ef546e40", size = 25371258, upload-time = "2026-04-09T12:09:40.52Z" }, + { url = "https://files.pythonhosted.org/packages/35/01/be0873f44b9c9bc250fcbf263367fcfc1f59feab996355bcb6b52fff080d/uv-0.11.6-py3-none-win_arm64.whl", hash = "sha256:a78f6d64b9950e24061bc7ec7f15ff8089ad7f5a976e7b65fcadce58fe02f613", size = 23869585, upload-time = "2026-04-09T12:09:29.425Z" }, ] [[package]] From dab00d50bccbc5d226fd0dde7a2029cdf306b082 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:10:45 +1000 Subject: [PATCH 76/90] Bump softprops/action-gh-release from 2 to 3 (#27) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build_addon.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 62ea4f4..bbd563f 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -73,7 +73,7 @@ jobs: sha256sum *.nvda-addon >> changelog.md - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: | *.nvda-addon From e699abc415a5e65ef0ac4eeaf2655efa1d773bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sat, 18 Apr 2026 07:04:49 +0200 Subject: [PATCH 77/90] Improve workflow to download files from Crowdin --- .github/workflows/crowdinL10n.yml | 37 +++++-------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index a875a6a..a9e23db 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -3,12 +3,6 @@ name: Crowdin l10n on: workflow_dispatch: - inputs: - dry-run: - description: 'Dry run mode (skip Crowdin upload/download)' - required: false - type: boolean - default: false schedule: # Every Monday at 00:00 UTC - cron: '0 0 * * 1' @@ -19,10 +13,7 @@ env: crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} downloadTranslationsBranch: l10n jobs: - build: - uses: ./.github/workflows/build_addon.yml crowdinSync: - needs: build runs-on: windows-latest permissions: contents: write @@ -44,25 +35,10 @@ jobs: uv sync uv run ./.github/workflows/setOutputs.py - name: Download l10nUtil from nvdal10n - if: ${{ inputs.dry-run != true }} run: | - gh run download --repo nvaccess/nvdal10n --name l10nUtil --dir _l10n - - name: Upload md - if: ${{ (steps.getAddonInfo.outputs.shouldAddMdFromScratch == 'true' || steps.getAddonInfo.outputs.shouldUpdateMd == 'true') && inputs.dry-run != true }} - run: | - Move-Item -Path readme.md -Destination "${{ steps.getAddonInfo.outputs.addonId }}.md" - uv run ./_l10n/l10nUtil.exe uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.md -c addon - - name: Download pot from build job - if: ${{ inputs.dry-run != true }} - uses: actions/download-artifact@v8 - with: - name: packaged_addon - - name: Upload pot - if: ${{ (steps.getAddonInfo.outputs.shouldAddPotFromScratch == 'true' || steps.getAddonInfo.outputs.shouldUpdatePot == 'true') && inputs.dry-run != true }} - run: | - uv run ./_l10n/l10nUtil.exe uploadSourceFile --localFilePath=${{ steps.getAddonInfo.outputs.addonId }}.pot -c addon + # Download the latest release asset matching the pattern from the specified repository + gh release download --repo nvaccess/nvdaL10n --pattern "l10nUtil.exe" - name: Download translations from Crowdin - if: ${{ inputs.dry-run != true }} run: | uv run _l10n/l10nUtil.exe exportTranslations -o _addonL10n -c addon New-Item -ItemType Directory -Force -Path addon/locale | Out-Null @@ -77,12 +53,11 @@ jobs: Write-Host "Moving $poFile to $targetDir/nvda.po" Move-Item -Path $poFile -Destination "$targetDir/nvda.po" -Force } - $mdFile = Join-Path $dir.FullName "${{ steps.getAddonInfo.outputs.addonId }}.md" - if (Test-Path -PathType Leaf $mdFile) { + $xliffFile = Join-Path $dir.FullName "${{ steps.getAddonInfo.outputs.addonId }}.xliff" + if (Test-Path -PathType Leaf $xliffFile) { $targetDir = "addon/doc/$langCode" - New-Item -ItemType Directory -Force -Path $targetDir | Out-Null - Write-Host "Moving $mdFile to $targetDir/readme.md" - Move-Item -Path $mdFile -Destination "$targetDir/readme.md" -Force + Write-Host "Moving $xliffFile to $targetDir/readme.md" + uv run l10nUtil.exe xliff2md $xliffFile "$targetDir/readme.md" } } git add addon/locale addon/doc From 5eafa641d1bb7a75563842f11f850122c41a6351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Sat, 18 Apr 2026 07:09:08 +0200 Subject: [PATCH 78/90] Update setOutputs to set just add-on id to download translations --- .github/workflows/setOutputs.py | 40 +-------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/.github/workflows/setOutputs.py b/.github/workflows/setOutputs.py index d8cce3b..a53aeb0 100644 --- a/.github/workflows/setOutputs.py +++ b/.github/workflows/setOutputs.py @@ -4,55 +4,17 @@ import os import sys -import json sys.path.insert(0, os.getcwd()) import buildVars -import sha256 def main(): addonId = buildVars.addon_info["addon_name"] - readmeFile = os.path.join(os.getcwd(), "readme.md") - i18nSources = sorted(buildVars.i18nSources) - readmeSha = None - i18nSourcesSha = None - shouldUpdateMd = False - shouldUpdatePot = False - shouldAddMdFromScratch = False - shouldAddPotFromScratch = False - if os.path.isfile(readmeFile): - readmeSha = sha256.sha256_checksum([readmeFile]) - i18nSourcesSha = sha256.sha256_checksum(i18nSources) - hashFile = os.path.join(os.getcwd(), "hash.json") - data = dict() - if os.path.isfile(hashFile): - with open(hashFile, "rt") as f: - data = json.load(f) - shouldUpdateMd = data.get("readmeSha") != readmeSha and data.get("readmeSha") is not None - shouldUpdatePot = ( - data.get("i18nSourcesSha") != i18nSourcesSha and data.get("i18nSourcesSha") is not None - ) - shouldAddMdFromScratch = data.get("readmeSha") is None - shouldAddPotFromScratch = data.get("i18nSourcesSha") is None - if readmeSha is not None: - data["readmeSha"] = readmeSha - if i18nSourcesSha is not None: - data["i18nSourcesSha"] = i18nSourcesSha - with open(hashFile, "wt", encoding="utf-8") as f: - json.dump(data, f, indent="\t", ensure_ascii=False) name = "addonId" value = addonId - name0 = "shouldUpdateMd" - value0 = str(shouldUpdateMd).lower() - name1 = "shouldUpdatePot" - value1 = str(shouldUpdatePot).lower() - name2 = "shouldAddMdFromScratch" - value2 = str(shouldAddMdFromScratch).lower() - name3 = "shouldAddPotFromScratch" - value3 = str(shouldAddPotFromScratch).lower() with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write(f"{name}={value}\n{name0}={value0}\n{name1}={value1}\n{name2}={value2}\n{name3}={value3}\n") + f.write(f"{name}={value}\n") if __name__ == "__main__": From 0eb3223e395c6c319d8c06c5162ce9dff840c013 Mon Sep 17 00:00:00 2001 From: Abdel792 Date: Mon, 20 Apr 2026 06:48:46 +0200 Subject: [PATCH 79/90] [crowdinL10n.yml] improve CI translation workflow - move Python helper scripts from .github/workflows to .github/scripts for better separation of concerns - add polib dependency and switch to uv sync for reproducible CI environment - fix missing GH_TOKEN required for GitHub CLI (gh) commands - fix l10nUtil.exe path resolution (use ./l10nUtil.exe instead of _l10n/l10nUtil.exe) - improve Crowdin download behavior by avoiding processing empty translation files - refine PO handling: preserve local translations, conditionally upload to Crowdin when needed - refine XLIFF handling: update local documentation only, no upload back to Crowdin - ensure safer, more deterministic, and more predictable translation synchronization logic --- .github/scripts/checkTranslation.py | 106 +++++++++++++++++++ .github/scripts/setOutputs.py | 21 ++++ .github/workflows/crowdinL10n.yml | 153 +++++++++++++++++++++++----- pyproject.toml | 1 + 4 files changed, 253 insertions(+), 28 deletions(-) create mode 100644 .github/scripts/checkTranslation.py create mode 100644 .github/scripts/setOutputs.py diff --git a/.github/scripts/checkTranslation.py b/.github/scripts/checkTranslation.py new file mode 100644 index 0000000..5d9e5a4 --- /dev/null +++ b/.github/scripts/checkTranslation.py @@ -0,0 +1,106 @@ +import sys +import os +import xml.etree.ElementTree as ET +import polib + + +def normalize(s: str) -> str: + # Normalize strings for reliable comparison (trim, lowercase, collapse spaces) + return " ".join((s or "").strip().lower().split()) + + +# ----------------------------- +# PO FILE CHECK +# ----------------------------- +def checkPo(path: str) -> float: + # Parse PO file using polib + po = polib.pofile(path) + + translated = 0 + total = 0 + + for entry in po: + # Skip empty msgid entries + if not entry.msgid.strip(): + continue + + total += 1 + + # Consider entry translated only if msgstr differs from msgid + if entry.msgstr and normalize(entry.msgstr) != normalize(entry.msgid): + translated += 1 + + return translated / total if total else 0.0 + + +# ----------------------------- +# XLIFF CHECK (skeleton-safe generic parsing) +# ----------------------------- +def checkXliff(path: str) -> float: + # Parse XML XLIFF file + tree = ET.parse(path) + root = tree.getroot() + + translated = 0 + total = 0 + + source = None + + for elem in root.iter(): + + # Capture source segments + if elem.tag.endswith("source"): + source = normalize(elem.text) + + # Compare with target segments + elif elem.tag.endswith("target"): + target = normalize(elem.text) + + if source: + total += 1 + + # Count as translated only if target differs from source + if target and target != source: + translated += 1 + + return translated / total if total else 0.0 + + +# ----------------------------- +# MAIN ENTRY POINT +# ----------------------------- +def main(): + if len(sys.argv) < 2: + print("Usage: checkTranslation.py ") + sys.exit(2) + + path = sys.argv[1] + + if not os.path.exists(path): + print(f"File not found: {path}") + sys.exit(2) + + ext = os.path.splitext(path)[1].lower() + + # Dispatch based on file type + if ext == ".po": + ratio = checkPo(path) + + elif ext in [".xliff", ".xlf"]: + ratio = checkXliff(path) + + else: + print(f"Unsupported file type: {ext}") + sys.exit(2) + + print(f"translation_ratio={ratio}") + + # Threshold: consider file translated if above 5% + if ratio > 0.05: + sys.exit(0) # translated + else: + sys.exit(1) # not translated + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/scripts/setOutputs.py b/.github/scripts/setOutputs.py new file mode 100644 index 0000000..a53aeb0 --- /dev/null +++ b/.github/scripts/setOutputs.py @@ -0,0 +1,21 @@ +# Copyright (C) 2025 NV Access Limited, Noelia Ruiz Martínez +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +import os +import sys + +sys.path.insert(0, os.getcwd()) +import buildVars + + +def main(): + addonId = buildVars.addon_info["addon_name"] + name = "addonId" + value = addonId + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"{name}={value}\n") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index a9e23db..be509ad 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -1,71 +1,168 @@ name: Crowdin l10n on: - workflow_dispatch: schedule: # Every Monday at 00:00 UTC - cron: '0 0 * * 1' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true + env: crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }} downloadTranslationsBranch: l10n + GH_TOKEN: ${{ github.token }} + jobs: crowdinSync: runs-on: windows-latest permissions: contents: write + steps: - name: Checkout add-on uses: actions/checkout@v6 with: submodules: true - - name: "Set up Python" + + - name: Set up Python uses: actions/setup-python@v6 with: python-version-file: ".python-version" - - name: Install the latest version of uv + + - name: Install uv uses: astral-sh/setup-uv@v7 + + - name: Install dependencies + run: uv sync + - name: Get add-on info id: getAddonInfo shell: pwsh + run: uv run ./.github/scripts/setOutputs.py + + - name: Download l10nUtil run: | - uv sync - uv run ./.github/workflows/setOutputs.py - - name: Download l10nUtil from nvdal10n - run: | - # Download the latest release asset matching the pattern from the specified repository - gh release download --repo nvaccess/nvdaL10n --pattern "l10nUtil.exe" + gh release download --repo nvaccess/nvdaL10n --pattern "l10nUtil.exe" + - name: Download translations from Crowdin + shell: pwsh run: | - uv run _l10n/l10nUtil.exe exportTranslations -o _addonL10n -c addon + ./l10nUtil.exe exportTranslations -o _addonL10n -c addon + New-Item -ItemType Directory -Force -Path addon/locale | Out-Null New-Item -ItemType Directory -Force -Path addon/doc | Out-Null - foreach ($dir in Get-ChildItem -Path "_addonL10n/${{ steps.getAddonInfo.outputs.addonId }}" -Directory) { - Write-Host "Processing: $($dir.FullName)" + + $addonId = "${{ steps.getAddonInfo.outputs.addonId }}" + + foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) { + + Write-Host "==============================" + Write-Host "Processing: $($dir.Name)" + Write-Host "==============================" + + # ============================ + # LANG SETUP (IMPORTANT) + # ============================ $langCode = $dir.Name - $poFile = Join-Path $dir.FullName "${{ steps.getAddonInfo.outputs.addonId }}.po" + + # ============================ + # FILE PATHS + # ============================ + $poFile = Join-Path $dir.FullName "$addonId.po" + $localPoPath = "addon/locale/$langCode/LC_MESSAGES/nvda.po" + + $xliffFile = Join-Path $dir.FullName "$addonId.xliff" + $targetDocDir = "addon/doc/$langCode" + $readmePath = "$targetDocDir/readme.md" + + # ============================ + # PO PROCESSING + # ============================ if (Test-Path -PathType Leaf $poFile) { - $targetDir = "addon/locale/$langCode/LC_MESSAGES" - New-Item -ItemType Directory -Force -Path $targetDir | Out-Null - Write-Host "Moving $poFile to $targetDir/nvda.po" - Move-Item -Path $poFile -Destination "$targetDir/nvda.po" -Force + + Write-Host "Running PO translation check..." + + uv run ./.github/scripts/checkTranslation.py "$poFile" + $isTranslated = ($LASTEXITCODE -eq 0) + + if ($isTranslated) { + + Write-Host "PO translated → updating local repo" + + $targetDir = "addon/locale/$langCode/LC_MESSAGES" + New-Item -ItemType Directory -Force -Path $targetDir | Out-Null + + Move-Item -Path $poFile -Destination "$targetDir/nvda.po" -Force + + } else { + + Write-Host "PO not translated → skipping local update" + + $addonName = "${{ steps.getAddonInfo.outputs.addonId }}" + $crowdinFile = "$addonName.po" + + if (Test-Path -PathType Leaf $localPoPath) { + + Write-Host "Uploading local PO to Crowdin (no translations found remotely)" + + ./l10nUtil.exe uploadTranslationFile $langCode $crowdinFile $localPoPath -c addon + + } else { + + Write-Host "Local PO file does not exist → skipping upload" + } + } } - $xliffFile = Join-Path $dir.FullName "${{ steps.getAddonInfo.outputs.addonId }}.xliff" + + # ============================ + # XLIFF PROCESSING + # ============================ + $isXliffTranslated = $false + if (Test-Path -PathType Leaf $xliffFile) { - $targetDir = "addon/doc/$langCode" - Write-Host "Moving $xliffFile to $targetDir/readme.md" - uv run l10nUtil.exe xliff2md $xliffFile "$targetDir/readme.md" + + Write-Host "Running XLIFF translation check..." + + uv run ./.github/scripts/checkTranslation.py "$xliffFile" + $isXliffTranslated = ($LASTEXITCODE -eq 0) + + if ($isXliffTranslated) { + + Write-Host "XLIFF translated → updating README" + + New-Item -ItemType Directory -Force -Path $targetDocDir | Out-Null + + uv run l10nUtil.exe xliff2md $xliffFile $readmePath + + } else { + + Write-Host "XLIFF not translated → skipping README update" + } + + Write-Host "XLIFF translation result: $isXliffTranslated" } } + + # ============================ + # COMMIT CHANGES + # ============================ + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add addon/locale addon/doc - $diff = git diff --staged --quiet - if ($LASTEXITCODE -eq 0) { - Write-Host "Nothing added to commit." - } else { - git commit -m "Update translations for ${{ steps.getAddonInfo.outputs.addonId }}" - git checkout -b ${{ env.downloadTranslationsBranch }} + + git diff --staged --quiet + if ($LASTEXITCODE -ne 0) { + git commit -m "Update translations for $addonId from Crowdin" + git switch ${{ env.downloadTranslationsBranch }} 2>$null + + if ($LASTEXITCODE -ne 0) { + git switch -c ${{ env.downloadTranslationsBranch }} + } git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} - } + } else { + Write-Host "Nothing to commit." + } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index aa8752d..1f06e05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "mdx_truly_sane_lists==1.3", "markdown-link-attr-modifier==0.2.1", "mdx-gh-links==0.4", + "polib", # Lint "uv==0.9.11", "ruff==0.14.5", From a1951107922df6e59d3be324a5f8676c3e8dc7bd Mon Sep 17 00:00:00 2001 From: Abdel792 Date: Mon, 20 Apr 2026 08:17:14 +0200 Subject: [PATCH 80/90] Remove duplicate setOutputs.py from .github/workflows The script is already available in .github/scripts/. --- .github/workflows/setOutputs.py | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .github/workflows/setOutputs.py diff --git a/.github/workflows/setOutputs.py b/.github/workflows/setOutputs.py deleted file mode 100644 index a53aeb0..0000000 --- a/.github/workflows/setOutputs.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2025 NV Access Limited, Noelia Ruiz Martínez -# This file is covered by the GNU General Public License. -# See the file COPYING for more details. - -import os -import sys - -sys.path.insert(0, os.getcwd()) -import buildVars - - -def main(): - addonId = buildVars.addon_info["addon_name"] - name = "addonId" - value = addonId - with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write(f"{name}={value}\n") - - -if __name__ == "__main__": - main() From e62bd5bfc84e9eb6c028c7820b448d251b053c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 18:40:14 +0200 Subject: [PATCH 81/90] Update lock --- uv.lock | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/uv.lock b/uv.lock index e0def29..0ac096f 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,7 @@ dependencies = [ { name = "mdx-gh-links" }, { name = "mdx-truly-sane-lists" }, { name = "nh3" }, + { name = "polib" }, { name = "pre-commit" }, { name = "pyright", extra = ["nodejs"] }, { name = "requests" }, @@ -30,6 +31,7 @@ requires-dist = [ { name = "mdx-gh-links", specifier = "==0.4" }, { name = "mdx-truly-sane-lists", specifier = "==1.3" }, { name = "nh3", specifier = "==0.3.2" }, + { name = "polib" }, { name = "pre-commit", specifier = "==4.2.0" }, { name = "pyright", extras = ["nodejs"], specifier = "==1.1.407" }, { name = "requests", specifier = "==2.32.5" }, @@ -270,6 +272,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, ] +[[package]] +name = "polib" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/9a/79b1067d27e38ddf84fe7da6ec516f1743f31f752c6122193e7bce38bdbf/polib-1.2.0.tar.gz", hash = "sha256:f3ef94aefed6e183e342a8a269ae1fc4742ba193186ad76f175938621dbfc26b", size = 161658, upload-time = "2023-02-23T17:53:56.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/99/45bb1f9926efe370c6dbe324741c749658e44cb060124f28dad201202274/polib-1.2.0-py2.py3-none-any.whl", hash = "sha256:1c77ee1b81feb31df9bca258cbc58db1bbb32d10214b173882452c73af06d62d", size = 20634, upload-time = "2023-02-23T17:53:59.919Z" }, +] + [[package]] name = "pre-commit" version = "4.2.0" From 91995bd24a9448bc95ae2bf6b33c0427aa2968be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 19:13:08 +0200 Subject: [PATCH 82/90] Try to fix pre-commit configuration --- .github/scripts/checkTranslation.py | 117 ++++++++++++++-------------- .github/scripts/setOutputs.py | 2 +- .github/workflows/crowdinL10n.yml | 2 +- .pre-commit-config.yaml | 5 +- sha256.py | 11 ++- 5 files changed, 68 insertions(+), 69 deletions(-) diff --git a/.github/scripts/checkTranslation.py b/.github/scripts/checkTranslation.py index 5d9e5a4..bf39e81 100644 --- a/.github/scripts/checkTranslation.py +++ b/.github/scripts/checkTranslation.py @@ -4,103 +4,102 @@ import polib -def normalize(s: str) -> str: - # Normalize strings for reliable comparison (trim, lowercase, collapse spaces) - return " ".join((s or "").strip().lower().split()) +def normalize(s: str | None) -> str: + # Normalize strings for reliable comparison (trim, lowercase, collapse spaces) + return " ".join((s or "").strip().lower().split()) # ----------------------------- # PO FILE CHECK # ----------------------------- def checkPo(path: str) -> float: - # Parse PO file using polib - po = polib.pofile(path) + # Parse PO file using polib + po = polib.pofile(path) - translated = 0 - total = 0 + translated = 0 + total = 0 - for entry in po: - # Skip empty msgid entries - if not entry.msgid.strip(): - continue + for entry in po: + # Skip empty msgid entries + if not entry.msgid.strip(): + continue - total += 1 + total += 1 - # Consider entry translated only if msgstr differs from msgid - if entry.msgstr and normalize(entry.msgstr) != normalize(entry.msgid): - translated += 1 + # Consider entry translated only if msgstr differs from msgid + if entry.msgstr and normalize(entry.msgstr) != normalize(entry.msgid): + translated += 1 - return translated / total if total else 0.0 + return translated / total if total else 0.0 # ----------------------------- # XLIFF CHECK (skeleton-safe generic parsing) # ----------------------------- def checkXliff(path: str) -> float: - # Parse XML XLIFF file - tree = ET.parse(path) - root = tree.getroot() + # Parse XML XLIFF file + tree = ET.parse(path) + root = tree.getroot() - translated = 0 - total = 0 + translated = 0 + total = 0 - source = None + source = None - for elem in root.iter(): + for elem in root.iter(): + # Capture source segments + if elem.tag.endswith("source"): + source = normalize(elem.text) - # Capture source segments - if elem.tag.endswith("source"): - source = normalize(elem.text) + # Compare with target segments + elif elem.tag.endswith("target"): + target = normalize(elem.text) - # Compare with target segments - elif elem.tag.endswith("target"): - target = normalize(elem.text) + if source: + total += 1 - if source: - total += 1 + # Count as translated only if target differs from source + if target and target != source: + translated += 1 - # Count as translated only if target differs from source - if target and target != source: - translated += 1 - - return translated / total if total else 0.0 + return translated / total if total else 0.0 # ----------------------------- # MAIN ENTRY POINT # ----------------------------- def main(): - if len(sys.argv) < 2: - print("Usage: checkTranslation.py ") - sys.exit(2) + if len(sys.argv) < 2: + print("Usage: checkTranslation.py ") + sys.exit(2) - path = sys.argv[1] + path = sys.argv[1] - if not os.path.exists(path): - print(f"File not found: {path}") - sys.exit(2) + if not os.path.exists(path): + print(f"File not found: {path}") + sys.exit(2) - ext = os.path.splitext(path)[1].lower() + ext = os.path.splitext(path)[1].lower() - # Dispatch based on file type - if ext == ".po": - ratio = checkPo(path) + # Dispatch based on file type + if ext == ".po": + ratio = checkPo(path) - elif ext in [".xliff", ".xlf"]: - ratio = checkXliff(path) + elif ext in [".xliff", ".xlf"]: + ratio = checkXliff(path) - else: - print(f"Unsupported file type: {ext}") - sys.exit(2) + else: + print(f"Unsupported file type: {ext}") + sys.exit(2) - print(f"translation_ratio={ratio}") + print(f"translation_ratio={ratio}") - # Threshold: consider file translated if above 5% - if ratio > 0.05: - sys.exit(0) # translated - else: - sys.exit(1) # not translated + # Threshold: consider file translated if above 5% + if ratio > 0.05: + sys.exit(0) # translated + else: + sys.exit(1) # not translated if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/.github/scripts/setOutputs.py b/.github/scripts/setOutputs.py index a53aeb0..a5d9161 100644 --- a/.github/scripts/setOutputs.py +++ b/.github/scripts/setOutputs.py @@ -14,7 +14,7 @@ def main(): name = "addonId" value = addonId with open(os.environ["GITHUB_OUTPUT"], "a") as f: - f.write(f"{name}={value}\n") + _ = f.write(f"{name}={value}\n") if __name__ == "__main__": diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index be509ad..8378d51 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -165,4 +165,4 @@ jobs: git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} } else { Write-Host "Nothing to commit." - } \ No newline at end of file + } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ea70058..e8c5026 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,10 +76,11 @@ repos: args: [ --fix ] - id: ruff-format name: format with ruff +- repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.11.4 + hooks: - id: uv-lock name: Verify uv lock file - # Override python interpreter from .python-versions as that is too strict for pre-commit.ci - args: ["-p3.13"] - repo: local hooks: diff --git a/sha256.py b/sha256.py index 51c903b..d2d455b 100644 --- a/sha256.py +++ b/sha256.py @@ -17,17 +17,16 @@ def sha256_checksum(binaryReadModeFiles: list[typing.BinaryIO], blockSize: int = :return: The Sha256 hex digest. """ sha256 = hashlib.sha256() - for f in binaryReadModeFiles: - with open(f, "rb") as file: - assert file.readable() and file.mode == "rb" - for block in iter(lambda: file.read(blockSize), b""): - sha256.update(block) + for file in binaryReadModeFiles: + assert file.readable() and file.mode == "rb" + for block in iter(lambda: file.read(blockSize), b""): + sha256.update(block) return sha256.hexdigest() def main(): parser = argparse.ArgumentParser() - parser.add_argument( + _ = parser.add_argument( type=argparse.FileType("rb"), dest="file", help="The NVDA addon (*.nvda-addon) to use when computing the sha256.", From 0c613be08597470b38cc03fb0fd2ed62f3aaea52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 19:53:00 +0200 Subject: [PATCH 83/90] Reset gitignore to master --- .gitignore | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 86b975e..a6ccee5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,12 @@ -# Python-generated files -__pycache__/ -*.py[oc] -build/ -dist/ -wheels/ -*.egg-info - -# Virtual environments -.venv - -# Files generated for add-ons addon/doc/*.css addon/doc/en/ *_docHandler.py *.html -addon/*.ini -addon/locale/*/*.ini +manifest.ini *.mo *.pot -*.pyc +*.py[co] *.nvda-addon .sconsign.dblite +/[0-9]*.[0-9]*.[0-9]*.json *.egg-info From 0b6507252d3e84641c437bbb1996766fdf86ec1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 20:00:46 +0200 Subject: [PATCH 84/90] Require polib 1.2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 43e1e84..b9233f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "mdx_truly_sane_lists==1.3", "markdown-link-attr-modifier==0.2.1", "mdx-gh-links==0.4", - "polib", + "polib==1.2.0", # Lint "uv==0.11.6", "ruff==0.14.5", From f9ba8fee37e8d5d0ee06faabe34f8950ff7ad2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 20:01:24 +0200 Subject: [PATCH 85/90] Update lock file --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 2b8a928..5176c15 100644 --- a/uv.lock +++ b/uv.lock @@ -31,7 +31,7 @@ requires-dist = [ { name = "mdx-gh-links", specifier = "==0.4" }, { name = "mdx-truly-sane-lists", specifier = "==1.3" }, { name = "nh3", specifier = "==0.3.2" }, - { name = "polib" }, + { name = "polib", specifier = "==1.2.0" }, { name = "pre-commit", specifier = "==4.2.0" }, { name = "pyright", extras = ["nodejs"], specifier = "==1.1.407" }, { name = "requests", specifier = "==2.33.0" }, From 8e514ba304e452d97d03afebf5121107e91f68eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 20:08:27 +0200 Subject: [PATCH 86/90] Remove sha256 file --- sha256.py | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 sha256.py diff --git a/sha256.py b/sha256.py deleted file mode 100644 index d2d455b..0000000 --- a/sha256.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (C) 2020-2025 NV Access Limited, Noelia Ruiz Martínez -# This file may be used under the terms of the GNU General Public License, version 2 or later. -# For more details see: https://www.gnu.org/licenses/gpl-2.0.html - -import argparse -import hashlib -import typing - -#: The read size for each chunk read from the file, prevents memory overuse with large files. -BLOCK_SIZE = 65536 - - -def sha256_checksum(binaryReadModeFiles: list[typing.BinaryIO], blockSize: int = BLOCK_SIZE): - """ - :param binaryReadModeFiles: A list of files (mode=='rb'). Calculate its sha256 hash. - :param blockSize: The size of each read. - :return: The Sha256 hex digest. - """ - sha256 = hashlib.sha256() - for file in binaryReadModeFiles: - assert file.readable() and file.mode == "rb" - for block in iter(lambda: file.read(blockSize), b""): - sha256.update(block) - return sha256.hexdigest() - - -def main(): - parser = argparse.ArgumentParser() - _ = parser.add_argument( - type=argparse.FileType("rb"), - dest="file", - help="The NVDA addon (*.nvda-addon) to use when computing the sha256.", - ) - args = parser.parse_args() - checksum = sha256_checksum(args.file) - print(f"Sha256:\t {checksum}") - - -if __name__ == "__main__": - main() From 9c642478ec4ac0dc9ec0497684c612a2535c009f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 20:13:41 +0200 Subject: [PATCH 87/90] Remove verification of lock file in pre-commit config, not present in master branch --- .pre-commit-config.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e8c5026..f4c3e91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,11 +76,6 @@ repos: args: [ --fix ] - id: ruff-format name: format with ruff -- repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.11.4 - hooks: - - id: uv-lock - name: Verify uv lock file - repo: local hooks: From 7589fef76911f4554a6309482199ba44ea50808e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 20:21:49 +0200 Subject: [PATCH 88/90] Remove Crowdin client --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9233f1..3d10490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,9 +25,7 @@ dependencies = [ "scons==4.10.1", "Markdown==3.10", # Translations management - "requests==2.33.0", "nh3==0.3.2", - "crowdin-api-client==1.24.1", "lxml==6.0.2", "mdx_truly_sane_lists==1.3", "markdown-link-attr-modifier==0.2.1", From 94b1a88c995b6904c2505c1d04a3f6b55c2de25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noelia=20Ruiz=20Mart=C3=ADnez?= Date: Mon, 20 Apr 2026 20:22:08 +0200 Subject: [PATCH 89/90] Update lock file --- uv.lock | 129 -------------------------------------------------------- 1 file changed, 129 deletions(-) diff --git a/uv.lock b/uv.lock index 5176c15..0701df0 100644 --- a/uv.lock +++ b/uv.lock @@ -6,7 +6,6 @@ requires-python = "==3.13.*" name = "addontemplate" source = { editable = "." } dependencies = [ - { name = "crowdin-api-client" }, { name = "lxml" }, { name = "markdown" }, { name = "markdown-link-attr-modifier" }, @@ -16,7 +15,6 @@ dependencies = [ { name = "polib" }, { name = "pre-commit" }, { name = "pyright", extra = ["nodejs"] }, - { name = "requests" }, { name = "ruff" }, { name = "scons" }, { name = "uv" }, @@ -24,7 +22,6 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "crowdin-api-client", specifier = "==1.24.1" }, { name = "lxml", specifier = "==6.0.2" }, { name = "markdown", specifier = "==3.10" }, { name = "markdown-link-attr-modifier", specifier = "==0.2.1" }, @@ -34,21 +31,11 @@ requires-dist = [ { name = "polib", specifier = "==1.2.0" }, { name = "pre-commit", specifier = "==4.2.0" }, { name = "pyright", extras = ["nodejs"], specifier = "==1.1.407" }, - { name = "requests", specifier = "==2.33.0" }, { name = "ruff", specifier = "==0.14.5" }, { name = "scons", specifier = "==4.10.1" }, { name = "uv", specifier = "==0.11.6" }, ] -[[package]] -name = "certifi" -version = "2025.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, -] - [[package]] name = "cfgv" version = "3.5.0" @@ -58,56 +45,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "crowdin-api-client" -version = "1.24.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/fc/ec5564928057aac9cae7e78ed324898b3134369b100bbb2b5c97ad1ad548/crowdin_api_client-1.24.1.tar.gz", hash = "sha256:d2a385c2b3f8e985d5bb084524ae14aef9045094fba0b2df1df82d9da97155b1", size = 70629, upload-time = "2025-08-26T13:20:34.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/74/118d8f5e592a1fe75b793346a599d57746b18b8875c31e956022b63ba173/crowdin_api_client-1.24.1-py3-none-any.whl", hash = "sha256:a07365a2a0d42830ee4eb188e3820603e1420421575637b1ddd8dffe1d2fe14c", size = 109654, upload-time = "2025-08-26T13:20:33.673Z" }, -] - -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -135,15 +72,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, ] -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - [[package]] name = "lxml" version = "6.0.2" @@ -333,21 +261,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, ] -[[package]] -name = "requests" -version = "2.33.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, -] - [[package]] name = "ruff" version = "0.14.5" @@ -392,15 +305,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - [[package]] name = "uv" version = "0.11.6" @@ -440,36 +344,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544 wheels = [ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] - -[[package]] -name = "wrapt" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" }, - { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" }, - { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" }, - { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" }, - { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" }, - { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" }, - { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" }, - { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" }, - { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" }, - { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" }, - { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" }, - { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" }, - { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, -] From 65290e4643019111e91b5f658eae3da776c55ffa Mon Sep 17 00:00:00 2001 From: Abdel792 Date: Thu, 23 Apr 2026 00:27:29 +0200 Subject: [PATCH 90/90] Enhance Crowdin l10n workflow with MD quality evaluation and comparison logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Markdown language scoring (langid) in checkTranslation.py - Extend script to support MD files and optional multi-file comparison - Update workflow to handle XLIFF → MD conversion only when translated - Implement multi-source comparison (XLIFF MD, remote MD, local MD) - Apply best-quality selection before updating or uploading files - Add full logging for all decision branches - Improve fallback behavior when only one source is available --- .github/scripts/checkTranslation.py | 132 +++++++++++++----- .github/workflows/crowdinL10n.yml | 206 +++++++++++++++++++++------- pyproject.toml | 1 + 3 files changed, 258 insertions(+), 81 deletions(-) diff --git a/.github/scripts/checkTranslation.py b/.github/scripts/checkTranslation.py index bf39e81..92100ec 100644 --- a/.github/scripts/checkTranslation.py +++ b/.github/scripts/checkTranslation.py @@ -2,78 +2,134 @@ import os import xml.etree.ElementTree as ET import polib - +import langid def normalize(s: str | None) -> str: - # Normalize strings for reliable comparison (trim, lowercase, collapse spaces) return " ".join((s or "").strip().lower().split()) - # ----------------------------- -# PO FILE CHECK +# PO CHECK # ----------------------------- + def checkPo(path: str) -> float: - # Parse PO file using polib po = polib.pofile(path) - translated = 0 total = 0 for entry in po: - # Skip empty msgid entries if not entry.msgid.strip(): continue total += 1 - # Consider entry translated only if msgstr differs from msgid if entry.msgstr and normalize(entry.msgstr) != normalize(entry.msgid): translated += 1 return translated / total if total else 0.0 - # ----------------------------- -# XLIFF CHECK (skeleton-safe generic parsing) +# XLIFF CHECK # ----------------------------- + def checkXliff(path: str) -> float: - # Parse XML XLIFF file tree = ET.parse(path) root = tree.getroot() - translated = 0 total = 0 - source = None for elem in root.iter(): - # Capture source segments if elem.tag.endswith("source"): source = normalize(elem.text) - # Compare with target segments elif elem.tag.endswith("target"): target = normalize(elem.text) if source: total += 1 - - # Count as translated only if target differs from source if target and target != source: translated += 1 return translated / total if total else 0.0 +# ----------------------------- +# MD LANGUAGE SCORE (langid) +# ----------------------------- + +def scoreMd(path: str, expected_lang: str) -> float: + try: + with open(path, "r", encoding="utf-8") as f: + text = f.read() + except Exception: + return 0.0 + + if not text.strip(): + return 0.0 + + lang, score = langid.classify(text) + + # Normalize score into positive confidence + confidence = 1 / (1 + abs(score)) + + if lang == expected_lang: + return confidence + else: + return 0.0 + +# ----------------------------- +# COMPARE MULTIPLE MD FILES +# ----------------------------- + +def compareMd(files: list[str], lang: str): + results = [] + + for f in files: + if not os.path.exists(f): + continue + + score = scoreMd(f, lang) + results.append((f, score)) + + if not results: + print("winner=None") + sys.exit(1) + + results.sort(key=lambda x: x[1], reverse=True) + + winner = results[0] + + print("comparison_results:") + for f, s in results: + print(f"{f}={s}") + + print(f"winner={winner[0]}") + print(f"winner_score={winner[1]}") + + sys.exit(0) # ----------------------------- -# MAIN ENTRY POINT +# MAIN # ----------------------------- + def main(): if len(sys.argv) < 2: - print("Usage: checkTranslation.py ") + print("Usage:") + print(" checkTranslation.py ") + print(" checkTranslation.py ") + print(" checkTranslation.py [...] ") sys.exit(2) - path = sys.argv[1] + args = sys.argv[1:] + + # ------------------------- + # MULTI FILE MODE + # ------------------------- + if len(args) >= 3: + *files, lang = args + compareMd(files, lang) + return + + path = args[0] if not os.path.exists(path): print(f"File not found: {path}") @@ -81,25 +137,39 @@ def main(): ext = os.path.splitext(path)[1].lower() - # Dispatch based on file type + # ------------------------- + # PO + # ------------------------- if ext == ".po": ratio = checkPo(path) + print(f"translation_ratio={ratio}") + sys.exit(0 if ratio > 0.05 else 1) + # ------------------------- + # XLIFF + # ------------------------- elif ext in [".xliff", ".xlf"]: ratio = checkXliff(path) + print(f"translation_ratio={ratio}") + sys.exit(0 if ratio > 0.05 else 1) - else: - print(f"Unsupported file type: {ext}") - sys.exit(2) + # ------------------------- + # MD (LANG SCORE) + # ------------------------- + elif ext == ".md": + if len(args) < 2: + print("Missing language argument for MD scoring") + sys.exit(2) - print(f"translation_ratio={ratio}") + lang = args[1] + score = scoreMd(path, lang) - # Threshold: consider file translated if above 5% - if ratio > 0.05: - sys.exit(0) # translated - else: - sys.exit(1) # not translated + print(f"md_score={score}") + sys.exit(0) + else: + print(f"Unsupported file type: {ext}") + sys.exit(2) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index 8378d51..f0022c1 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -3,7 +3,6 @@ name: Crowdin l10n on: workflow_dispatch: schedule: - # Every Monday at 00:00 UTC - cron: '0 0 * * 1' concurrency: @@ -20,7 +19,6 @@ jobs: runs-on: windows-latest permissions: contents: write - steps: - name: Checkout add-on uses: actions/checkout@v6 @@ -60,95 +58,202 @@ jobs: foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) { Write-Host "==============================" - Write-Host "Processing: $($dir.Name)" + Write-Host "Processing language: $($dir.Name)" Write-Host "==============================" - # ============================ - # LANG SETUP (IMPORTANT) - # ============================ $langCode = $dir.Name + $langShort = $langCode.Split('_')[0] - # ============================ - # FILE PATHS - # ============================ + # Paths $poFile = Join-Path $dir.FullName "$addonId.po" $localPoPath = "addon/locale/$langCode/LC_MESSAGES/nvda.po" $xliffFile = Join-Path $dir.FullName "$addonId.xliff" + $remoteMd = Join-Path $dir.FullName "$addonId.md" + $targetDocDir = "addon/doc/$langCode" - $readmePath = "$targetDocDir/readme.md" + $localMd = "$targetDocDir/readme.md" + + # ---------------------------- + # SKIP ENGLISH (source language) + # ---------------------------- + if ($langCode -eq "en") { + Write-Host "Skipping English (source language) → no MD/XLIFF processing required" + continue + } - # ============================ + # ---------------------------- # PO PROCESSING - # ============================ - if (Test-Path -PathType Leaf $poFile) { - - Write-Host "Running PO translation check..." + # ---------------------------- + if (Test-Path $poFile) { + Write-Host "Checking PO file..." uv run ./.github/scripts/checkTranslation.py "$poFile" - $isTranslated = ($LASTEXITCODE -eq 0) + $isPoTranslated = ($LASTEXITCODE -eq 0) - if ($isTranslated) { + Write-Host "PO translated: $isPoTranslated" - Write-Host "PO translated → updating local repo" + if ($isPoTranslated) { + Write-Host "Updating local PO" + New-Item -ItemType Directory -Force -Path (Split-Path $localPoPath) | Out-Null + Move-Item $poFile $localPoPath -Force + } else { + Write-Host "PO not translated" + if (Test-Path $localPoPath) { + Write-Host "Uploading local PO to Crowdin" + ./l10nUtil.exe uploadTranslationFile $langCode "$addonId.po" $localPoPath -c addon + } else { + Write-Host "No local PO available" + } + } + } - $targetDir = "addon/locale/$langCode/LC_MESSAGES" - New-Item -ItemType Directory -Force -Path $targetDir | Out-Null + # ---------------------------- + # XLIFF PROCESSING + # ---------------------------- + $xliffValid = $false + $tempMd = $null - Move-Item -Path $poFile -Destination "$targetDir/nvda.po" -Force + if (Test-Path $xliffFile) { + Write-Host "Checking XLIFF..." - } else { + uv run ./.github/scripts/checkTranslation.py "$xliffFile" + $xliffValid = ($LASTEXITCODE -eq 0) + + Write-Host "XLIFF valid: $xliffValid" + + if ($xliffValid) { + Write-Host "Converting XLIFF → MD" + $tempMd = "$env:TEMP\readme_$langCode.md" + ./l10nUtil.exe xliff2md $xliffFile $tempMd + } + } + + $remoteExists = Test-Path $remoteMd + $localExists = Test-Path $localMd + + Write-Host "Remote MD exists: $remoteExists" + Write-Host "Local MD exists: $localExists" + + # ---------------------------- + # DECISION ENGINE + # ---------------------------- + + # CASE: XLIFF VALID + if ($xliffValid) { + Write-Host "Entering XLIFF-driven logic" + + if ($remoteExists -and $localExists) { + Write-Host "3-way comparison (xliff, remote, local)" + + $scoreX = (uv run python .github/scripts/checkTranslation.py "$tempMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] + $scoreR = (uv run python .github/scripts/checkTranslation.py "$remoteMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] + $scoreL = (uv run python .github/scripts/checkTranslation.py "$localMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] + + $scoreX = [double]$scoreX + $scoreR = [double]$scoreR + $scoreL = [double]$scoreL - Write-Host "PO not translated → skipping local update" + Write-Host "Scores → XLIFF:$scoreX Remote:$scoreR Local:$scoreL" - $addonName = "${{ steps.getAddonInfo.outputs.addonId }}" - $crowdinFile = "$addonName.po" + $best = [Math]::Max($scoreX, [Math]::Max($scoreR, $scoreL)) - if (Test-Path -PathType Leaf $localPoPath) { + if ($best -eq $scoreX) { + Write-Host "Winner: XLIFF" + Move-Item $tempMd $localMd -Force + } elseif ($best -eq $scoreR) { + Write-Host "Winner: Remote MD" + Move-Item $remoteMd $localMd -Force + } else { + Write-Host "Winner: Local MD → uploading" + ./l10nUtil.exe uploadTranslationFile $langCode "$addonId.md" $localMd -c addon + } + + } elseif ($remoteExists -and -not $localExists) { + Write-Host "Comparing XLIFF vs Remote" - Write-Host "Uploading local PO to Crowdin (no translations found remotely)" + $scoreX = (uv run python .github/scripts/checkTranslation.py "$tempMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] + $scoreR = (uv run python .github/scripts/checkTranslation.py "$remoteMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] - ./l10nUtil.exe uploadTranslationFile $langCode $crowdinFile $localPoPath -c addon + $scoreX = [double]$scoreX + $scoreR = [double]$scoreR + if ($scoreX -ge $scoreR) { + Write-Host "Winner: XLIFF → creating local" + Move-Item $tempMd $localMd -Force } else { + Write-Host "Winner: Remote → creating local" + Move-Item $remoteMd $localMd -Force + } + + } elseif (-not $remoteExists -and $localExists) { + Write-Host "Comparing XLIFF vs Local" + + $scoreX = (uv run python .github/scripts/checkTranslation.py "$tempMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] + $scoreL = (uv run python .github/scripts/checkTranslation.py "$localMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] - Write-Host "Local PO file does not exist → skipping upload" + $scoreX = [double]$scoreX + $scoreL = [double]$scoreL + + if ($scoreX -gt $scoreL) { + Write-Host "Winner: XLIFF → overwrite local" + Move-Item $tempMd $localMd -Force + } else { + Write-Host "Winner: Local → uploading" + ./l10nUtil.exe uploadTranslationFile $langCode "$addonId.md" $localMd -c addon } + + } else { + Write-Host "Only XLIFF available → importing directly" + Move-Item $tempMd $localMd -Force } - } - # ============================ - # XLIFF PROCESSING - # ============================ - $isXliffTranslated = $false + } else { + Write-Host "XLIFF not usable → fallback logic" - if (Test-Path -PathType Leaf $xliffFile) { + if ($remoteExists -and $localExists) { + Write-Host "Comparing Remote vs Local" - Write-Host "Running XLIFF translation check..." + $scoreR = (uv run python .github/scripts/checkTranslation.py "$remoteMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] + $scoreL = (uv run python .github/scripts/checkTranslation.py "$localMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] - uv run ./.github/scripts/checkTranslation.py "$xliffFile" - $isXliffTranslated = ($LASTEXITCODE -eq 0) + $scoreR = [double]$scoreR + $scoreL = [double]$scoreL - if ($isXliffTranslated) { + if ($scoreR -gt $scoreL) { + Write-Host "Winner: Remote → overwrite local" + Move-Item $remoteMd $localMd -Force + } else { + Write-Host "Winner: Local → uploading" + ./l10nUtil.exe uploadTranslationFile $langCode "$addonId.md" $localMd -c addon + } - Write-Host "XLIFF translated → updating README" + } elseif ($remoteExists -and -not $localExists) { + Write-Host "Remote only → checking quality" - New-Item -ItemType Directory -Force -Path $targetDocDir | Out-Null + $scoreR = (uv run python .github/scripts/checkTranslation.py "$remoteMd" $langShort | Select-String "md_score=").ToString().Split("=")[1] + $scoreR = [double]$scoreR - uv run l10nUtil.exe xliff2md $xliffFile $readmePath + if ($scoreR -gt 0.5) { + Write-Host "Remote is valid → importing" + Move-Item $remoteMd $localMd -Force + } else { + Write-Host "Remote not valid → skipping" + } - } else { + } elseif (-not $remoteExists -and $localExists) { + Write-Host "Only local exists → uploading without scoring" + ./l10nUtil.exe uploadTranslationFile $langCode "$addonId.md" $localMd -c addon - Write-Host "XLIFF not translated → skipping README update" + } else { + Write-Host "No MD available → nothing to do" } - - Write-Host "XLIFF translation result: $isXliffTranslated" } } - # ============================ - # COMMIT CHANGES - # ============================ + # ---------------------------- + # COMMIT + # ---------------------------- git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -157,11 +262,12 @@ jobs: git diff --staged --quiet if ($LASTEXITCODE -ne 0) { git commit -m "Update translations for $addonId from Crowdin" - git switch ${{ env.downloadTranslationsBranch }} 2>$null + git switch ${{ env.downloadTranslationsBranch }} 2>$null if ($LASTEXITCODE -ne 0) { git switch -c ${{ env.downloadTranslationsBranch }} } + git push -f --set-upstream origin ${{ env.downloadTranslationsBranch }} } else { Write-Host "Nothing to commit." diff --git a/pyproject.toml b/pyproject.toml index 3d10490..e1b8a3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "markdown-link-attr-modifier==0.2.1", "mdx-gh-links==0.4", "polib==1.2.0", + "langid==1.1.6", # Lint "uv==0.11.6", "ruff==0.14.5",