Skip to content

Crowdin – Download Translations & Open PR #20

Crowdin – Download Translations & Open PR

Crowdin – Download Translations & Open PR #20

name: Crowdin – Download Translations & Open PR
on:
# schedule:
# - cron: '0 6 * * 1' # Every Monday 06:00 UTC — adjust to your cadence
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
crowdin-sync:
name: Sync translations from Crowdin
runs-on: ubuntu-latest
env:
TARGET_BRANCH: locale_community # stable base — PR merges INTO this
PATCH_BRANCH: patch # short-lived — PR is opened FROM this
steps:
# ── 1. Checkout ──────────────────────────────────────────────────────────
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
# ── 2. Restore last-run timestamp from cache ──────────────────────────────
- name: Restore last-run timestamp
uses: actions/cache/restore@v4
with:
path: .crowdin-last-run
key: crowdin-last-run-timestamp-${{ github.run_id }}
restore-keys: |
crowdin-last-run-timestamp-
- name: Resolve sync window timestamps
id: sync_time
run: |
if [ -f .crowdin-last-run/timestamp ]; then
DATE_FROM=$(cat .crowdin-last-run/timestamp)
echo "Using cached last-run timestamp: $DATE_FROM"
else
DATE_FROM=$(date -u -d "7 days ago" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null \
|| date -u -v-7d +"%Y-%m-%dT%H:%M:%SZ")
echo "No cache found — defaulting to 7 days ago: $DATE_FROM"
fi
DATE_TO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "date_from=$DATE_FROM" >> "$GITHUB_OUTPUT"
echo "date_to=$DATE_TO" >> "$GITHUB_OUTPUT"
echo "Sync window: $DATE_FROM → $DATE_TO"
# Snapshot current SHA of locale_community for diff check
if git ls-remote --exit-code --heads origin "$TARGET_BRANCH"; then
echo "prev_sha=$(git rev-parse origin/$TARGET_BRANCH)" >> "$GITHUB_OUTPUT"
else
echo "prev_sha=" >> "$GITHUB_OUTPUT"
fi
# ── 3. Prepare the patch branch ───────────────────────────────────────────
- name: Prepare patch branch
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git ls-remote --exit-code --heads origin "$TARGET_BRANCH"; then
git fetch origin "$TARGET_BRANCH"
git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH"
else
echo "locale_community does not exist yet — creating from main"
git checkout -B "$TARGET_BRANCH" origin/main
git push origin "$TARGET_BRANCH"
fi
git checkout -B "$PATCH_BRANCH" "$TARGET_BRANCH"
git push --force origin "$PATCH_BRANCH"
# ── 4. Download translations from Crowdin onto the patch branch ───────────
- name: Download translations from Crowdin
uses: crowdin/github-action@v2
with:
upload_sources: false
upload_translations: false
download_translations: true
create_pull_request: false
localization_branch_name: ${{ env.PATCH_BRANCH }}
commit_message: 'chore(i18n): sync translations from Crowdin'
github_user_name: 'crowdin-bot'
github_user_email: 'support+bot@crowdin.com'
project_id: ${{ secrets.CROWDIN_PROJECT_ID }}
token: ${{ secrets.CROWDIN_TOKEN }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ── 5. Check whether anything actually changed ────────────────────────────
- name: Check for translation changes
id: changed
run: |
git fetch origin "$TARGET_BRANCH" "$PATCH_BRANCH"
CHANGED_FILES=$(git diff --name-only \
"origin/$TARGET_BRANCH" "origin/$PATCH_BRANCH" \
| grep -E '\.(po|pot)$' || echo "")
if [ -z "$CHANGED_FILES" ]; then
echo "No translation files changed. Skipping PR."
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "Changed files:"
echo "$CHANGED_FILES"
{
echo "changed_files<<EOF"
echo "$CHANGED_FILES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
fi
# ── 6. Fetch contributors for the changed files only ─────────────────────
# 1. Map each changed .po file back to its Crowdin source path (/en/ version)
# 2. Look up the Crowdin file ID for each source path
# 3. Query approvals scoped to those file IDs + sync date window
# 4. Only people who approved strings in the changed files are listed
- name: Fetch contributors for changed files
id: contributors
if: steps.changed.outputs.has_changes == 'true'
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_TOKEN: ${{ secrets.CROWDIN_TOKEN }}
DATE_FROM: ${{ steps.sync_time.outputs.date_from }}
DATE_TO: ${{ steps.sync_time.outputs.date_to }}
CHANGED_FILES: ${{ steps.changed.outputs.changed_files }}
run: |
echo "Fetching contributors between $DATE_FROM and $DATE_TO"
# ── A: Fetch all Crowdin file IDs once — matched per changed file below ───
ALL_FILES=$(curl -sf \
"https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/files?limit=500" \
-H "Authorization: Bearer ${CROWDIN_TOKEN}")
# ── B: Fetch approvals per file+language, collect unique approvers ────────
# The approvals endpoint requires both fileId AND languageId.
# We extract the language code from each changed file path directly
# e.g. locale/fr/LC_MESSAGES/django.po → "fr"
# Then query approvals for each unique file+language combination.
declare -A SEEN_USERS
declare -A SEEN_FILE_LANG # avoid duplicate file+lang lookups
CO_AUTHORS=""
while IFS= read -r changed_file; do
[ -z "$changed_file" ] && continue
# Extract language code from path e.g. locale/fr/LC_MESSAGES → fr
lang_code=$(echo "$changed_file" | grep -oP '(?<=/locale/)[^/]+(?=/LC_MESSAGES)')
[ -z "$lang_code" ] && continue
# Map to source path to get file ID
src_path="/$(echo "$changed_file" | sed 's|/locale/[^/]*/LC_MESSAGES/|/locale/en/LC_MESSAGES/|g')"
file_id=$(echo "$ALL_FILES" | jq -r \
--arg p "$src_path" \
'.data[] | select(.data.path == $p) | .data.id')
[ -z "$file_id" ] || [ "$file_id" = "null" ] && continue
# Skip if we already fetched this file+language combination
key="${file_id}:${lang_code}"
[ -n "${SEEN_FILE_LANG[$key]+_}" ] && continue
SEEN_FILE_LANG[$key]=1
echo "Fetching approvals for file ID $file_id, language $lang_code"
APPROVALS=$(curl -sf \
"https://api.crowdin.com/api/v2/projects/${CROWDIN_PROJECT_ID}/approvals?fileId=${file_id}&languageId=${lang_code}&limit=500" \
-H "Authorization: Bearer ${CROWDIN_TOKEN}")
while IFS=$'\t' read -r fullname username; do
[ -z "$username" ] && continue
if [ -z "${SEEN_USERS[$username]+_}" ]; then
SEEN_USERS[$username]=1
name="${fullname:-$username}"
CO_AUTHORS+="Co-authored-by: $name <${username}@users.noreply.github.com>"$'\n'
fi
done < <(echo "$APPROVALS" | jq -r \
--arg from "$DATE_FROM" \
--arg to "$DATE_TO" \
'.data[]
| select(.data.createdAt >= $from and .data.createdAt <= $to)
| select(.data.user != null)
| [(.data.user.fullName // .data.user.username), .data.user.username]
| @tsv')
done <<< "$CHANGED_FILES"
CO_AUTHORS=$(echo "$CO_AUTHORS" | sort -u | sed '/^$/d')
if [ -z "$CO_AUTHORS" ]; then
echo "No approvers found in window for the changed files."
CO_AUTHORS="Co-authored-by: Crowdin Community <crowdin@noreply.com>"
fi
echo "Contributors:"
echo "$CO_AUTHORS"
{
echo "co_authors<<EOF"
echo "$CO_AUTHORS"
echo "EOF"
} >> "$GITHUB_OUTPUT"
# ── 7. Amend the commit on patch with Co-authored-by trailers ────────────
- name: Amend commit with Co-authored-by
if: steps.changed.outputs.has_changes == 'true'
run: |
sudo chown -R "$(id -u):$(id -g)" .git
git config --global safe.directory "$GITHUB_WORKSPACE"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch origin "$PATCH_BRANCH"
git checkout "$PATCH_BRANCH"
COMMIT_MSG="chore(i18n): sync Crowdin translations
${{ steps.contributors.outputs.co_authors }}"
git commit --allow-empty --amend -m "$COMMIT_MSG"
git push --force-with-lease origin "$PATCH_BRANCH"
# ── 8. Open PR from patch → locale_community ─────────────────────────────
- name: Create or update Pull Request
if: steps.changed.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ env.PATCH_BRANCH }}
base: ${{ env.TARGET_BRANCH }}
title: '🌐 Crowdin translation sync – community contributions'
body: |
## 🌐 Crowdin Translation Sync
**Sync window:** `${{ steps.sync_time.outputs.date_from }}` → `${{ steps.sync_time.outputs.date_to }}`
### 📄 Changed translation files
```
${{ steps.changed.outputs.changed_files }}
```
### 👥 Translators in this batch
Only contributors who approved strings in the changed files during
this window are credited via `Co-authored-by:` in the commit:
```
${{ steps.contributors.outputs.co_authors }}
```
---
*Merge this PR into `locale_community` to accept the translations.*
*Generated by the **Crowdin Translations** workflow.*
labels: |
translations
community
crowdin
draft: false
# ── 9. Persist timestamp — only on full success ───────────────────────────
- name: Write last-run timestamp
if: steps.changed.outputs.has_changes == 'true'
run: |
mkdir -p .crowdin-last-run
echo "${{ steps.sync_time.outputs.date_to }}" > .crowdin-last-run/timestamp
- name: Save last-run timestamp cache
if: steps.changed.outputs.has_changes == 'true'
uses: actions/cache/save@v4
with:
path: .crowdin-last-run
key: crowdin-last-run-timestamp-${{ github.run_id }}
- name: Write last-run timestamp (no changes)
if: steps.changed.outputs.has_changes == 'false'
run: |
mkdir -p .crowdin-last-run
echo "${{ steps.sync_time.outputs.date_to }}" > .crowdin-last-run/timestamp
- name: Save last-run timestamp cache (no changes)
if: steps.changed.outputs.has_changes == 'false'
uses: actions/cache/save@v4
with:
path: .crowdin-last-run
key: crowdin-last-run-timestamp-${{ github.run_id }}