Skip to content
Draft
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
112 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
6089057
Add workflow call to build the add-on, so it can be reused in other w…
nvdaes Mar 25, 2026
d8154a9
Update workflow for testing
nvdaes Mar 25, 2026
e699abc
Improve workflow to download files from Crowdin
nvdaes Apr 18, 2026
5eafa64
Update setOutputs to set just add-on id to download translations
nvdaes Apr 18, 2026
0eb3223
[crowdinL10n.yml] improve CI translation workflow
abdel792 Apr 20, 2026
a195110
Remove duplicate setOutputs.py from .github/workflows
abdel792 Apr 20, 2026
9ed44a3
Merge pull request #2 from abdel792/l10nImprovements
nvdaes Apr 20, 2026
e62bd5b
Update lock
nvdaes Apr 20, 2026
37c4d4f
Merge
nvdaes Apr 20, 2026
91995bd
Try to fix pre-commit configuration
nvdaes Apr 20, 2026
0c613be
Reset gitignore to master
nvdaes Apr 20, 2026
0b65072
Require polib 1.2.0
nvdaes Apr 20, 2026
f9ba8fe
Update lock file
nvdaes Apr 20, 2026
8e514ba
Remove sha256 file
nvdaes Apr 20, 2026
9c64247
Remove verification of lock file in pre-commit config, not present in…
nvdaes Apr 20, 2026
7589fef
Remove Crowdin client
nvdaes Apr 20, 2026
94b1a88
Update lock file
nvdaes Apr 20, 2026
65290e4
Enhance Crowdin l10n workflow with MD quality evaluation and comparis…
abdel792 Apr 22, 2026
059bb53
Merge pull request #3 from abdel792/l10nImprovements
nvdaes Apr 22, 2026
c9c3bab
Run pre-commit
nvdaes Apr 22, 2026
25dcb5d
Fix conflicts
nvdaes Apr 23, 2026
b1d2acf
Restore deleted space
nvdaes Apr 24, 2026
263b7e2
Update lock file
nvdaes Apr 24, 2026
f6e18c2
Remove score staff
nvdaes Apr 24, 2026
5573061
Add ps1 script and rename setOutputs.py
nvdaes Apr 24, 2026
87cbed4
Run ps1 script from workflow
nvdaes Apr 24, 2026
1a18717
Change python version to 3.13
nvdaes Apr 24, 2026
471a320
Update lock file
nvdaes Apr 24, 2026
e50a716
Fix checkTranslation script according to pre-commit
nvdaes Apr 24, 2026
f5150cb
Remove getAddonInfo step
nvdaes Apr 24, 2026
3c7093c
Update scripts and workflow
nvdaes Apr 25, 2026
065a16f
Fix pyright
nvdaes Apr 25, 2026
c4dd521
Add markdownTranslate
nvdaes Apr 25, 2026
00cf8cf
Update markdownTranslate
nvdaes Apr 25, 2026
480c70e
Exclude markdownTranslate from pyright, it contains many errors
nvdaes Apr 26, 2026
ce2d2da
Update source files
nvdaes Apr 26, 2026
3981ece
Updates
nvdaes Apr 26, 2026
0def05d
Fix files with pre-commit
nvdaes Apr 26, 2026
1c92aab
refactor: use Crowdin API for translation checks and optimize file se…
abdel792 Apr 26, 2026
9b5bf84
Merge pull request #4 from abdel792/l10nImprovements
nvdaes Apr 26, 2026
c5e4076
Add crowdin api client
nvdaes Apr 26, 2026
c098392
Add dependencies
nvdaes Apr 26, 2026
39416a0
Remove limit for list of files, fetch all
nvdaes Apr 26, 2026
2396864
Improvements
nvdaes Apr 26, 2026
94929e7
Updates
nvdaes Apr 26, 2026
57d46f5
Add language mappings
nvdaes Apr 27, 2026
74388d0
Fix
nvdaes Apr 27, 2026
1f8eb84
refactor: modernize translation workflow and integrate language mappings
abdel792 Apr 28, 2026
ef0a614
docs: update readme with translation workflow instructions
abdel792 Apr 28, 2026
92e1cd6
docs: fix translation mailing list address in readme.md
abdel792 Apr 28, 2026
f92a73f
Merge pull request #5 from abdel792/l10nImprovements
nvdaes Apr 28, 2026
6a884c0
Fix readme
nvdaes Apr 28, 2026
3100c81
Updates
nvdaes Apr 29, 2026
6fe4efc
Run pre-commit
nvdaes Apr 29, 2026
a3209ac
Exclude langCodes.py from pyright
nvdaes Apr 29, 2026
6c09158
Improve Crowdin language handling and script clarity
abdel792 Apr 29, 2026
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
187 changes: 187 additions & 0 deletions .github/scripts/checkTranslation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import sys
import os
import xml.etree.ElementTree as ET
import polib
import langid


def normalize(s: str | None) -> str:
return " ".join((s or "").strip().lower().split())


# -----------------------------
# PO CHECK
# -----------------------------


def checkPo(path: str) -> float:
po = polib.pofile(path)
translated = 0
total = 0

for entry in po:
if not entry.msgid.strip():
continue

total += 1

if entry.msgstr and normalize(entry.msgstr) != normalize(entry.msgid):
translated += 1

return translated / total if total else 0.0


# -----------------------------
# XLIFF CHECK
# -----------------------------


def checkXliff(path: str) -> float:
tree = ET.parse(path)
root = tree.getroot()
translated = 0
total = 0
source = None

for elem in root.iter():
if elem.tag.endswith("source"):
source = normalize(elem.text)

elif elem.tag.endswith("target"):
target = normalize(elem.text)

if source:
total += 1
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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain the point of calculating a score? I don't really understand why this exists. If the score is important, I don't think we should be relying on it. Some languages cannot be accurately detected via mechanisms like this, particularly when we support different dialects of the same language

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been removed. I don't know if Abdel will want to create a new PR. Translators prefer to use xliff.

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
# -----------------------------


def main():
if len(sys.argv) < 2:
print("Usage:")
print(" checkTranslation.py <file>")
print(" checkTranslation.py <file> <lang>")
print(" checkTranslation.py <file1> <file2> [...] <lang>")
sys.exit(2)

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}")
sys.exit(2)

ext = os.path.splitext(path)[1].lower()

# -------------------------
# 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)

# -------------------------
# MD (LANG SCORE)
# -------------------------
elif ext == ".md":
if len(args) < 2:
print("Missing language argument for MD scoring")
sys.exit(2)

lang = args[1]
score = scoreMd(path, lang)

print(f"md_score={score}")
sys.exit(0)

else:
print(f"Unsupported file type: {ext}")
sys.exit(2)


if __name__ == "__main__":
main()
21 changes: 21 additions & 0 deletions .github/scripts/setOutputs.py
Original file line number Diff line number Diff line change
@@ -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:
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GITHUB_OUTPUT is accessed unconditionally; if this script is run outside GitHub Actions (or the env var is missing), it will raise a KeyError with a confusing traceback. Consider validating os.environ.get("GITHUB_OUTPUT") and exiting with a clear error message when it’s not set.

Suggested change
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
githubOutput = os.environ.get("GITHUB_OUTPUT")
if not githubOutput:
print(
"Error: GITHUB_OUTPUT is not set. This script must be run in a GitHub Actions step "
"with GITHUB_OUTPUT available.",
file=sys.stderr,
)
raise SystemExit(1)
with open(githubOutput, "a") as f:

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be run inside GitHub Actions.

_ = f.write(f"{name}={value}\n")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions .github/workflows/build_addon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
branches: [ main, master ]

workflow_dispatch:
workflow_call:

jobs:
build:
Expand Down
Loading