Skip to content

Add scripts to allow addons from personal repos to be synchronized with Crowdin#1

Merged
seanbudd merged 151 commits into
nvaccess:masterfrom
nvdaes:l10n
Jun 10, 2026
Merged

Add scripts to allow addons from personal repos to be synchronized with Crowdin#1
seanbudd merged 151 commits into
nvaccess:masterfrom
nvdaes:l10n

Conversation

@nvdaes

@nvdaes nvdaes commented Nov 24, 2025

Copy link
Copy Markdown

This pull request introduces a complete, automated workflow for synchronizing translations with Crowdin, including new scripts, a scheduled GitHub Actions workflow, and supporting documentation. The main changes add Python and PowerShell scripts for translation status checking and synchronization, a workflow for scheduled and manual Crowdin sync, language mapping configuration, and documentation updates. These improvements enable seamless translation management for add-on projects.

Specifically:

  • The .github/scripts folder includes:
  • setOutputs.py: Allows to write the addonId, extracted from buildVars.py, to the github output environment variable in GitHub Actions.
    • checkTranslation.py: Makes possible to calculate the score (percentage of translated strings) of files (.po, .xliff or .md), using the Crowdin API. This is useful to prevent untranslated files to be included in the add-on folder, reducing the add-on size and avoiding that poor documentation can be accessed when the Help button is pressed from the store.
  • crowdinSync.ps1: This script generates/updates an xliff file from the readme.md, creates the pot file and upload these updated source files to Crowdin; commits the updated xliff file to the add-on repo; exports translations from Crowdin, imports translated files with a score/percentage of 50 % or more including these to the addon/locale/lc_messages (po files) or addon/doc (documentation) in the corresponding languages. If translatied files are poor (with low percentages), and a translated file is available inside the add-on, it's considered caming from an old translation produced with the legacy translation system, and it's uploaded to Crowdin. Then changes in the add-on repo are commited to a dedicated branch.
  • languageMappings.json: these are mappings between folders used in the add-on, like es, and Crowdin languages, like es-ES (this may need reviewed with advice from NV Access, since for example ne or ga aren't well mapped).
  • markdownTranslate.py: Used to generate the xliff file.

Comment thread pyproject.toml
@nvdaes

nvdaes commented Nov 28, 2025

Copy link
Copy Markdown
Author

Purpose

Add-on authors may wish to help translators use Crowdin, the same framework where they translate NVDA. to translate messages and documentation for maintained add-ons:

Other details

  • Pot file are created/updated, and uploaded to a Crowdin project.
  • The readme.md file is converted to xliff and uploaded to a Crowdin project.
  • Po and xfiles are translated.
  • Translated files are downloaded and processed to be copied to locale/langCode/LC_MESSAGES/nvda.po, and doc/langCode/readme.md, in the addon folder.
    Authors need to store a Crowdin token with permissions to upload files to the Crowdin project as a repository secret.

Development approach

A workflow (GitHub Actions), and several scripts (Python and Powershell), as well as a json file with language mappings have been added.
Also NV Access l10nUtil.exe is used to synchronize files between the add-on and Crowdin, and to convert between xliff and markdown formats.

@nvdaes

nvdaes commented Nov 28, 2025

Copy link
Copy Markdown
Author

I've tested that all check pass using this pyproject.toml file on this PR:

nvdaes/translateNvdaAddonsWithCrowdin#11

I use precommit, CodeQL and a workflow to check that all translatable messages have comments for translators.

I'll try to use the cache action to cache some add-on metadata like its id, and also hashfiles from l10nSources (taking the value of buildVars.py), and the hasf¡hfile of the readme.md, to determine if pot and xliff files should be updated.

@nvdaes

nvdaes commented Nov 30, 2025

Copy link
Copy Markdown
Author

Export translations to Crowdin running the workflow with update=False works properly:

https://github.com/nvdaes/translateNvdaAddonsWithCrowdin/actions/runs/19802210157

@nvdaes

nvdaes commented Nov 30, 2025

Copy link
Copy Markdown
Author

This time, updatexLiff is failing. Seems that adding blank lines to readme may cause problems:

https://github.com/nvdaes/translateNvdaAddonsWithCrowdin/actions/runs/19802391926/job/56731562709

@nvdaes

nvdaes commented Nov 30, 2025

Copy link
Copy Markdown
Author

If someone can help with this issue when update xliff, I'll be grateful.
I think that this is one of the bugest problems with xliff files. Sometimes sel lines are None and they don't have a strip method. I don't know if this should be also improved in NVDA
cc: @seanbudd

@seanbudd

seanbudd commented Dec 1, 2025

Copy link
Copy Markdown
Member

It might be easier to avoid xliff and just translate the markdown files directly. This won't support diffs very well but worth experimenting with

@nvdaes

nvdaes commented Dec 1, 2025

Copy link
Copy Markdown
Author

@seanbudd wrote:

It might be easier to avoid xliff and just translate the markdown files directly. This won't support diffs very well but worth experimenting with

OK.

@nvdaes

nvdaes commented Dec 3, 2025

Copy link
Copy Markdown
Author

@CyrilleB79, you were interested in this framework. If you want, feel free to see how the translateNvdaAddonsWithCrowdin.md can be translated in the project. Using xliff files is causing problems, as mentioned, and we are experimenting uploading md files instead.

Document XLIFF-based documentation synchronization, translation threshold configuration, GitHub Actions variables, and related Crowdin workflow changes.
@abdel792

abdel792 commented Jun 6, 2026

Copy link
Copy Markdown

Hi @nvdaes and everyone,

I have just pushed two new commits for review.

The first commit updates the Crowdin synchronization logic to address the concerns raised by @wmhn1872265132 regarding the coexistence of Markdown and XLIFF documentation files in Crowdin.

The main changes are:

  • Documentation synchronization now uses XLIFF files as the authoritative translation source.
  • Markdown documentation continues to be generated locally from translated XLIFF files using l10nUtil.exe xliff2md.
  • The synchronization logic has been updated to match the XLIFF-only workflow discussed in this PR.

Following @nvdaes' review, I also revisited the translation threshold logic and found an issue while testing the workflow.

In crowdinL10n.yml, the threshold is configured through the MIN_PERCENTAGE_TRANSLATED variable and defaults to 50 when no repository variable is defined:

MIN_PERCENTAGE_TRANSLATED: ${{ vars.MIN_PERCENTAGE_TRANSLATED || 50 }}

This indicates that the workflow expects percentage values between 0 and 100.

However, the getScoreFromApi function in checkTranslation.py was returning normalized values between 0.0 and 1.0:

return progress / 100

As a result, a translation progress of 93% became 0.93, which was then compared against percentage thresholds such as 50.

To keep both values on the same scale, I changed the function to return the percentage directly:

return progress

I tested this modification in a workflow run and the threshold validation behaved correctly afterwards:

https://github.com/abdel792/dayOfTheWeek/actions/runs/27070359050/job/79898355233

I also updated the comparison operator from -gt to -ge.

This means that translations reaching exactly the configured threshold are now accepted. For example, if a repository maintainer sets:

MIN_PERCENTAGE_TRANSLATED=100

a file translated at exactly 100% will be synchronized, which seems more intuitive than requiring a value strictly greater than the configured threshold.

The second commit updates the README documentation accordingly.

The documentation now:

  • Explains how to configure the MIN_PERCENTAGE_TRANSLATED repository variable.
  • Clarifies the distinction between GitHub secrets and repository variables.
  • Documents the current XLIFF-based documentation workflow.
  • Explains the use of l10nUtil.exe md2xliff and l10nUtil.exe xliff2md.
  • Updates the localization workflow documentation to reflect the current implementation.

I also took into account the recent comments from @wmhn1872265132 regarding the migration path from Markdown to XLIFF and the current state of the tooling.

Please review the changes when you have time and let me know if you see anything that should be adjusted before merging.

One additional note regarding the README updates.

I did not document the AddonTemplate update procedure for now.

I understand the workflow that was suggested:

git remote add addonTemplate https://github.com/nvaccess/AddonTemplate.git
git fetch addonTemplate
git branch -u addonTemplate/master
git pull --allow-unrelated-histories

However, in my own tests, especially with add-ons based on older versions of AddonTemplate, the git pull --allow-unrelated-histories step often resulted in a large number of merge conflicts.

My concern is not only the conflicts themselves, but also the possibility of accidentally overwriting add-on-specific metadata while resolving them.

For example, files such as:

  • buildVars.py
  • pyproject.toml
  • changelog.md
  • readme.md

typically contain information that is specific to the local add-on repository and should not be replaced by the template version.

These files frequently contain add-on-specific information such as metadata, version history, build configuration, documentation, and release notes that cannot always be merged automatically.

When using git pull --allow-unrelated-histories, Git attempts to merge two histories that may have diverged significantly over time. In practice, this can require extensive manual conflict resolution, and there is a risk of unintentionally losing local metadata or customizations that are specific to the add-on being maintained.

Because of that, I was not fully comfortable documenting this procedure as a general recommendation in the README without additional guidance, warnings, or testing.

I think this topic may deserve a dedicated section in the future explaining the expected conflicts and the best practices for safely merging AddonTemplate updates into existing add-ons, particularly those created from much older template versions.

Thanks everyone for the feedback, testing, and suggestions.

@nvdaes

nvdaes commented Jun 7, 2026

Copy link
Copy Markdown
Author

@abdel792 , I've reviewed diffs:

git diff HEAD..origin/l10n

And for me this is ready to request a review.

Note: In the documentation, it's recommended to avoiding to run the workflow at the same time if various add-ons are managed. Anyway, as requested by Sean, now the workflow includes a procedure to start with a random delay:

      - name: Random startup delay (0-5 minutes)
        if: github.event_name == 'schedule'
        shell: pwsh
        run: |
          $delaySeconds = Get-Random -Minimum 0 -Maximum 301
          Write-Host "Sleeping for $delaySeconds seconds..."
          Start-Sleep -Seconds $delaySeconds

Anyway, we have also the possibility of collisions even with the random delay.

I have seen your testing workflow. Let's request a review from NV Access.
Thanks everyone.

@nvdaes nvdaes marked this pull request as ready for review June 7, 2026 05:03
@abdel792

abdel792 commented Jun 7, 2026

Copy link
Copy Markdown

Hi @nvdaes,

Thanks for your review and feedback.

I have just pushed an additional documentation update.

Regarding your comment about workflow scheduling, I noticed that the README was still recommending the use of different cron schedules without mentioning the random startup delay that was later added to the workflow.

I have therefore updated the documentation to clarify that the workflow already includes a random delay before scheduled executions, while noting that collisions may still occur in some situations.

I also revisited the GitHub Secrets and Variables section.

Previously, the documentation explained how to create the MIN_PERCENTAGE_TRANSLATED repository variable only after introducing several other repository variables. I felt that this was slightly inconsistent from a reader's perspective, since CROWDIN_PROJECT_ID and L10N_UTIL_CONFIG are also repository variables.

The section has therefore been reorganized so that the process for creating repository variables is introduced before listing the supported variables, making the overall structure more consistent and easier to follow.

Please let me know if you think any further adjustments are needed before requesting the NV Access review.

Thanks again for all your work on this PR.

@nvdaes

nvdaes commented Jun 7, 2026

Copy link
Copy Markdown
Author

@abdel792 , I've reviewed your last commit, and for me it's right. I requested a review from NV Access, and in fact I think that this PR is ready for review.
Thanks for all your work.

@abdel792

abdel792 commented Jun 8, 2026

Copy link
Copy Markdown

Hi @wmhn1872265132,

Thank you for the clarification regarding the translateXliff command.

After reading your comment, I decided to perform a local test using the Clock add-on documentation.

For local testing, I temporarily replaced the GitHub Actions authentication used by checkTranslation.py with my personal Crowdin API token. This was necessary because the script normally expects the Crowdin credentials to be provided through the GitHub Actions environment.

First, I checked the translation status reported by Crowdin for the existing XLIFF file:

python checkTranslation.py clock.xliff zh-CN

Output:

DEBUG: Searching for source file: clock.xliff
DEBUG: Match found: /clock.xliff (ID: 452)
translationRatio=0.0
xliffScore=0.0

I then checked the legacy Markdown documentation:

python checkTranslation.py clock.md zh-CN

Output:

DEBUG: Searching for source file: clock.md
DEBUG: Match found: /clock.md (ID: 460)
translationRatio=61.0
mdScore=61.0

This showed that Crowdin currently reports 61% translation progress for the Markdown documentation, while the corresponding XLIFF file still reports 0%.

Since the translated Markdown documentation already contained a significant amount of work, I decided to try the migration procedure you described.

Before running the conversion, I adjusted the translated readme.md so that its structure matched the source document. In particular, I replaced the remaining list markers using dashes (-) with asterisks (*) where necessary, since the structure must match exactly for the conversion to work correctly.

I then executed:

python markdownTranslate.py translateXliff -x=clock.xliff -l=zh-CN -p=readme.md -o=readme.xliff

Output:

Creating zh-CN translated xliff file readme.xliff from clock.xliff using readme.md...
Translated xliff file with 50 translated strings

The conversion completed successfully and produced a translated XLIFF file.

I will attach the generated readme.xliff file to this discussion.

If you wish, you may continue reviewing and completing the translation in Poedit and then upload the updated XLIFF file to Crowdin whenever convenient.

Since the workflow changes proposed in this PR now use XLIFF files exclusively as the source for documentation synchronization, having an initial translated XLIFF file should make the migration easier and allow existing translation work to be preserved.

Once uploaded to Crowdin, future synchronization cycles between the Clock repository and Crowdin should use this XLIFF translation instead of relying on the legacy Markdown file.

Thank you again for pointing out this migration path. My test suggests that it can be a practical way to reuse existing Markdown translations when their structure is still sufficiently close to the source document.

@wmhn1872265132

Copy link
Copy Markdown

Hi @abdel792,

Please note that I uploaded the translation for the Clock Add-on's XLIFF file last Monday.

I checked the Crowdin history and saw that the file was recreated last Thursday, causing all translations to be lost. I'm not sure which version of the XLIFF file should be used. Once you have confirmed that the file is stable, I can re-upload the translation for it. Please be very careful: once translators start working on an XLIFF file, modifications should only be made in one place. Creating the same file from multiple locations very easily leads to loss of existing translations. This is the main reason I opened nvaccess/nvda#20215

@abdel792

abdel792 commented Jun 8, 2026

Copy link
Copy Markdown

Hi @wmhn1872265132,

Thank you for the clarification.

I now better understand what happened.

After reviewing the workflow, I believe the recreation of the XLIFF file that you observed last Thursday is most likely related to the synchronization process introduced in this PR.

As implemented by @nvdaes, the workflow currently regenerates the source XLIFF file during each synchronization cycle using l10nUtil.exe md2xliff. The purpose is to ensure that any updates made by the add-on author to the official English documentation are automatically reflected in the translation source uploaded to Crowdin.

For the Clock repository, synchronization currently runs every Thursday, which matches the timing you observed.

I am starting to suspect that this regeneration process may be responsible for the loss of previously uploaded translations.

My concern is that generating a completely new XLIFF source file with md2xliff may also generate new internal identifiers within the XLIFF skeleton. If Crowdin relies on those identifiers to associate existing translations with source strings, regenerating them could prevent previously uploaded translations from being matched correctly.

This would explain why a translation uploaded on Monday could appear to be lost after the Thursday synchronization.

GitHub did not accept the generated XLIFF file that I mentioned in my previous comment, so it never appeared in the discussion.

I have therefore uploaded it here:

http://cyber25.free.fr/nvda-addons/zh/readme.xliff

You may download it and compare it with your current translation work if you find it useful.

@nvdaes, this also made me think about the old markdownTranslate.py updateXliff workflow, which was designed to update an existing XLIFF file rather than recreate it from scratch.

Perhaps it would be worth investigating whether similar functionality should eventually be added to l10nUtil.exe. My intuition is that updating an existing XLIFF while preserving its internal structure and identifiers could help maintain translation continuity across documentation updates and reduce the risk of losing previously uploaded translations.

Of course, this is only a hypothesis at this stage and would need to be confirmed by comparing the source XLIFF files generated before and after synchronization.

Thank you again for raising this issue and for all the work you have contributed to the Chinese translation.

@wmhn1872265132

Copy link
Copy Markdown

No, I've looked at the commits in the Clock repository. The translation loss was not caused by the scheduled run of the workflow. If you hadn't made any changes at that time, then I'm not sure whether there is also a scheduled workflow running in your forked repository. If there is, you need to disable the scheduled run in the forked repository, and then manually run the translation synchronization workflow in the Clock repository once. That should resolve the issue.

@wmhn1872265132

Copy link
Copy Markdown

I have confirmed that the translation loss was caused by
https://github.com/abdel792/clock/actions/runs/26951588086

@nvdaes

nvdaes commented Jun 8, 2026 via email

Copy link
Copy Markdown
Author

@wmhn1872265132

Copy link
Copy Markdown

@nvdaes I have confirmed that there is no problem with the workflow. The issue is due to a planned workflow running in the Fork repository of clock

@abdel792

abdel792 commented Jun 8, 2026

Copy link
Copy Markdown

Thank you both for investigating this.

I have now deleted the abdel792/clock fork repository that was used for my tests.

Indeed, this was only a temporary fork created for testing purposes, and it appears that its scheduled workflow was the source of the issue identified by @wmhn1872265132.

Thank you @wmhn1872265132 for tracking this down and confirming the actual cause of the translation loss.

Thank you as well @nvdaes for your clarification regarding the md2xliff implementation and the --oldXliffPath option. It is good to know that preserving existing translations was already taken into account in the design of l10nUtil.exe.

With the test fork now removed, everything should be fine. Future synchronizations between the Clock repository and Crowdin should no longer be affected by the issue that was observed.

For reference, the Clock repository is still using the previous workflow version. I have not yet integrated the latest changes from this PR that switch the documentation synchronization to the XLIFF-only workflow.

Thanks again to both of you for the investigation and explanations.

Comment thread .github/workflows/crowdinL10n.yml
@abdel792

abdel792 commented Jun 8, 2026

Copy link
Copy Markdown

Thanks @wmhn1872265132 for the explanation.

I believe I now understand what happened.

In abdel792/clock, I was using the same Crowdin token as in hkatic/clock, namely my personal Crowdin API token.

Since the synchronization workflows were able to run against the same Crowdin project using the same token, this likely created the conflict you identified during the last synchronization cycle.

At the time, abdel792/clock was only a temporary fork that I had created for testing purposes and for preparing pull requests.

I have now deleted that fork to prevent this situation from happening again.

For future pull requests, I will avoid configuring CROWDIN_TOKEN in such testing forks, so that they cannot interact with the Crowdin project and interfere with the synchronization process.

I also have contributor access to hkatic/clock, so I can integrate future AddonTemplate updates directly in the main repository without necessarily relying on a fork for testing.

Thanks for investigating this and identifying the real cause of the issue. Your explanation makes perfect sense now.

@seanbudd

seanbudd commented Jun 9, 2026

Copy link
Copy Markdown
Member

There's been a long conversation here - does anything need further action? is this still ready for review?

@nvdaes

nvdaes commented Jun 9, 2026

Copy link
Copy Markdown
Author

@seanbudd , this is ready for review.

Note: When this is merged, we may have to create a PR to the add-on-datastore repo to inform that this is not in progress.

@abdel792

abdel792 commented Jun 9, 2026

Copy link
Copy Markdown

Hi @seanbudd,

Apologies for the length of the discussion. Several topics were investigated during the review process, which probably makes it difficult to quickly identify the final state of the PR from the conversation alone.

Since then, @nvdaes has addressed the remaining comments and pushed two additional commits, including a pre-commit cleanup and the corresponding uv.lock update.

For reference, the workflow validation run I shared previously completed successfully:

https://github.com/abdel792/dayOfTheWeek/actions/runs/27070359050/job/79898355233

I would also like to thank @nvdaes, @wmhn1872265132 and @CyrilleB79 for their reviews, testing, suggestions, and feedback throughout this process. Their comments helped improve both the implementation and the documentation.

Following @nvdaes' assessment, I can also confirm from my side that the requested changes have been implemented, the workflow behavior has been validated, and the documentation has been updated accordingly.

As far as I can tell, this PR is ready for NV Access review.

Comment thread readme.md Outdated
Comment thread readme.md Outdated
Comment thread readme.md Outdated
Co-authored-by: Sean Budd <seanbudd123@gmail.com>

@seanbudd seanbudd left a comment

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.

Thanks @nvdaes @abdel792 @wmhn1872265132 and everyone else who worked on this.

seanbudd and others added 2 commits June 10, 2026 11:16
@seanbudd seanbudd merged commit 73ff36c into nvaccess:master Jun 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants