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.
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.
@echo-lingua/i18n
Wrap strings in _(); the i18n-cli extracts a .po per locale and compiles a tiny runtime.
@echo-lingua/sdk
Fill every locale through the API — from the CLI or a typed client.
Translate →@echo-lingua/inline-editor
A dev-only toolbar to edit translations in the running app.
Edit in the browser →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 themimport { _, 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
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
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 clientimport { 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 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.
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.
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/estimatecurl -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/jobscurl -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
}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 requestAuthorization: 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.
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 + bodyPOST 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 bodyimport { 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); }
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.ymlname: 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.The endpoints you’ll actually use.
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