Which translation file formats are supported?
Uploads accept JSON, YAML, XML/XLIFF and .properties. GitHub sync reads JSON only, and output is always JSON.
Better i18n reads four file formats on the way in, and always writes JSON on the way out. Those are two different lists, and knowing which one you are asking about saves a lot of confusion.
Formats you can upload #
Uploading a translation file directly — in the dashboard or through the API — accepts:
| Format | Extensions |
|---|---|
| JSON | .json |
| YAML | .yaml, .yml |
| XML / XLIFF | .xml, .xliff |
| Java properties | .properties |
The format is detected from the file extension, so name the file normally and upload it. Once the keys are in, the original format stops mattering — everything after that point works the same regardless of what you uploaded.
GitHub sync reads JSON only #
If you connect a repository instead of uploading, the importer looks for .json files. YAML, XLIFF and .properties are upload-only today.
So a repo with locales/en.yaml will not import over GitHub sync. Two options: upload the YAML once to get your keys in, or convert the files in your repo to JSON.
What Better i18n writes back #
Output is always JSON, in both destinations:
| Destination | Shape |
|---|---|
| CDN | {org}/{project}/{lang}/{namespace}.json |
| GitHub pull requests | {yourTranslationPath}/{lang}.json, or {lang}/{namespace}.json when you use namespaced folders |
This is deliberate — every SDK we ship (Next.js, Remix, Vite, Expo, Flutter, plain @better-i18n/core) consumes JSON, and the CDN serves it with a manifest so a string change reaches production without a rebuild. If you need a different shape in your repo, transform it in your build step after pulling.
The three layouts #
Independently of format, one setting decides how the files are laid out. The GitHub integration page lists it as Supported Formats, and there are three:
| Layout | Example path | What it means |
|---|---|---|
| Flat JSON | /locales/en.json | One file per language, all keys at the root with dot-notation: "auth.login.title": "Sign in" |
| Nested JSON | /locales/en.json | One file per language, top-level keys as namespaces and nested objects underneath |
| Namespaced Folders | /locales/en/common.json | One folder per language, one file per namespace inside it |
Pick the one your app already reads and the files Better i18n writes drop in without a migration. If you connected a repository, we detect the layout during setup and show you what we found before anything is written — check it there rather than setting it blind.
Locale codes in filenames #
Filenames carry the locale, and regional codes are fine: en.json, en-US.json, pt-BR.json, zh-Hans.json all resolve correctly. You do not have to flatten en-US.json down to en.json before importing.
Can you translate a PDF and send it back? #
No. Better i18n does not translate documents — not PDF, not DOCX, not slide decks or spreadsheets — and there is no way to upload a file and get the same file back in another language with the layout intact. That is a different product category (document translation); this one translates the strings your application ships.
If you uploaded a PDF and nothing happened, that is why. Nothing is wrong with the file.
What we do instead: your app's text lives as keys, you add target languages, and every language is delivered as JSON your app reads at runtime. If the text you want translated is currently only inside a document, it has to become keys first — copy the strings into a JSON file and upload that.
What is not a translation file #
A translation file is a key-value file your application already reads: .json, .yaml, .xml / .xliff, .properties. Product documents are not, and neither are design files, screenshots, or database dumps. The test is simple — if your code does not load it to render text, it is not a translation file.
Better I18N