Skip to content
better-i18n.com

GitHub sync imports the translation JSON files that live in your repository into your Better i18n project.

Worth being precise about what it does, because the name suggests more: it reads files, not code. It does not scan your source for t() calls — that is the CLI's job (better-i18n scan / sync), which runs on your machine or in CI.

How GitHub sync works #

Code
You trigger a sync (dashboard or CLI)
  → Better i18n reads the translation JSON files on the configured branch + path
  → Language comes from the file layout
  → Keys and translations are imported into the project
  → The result is written to the CDN and a sync job is logged

Setting up GitHub sync #

Step 1: Connect your GitHub account #

  1. Go to Settings → Integrations → GitHub
  2. Click "Connect GitHub"
  3. Authorize the Better i18n GitHub App
  4. Select which repositories it may read

Step 2: Point it at your translation files #

  1. Pick the repository
  2. Pick the branch (usually main)
  3. Set the base path where your translation files live (e.g. locales/ or src/messages/)

The connect screen inspects the repository tree and pre-fills the path and format for you. Check what it detected rather than accepting it blindly — a wrong path is the usual cause of a first sync failing with "Source language files not found".

Step 3: Confirm the locale bindings #

Better i18n records which file belongs to which language and shows that mapping on the connect screen and in integration settings. If a language is bound to the wrong file, fix it there before the first sync.

Default Branch and Target Branch are not the same thing #

This is the single most common GitHub sync misconfiguration, so it is worth being precise about. There are two branch settings and they point in opposite directions:

SettingDefaultWhat it actually does
Default BranchmainThe branch we read your source files from, and the base every translation PR merges into
Target Branchi18n-syncThe branch we create and push to, then open the PR from

Reads always come from Default Branch. Every import path uses it: the first import, each source sync, and a full re-sync. Target Branch never affects what we read — it only names the head branch of the pull request.

It is syncing main instead of the branch I specified #

You set Target Branch. Set Default Branch instead. Target Branch is where the PR comes from, so changing it does not change which branch your keys are read from.

Keys I add on my branch never get imported #

Same cause: Default Branch is still main, so your branch is never read. Point Default Branch at the branch your locale files actually live on.

Both are set to the same value #

Then we read your source from that branch and open pull requests back into it. That is a legal configuration and it does work, but it is rarely what people intend — a real customer had both set to i18n-sync while their source files lived on main, so nothing they added on main was ever imported. If your source lives on main, Default Branch belongs on main and Target Branch stays something separate.

Which one do I change? #

Ask what you want:

  • "Read my translations from branch X" → Default Branch = X
  • "Open the translation PRs against branch X" → Default Branch = X (it is the PR base too)
  • "Put the translation commits on branch Y instead of i18n-sync" → Target Branch = Y

How the language is detected #

LayoutExampleLanguage from
Flaten.json, tr.jsonthe filename
Namespaceden/common.json, tr/nav.jsonthe first directory

A file whose name is not a locale (messages.json, strings.json) will not bind to a language on its own — put it under a locale directory.

Sync is triggered, not automatic #

There is no push webhook and no background polling: a git push on its own does not start a sync. You start one from the dashboard, or from the terminal:

Bash
better-i18n sync

If you want sync on every push, run that command as a step in your CI workflow with BETTER_I18N_API_KEY in the environment. That is the honest way to get "automatic" — and it puts the trigger somewhere you can see it fail.

Sync history #

Every run is a job you can inspect:

Bash
better-i18n syncs list
better-i18n syncs get <syncId>
better-i18n syncs cancel <syncId>

The same history is in the dashboard, with the per-file activity log — how many files were parsed and how many keys came out of each. That log is the first place to look when a sync "worked" but a language is short of keys.

A job whose worker died is closed automatically after 15 minutes, so a run stuck at "in progress" resolves itself rather than blocking the next sync forever.

Concurrent edits #

Sync imports values from your files, and translations edited in the dashboard are not silently overwritten by a stale value: when a value has moved on the server since the copy you started from, Better i18n reports the conflict instead of picking a winner behind your back. Resolve it and re-run.

If your repository is the single source of truth for source strings, keep the editing in the repo and treat the dashboard as review. If the dashboard is the source of truth, use better-i18n pull to bring translations back down rather than hand-editing files that the next sync will overwrite.

Monorepos #

Each Better i18n project connects to one repository, branch and base path, so a monorepo with two apps means two projects — one per app — each pointed at its own path (apps/web/locales, apps/mobile/locales). Both can live in the same repository.