Back to blog
A git diff view showing a locale file with three changed keys highlighted, and only those three keys being sent to the translation API below.

You don't need to re-translate everything: how delta translation works in CI

By Robert M

You don't need to re-translate everything: how delta translation works in CI

Once you have automated translation wired into your CI pipeline, a natural question comes up quickly: does this re-translate the entire locale file every time someone pushes a change?

Without delta mode, yes. Every push that touches your source locale file sends the whole thing to the API, every key, every string, whether it changed or not. For a small project early on that is fine. For a mature project with hundreds of translation keys across 10 or 20 languages, you are burning tokens on strings that have not changed since the last run and getting back identical output for the privilege.

Delta translation solves this. Instead of sending the full file, the Action compares your current source file against a stored baseline, identifies only the keys that were added or modified, and sends just those to the API. The output for unchanged keys is taken from the existing translated files. Token usage drops to match the actual work being done.


How the baseline works

When delta mode runs for the first time, or when you force a full refresh, the Action translates the complete source file and stores a flat JSON representation of it as a baseline file in your repository. On subsequent runs, the Action loads that baseline, diffs it against the current source file, and builds a payload containing only the changed keys.

For JSON locale files the baseline is stored as .polylingo-baseline.json in your messages directory. For YAML locale files it is .polylingo-yaml-baseline.json in your locales directory. For Laravel PHP lang files it is .polylingo-laravel-php-baseline.json in your lang directory.

The baseline is committed alongside your translated files so it travels with the repository. Any developer who clones the repo gets the same baseline the pipeline is working from.


What counts as a change

The diff operates on the flattened key representation of your source file. Nested structures are flattened to dot-notation keys before comparison:

{
  "nav.home": "Home",
  "nav.about": "About us",
  "hero.title": "Welcome to our platform",
  "hero.cta": "Get started"
}

A key is included in the delta payload if:

  • It exists in the current source file but not in the baseline (new key)
  • It exists in both but the value has changed (modified key) Keys that exist in the baseline but not in the current source file (deleted keys) are removed from the translated output files automatically. Keys that are identical in both are skipped entirely and their existing translations are left in place.

What this looks like in practice

Say you have a Next.js project with 200 translation keys in messages/en.json, already translated into 12 languages. A developer updates the hero section copy and adds two new keys for a feature announcement. That is 4 changed keys out of 200.

Without delta mode, the pipeline sends all 200 keys multiplied across 12 languages on every push. With delta mode it sends 4 keys. The token usage for that run is roughly 2% of what a full translation would cost. The pipeline is also faster because there is less to send and less to wait for.

Over a month of regular development, the saving compounds significantly. Most pushes touch a handful of strings. Full retranslation only happens when you add a new language or explicitly reset the baseline.


The three PolyLingo GitHub Actions

Delta mode is available across all three PolyLingo translation Actions. Each one is built for a specific content type and project structure.

translateAction — JSON and Markdown

The original Action. Handles flat JSON locale files in the next-intl and i18next style, with an optional Markdown documentation pass via the async jobs API for larger files.

- uses: UsePolyLingo/translate-action@v1
  with:
    api_key: ${{ secrets.POLYLINGO_API_KEY }}
    source_file: messages/en.json
    locales: fr,de,es,ja,zh
    delta: true
    commit: true
    commit_message: "chore(i18n): sync translations"

Delta baseline: messages/.polylingo-baseline.json

github.com/UsePolyLingo/translateActionView on Marketplace


translate-action-yaml — YAML locale files

For projects using nested YAML locale files: Rails i18n, Vue i18n, and any other framework that uses the YAML format. The Action handles the Rails convention of a root locale key automatically — en.yml has an en: root key, and each output file gets the correct target locale key (fr:, de: etc).

Since the PolyLingo API works with JSON natively, the Action flattens nested YAML to dot-notation JSON before sending, translates, then reconstructs the nested structure and writes valid YAML output. One v1 caveat worth knowing: keys that contain dots naturally are not supported, as they conflict with the dot-notation flattening.

- uses: UsePolyLingo/translate-action-yaml@v1
  with:
    api_key: ${{ secrets.POLYLINGO_API_KEY }}
    locales_dir: config/locales
    source_file: config/locales/en.yml
    locales: fr,de,es,ja
    delta: true
    commit: true

Delta baseline: config/locales/.polylingo-yaml-baseline.json

github.com/UsePolyLingo/translate-action-yamlView on Marketplace


translate-action-laravel — Laravel lang files

For Laravel projects using either JSON translation files (lang/en.json, supporting both nested and flat structures in Laravel 9+ style) or PHP array lang files (lang/en/*.php). The Action auto-detects which format your project uses via php_format: auto — it checks for lang/en.json first and falls back to PHP array files if not found.

For PHP files it shells out to the PHP CLI to read source files and serializes translated output back to valid PHP array syntax in JavaScript, with no additional dependencies. GitHub-hosted ubuntu-latest runners include PHP 8.x by default so no extra setup step is needed. PHP 7.4 or later is required.

One v1 caveat: keys containing dots are not supported in PHP mode due to the dot-notation flattening strategy.

- uses: UsePolyLingo/translate-action-laravel@v1
  with:
    api_key: ${{ secrets.POLYLINGO_API_KEY }}
    lang_dir: lang
    source_locale: en
    locales: fr,de,es,pt,nl
    php_format: auto
    delta: true
    open_pr: true

Delta baseline: lang/.polylingo-laravel-json-baseline.json or lang/.polylingo-laravel-php-baseline.json depending on format.

github.com/UsePolyLingo/translate-action-laravelView on Marketplace


PR mode vs commit mode

All three Actions support two output modes.

Commit mode (commit: true) writes the translated files and commits them directly to the current branch. Simple setup, good for teams that treat translation as an automated process that does not need review.

PR mode (open_pr: true) creates a new branch (polylingo/yaml-<sha>, polylingo/laravel-<sha> etc), writes the translated files there, and opens a pull request against your base branch. Better for teams that want a human review step before translated content merges, or for projects where translation quality directly affects the user experience.

When both are set, PR mode wins.

PR mode requires pull-requests: write in your workflow permissions:

permissions:
  contents: write
  pull-requests: write

Forcing a full refresh

Delta mode compares against the stored baseline. If you want to retranslate everything regardless of what the baseline shows, set delta: false. This also updates the baseline to match the current source file, so subsequent delta runs start from the new state.

A full refresh is useful when you add a new target language, when you want to pick up translation quality improvements in a new model version, or when the baseline has drifted from reality for any reason.

- uses: UsePolyLingo/translate-action-yaml@v1
  with:
    api_key: ${{ secrets.POLYLINGO_API_KEY }}
    locales_dir: config/locales
    locales: fr,de,es,ja,zh,ko,ar
    delta: false  # full refresh, updates baseline
    commit: true

Outputs for downstream steps

All three Actions expose the same outputs so you can use them in subsequent workflow steps:

- uses: UsePolyLingo/translate-action@v1
  id: translate
  with:
    api_key: ${{ secrets.POLYLINGO_API_KEY }}
    source_file: messages/en.json
    locales: fr,de,es
    delta: true
    commit: true
 
- name: Log translation stats
  run: |
    echo "Locales translated: ${{ steps.translate.outputs.locales_translated }}"
    echo "Files changed: ${{ steps.translate.outputs.files_changed }}"
    echo "Tokens used: ${{ steps.translate.outputs.tokens_used }}"

Each run also writes a summary table to the GitHub Actions step summary so you can see token usage and translation results without digging through logs.


Getting started

All three Actions are available on the GitHub Marketplace. You will need a PolyLingo API key, available free at usepolylingo.com. The free tier includes 50,000 tokens per month. With delta mode enabled, most projects will stay well within that on routine development pushes.

Add your API key as a repository secret (POLYLINGO_API_KEY) and drop the relevant Action into your workflow. The first run does a full translation and sets the baseline. Every run after that only translates what changed.