locales/en.json is the source of truth; every other locales/<lang>.json is round-tripped through Crowdin so non-developer translators can review and edit copy in a friendly UI without touching JSON by hand.
High-level loop
One-time setup (project owner only)
1
Create the Crowdin project
Sign up at crowdin.com (free for open-source projects). Create a new project. Source language: English. Target language(s): Simplified Chinese and any others you plan to support.Project Settings → File format defaults: keep “Use Crowdin’s placeholder syntax: yes” so
{count, plural, …} is validated automatically.2
Find your project ID
Open your project in Crowdin. The URL contains the slug; Settings → API shows the numeric Project ID.
3
Generate a personal token
Account Settings → API → New Token. Scopes: Project (read+write). Save the token somewhere secure — Crowdin shows it once.
4
Add repo secrets
GitHub repo → Settings → Secrets and variables → Actions:
CROWDIN_PROJECT_ID— the numeric ID from step 2.CROWDIN_PERSONAL_TOKEN— the token from step 3.
5
First push of en.json
Either trigger the workflow manually (Actions → Crowdin → Run workflow), or push a no-op edit to
locales/en.json on main. Either way, crowdin/github-action uploads the source bundle on first run. In the Crowdin UI you should now see locales/en.json with all keys ready to translate.Day-to-day (developer)
When you add or rename a string in code:1
Edit en.json (and optionally other locales)
Edit
locales/en.json. You can also update locales/zh.json with a placeholder you want — Crowdin will re-flag it as unapproved so the translator re-checks it, but having a value avoids leaking the English fallback into zh in the meantime.2
Run the parity check
3
Commit + push
The Crowdin GitHub Action picks up the change to
locales/en.json and uploads it the next time main advances.Day-to-day (translator)
- Open the Crowdin project.
- Pick the file (
locales/en.json) and target language (e.g.zh-CN). - Translate strings; use the in-line preview, glossary, and screenshot context where available.
- Approve entries you’re confident about. Anything left
unapprovedis still pulled into the PR but flagged in the diff so developers know it’s a draft.
When translations land back
The workflow opens a PR titledi18n: new translations from Crowdin into the l10n_main branch every Monday at 08:00 UTC (and on manual dispatch). To merge:
locales/<new-lang>.json placeholder if Crowdin hasn’t already added one — next-intl needs the file to exist before its dynamic import resolves. Then squash-merge.
Adding a new locale
1
Add it in Crowdin
Project Settings → Target Languages → add the language. Wait for the Crowdin workflow to upload the source again so the language appears in the project tree.
2
Update crowdin.yml mapping
crowdin.yml → languages_mapping.two_letters_code: add the row if Crowdin’s ID doesn’t match your bundle filename (e.g. pt-BR: pt-br for pt-br.json; nothing needed for plain es: es).3
Update next-intl config
Add the new locale to the supported list in
i18n.ts so the bundle gets loaded. The route tree is flat — there’s no [locale] URL segment; the active locale is selected at render time from app settings.4
Drop a placeholder JSON
locales/<new>.json containing {} so the first build succeeds before Crowdin populates it.5
Trigger the workflow
Manually run the Crowdin workflow (or wait for the next scheduled run) — translations flow back as a PR.
Why this layout
- One flat JSON per locale — what next-intl expects natively. No build-time merge step, no per-page chunking that would break
useTranslations('foo.bar')lookups. - Crowdin (vs Tolgee / Weblate / Lokalise) — Crowdin Free fits OSS, has native ICU MessageFormat validation, supports branched translation flows, and the GitHub Action is mature.
update_as_unapproved— when an English string changes, the paired translation isn’t silently kept. Translators see it as unapproved next time they open Crowdin.- Weekly pull (not on-push) — translations get a coherent batch. An on-push pull cycle would generate dozens of half-translated PRs during active development.