for developers

Localization as a pipeline step.

Wrap a string in _(), fill every locale with one command, and edit any translation live in your running app — .po files end to end, written straight back to your repo. Or drive the whole thing yourself with a real HTTP API.

developer & build tools

Build tools for JavaScript & TypeScript.

Four npm packages take Echo Lingua from a marked string to a shipped translation, without leaving your codebase: the @echo-lingua/i18n runtime, extract and compile with @echo-lingua/i18n-cli, fill every locale with @echo-lingua/sdk, and edit live in the browser with @echo-lingua/inline-editor. The same @echo-lingua/i18n-cli also handles PHP and Laravel; see build tools for every platform below.

extract & compile

Wrap a string. Ship a tiny runtime.

Wrap UI text in _() from the @echo-lingua/i18n runtime; the @echo-lingua/i18n-cli tool scans your source, hydrates a .po per locale, and compiles the translated ones into a compact runtime with placeholders, plurals, and rich text. Add a context, flags, or a note to the model right at the call site. The runtime is optional and JS-only; the CLI is the project-agnostic half.

mark strings, add flags when you need them
import { _, EL } from '@echo-lingua/i18n';

_('Save changes');
_('Hello {name}', { name });
_('You have {count} item', 'You have {count} items', { count });

// The options tail: flags, a msgctxt, and a note to the model.
_('Order', {}, { context: 'verb' });
_('Save', {}, { flags: [EL.CONTEXT_UI_LABEL] });
_('There is a file', 'There are {count} files', { count }, {
  flags: [EL.QUALITY_PRO, EL.CONTEXT_LEGAL],
  comment: 'Shown under the section title',
});
extract → compile
# The runtime for your app, the toolchain for your build
npm i @echo-lingua/i18n
npm i -D @echo-lingua/i18n-cli

# Scan the code, hydrate a .po per locale
npx echo-lingua-i18n extract

# Compile the translated catalogs into the runtime
npx echo-lingua-i18n compile
beyond javascript

Build tools for every platform.

The extractor and compiler, @echo-lingua/i18n-cli, is project-agnostic. It scans JavaScript, TypeScript, and PHP (including .phtml templates), hydrates the same .po files, and compiles them to whatever your runtime reads natively: the JS catalog, a binary .mo for PHP gettext(), or flat JSON for Laravel __(). There is no Echo Lingua runtime to adopt outside JS; you keep your own. Any other gettext stack (Python, Go, C) drives the same platform through the HTTP API below.

mark strings in PHP or .phtml
<?php
// Mark strings with the gettext / Laravel functions you already use
_('Account settings');
ngettext('one invoice', '{count} invoices', $n);
pgettext('nav', 'Home');
__('Welcome back');   // Laravel string-key
compile native targets
// i18n.config.mjs — one config for the whole toolchain
export default {
  locales: ['de', 'fr'],
  include: ['src/**/*.{php,phtml}'],
  compileTargets: ['mo'],   // or ’laravel’ (flat JSON), or ’js’
};

# Writes a native .mo per locale for PHP gettext()
npx echo-lingua-i18n sync
fill every locale

Translate from the CLI or your own code.

The API client and translate CLI. echo-lingua translate reads your config, fills every locale’s .po through the API, and writes them back. Or call the typed client from your own code — create a job, wait for it, download the repo-shaped output.

the translate CLI
# Fill every locale’s .po through the API, in place
npm i @echo-lingua/sdk

# Reads your config, writes the translations back
npx echo-lingua translate
or the typed client
import { EchoLingua } from '@echo-lingua/sdk';

const el = new EchoLingua({ apiKey: process.env.ECHO_LINGUA_KEY });

// Create a job, wait for it, pull the repo-shaped zip.
const job = await el.createJob({
  sourceLanguage: 'en',
  contextMode: 'ui-label',
  quality: 'standard',
  inputs: [{ filename: 'de/messages.po', targetLanguage: 'de', content }],
});
const done = await el.waitForJob(job.id, { onProgress: (j) => log(j.status) });
const zip = await el.downloadJob(done.id);
edit in the browser

Edit translations in the running app.

A dev-only toolbar drops into your app. Hold Alt and click any translated string to edit its translation live, fix the English source, add a context or flags, or let AI Smart Fix, Shorten, or re-translate it. Manage plural forms, override the displayed English, and run the whole extract → translate → compile loop — every change written straight back to your repo.

add the Vite plugin (dev only)
// vite.config.ts — dev only
import { echoLinguaBabel, echoLinguaInlineEditor } from '@echo-lingua/inline-editor';

export default defineConfig(({ command }) => ({
  plugins: [
    react(command === 'serve' ? { babel: { plugins: [echoLinguaBabel()] } } : {}),
    echoLinguaInlineEditor(),
  ],
}));

@echo-lingua/inline-editor

The in-browser editor. Pairs with @echo-lingua/i18n — a Babel transform maps each click to the exact _() call, and a dev server reads and writes your .po files and source. Never ships to production.

the http api

Or wire it up yourself.

Not on JavaScript, or automating from another stack? Every feature above is a plain HTTP call — a scoped key, one POST, a signed webhook when it’s done. It drives any gettext project, in any language.

quickstart

Estimate the cost, then create the job.

Dry-run the size and cost first with /v1/jobs/estimate — nothing is charged — then create the run with the same body. Each inputs entry is a .po file with its own targetLanguage; sourceLanguage and contextMode are required, quality is draft, standard or pro.

POST /v1/jobs/estimate
curl -X POST https://api.echo-lingua.com/v1/jobs/estimate \
  -H "Authorization: Bearer $ECHO_LINGUA_KEY" \
  -H "Content-Type: application/json" \
  -d @job.json   # same body as the create call — nothing is charged
200 · size + cost, nothing charged
{
  "entries": 550,
  "chunks": 10,
  "words": 7400,
  "chargedMicros": 2386500,
  "creditRequiredMicros": 2505825
}
POST /v1/jobs
curl -X POST https://api.echo-lingua.com/v1/jobs \
  -H "Authorization: Bearer $ECHO_LINGUA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceLanguage": "en",
    "contextMode": "ui-label",
    "quality": "standard",
    "inputs": [
      { "filename": "de/messages.po", "targetLanguage": "de", "content": "…" },
      { "filename": "ar/messages.po", "targetLanguage": "ar", "content": "…" }
    ]
  }'
201 · the created job
{
  "id": "job_8f2c41",
  "status": "QUEUED",
  "sourceLanguage": "en",
  "contextMode": "ui-label",
  "quality": "standard",
  "createdAt": 1726669120000
}
auth & scoped keys

One bearer token. The scopes you grant it.

Create keys in the app, each is shown once, carries an expiry, and is revocable. Usage is attributed per key. Authenticate with a single header:

every request
Authorization: Bearer elk_live_2b7f…
# elk_-prefixed. Shown once at creation, store it in a secret.

translate:write

Create jobs, retry failed entries, cancel runs.

jobs:read

Read job status and counts, list jobs, download output.

webhooks

Signed delivery, not polling.

Register an endpoint for job.completed and job.failed. Each delivery is HMAC-SHA256 signed over the raw body, verify it before you trust it. Retries use bounded exponential backoff.

delivery · headers + body
POST https://your-app.example/hooks/echo-lingua
X-EchoLingua-Signature: 9f86d0818… (hex HMAC-SHA256 of the raw body)
X-EchoLingua-Timestamp: 1726669123456
X-EchoLingua-Delivery: whd_3f9c8a

{
  "id": "whd_3f9c8a",
  "type": "job.completed",
  "createdAt": 1726669123456,
  "data": {
    "jobId": "job_8f2c41",
    "status": "COMPLETE",
    "downloadUrl": "/v1/jobs/job_8f2c41/download"
  }
}
verify.ts, over the raw body
import { createHmac, timingSafeEqual } from 'node:crypto';

// Verify over the RAW request body, before JSON parsing.
export function verify(rawBody, signature, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}
in your pipeline

Translate on release, open the PR.

Kick a job off from CI, let the signed webhook tell you it’s done, then pull the repo-shaped output and open a pull request, no browser in the loop.

.github/workflows/translate.yml
name: Translate on release
on:
  release:
    types: [published]
jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Kick off a translation job
        run: |
          curl -sS -X POST https://api.echo-lingua.com/v1/jobs \
            -H "Authorization: Bearer ${{ secrets.ECHO_LINGUA_KEY }}" \
            -H "Content-Type: application/json" \
            -d @job.json
      # A signed job.completed webhook opens the PR with the translated catalogs.
reference

The endpoints you’ll actually use.

POST/v1/jobstranslate:writeCreate a translation job.
POST/v1/jobs/estimatetranslate:writeDry-run sizing + cost, no job created.
POST/v1/jobs/plantranslate:writeDry-run per-entry classification.
GET/v1/jobsjobs:readList your jobs.
GET/v1/jobs/{id}jobs:readFetch one job’s status + counts.
GET/v1/jobs/{id}/downloadjobs:readDownload the output zip (repo-shaped).
GET/v1/jobs/{id}/reviewjobs:readEntries flagged for review.
POST/v1/jobs/{id}/retrytranslate:writeRe-run just the failed entries.
POST/v1/jobs/{id}/canceltranslate:writeCancel a running job.
POST/v1/translate/texttranslate:writeTranslate a block of text inline, no job.
POST/v1/translate/codetranslate:writeTranslate the string literals in a code snippet.
POST/v1/translate/estimatetranslate:writePrice a sync translation, no charge.
POST/v1/templatestranslate:writeGenerate blank per-locale PO templates.
GET/v1/usagejobs:readCurrent-period usage, cost, and limits.

The endpoints above are the ones you’ll reach for first. See all endpoints in the docs →

Ship every locale from your pipeline.

Create a scoped key, POST one job, and wire the webhook. The first month’s standing charge is on us.

Get an API key