Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
106 changes: 106 additions & 0 deletions .github/workflows/exportAddonToCrowdin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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
env:
crowdinProjectID: ${{ vars.CROWDIN_ID }}
crowdinAuthToken: ${{ secrets.CROWDIN_TOKEN }}
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
pip install scons markdown
sudo apt update
sudo apt install gettext
- 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
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")
- 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' }}
run: |
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: |
uv run ./_l10n/crowdinSync.py uploadSourceFile ${{ steps.getAddonInfo.outputs.addonId }}.pot
- name: Commit and push json and xliff files
id: commit
run: |
git config --local user.name github-actions
git config --local user.email [email protected]
git status
git add hash.json _l10n/l10n.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
18 changes: 15 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
97 changes: 91 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
92 changes: 92 additions & 0 deletions _l10n/crowdinSync.py
Original file line number Diff line number Diff line change
@@ -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()
Loading