---
title: Localization quickstart
description: Translate an English string file into German and Hong Kong Chinese in three calls with one key - create a catalog, sync the source, and export the checked files.
type: tutorial
product: localization
summary: A catalog, a synced source file, and exported translations
updated: 2026-10-07
order: 1
---

This takes you from one English string file to checked translations, using
only a key and `curl`. Nothing is deployed: the catalog lives in Sylphx
Localization, and the same three calls run in CI.

<Prerequisites
	items={[
		'An account, and a project with an environment',
		'A secret key holding the localization:write and localization:read scopes, as SYLPHX_API_KEY',
		'curl and jq',
	]}
/>

The examples use the environment `orgs/acme/projects/shop/envs/production`;
use your own. `sylphx access whoami` prints the environment your key belongs
to, and a key only reaches its own environment.

```bash
export ENV=orgs/acme/projects/shop/envs/production
```

## 1. Create the catalog

Name the catalog yourself (here `app`), since CI will name it on every run.

**cURL**

```bash
curl -sS -X POST "https://api.sylphx.com/v1/$ENV/catalogs?catalog_id=app" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"spec": {
        "source_locale": "en",
        "target_locales": ["de", "zh-Hant-HK"],
        "pseudo_locales": ["accented"],
        "tone": "Friendly and short."
      }}'
```

**TypeScript**

```ts
import { Sylphx } from '@sylphx/sdk'

const sylphx = new Sylphx()
const { env } = await sylphx.access.whoami({})
await sylphx.localization.catalogs.create({
  parent: env,
  catalogId: 'app',
  catalog: {
    spec: { sourceLocale: 'en', targetLocales: ['de', 'zh-Hant-HK'], pseudoLocales: ['accented'] },
  },
})
```

`zh-Hant-HK` and `zh-Hant-TW` are separate locales; a catalog takes up to 60.

## 2. Write a source file

The canonical format is one JSON file per locale. Each key is the source text
(add `|context` to tell two identical texts apart), and each `text` is a
Unicode MessageFormat 2 message, plurals included.

```bash
mkdir -p i18n
cat > i18n/en.json <<'JSON'
{
  "locale": "en",
  "messages": {
    "Play": {"text": "Play"},
    "Hello {$name}": {"text": "Hello {$name}"},
    "Save|menu": {"text": "Save", "description": "The menu item, not the verb"},
    "{$count} coins": {"text": ".input {$count :number}\n.match $count\none {{{$count} coin}}\n* {{{$count} coins}}"}
  }
}
JSON
```

## 3. Sync

Send the source file. Add the target files you already have under
`translations`, so translations you edited by hand are kept as pinned overrides.

**cURL**

```bash
jq -n --rawfile en i18n/en.json \
  '{format: "sylphx_json", sources: [{path: "app.json", locale: "en", content: $en}]}' > sync.json
curl -sS -X POST "https://api.sylphx.com/v1/$ENV/catalogs/app:sync" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d @sync.json | jq '{added, translated, pending, failed, usage}'
```

**TypeScript**

```ts
import { readFileSync } from 'node:fs'

const sources = [{ path: 'app.json', locale: 'en', content: readFileSync('i18n/en.json', 'utf8') }]
let pending = 1
while (pending > 0) {
  const res = await sylphx.localization.catalogs.sync({
    name: `${env}/catalogs/app`,
    format: 'sylphx_json',
    sources,
  })
  pending = res.pending
}
```

A sync works within a bounded time. While the answer's `pending` is above 0,
send the same request again; nothing is translated twice. `"validate_only":
true` checks the files and returns the counts without writing or translating,
which suits a pull-request check.

## 4. Export

**cURL**

```bash
curl -sS -X POST "https://api.sylphx.com/v1/$ENV/catalogs/app:export" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"format": "sylphx_json"}' > export.json
jq '{pending, passed: .report.passed, errors: .report.errors}' export.json
jq -c '.files[]' export.json | while read -r f; do
  printf '%s\n' "$(jq -r .content <<<"$f")" > "i18n/$(jq -r .locale <<<"$f").json"
done
```

You now have `i18n/de.json`, `i18n/zh-Hant-HK.json` and `i18n/en-XA.json`.
Only current translations that passed QA are exported; `report.findings`
names anything left out and why. Set `"source_fallback": true` for a runtime
that does not fall back to the source text itself. Commit the files; the next
sync reads them back under `translations`.

## Read it back

```bash
curl -sS "https://api.sylphx.com/v1/$ENV/catalogs/app" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" | jq .status.locales
```

Each target locale's `translated`, `pending`, `failed` and `pinned` counts.
`…/catalogs/app/entries` with the filter `qa_state = QA_STATE_FAILED` lists
the messages that need a person.

## Next

<RelatedDocs
	links={[
		{
			href: '/docs/localization',
			label: 'Localization',
			description: 'Catalogs, QA, pinned overrides and pseudo-locales.',
		},
		{
			href: '/docs/api/glossaries',
			label: 'The glossaries collection',
			description: 'Fix how your product names and terms are translated.',
		},
		{
			href: '/docs/api/catalogs',
			label: 'The catalogs collection',
			description: 'Every method, its scope and its examples.',
		},
	]}
/>
