From 901ab87717f642f2ab5ce68ef0d01be38ca6726d Mon Sep 17 00:00:00 2001
From: Alexandre Flament <alex@al-f.net>
Date: Thu, 5 Aug 2021 13:57:48 +0200
Subject: [PATCH] [translations] web integration

* make babel.translations.to.master: pull weblate updates
* make babel.master.to.translations: push .pot and .po files to weblate
---
 .github/workflows/integration.yml         |  40 +++---
 .github/workflows/translations-update.yml |  56 ++++++++
 .weblate                                  |   3 +
 Makefile                                  |   2 +-
 README.rst                                |   2 +
 docs/dev/translation.rst                  |  81 ++++--------
 docs/dev/translation.svg                  | Bin 0 -> 17236 bytes
 manage                                    | 153 +++++++++++++++++-----
 requirements-dev.txt                      |   1 +
 9 files changed, 233 insertions(+), 105 deletions(-)
 create mode 100644 .github/workflows/translations-update.yml
 create mode 100644 .weblate
 create mode 100644 docs/dev/translation.svg

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 78fc66e3f..886df3b2f 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -91,7 +91,7 @@ jobs:
         COMMIT_MESSAGE: build from commit ${{ github.sha }}
 
   babel:
-    name: Babel
+    name: Update translations branch
     runs-on: ubuntu-20.04
     if: ${{ github.repository_owner == 'searxng' && github.ref == 'refs/heads/master' }}
     needs:
@@ -102,32 +102,32 @@ jobs:
     - name: Checkout
       uses: actions/checkout@v2
       with:
-        persist-credentials: false
+        fetch-depth: '0'
+        token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
     - name: Set up Python
       uses: actions/setup-python@v2
       with:
         python-version: '3.9'
         architecture: 'x64'
+    - name: Cache Python dependencies
+      id: cache-python
+      uses: actions/cache@v2
+      with:
+        path: ./local
+        key: python-ubuntu-20.04-3.9-${{ hashFiles('requirements*.txt', 'setup.py') }}
+    - name: weblate & git setup
+      env:
+        WEBLATE_CONFIG: ${{ secrets.WEBLATE_CONFIG }}
+      run: |
+        mkdir -p ~/.config
+        echo "${WEBLATE_CONFIG}" > ~/.config/weblate
+        git config --global user.email "searxng-bot@users.noreply.github.com"
+        git config --global user.name "searxng-bot"
     - name: Update transations
       id: update
-      continue-on-error: true
-      run: make V=1 ci.babel.update
-    - name: Open pull request
-      if: steps.update.outcome == 'success'
-      uses: peter-evans/create-pull-request@v3
-      with:
-        commit-message: Update translations (pot, po)
-        committer: searx-bot <noreply@github.com>
-        author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
-        signoff: false
-        branch: update_translations_pot
-        delete-branch: true
-        draft: false
-        title: 'Update translations (pot, po)'
-        body: |
-          Update messages.pot and messages.po files
-        labels: |
-          translation
+      run: |
+        git restore utils/brand.env
+        make V=1 babel.master.to.translations
 
   dockers:
     name: Docker
diff --git a/.github/workflows/translations-update.yml b/.github/workflows/translations-update.yml
new file mode 100644
index 000000000..274a093df
--- /dev/null
+++ b/.github/workflows/translations-update.yml
@@ -0,0 +1,56 @@
+name: "Update translations"
+on:
+  schedule:
+    - cron: "05 07 * * 5"
+  workflow_dispatch:
+
+jobs:
+  babel:
+    name: "translations: update master branch"
+    runs-on: ubuntu-20.04
+    if: ${{ github.repository_owner == 'searxng' && github.ref == 'refs/heads/master' }}
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v2
+      with:
+        fetch-depth: '0'
+        token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: '3.9'
+        architecture: 'x64'
+    - name: Cache Python dependencies
+      id: cache-python
+      uses: actions/cache@v2
+      with:
+        path: ./local
+        key: python-ubuntu-20.04-3.9-${{ hashFiles('requirements*.txt', 'setup.py') }}
+    - name: weblate & git setup
+      env:
+        WEBLATE_CONFIG: ${{ secrets.WEBLATE_CONFIG }}
+      run: |
+        mkdir -p ~/.config
+        echo "${WEBLATE_CONFIG}" > ~/.config/weblate
+        git config --global user.email "searxng-bot@users.noreply.github.com"
+        git config --global user.name "searxng-bot"
+    - name: Merge and push transation updates
+      run: |
+        make V=1 babel.translations.to.master
+    - name: Create Pull Request
+      id: cpr
+      uses: peter-evans/create-pull-request@v3
+      with:
+        token: ${{ secrets.WEBLATE_GITHUB_TOKEN }}
+        commit-message: Update translations
+        committer: searxng-bot <searxng-bot@users.noreply.github.com>
+        author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
+        signoff: false
+        branch: translations_update
+        delete-branch: true
+        draft: false
+        title: 'Update translations'
+        body: |
+          Update translations
+        labels: |
+          translation
diff --git a/.weblate b/.weblate
new file mode 100644
index 000000000..80bdba884
--- /dev/null
+++ b/.weblate
@@ -0,0 +1,3 @@
+[weblate]
+url = https://weblate.bubu1.eu/api/
+translation = searxng/searxng
diff --git a/Makefile b/Makefile
index d48f5c531..cee1e4b97 100644
--- a/Makefile
+++ b/Makefile
@@ -77,7 +77,7 @@ test.shell:
 # wrap ./manage script
 
 MANAGE += buildenv
-MANAGE += ci.babel.update babel.extract babel.update babel.compile
+MANAGE += babel.translations.to.master babel.master.to.translations
 MANAGE += data.all data.languages data.useragents data.osm_keys_tags
 MANAGE += docs.html docs.live docs.gh-pages docs.prebuild docs.clean
 MANAGE += docker.build docker.push docker.buildx
diff --git a/README.rst b/README.rst
index 1ab7ee201..a4d2a30ef 100644
--- a/README.rst
+++ b/README.rst
@@ -32,6 +32,8 @@ Privacy-respecting, hackable `metasearch engine`_
 .. |commits| image:: https://img.shields.io/github/commit-activity/y/searxng/searxng?color=yellow&label=commits
    :target: https://github.com/searxng/searxng/commits/master
 
+.. |weblate| image:: https://weblate.bubu1.eu/widgets/searxng/-/searxng/svg-badge.svg
+   :target: https://weblate.bubu1.eu/projects/searxng/
 
 If you are looking for running instances, ready to use, then visit searx.space_.
 
diff --git a/docs/dev/translation.rst b/docs/dev/translation.rst
index 523dcf78c..be0cf5bef 100644
--- a/docs/dev/translation.rst
+++ b/docs/dev/translation.rst
@@ -4,69 +4,46 @@
 Translation
 ===========
 
-.. _searx@transifex: https://www.transifex.com/asciimoo/searx/
+.. _weblate.bubu1.eu: https://weblate.bubu1.eu/projects/searxng/
 
-Translation currently takes place on `searx@transifex`_
+Translation takes place on `weblate.bubu1.eu`_ ( `documentation <https://docs.weblate.org/en/latest/index.html>`_ ).
 
-Requirements
-============
+New messages on the master branch are extracted and pushed to Weblate automatically.
 
-* Transifex account
+Every Friday, a GitHub workflow creates a pull request with the updated translations (messages.mo, messages.po, messages.mo files).
 
-Init Transifex project
-======================
+.. image:: https://weblate.bubu1.eu/widgets/searxng/-/searxng/svg-badge.svg
+   :target: https://weblate.bubu1.eu/projects/searxng/
 
-After installing ``transifex`` using pip, run the following command to
-initialize the project.
-
-.. code:: sh
-
-   ./manage pyenv.cmd tx init # Transifex instance: https://www.transifex.com/asciimoo/searx/
-
-
-After ``$HOME/.transifexrc`` is created, get a Transifex API key and insert it
-into the configuration file.
-
-Create a configuration file for ``tx`` named ``$HOME/.tx/config``.
-
-.. code:: ini
-
-    [main]
-    host = https://www.transifex.com
-    [searx.messagespo]
-    file_filter = searx/translations/<lang>/LC_MESSAGES/messages.po
-    source_file = messages.pot
-    source_lang = en
-    type = PO
-
-
-Then run ``tx set``:
-
-.. code:: shell
-
-    ./manage pyenv.cmd tx set --auto-local \
-        -r searx.messagespo 'searx/translations/<lang>/LC_MESSAGES/messages.po' \
-        --source-lang en --type PO --source-file messages.pot --execute
-
-
-Update translations
+Weblate integration
 ===================
 
-To retrieve the latest translations, pull it from Transifex.
+Weblate monitors the `translations branch <https://github.com/searxng/searxng/tree/translations>`_, not the master branch.
 
-.. code:: sh
+This branch contains only the .pot and pot files, nothing else.
 
-   ./manage pyenv.cmd tx pull -a
-   [?] Enter your api token: ....
+Documentation
+-------------
 
-Then check the new languages.  If strings translated are not enough, delete those
-folders, because those should not be compiled.  Call the command below to compile
-the ``.po`` files.
+* `wlc <https://docs.weblate.org/en/latest/wlc.html>`_
+* `pybabel <http://babel.pocoo.org/en/latest/cmdline.html>`_
+* `weblate workflow <https://docs.weblate.org/en/latest/workflows.html>`_
 
-.. code:: shell
+Worfklow
+--------
 
-   ./manage pyenv.cmd pybabel compile -d searx/translations
+.. image:: translation.svg
 
 
-After the compilation is finished commit the ``.po`` and ``.mo`` files and
-create a PR.
+wlc
+---
+
+All weblate integration is done by GitHub workflows, but if you want to use wlc, copy this content into `~/.config/weblate <https://docs.weblate.org/en/latest/wlc.html#wlc-config>`_ :
+
+.. code-block:: ini
+
+  [keys]
+  https://weblate.bubu1.eu/api/ = APIKEY
+
+
+Replace `APIKEY` by `your API key <https://weblate.bubu1.eu/accounts/profile/#api>`_.
diff --git a/docs/dev/translation.svg b/docs/dev/translation.svg
new file mode 100644
index 0000000000000000000000000000000000000000..060e58420afb9e84eb6412a3e27b72f97634d20b
GIT binary patch
literal 17236
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

literal 0
HcmV?d00001

diff --git a/manage b/manage
index 42c617f5b..7a7c98143 100755
--- a/manage
+++ b/manage
@@ -45,9 +45,8 @@ help() {
 buildenv:
   rebuild ./utils/brand.env
 babel.:
-  extract   : extract messages from source files and generate POT file
-  update    : update existing message catalogs from POT file
-  compile   : compile translation catalogs into binary MO files
+  master.to.translations: update the translations branch from the messages of the master branch.
+  translations.to.master: copy change from the translations branch to the master branch.
 data.:
   all       : update searx/languages.py and ./data/*
   languages : update searx/data/engines_languages.json & searx/languages.py
@@ -122,46 +121,136 @@ buildenv() {
     return "${PIPESTATUS[0]}"
 }
 
-babel.sha256sum() {
-    grep "msgid" "searx/translations/messages.pot" | sort | sha256sum | cut -f1 -d ' '
+TRANSLATIONS_WORKTREE="$CACHE/translations"
+
+babel.setup.translations.worktree() {
+    (   set -e
+        if ! git remote get-url weblate 2> /dev/null; then
+            git remote add weblate https://weblate.bubu1.eu/git/searxng/searxng/
+        fi
+        if [ -d "${TRANSLATIONS_WORKTREE}" ]; then
+            pushd .
+            cd "${TRANSLATIONS_WORKTREE}"
+            git reset --hard HEAD
+            git pull origin translations
+            popd
+        else
+            mkdir -p "${TRANSLATIONS_WORKTREE}"
+            git worktree add "${TRANSLATIONS_WORKTREE}" translations
+        fi
+    )
 }
 
-ci.babel.update() {
-    local sha_before
+babel.weblate.to.translations() {
     (   set -e
-        sha_before="$(babel.sha256sum)"
-        babel.extract
-        if [ "$(babel.sha256sum)" = "${sha_before}" ]; then
-            build_msg BABEL 'no changes detected, exiting'
-            return 1
+        if [ "$(pyenv.cmd wlc lock-status)" != "locked: True" ]; then
+            build_msg BABEL "weblate must be locked, currently: $(pyenv.cmd wlc lock-status)"
+            exit 1
         fi
-        babel.update
-        build_msg BABEL 'update done, edit .po files if required and run babel.compile'
+        # weblate: commit pending changes
+        pyenv.cmd wlc pull
+        pyenv.cmd wlc commit
+        # get the translations in a worktree
+        babel.setup.translations.worktree
+        cd "${TRANSLATIONS_WORKTREE}"
+        git remote update weblate
+        git merge weblate/translations
+        git push
     )
     dump_return $?
 }
 
-babel.extract() {
-    build_msg BABEL 'extract messages from source files and generate POT file'
-    pyenv.cmd pybabel extract -F babel.cfg \
-            -o "searx/translations/messages.pot" \
-            "searx/"
-    dump_return $?
+babel.translations.to.master() {
+    local existing_commit_hash commit_body commit_message exitcode
+    (   set -e
+        pyenv.cmd wlc lock
+        babel.setup.translations.worktree
+        existing_commit_hash=$(cd "${TRANSLATIONS_WORKTREE}"; git log -n1  --pretty=format:'%h')
+        # pull weblate commits
+        babel.weblate.to.translations
+        # copy the changes to the master branch
+        cp -rv --preserve=mode,timestamps "${TRANSLATIONS_WORKTREE}/searx/translations" "searx"
+        # compile translations
+        build_msg BABEL 'compile translation catalogs into binary MO files'
+        pyenv.cmd pybabel compile --statistics \
+                -d "searx/translations"
+        # git add/commit (no push)
+        commit_body=$(cd "${TRANSLATIONS_WORKTREE}"; git log --pretty=format:'%h - %as - %aN <%ae>' "${existing_commit_hash}..HEAD")
+        commit_message=$(echo -e "[translations] update\n${commit_body}")
+        git add searx/translations
+        git commit -m "${commit_message}"
+    )
+    exitcode=$?
+    (   # make sure to always unlock weblate
+        set -e
+        pyenv.cmd wlc unlock
+    )
+    dump_return $exitcode
 }
 
-babel.update() {
-    build_msg BABEL 'update existing message catalogs from POT file'
-    pyenv.cmd pybabel update -N \
-              -i "searx/translations/messages.pot" \
-              -d "searx/translations"
-    dump_return $?
-}
+babel.master.to.translations() {
+    local messages_pot diff_messages_pot last_commit_hash last_commit_detail last_commit_message exitcode
+    (   set -e
+        # lock change on weblate
+        pyenv.cmd wlc lock
 
-babel.compile() {
-    build_msg BABEL 'compile translation catalogs into binary MO files'
-    pyenv.cmd pybabel compile --statistics \
-              -d "searx/translations"
-    dump_return $?
+        # get translation branch
+        babel.setup.translations.worktree
+
+        # update messages.pot
+        build_msg BABEL 'extract messages from source files and generate POT file'
+        messages_pot="${TRANSLATIONS_WORKTREE}/searx/translations/messages.pot"
+        pyenv.cmd pybabel extract -F babel.cfg \
+                -o "${messages_pot}" \
+                "searx/"
+
+        # stop if there is no meaningful change
+        diff_messages_pot=$(cd "${TRANSLATIONS_WORKTREE}"; git diff -- "searx/translations/messages.pot")
+        if ! echo "$diff_messages_pot" | grep -qE "[\+\-](msgid|msgstr)"; then
+            build_msg BABEL 'no changes detected, exiting'
+            return 0
+        fi
+
+        # save messages.pot for later
+        cd "${TRANSLATIONS_WORKTREE}"
+        git stash push
+        cd -
+
+        # merge weblate commits into the translations branch
+        babel.weblate.to.translations
+
+        # restore messages.pot
+        cd "${TRANSLATIONS_WORKTREE}"
+        git stash pop
+        cd -
+
+        set -x
+
+        # update messages.po files
+        build_msg BABEL 'update existing message catalogs from POT file'
+        pyenv.cmd pybabel update -N \
+            -i "${messages_pot}" \
+            -d "${TRANSLATIONS_WORKTREE}/searx/translations"
+
+        # git add/commit/push
+        last_commit_hash=$(git log -n1  --pretty=format:'%h')
+        last_commit_detail=$(git log -n1 --pretty=format:'%h - %as - %aN <%ae>' "${last_commit_hash}")
+        last_commit_message=$(echo -e "[translations] update messages.pot and messages.po files\nFrom ${last_commit_detail}")
+        cd "${TRANSLATIONS_WORKTREE}"
+        git add searx/translations
+        git commit -m "${last_commit_message}"
+        git push
+        cd -
+
+        # notify weblate
+        pyenv.cmd wlc pull
+    )
+    exitcode=$?
+    (   # make sure to always unlock weblate
+        set -e
+        pyenv.cmd wlc unlock
+    )
+    dump_return $exitcode
 }
 
 data.all() {
diff --git a/requirements-dev.txt b/requirements-dev.txt
index a6f29db55..e8dc0221c 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -17,3 +17,4 @@ sphinx-autobuild==2021.3.14
 linuxdoc==20210324
 aiounittest==1.4.0
 yamllint==1.26.2
+wlc==1.12