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.
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.
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.
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.
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.
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
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
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.
-
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.
-
Store it as a repository secret
Name it
LOCAMORPH_API_KEY, so the workflows below work unchanged.gh secret set LOCAMORPH_API_KEY -
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 initlocamorph.yaml project_id: "your-project-uuid" translations_dir: ./locales format: json file_structure: "{LANG_ISO}.{FORMAT}" # en.json, de.json, ... -
Add a workflow
Start with
previewonpull_request. It changes nothing anywhere, so a wrong config costs you a red job and no cleanup. Addpushonmainonce that goes green. Those two are the smallest continuous translation integration worth running. -
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 |
|---|---|---|---|
.json | Yes | Yes | Yes |
.yaml / .yml | Yes | Yes | Yes |
.xliff, .arb, .strings, strings.xml, .properties, PO, CSV, Excel | Yes | Yes | In 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.
Your API key is missing: translations:write
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.
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.
chore: update translations #11
"menu": { "home": "Startseite",+ "projects": "Projekte",+ "collaborators": "Mitarbeiter", "settings": "Einstellungen" },
3 more files changed · locales/fr.json, locales/tr.json, locales/es.json
Related
Where to go next
Example repository
All four modes as complete, working workflow files you can fork.
Action reference
Every input and output, with defaults, in the action’s README.
All integrations
GitHub, GitLab, Slack, S3, webhooks, the CLI and the REST API.
Don’t see your tool?
Tell us what you’d want connected.
Action, or the hosted GitHub integration?
Take the action if you have CI. The schedule, the branch names, the PR body and
reviewers, and the order of steps around the sync all sit in
.github/workflows. They get reviewed like any other change and revert like
any other change, and a new engineer can read the whole pipeline without an account.
The hosted integration answers one case better: nobody on the team wants to own a workflow file. Authorize the app from the integrations page, choose a repository and a branch, and locamorph opens the pull requests. Nothing to maintain — and nothing in your repo either. It’s the same continuous localization loop from the outside; the difference is who holds the wiring.
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.
📝 Translation Preview
2 added · 1 updated across 1 language
tr)▾ Show 3 changed keys
Preview only. Nothing was uploaded.
translations:write.