Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
28c060d
Add scripts to allow addons from personal repos to be synchronized wi…
nvdaes Nov 24, 2025
4669430
use a json file to store addonId, and use it to filter files to get C…
nvdaes Nov 24, 2025
b188560
Try to get files just for the current add-on
nvdaes Nov 24, 2025
7092615
Add workflow to export an add-on to Crowdin (authors would need to be…
nvdaes Nov 24, 2025
e89640d
Use buildVars, not metadata.json file
nvdaes Nov 25, 2025
4c7771b
Add userAccount to buildVars, and step to get addon-id to GitHub work…
nvdaes Nov 26, 2025
c529cee
Update files after testing exporting an add-on to Crowdin, needs refi…
nvdaes Nov 26, 2025
186b755
Add python version file
nvdaes Nov 26, 2025
f1fbf8e
Improve pyproject and update precommit config after testing that chec…
nvdaes Nov 26, 2025
b867a9a
Restore rules
nvdaes Nov 27, 2025
47ed91c
Restore pyproject
nvdaes Nov 27, 2025
402002e
Improve uv project
nvdaes Nov 27, 2025
d820711
Remove files
nvdaes Nov 27, 2025
9f6b3dc
Calculate hash of i18nSources
nvdaes Nov 29, 2025
4c938ec
Update workflow
nvdaes Nov 30, 2025
a303210
Update _l10n
nvdaes Nov 30, 2025
a0d02da
Upload md file
nvdaes Dec 1, 2025
a8d4252
Updates
nvdaes Dec 3, 2025
1a1e6fd
Update l10nUtil
nvdaes Dec 14, 2025
d2395b0
Update workflow
nvdaes Dec 14, 2025
e4dafe1
Update readme
nvdaes Dec 16, 2025
f76904e
Update readme.md
nvdaes Dec 16, 2025
aea5eba
Update _l10n/crowdinSync.py
nvdaes Dec 16, 2025
f7ccaf6
Add setOutput.py to separate Python code from yaml file
nvdaes Dec 16, 2025
0276e22
Remove bad comment
nvdaes Dec 16, 2025
253eb46
Reset pyproject to master
nvdaes Dec 16, 2025
c51e7ad
reset .pre-commit configuration to master
nvdaes Dec 16, 2025
cd4816c
Remove userAccount variable, since we use markdown, not xliff
nvdaes Dec 17, 2025
314220b
Update or add files from scratch depending on existence of hashFile
nvdaes Dec 17, 2025
f3e8b8d
Use addMd and addPotFromScratch outputs
nvdaes Dec 17, 2025
de4fa15
Update dependencies
nvdaes Dec 20, 2025
46a105a
Update setOutput
nvdaes Dec 20, 2025
053d4de
Update workflow
nvdaes Dec 20, 2025
4a3f5a0
Update lock
nvdaes Dec 20, 2025
3c1a73e
Merge branch 'master' into l10n
nvdaes Dec 21, 2025
dbe74dc
Verify uv lock
nvdaes Dec 21, 2025
e717292
Add uv to dependencies in case this is relevant to verify the lock ac…
nvdaes Dec 21, 2025
c4ed575
Remove debug statement
nvdaes Dec 21, 2025
befa647
Run pre-commit
nvdaes Dec 21, 2025
05c8161
Update dependencies
nvdaes Dec 22, 2025
9a0f62a
Deleted Pyproject to avoid conflicts
nvdaes Dec 22, 2025
4abd788
Reset pyproject to master
nvdaes Dec 22, 2025
c256364
Remove _l10n since this will be added as a submodule
nvdaes Dec 22, 2025
0505a3b
Don't run pre-commit since it requires a different token to access hooks
nvdaes Dec 22, 2025
fd2554b
Merge translations into branch
nvdaes Dec 22, 2025
b30f46f
Add project id without using vars
nvdaes Dec 22, 2025
70293c8
Schedule workflow
nvdaes Dec 22, 2025
da09c8c
Rename workflow
nvdaes Dec 22, 2025
5c52f33
Create PR
nvdaes Dec 22, 2025
d0d5e03
Don't create a PR since this n¡may need a personal access token
nvdaes Dec 22, 2025
b40f94a
Update removing permissions for PR
nvdaes Dec 22, 2025
cb7807e
Update Python version compatible with ubuntu-latest
nvdaes Dec 28, 2025
1449a01
Add dry-run
nvdaes Dec 28, 2025
697d048
Optimize workflow to test with act and docker locally
nvdaes Dec 28, 2025
8c9247b
Update uv.lock
nvdaes Dec 30, 2025
a8469fb
Merge branch 'master' into l10n
nvdaes Dec 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/workflows/crowdinL10n.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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'
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
permissions:
contents: write
steps:
- name: Checkout add-on
uses: actions/checkout@v6
with:
submodules: true
- name: "Set up Python"
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 }}
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 }}
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 }}
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
git config --local user.email [email protected]
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
- 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
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
59 changes: 59 additions & 0 deletions .github/workflows/setOutputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 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
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")


if __name__ == "__main__":
main()
21 changes: 18 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
# 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

# act configuration
.actrc
5 changes: 4 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13.11
1 change: 0 additions & 1 deletion buildVars.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
# which returns whatever is given to it as an argument.
from site_scons.site_tools.NVDATool.utils import _


# Add-on information variables
addon_info = AddonInfo(
# add-on Name/identifier, internal for NVDA
Expand Down
11 changes: 11 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ 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:
Expand Down
41 changes: 41 additions & 0 deletions sha256.py
Original file line number Diff line number Diff line change
@@ -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()