Git-based localization workflows

How to keep language files in Git without merge conflicts or overwritten translations: branch strategy, CI sync, and what to review in a translation pull request.

Language files are code. They are versioned, they are deployed, and the application misbehaves without them. Keeping them in Git is the right default — the question is how, because the naive approach produces merge conflicts and, worse, silently deleted translations.

The failure mode

Two developers, two feature branches, both adding strings.

Developer A pushes updated language files to the translation platform. Developer B pushes theirs. Because the platform treats an uploaded source file as the complete current state, B’s push removes A’s strings — and with them any translation work already done on them.

Meanwhile, in the repository, both branches have regenerated en.yml with different additions, so the merge conflicts on a file nobody wants to resolve by hand.

Both halves have the same root cause: language files are whole-file artefacts, and more than one place is authoritative.

The fix: one branch owns the language files

Designate a single branch as the source of truth for pushing to the translation platform. Everything else follows.

Option 1 — push from main. Feature branches merge into main as they are ready; CI pushes source files from main only. New strings become translatable as soon as they land. You ship them untranslated and they fill in on a later deploy.

Best for web applications that deploy frequently. Requires a sensible fallback so an untranslated string renders in the source language rather than as a key.

Option 2 — a dedicated integration branch. Merge finished features into an i18n branch on a cadence — say every Friday. Push from there, let translators work, pull the results back and release. Nothing ships untranslated.

Best when untranslated content is unacceptable: regulated products, marketing-critical launches, mobile releases gated by store review.

Either way the rule is the same, and it is worth enforcing in CI rather than documenting in a wiki: only the designated branch pushes.

yaml
# Only main pushes source strings
- name: Push source strings
  if: github.ref == 'refs/heads/main'
  run: wti push

Which files to commit

Commit source and target language files. They are build inputs.

Do not commit generated artefacts — compiled .mo files from .po sources, or anything else your build produces. They conflict constantly and carry no information the source does not.

Add a .gitattributes entry so Git treats language files sensibly in diffs:

*.yml     diff=yaml
*.po      diff=po
*.strings text

And keep language files out of any auto-formatter that reorders keys. A formatter that alphabetises a YAML file will produce a diff touching every line each time the file is regenerated in a different order, which makes every translation pull request unreviewable.

Sync in both directions

The property that makes this work is that no human is in the loop for the routine case. A manual sync step gets skipped, and the two sides drift silently until someone notices a language is months stale.

A workable arrangement with WebTranslateIt’s CLI:

bash
wti push      # send updated source files up
wti pull      # bring translations back down
wti status    # per-language completeness
wti diff      # what would change, without changing it

On merge to the designated branch: wti push, source files only.

Nightly, or before release: wti pull, then open a pull request if anything changed.

On every pull request: wti diff, reported as a check.

That last one is the most useful and the least known. wti diff shows which strings a merge would add or remove from the translation project without changing anything. It turns “this refactor silently deletes 40 translated segments” into a visible line in code review, which is exactly where you want to find out.

There is also a GitHub Action if you would rather not manage the CLI in your workflow files.

Translations arrive as a pull request

Pull automatically; land the result as a PR rather than a direct commit to main.

Nobody needs to read the translations — you would not be able to review most of them anyway. The reason for the PR is the diff shape:

  • A large unexpected deletion means something went wrong upstream: a source file pushed from the wrong branch, or a bad import.
  • Changes to a language nobody is working on are worth a question.
  • Structural changes — indentation, key order, encoding — mean a tool changed behaviour.

Automate the creation, keep the human glance. It costs seconds and catches the failures that are otherwise invisible until production.

Run validation on the same PR: placeholder integrity, plural completeness, character limits. A translation that breaks the build should fail the check, not the deploy.

Reviewing changes to source strings

The other direction deserves review too, and it usually gets none.

Changing a source string is not a free action. It invalidates the existing translation in every language, and the cost scales with your language count. A one-word improvement to a button label costs a round trip in twelve languages.

Worth flagging in review:

  • Wording changes to existing strings. Is the improvement worth retranslation? Sometimes clearly yes. Sometimes it is a preference that costs a week.
  • Key renames. Renaming a key usually reads as “delete this string, add an unrelated one”, discarding the translation history. Most platforms cannot tell the difference.
  • New strings without context. A developer comment is part of writing the string, and its absence is as reviewable as a missing test.

A lightweight CI check that comments on a PR touching language files — this changes 6 source strings, affecting 12 languages — makes the cost visible at the moment the decision is being made.

Monorepos and multiple projects

A monorepo holding a web app, a mobile app and a marketing site raises a question: one translation project or several?

Several projects, one shared memory and term base is usually right. Each application has its own file formats, its own release cadence and often its own translators, so a single project mixing strings.xml, Localizable.strings and en.yml produces a segment list nobody can navigate. Splitting them keeps each one coherent.

What you do not want split is the translation memory and the term base. The same sentences appear across all three surfaces, and the whole point is translating them once. Point each project’s translation resources at a shared source.

In CI, scope the sync per project rather than running one command at the repository root:

bash
(cd apps/web    && wti push)
(cd apps/mobile && wti push)

Each directory carries its own .wti configuration, so the credentials and project id travel with the code they belong to.

A reference setup

Putting it together:

  1. main owns language files. Enforced in CI.
  2. On merge to main: wti push, source only.
  3. On every PR: wti diff as a check, plus a comment when source strings change.
  4. Nightly: wti pull, open a PR if anything changed.
  5. On that PR: run validation, and have a human look at the diff shape.
  6. Never edit target language files by hand. They are generated; hand edits are overwritten and the person who made them learns to distrust the pipeline.

That last rule is the one that gets broken under deadline pressure, and it is the one that quietly destroys confidence in the whole setup. If a translation is wrong, fix it in the platform so the fix survives the next pull.

Frequently asked questions

Should translation files be committed to Git?
Yes. They are part of the build, and the application cannot run correctly without them. Keeping them in the repository means translations are versioned, reviewable and deployable through the same pipeline as the code.
How do I avoid merge conflicts in language files?
Push language files from exactly one branch, and let translations come back as their own commits rather than being edited by hand. Most conflicts come from two branches independently regenerating the same file, which a single designated source branch eliminates.
Should translations be pulled automatically or reviewed?
Pull automatically, but land the result as a pull request rather than a direct commit. Nobody needs to read the translations, but somebody should see the diff — an unexpectedly large deletion is the signal that something went wrong upstream.
Can I use Git branches for translation?
Not usually. Most translation platforms model a project as a single current state rather than a branch tree, so pushing from two branches makes them fight. The workable pattern is one designated integration branch that owns the language files.

Keep reading

Translate your app without the spreadsheet round-trip

WebTranslateIt reads the file formats and placeholder syntax described on this page, validates them as translators work, and syncs the results straight back into your repository.