From 65b6da552e73715afc2423847a017f73d65bd305 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 16:30:59 +0000 Subject: [PATCH] ci: sync translations from develop to version-16-hotfix (backport #55348) (#55353) Co-authored-by: Diptanil Saha Co-authored-by: Claude Sonnet 4.6 fix (#55348) --- .github/helper/merge_po_files.py | 49 +++++++++++ .github/helper/sync_hotfix_translations.sh | 85 +++++++++++++++++++ .../workflows/sync-hotfix-translations.yml | 62 ++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 .github/helper/merge_po_files.py create mode 100644 .github/helper/sync_hotfix_translations.sh create mode 100644 .github/workflows/sync-hotfix-translations.yml diff --git a/.github/helper/merge_po_files.py b/.github/helper/merge_po_files.py new file mode 100644 index 00000000000..e5526208025 --- /dev/null +++ b/.github/helper/merge_po_files.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +"""Overlay develop's .po translations onto hotfix's .po files. + +Called by sync_hotfix_translations.sh before `bench update-po-files`. +Merge rules: + a. msgid absent from develop → keep hotfix's existing msgstr + b. language not yet in hotfix → copy file as-is (bench will filter to main.pot) + c. msgid present in both → use develop's msgstr +""" +import shutil +from pathlib import Path + +from babel.messages.pofile import read_po, write_po + +DEVELOP = Path("/tmp/develop-po/erpnext/locale/") +LOCALE = Path("./apps/erpnext/erpnext/locale/") + +added = updated = 0 + +for src in sorted(DEVELOP.glob("*.po")): + dst = LOCALE / src.name + + with src.open("rb") as f: + dev = read_po(f) + + if not dst.exists(): + shutil.copy(src, dst) + added += 1 + print(f" [new] {src.name}") + continue + + with dst.open("rb") as f: + hf = read_po(f) + + changes = 0 + for msg in hf: + if msg.id and msg.id in dev and dev[msg.id].string and dev[msg.id].string != msg.string: + msg.string = dev[msg.id].string + changes += 1 + + if changes: + with dst.open("wb") as f: + write_po(f, hf) + updated += 1 + print(f" [updated] {src.name} ({changes} msgstr(s) from develop)") + else: + print(f" [no-op] {src.name}") + +print(f"\n{added} new language(s), {updated} updated.") diff --git a/.github/helper/sync_hotfix_translations.sh b/.github/helper/sync_hotfix_translations.sh new file mode 100644 index 00000000000..73b9233ef81 --- /dev/null +++ b/.github/helper/sync_hotfix_translations.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# Syncs Crowdin translations from develop to version-16-hotfix. +# Merge logic: see merge_po_files.py. +# Env: GH_TOKEN, PR_REVIEWER, GITHUB_WORKSPACE (all set by Actions). + +set -e +cd ~ || exit + +echo "=== Setting up bench ===" +pip install frappe-bench +bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)" +cd ./frappe-bench || exit +bench get-app --skip-assets erpnext "${GITHUB_WORKSPACE}" + +echo "=== Fetching develop's .po files ===" +mkdir -p /tmp/develop-po +git -C "${GITHUB_WORKSPACE}" fetch origin develop +git -C "${GITHUB_WORKSPACE}" archive origin/develop erpnext/locale/ \ + | tar -xf - -C /tmp/develop-po/ + +po_count=$(find /tmp/develop-po/erpnext/locale -name "*.po" | wc -l) +if [ "${po_count}" -eq 0 ]; then + echo "ERROR: No .po files found in develop's archive. Aborting." >&2 + exit 1 +fi +echo "Extracted ${po_count} .po file(s) from develop." + +echo "=== Merging and reconciling ===" +env/bin/python "${GITHUB_WORKSPACE}/.github/helper/merge_po_files.py" +bench update-po-files --app erpnext + +cd ./apps/erpnext || exit + +git config user.email "developers@erpnext.com" +git config user.name "frappe-pr-bot" +git remote set-url upstream https://github.com/frappe/erpnext.git + +if git diff --quiet erpnext/locale/; then + echo "Translations are already up to date. No PR needed." + exit 0 +fi + +echo "Changed files:" +git diff --name-only erpnext/locale/ + +echo "=== Committing and pushing ===" +git checkout -B translations_hotfix +git add erpnext/locale/ +git commit -m "fix: sync translations to version-16-hotfix" +gh auth setup-git +git push -u upstream translations_hotfix --force + +echo "=== Opening PR (if not already open) ===" +existing_pr=$(gh pr list \ + --base "version-16-hotfix" \ + --head "translations_hotfix" \ + --state open \ + --json number \ + --jq 'length' \ + -R frappe/erpnext) + +if [ "${existing_pr}" -gt 0 ]; then + echo "PR already open — branch updated in place. No new PR needed." + exit 0 +fi + +gh pr create \ + --base "version-16-hotfix" \ + --head "translations_hotfix" \ + --title "fix: sync translations to version-16-hotfix" \ + --body "Automated sync of Crowdin translations from \`develop\` to \`version-16-hotfix\`. + +A 3-way merge is performed per language, then \`bench update-po-files\` reconciles each \`.po\` against hotfix's \`main.pot\`: + +| Case | Condition | Result | +|------|-----------|--------| +| **a** | \`msgid\` in hotfix's \`main.pot\`, **not** in develop's \`.po\` | Hotfix's existing \`msgstr\` is **preserved** (string removed from develop but still needed in hotfix) | +| **b** | \`msgid\` **not** in hotfix's \`main.pot\` | **Dropped** from hotfix's \`.po\` | +| **c** | \`msgid\` in both hotfix's \`main.pot\` and develop's \`.po\` | Develop's \`msgstr\` is used (Crowdin translation wins) | + +Generated by the \`sync-hotfix-translations\` workflow." \ + --label "translation" \ + --label "skip-release-notes" \ + --reviewer "${PR_REVIEWER}" \ + -R frappe/erpnext diff --git a/.github/workflows/sync-hotfix-translations.yml b/.github/workflows/sync-hotfix-translations.yml new file mode 100644 index 00000000000..fcd29fc658a --- /dev/null +++ b/.github/workflows/sync-hotfix-translations.yml @@ -0,0 +1,62 @@ +# Syncs Crowdin translations from develop into version-16-hotfix, +# filtered to only the strings present in hotfix's main.pot. +# +# Trigger: fires when version-16-hotfix's main.pot is updated — i.e., when +# the POT update PR from generate-pot-file.yml is merged. At that point +# hotfix's main.pot is authoritative, and this workflow fetches develop's +# latest .po files (Crowdin translations) and merges them against it. +# +# The weekly schedule acts as a safety net to pick up any Crowdin translations +# that arrived on develop between POT update cycles. +# +# POT file generation remains in generate-pot-file.yml (unchanged). +# Maintain this file on develop only; it always acts on version-16-hotfix. + +name: Sync translations to version-16-hotfix + +on: + push: + branches: + - version-16-hotfix + paths: + - "erpnext/locale/main.pot" # fires exactly when the POT update PR merges + schedule: + # 10:00 UTC Monday — safety net for Crowdin translations that arrived + # on develop since the last POT update PR merged to version-16-hotfix + - cron: "0 10 * * 1" + workflow_dispatch: + +# Prevent concurrent runs. cancel-in-progress: false because a mid-flight +# `git push` + `gh pr create` cancellation can leave an orphaned remote branch. +concurrency: + group: sync-hotfix-translations + cancel-in-progress: false + +jobs: + sync-translations: + name: Sync translations to version-16-hotfix + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout version-16-hotfix + uses: actions/checkout@v6 + with: + ref: version-16-hotfix + # Full history so `git fetch origin develop` inside the helper works + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + # Match generate-pot-file.yml — bench runs under the same frappe + # stack and must use the same Python version. + python-version: "3.14" + + - name: Run sync script + run: | + bash ${GITHUB_WORKSPACE}/.github/helper/sync_hotfix_translations.sh + env: + GH_TOKEN: ${{ secrets.RELEASE_TOKEN }} + PR_REVIEWER: barredterra # change to your GitHub username if you copied this file