GitHub Action

A GitHub Action for translation sync

If your code is on GitHub, this is the way to connect it to locamorph. You commit a workflow file; sync then runs on your triggers, in the CI you already trust, using the same @locamorph/cli your developers run locally. One input decides what a run does — comment on a pull request, send new strings up, or open a PR with finished translations.

Runs on your triggers, in your repo, where you can review it. API keys work on the free plan, and a workflow doesn’t use up an integration slot.

.github/workflows/translation-preview.yml
name: Translation preview
on: pull_request

permissions:
  contents: read
  pull-requests: write

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Locamorph/sync-action@v1
        with:
          action: preview
          api_key: ${{ secrets.LOCAMORPH_API_KEY }}
          github_token: ${{ secrets.GITHUB_TOKEN }}

Comments the diff on every pull request. Start here — it writes nothing.

.github/workflows/translations-push.yml
name: Sync strings to locamorph
on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

jobs:
  push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Locamorph/sync-action@v1
        with:
          action: push
          api_key: ${{ secrets.LOCAMORPH_API_KEY }}

Sends new source strings to locamorph when a branch lands on main.

.github/workflows/translations-pull-request.yml
name: Translation pull request
on:
  schedule:
    - cron: '0 6 * * 1'   # Mondays, 06:00 UTC
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  translations:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Locamorph/sync-action@v1
        with:
          action: pull-request
          api_key: ${{ secrets.LOCAMORPH_API_KEY }}
          # PRs opened with GITHUB_TOKEN trigger no
          # workflows — use a PAT if you require checks.
          github_token: ${{ secrets.GITHUB_TOKEN }}
          pr_title: 'chore: update translations'
          pr_labels: translations,automated

Opens a PR with finished translations on a schedule you choose.

.github/workflows/translations-pull.yml
name: Pull translations
on:
  workflow_dispatch:

permissions:
  contents: write   # for the commit step below, not the action

jobs:
  pull:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Locamorph/sync-action@v1
        with:
          action: pull
          api_key: ${{ secrets.LOCAMORPH_API_KEY }}

      # pull writes files and stops. What happens next is yours.
      - name: Commit if anything changed
        run: |
          git config user.name 'locamorph-bot'
          git config user.email '[email protected]'
          git add locales
          git diff --staged --quiet && exit 0
          git commit -m 'chore: update translations'
          git push

Downloads files and hands control back to your job. No PR, no commit.

Complete files, not fragments. Copy one in and open a pull request.

How it works

Three places hold configuration

Worth learning once, because the split is deliberate. Change the formatting in the app and every repo picks it up, including developers running the CLI locally. Change locamorph.yaml and only that repo changes.

Workflow file

When it runs

Which mode runs, on which event, and what any pull request looks like — title, branch, base, body, labels, reviewers, assignees.

.github/workflows/*.yml

locamorph.yaml

What and where

Project id, translations directory, format, file structure, languages, include and exclude rules, and per-direction overrides under upload and download.

repo root

Project settings

How files come out

Indentation, nested or flat keys, key sorting, plural format, and what happens to empty translations. Shared by every repo and every developer.

the locamorph app

Four modes, one input

Two need nothing but an API key. Two write back to GitHub and need a token. Which mode you pick matters less than the event you pair it with — that pairing is where first setups usually go wrong.

Mode Runs Trigger Token permissions: Writes to You see
pull locamorph download manual / job step no contents: read working tree, no commit changed files in the job
push locamorph upload push to main no contents: read your locamorph project job log
pull-request download + create-pull-request schedule + dispatch yes contents: write, pull-requests: write a pull request a PR, plus has_changes
preview locamorph upload --dryrun pull_request yes pull-requests: write nothing a sticky PR comment

Never pair push with pull_request. That uploads source strings into your translation management system from a branch nobody has merged, leaving keys behind that no shipped code refers to. Use preview for that job — it shows the same information and writes nothing.

Built to fit the pipeline you already have

The four modes are building blocks, not a fixed route. Which ones you run, on which events, and what happens on either side of them are decisions you make in your own workflow file — so continuous localization ends up shaped like your process rather than someone else’s.

Every run sets outputs — has_changes, pr_url, pr_number, preview_output — and a later step can read them. Post the preview to Slack. Skip a deploy when nothing changed. Sit the sync between your existing build and test steps, behind a manual approval, or inside a matrix that syncs one locale per job. Run it in a monorepo with a different locamorph.yaml per package. None of that needs support from us; it is a workflow file doing what workflow files do.

A hosted integration decides when a sync happens and what the result looks like. Here both are yours, and changing your mind is an edit to a file you already own.

Setup

A secret and two files

About five minutes, most of it waiting for GitHub’s settings pages to load. The example repo has all four modes wired up if you’d rather read a working project than a code block.

  1. Create an API key

    In your project settings, generate a key with the scopes the modes you plan to run need — see permissions below. API keys are available on every plan, including Free.

  2. Store it as a repository secret

    Name it LOCAMORPH_API_KEY, so the workflows below work unchanged.

    gh secret set LOCAMORPH_API_KEY
  3. Write a locamorph.yaml

    The CLI generates one interactively and commits nothing you didn’t confirm. Check the paths and languages it guessed before committing.

    npx @locamorph/cli init
    locamorph.yaml
    project_id: "your-project-uuid"
    translations_dir: ./locales
    format: json
    file_structure: "{LANG_ISO}.{FORMAT}"   # en.json, de.json, ...
  4. Add a workflow

    Start with preview on pull_request. It changes nothing anywhere, so a wrong config costs you a red job and no cleanup. Add push on main once that goes green. Those two are the smallest continuous translation integration worth running.

  5. Check it worked

    Open a pull request that touches a translation file. Within a minute the action comments with the strings that branch would add, update or remove. A green job and no comment means it found nothing to change. That is also a pass.

What’s supported

Formats, plans and quota

JSON and YAML today. Everything else locamorph handles goes through the web app and the REST API while CLI support is being finished.

Format Web app REST API Action & CLI
.jsonYesYesYes
.yaml / .ymlYesYesYes
.xliff, .arb, .strings, strings.xml, .properties, PO, CSV, ExcelYesYesIn progress

The two directions behave differently while that lands: an upload skips those files with a “coming soon” notice, and a download warns and writes JSON into the configured paths anyway. Keep the unsupported formats out of locamorph.yaml for now — or tell us which one you need next.

API keys work on Free

Creating a key isn’t gated on a paid plan, so CI sync is available from day one.

No integration slot used

Integration limits apply to the hosted integrations you connect in the app. A workflow just uses an API key.

Runs are metered

Every API call counts, reads included — at least one request per language file, even for a preview. The Free plan includes 500 a month.

Worth knowing before you set a cron to run every ten minutes. A preview writes nothing and adds no strings, but it still costs requests: ten languages across a few busy pull requests adds up faster than most people budget for.

Permissions

What each mode needs

pull and push barely touch GitHub. They read and write files in the checkout, so contents: read and an API key covers it. pull-request pushes a branch and opens a PR. preview writes a comment. Those two need a token.

Mode Workflow permissions API key scopes
pull contents: read projects:read, languages:read, translations:read
push contents: read projects:read, languages:read, translations:write
pull-request contents: write, pull-requests: write projects:read, languages:read, translations:read
preview pull-requests: write projects:read, languages:read, translations:write

Why preview needs a write scope

Because the diff is computed server-side. --dryrun still posts the full payload to the bulk update endpoint; the server works out what would change, returns the summary, and discards the write. The runner never sees enough to calculate that itself, so a read-only key can’t produce a preview.

When a run fails on a missing scope, the action names the exact permission in a GitHub error annotation and repeats it as a table in the job summary, rather than leaving you to hold a token’s permissions up against a docs page.

Annotations
locamorph upload failed (exit 1)

Your API key is missing: translations:write

Illustration of the annotation the action prints on a scope failure.

Common workflows

Two files cover most teams

Preview on pull requests, push on merges to main — both are in the tabs at the top of this page. Add the scheduled pull request when your translators work to their own rhythm rather than yours. The example repo has all four running against a real project.

What the preview comment looks like

Created once, then edited in place on every later push. A pull request that runs for a week ends up with one comment, not fourteen.

Most translation integrations give you two verbs, push and pull, so you learn what a merge did to your string catalogue afterwards — by going and looking at the catalogue. Preview inverts that: the diff shows up on the pull request while someone can still do something about it, and the run writes nothing to get it.

github-actions bot commented

📝 Translation Preview

2 added · 1 updated across 1 language

LanguageAddedUpdatedRemoved
Turkish (tr)210

▾ Show 3 changed keys

+ Menu.Projects
+ Menu.Collaborators
~ Common.Save

Preview only. Nothing was uploaded.

Illustration. Note what it costs: nothing is written to your project, but the run still spends one API request per language file, and it needs translations:write.

And what the pull request looks like

When translators finish, pull-request brings the files back as an ordinary PR. Same review, same approvals, same merge button as any other change to the repo.

github.com/Locamorph/web-app/pull/11

chore: update translations #11

Open locamorph-devbot wants to merge 1 commit into main from locamorph/translations-update

locales/de.json +2−0
@@ -12,6 +12,8 @@   "menu": {     "home": "Startseite",+    "projects": "Projekte",+    "collaborators": "Mitarbeiter",     "settings": "Einstellungen"   },

3 more files changed · locales/fr.json, locales/tr.json, locales/es.json

Illustration. Title, branch and labels all come from the workflow inputs, so the PR arrives looking the way your team expects.

Start with a preview

Nothing in your repo changes, nothing in your project changes, and one run on your next pull request tells you whether the config is right. Adding push on main after that is four more lines.