diff --git a/.forgejo/workflows/tag-release.yml b/.forgejo/workflows/tag-release.yml index 224bfb0..91b476b 100644 --- a/.forgejo/workflows/tag-release.yml +++ b/.forgejo/workflows/tag-release.yml @@ -19,6 +19,7 @@ on: - cache - checkout - helm-deploy + - i18n-sync - inject-content - maven-build - pnpm-build diff --git a/README.md b/README.md index 07aa3e3..079207e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Shared actions for Forgejo CI/CD pipelines. | [cache](cache) | Cache files between workflow runs | | [checkout](checkout) | Action for checking out a repository | | [helm-deploy](helm-deploy) | Deploy a service to Kubernetes via Helm over SSH | +| [i18n-sync](i18n-sync) | Fetch translations from i18n.schmalz.com and open a pull request | | [inject-content](inject-content) | Inject content into a file by appending or overwriting | | [maven-build](maven-build) | Action for building and validating Maven projects | | [pnpm-build](pnpm-build) | Action for building and validating with PNPM | diff --git a/i18n-sync/README.md b/i18n-sync/README.md new file mode 100644 index 0000000..5adeee7 --- /dev/null +++ b/i18n-sync/README.md @@ -0,0 +1,46 @@ +# i18n-sync + +Fetches the latest translations from i18n.schmalz.com, commits them to a `chore/i18n` branch, and opens a pull request against the destination branch. + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `i18n-application` | Yes | — | Application key in i18n.schmalz.com (e.g. `calculator`) | +| `locales-folder` | No | `locales` | Path to the locales directory relative to the repository root | +| `format-folder` | No | `""` | Directory containing the `package.json` whose format script should be run after downloading translations. Leave empty to skip formatting. | +| `format-script` | No | `format` | pnpm script name to run for formatting (e.g. `format`, `format:fix`) | +| `jfrog-token` | No | `""` | JFrog npm auth token (required when `format-folder` is set) | +| `forgejo-token` | Yes | — | Forgejo token with `contents:write` and `pull-requests:write` access | +| `destination-branch` | No | `dev` | Target branch for the pull request | + +## Usage + +Minimal — no formatting: + +```yaml +- uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/i18n-sync@i18n-sync-v1 + with: + i18n-application: my-app + forgejo-token: ${{ secrets.FORGEJO_I18N_UPDATE_TOKEN }} +``` + +With formatting after download: + +```yaml +- uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/i18n-sync@i18n-sync-v1 + with: + i18n-application: my-app + locales-folder: frontend/locales + format-folder: frontend + jfrog-token: ${{ secrets.JFROG_TOKEN }} + forgejo-token: ${{ secrets.FORGEJO_I18N_UPDATE_TOKEN }} +``` + +## Notes + +- The action fails fast if the `chore/i18n` branch already exists, indicating a previous update is still pending review. +- If no translation changes are detected after downloading, the action exits cleanly without creating a branch or PR. +- Deleted languages (files removed from i18n.schmalz.com) are also staged for removal via `git add --all`. +- `jq` is installed automatically if not present on the runner. +- The `forgejo-token` must belong to a user or bot with `contents:write` and `pull-requests:write` permissions on the repository. diff --git a/i18n-sync/action.yml b/i18n-sync/action.yml new file mode 100644 index 0000000..ac28fd4 --- /dev/null +++ b/i18n-sync/action.yml @@ -0,0 +1,131 @@ +name: i18n Sync +description: > + Fetches the latest translations from i18n.schmalz.com, commits them to a + chore/i18n branch, and opens a pull request against the destination branch. + +inputs: + i18n-application: + description: Application key in i18n.schmalz.com (e.g. "calculator") + required: true + locales-folder: + description: Path to the locales directory relative to the repository root + required: false + default: locales + format-folder: + description: > + Directory containing the package.json whose format script should be run + after downloading translations. Leave empty to skip formatting. + required: false + default: "" + format-script: + description: pnpm script name to run for formatting (e.g. "format", "format:fix") + required: false + default: format + jfrog-token: + description: JFrog npm auth token (required when format-folder is set) + required: false + default: "" + forgejo-token: + description: Forgejo token with contents:write and pull-requests:write access + required: true + destination-branch: + description: Target branch for the pull request + required: false + default: dev + +runs: + using: composite + steps: + - name: Install jq if missing + shell: bash + run: | + set -euo pipefail + command -v jq >/dev/null 2>&1 || sudo apt-get install -y --no-install-recommends jq + + - name: Configure git + shell: bash + env: + FORGEJO_TOKEN: ${{ inputs.forgejo-token }} + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + ORIGIN_URL=$(git remote get-url origin) + CLEAN_URL=$(echo "$ORIGIN_URL" | sed 's|https://[^@]*@|https://|') + git remote set-url origin "$(echo "$CLEAN_URL" | sed "s|https://|https://github-actions[bot]:${FORGEJO_TOKEN}@|")" + + - name: Check if i18n branch already exists + shell: bash + run: | + set -euo pipefail + if git ls-remote --exit-code --heads origin "refs/heads/chore/i18n" >/dev/null 2>&1; then + echo "Branch chore/i18n already exists. A previous i18n update is still pending." + exit 1 + fi + + - name: Download translations + shell: bash + env: + I18N_APPLICATION: ${{ inputs.i18n-application }} + LOCALES_FOLDER: ${{ inputs.locales-folder }} + run: | + set -euo pipefail + git checkout -b chore/i18n + + echo "Fetching available languages for application: $I18N_APPLICATION" + languages_json=$(curl --fail-with-body -sSL "https://i18n.schmalz.com/api/languages?projectKey=$I18N_APPLICATION") + + echo "$languages_json" | jq -r '.[]' | while read -r lang; do + echo "Downloading language: $lang" + curl --fail-with-body -sSL -o "${LOCALES_FOLDER}/${lang}.json" \ + "https://i18n.schmalz.com/api/${I18N_APPLICATION}/${lang}.json" + done + + - name: Format translations + if: ${{ inputs.format-folder != '' }} + uses: https://schmalz-git.git.onstackit.cloud/schmalz/shared-actions/pnpm-build@pnpm-build-v1 + with: + working-directory: ${{ inputs.format-folder }} + jfrog-token: ${{ inputs.jfrog-token }} + run-scripts: ${{ inputs.format-script }} + check-dedupe: "false" + + - name: Commit and push translations + id: commit + shell: bash + env: + LOCALES_FOLDER: ${{ inputs.locales-folder }} + run: | + set -euo pipefail + git add --all "${LOCALES_FOLDER}/" + + if git diff-index --cached --quiet HEAD; then + echo "No translation changes detected. Nothing to do." + echo "has_changes=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git commit -m "chore: update translations via i18n" + git push origin chore/i18n + echo "has_changes=true" >> "$GITHUB_OUTPUT" + + - name: Create pull request + if: ${{ steps.commit.outputs.has_changes == 'true' }} + shell: bash + env: + TOKEN: ${{ inputs.forgejo-token }} + DESTINATION_BRANCH: ${{ inputs.destination-branch }} + run: | + set -euo pipefail + curl --fail-with-body -s -X POST \ + -H "Authorization: token ${TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/pulls" \ + -d "$(jq -n \ + --arg title "chore: update translations via i18n" \ + --arg head "chore/i18n" \ + --arg base "$DESTINATION_BRANCH" \ + --arg body "Automated translation update from i18n.schmalz.com" \ + '{title: $title, head: $head, base: $base, body: $body, delete_branch_after_merge: true}' + )"