FeaturesPluginPricingResources
Change Language
ResourcesHow to Automate .po Translation in Your CI/CD Pipeline

How to Automate .po Translation in Your CI/CD Pipeline

SimplePoTranslate TeamJune 9, 2026
How to Automate .po Translation in Your CI/CD Pipeline

Your team merges to main fourteen times a day. Every one of those merges might add a new user-facing string - a button label, an error message, a tooltip. And every one of those strings starts life in English only. Somewhere in your release process, a human has to notice the new strings, export them, get them translated, paste them back, and recompile. That human is a bottleneck, and on a fast-shipping team that bottleneck means your German users see English placeholders for two sprints.

The fix is to automate translation in your CI/CD pipeline so that new strings get translated on the same merge that introduced them - no human in the critical path for the routine case. This is entirely achievable today, but only if you avoid the trap most teams fall into: hand-rolling raw LLM calls that quietly corrupt your %s placeholders and .po structure. This guide walks through a realistic CI pipeline pattern, a working GitHub Actions skeleton, and the design decisions - idempotency, translate-only-new, review gates - that separate a pipeline that helps from one that ships broken builds.

Why Manual Translation Is a Release Bottleneck

The answer to "why not just translate before each release" is that the timing never lines up. Strings are added continuously by developers, but translation happens in batches by a different person on a different schedule. The gap between those two cadences is where your localization debt accumulates.

The Two-Cadence Problem

Picture the manual flow. A developer adds __( 'Export to CSV', 'mytextdomain' ) and merges. Nobody regenerates the .pot. Two weeks later someone runs wp i18n make-pot, notices forty new untranslated strings (some from developers who have since left for vacation), guesses at the intent of half of them, and sends a .po to a translator. The translation comes back, gets pasted in, and maybe the placeholders survived and maybe they did not. Meanwhile three releases shipped with those strings in English.

Every step there is manual, batched, and error-prone. The goal of CI/CD automation is to collapse this into something that runs automatically on merge, translates only what actually changed, and fails loudly when something looks wrong - turning translation from a periodic chore into an invisible part of the pipeline.

The Pipeline Pattern: Extract, Diff, Translate, Commit

At a high level, an automated translation job runs four steps on merge to main: regenerate the template, detect what is new, translate only those strings, and commit the results back. The whole thing should be idempotent - running it on a merge with no string changes must produce zero diff and zero noise.

Extract and Diff

Step one is extraction. Regenerate the .pot from current source so it reflects every string in the codebase:

wp i18n make-pot . languages/myplugin.pot

Step two is the diff. This is the most important design decision in the whole pipeline. You do not want to re-translate every string on every merge - that wastes API calls, risks re-translating strings a human already corrected, and produces enormous review diffs. Instead, you merge the fresh .pot into each existing .po and translate only the entries that are now empty (the genuinely new strings):

# Merge new template into each locale, preserving existing translations.
# Newly-added strings appear with empty msgstr; --backup=none keeps the tree clean.
for po in languages/*.po; do
    msgmerge --update --backup=none "$po" languages/myplugin.pot
done

After msgmerge, only brand-new strings have an empty msgstr. Everything previously translated is untouched. That property is what makes the pipeline idempotent and what keeps your review diffs small and reviewable.

Translate and Commit

Step three sends just those empty entries to a translation step. Step four commits the updated .po, compiles .mo, and regenerates any JSON. We will wire all of that into GitHub Actions next.

A GitHub Actions Skeleton

Here is the answer to "what does this actually look like in a workflow file": a job triggered on push to main that runs the extract-diff-translate-commit cycle and opens a pull request with the results rather than committing straight to main.

The Workflow File

name: Translate on merge

on:
  push:
    branches: [ main ]

jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install WP-CLI and gettext
        run: |
          sudo apt-get update && sudo apt-get install -y gettext
          curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
          sudo mv wp-cli.phar /usr/local/bin/wp && sudo chmod +x /usr/local/bin/wp

      - name: Regenerate template and merge into locales
        run: |
          wp i18n make-pot . languages/myplugin.pot
          for po in languages/*.po; do
            msgmerge --update --backup=none "$po" languages/myplugin.pot
          done

      - name: Translate only new (empty) strings
        env:
          TRANSLATE_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
        run: ./scripts/translate-new-strings.sh languages/

      - name: Compile .mo and JSON
        run: |
          wp i18n make-mo languages/
          wp i18n make-json languages/ --no-purge

      - name: Open pull request with translations
        uses: peter-evans/create-pull-request@v6
        with:
          branch: chore/auto-translations
          title: "chore: auto-translate new strings"
          commit-message: "chore: auto-translate new strings"

Where the Real Work Hides

The translate-new-strings.sh step is where the actual translation happens. The naive version of that script reads each empty entry, fires it at an LLM API one string at a time, and pastes the response back. That naive version is exactly the trap, and it is worth being explicit about why.

Why Hand-Rolled LLM Calls Break Your Files

The short answer: raw LLM calls treat your .po strings as prose, and your .po strings are not prose - they are structured data with placeholders, plural forms, and context that a chat-completion call happily destroys.

The Placeholder Corruption You Will Not See in Tests

Send Deleted %d of %s files to a general chat endpoint and you might get back Supprimé %d des %s fichiers (fine) or Supprimé %d de % s fichiers (a space crept into the placeholder, and now sprintf() throws at runtime). Send a plural entry and the model may collapse two forms into one, breaking languages with more than two plural categories. Send a string with <a href="%s"> and the model may translate the URL or drop the tag. None of these failures show up in your tests unless you specifically test rendered output in every locale - they show up as runtime errors in production for users you cannot read bug reports from.

The Maintenance Trap

You can try to defend against this with prompt engineering and regex post-processing, and many teams do. The problem is that you are now maintaining a brittle translation engine as a side project, re-discovering every placeholder edge case the Gettext ecosystem already solved. We catalog the specific ways variables get mangled - and how to prevent them - in translating PO files without breaking code variables. The lesson there applies directly: the model quality is rarely the issue; the structural handling around it is.

Fitting an API-Driven Cloud Translator Into CI

This is where an API-driven translation service replaces your translate-new-strings.sh guesswork. Instead of hand-rolling LLM calls and post-processing regex, your CI step uploads the changed .po to a service that already understands Gettext structure and returns clean output. The pipeline shape stays identical - extract, diff, translate, commit - but the fragile middle step becomes a single API call.

The API Call That Replaces Your Script

SimplePoTranslate is built for exactly this. It exposes a cloud API suited to automation and CI, so your workflow's translate step becomes a request that hands off the .po and gets back a translated one, no per-string looping. Its Syntax Locking holds %s, %1$s, {count}, HTML, and code tokens in place automatically - the entire class of bug from the previous section is handled by the engine rather than by regex you maintain. With full Gettext plural and msgctxt support, plural forms and context survive the round trip, which a chat-completion call cannot guarantee.

Idempotency and the Review Gate Stay Yours

Two design decisions still belong to you regardless of which engine you use. First, idempotency: keep translating only the empty entries after msgmerge, so no-op merges produce no diff. Second, a review gate: have the job open a pull request, as the skeleton above does, rather than committing straight to main. Machine translation is excellent for getting strings live fast, but a human glance before merge catches the rare context miss - and a PR gives you that glance without blocking the common case. Teams juggling many locales or many client sites will recognize this shape from the ideal localization workflow for agencies, where the same automate-then-review pattern scales across dozens of projects.

Conclusion

To automate translation in CI/CD without shipping broken builds, the pattern is consistent: regenerate the .pot on merge, msgmerge it into each locale to surface only new strings, translate just those strings, and commit the result behind a pull-request review gate. Idempotency keeps your diffs clean; the review gate keeps the rare bad translation out of production.

The part to get right is the translate step itself. Hand-rolled LLM calls will corrupt placeholders and plural forms in ways your tests will not catch, and you will spend more time maintaining the translation glue than the feature code it serves. An API-driven engine with Syntax Locking removes that entire failure mode, so your pipeline translates new strings on the same merge that introduced them - and your non-English users stop seeing English placeholders.

Ready to automate translation in your pipeline without breaking placeholders? Try SimplePoTranslate free — no credit card required. Start on the free tier, validate the API against your own .po files, and wire it into CI when you are ready.