@echo-lingua/sdk
v0.0.1
Published
Echo Lingua API client and translate CLI: send gettext PO files, get translated PO files back.
Maintainers
Readme
@echo-lingua/sdk
The Echo Lingua API client and translate CLI. You send gettext .po files and the locales to
fill; you get back translated .po files, with PO comments, references, order, and flags
preserved. It works with any gettext ecosystem (JS, PHP, Python, C, and so on), not just JS.
- CLI (
echo-lingua,@echo-lingua/sdk/cli): the batteries-included path. It translates a whole directory of PO catalogs end to end (read, estimate, confirm, wait, download, write back) and reads your config. This is what most projects want. Node only. - Client (
@echo-lingua/sdk): a small fetch-based wrapper over the REST API, for writing your own scripts and integrations when the CLI does not fit. Runs in Node (>=18) and the browser.
If you just want to fill your project's .po files, use the CLI and skip the Client section. Reach
for the Client only when you are building something custom in code (a bespoke pipeline, a backend
service, a progress dashboard, translating a single string on the fly). The CLI is built on the
Client, so anything the CLI does is possible with the Client too.
This is the translate half of the workflow. Extraction, hydration, and the runtime live in
@echo-lingua/i18n. Together:
echo-lingua-i18n extract # scan source, hydrate each locale's .po
echo-lingua translate # fill the .po files through the API
echo-lingua-i18n compile # compile the translated .po into the runtime catalogInstall
npm install @echo-lingua/sdkAuthentication
Create an API key under Settings then API Keys in the app. Give it the translate:write scope.
The secret is shown once, so store it in your CI secrets. The CLI reads it from the environment
(or config); the Client takes the key directly.
CLI
The CLI translates a whole directory of PO catalogs. It reads your config (see below), so one config can drive extract, translate, and compile.
echo-lingua translate # estimate, confirm, create, wait, download, write POs
echo-lingua translate --yes # skip the confirmation (CI)
echo-lingua pull <jobId> # resume a run by id, then download + write
echo-lingua status <jobId> # read-only status + per-locale progressOptions for translate:
--yes,-yskip the estimate confirmation.--quality <tier>one ofdraft,standard,pro.--context <mode>one ofnone,ui-label,marketing-copy,documentation,legal.--dir <pattern>override the PO path pattern (uses{locale}).
translate estimates first (nothing is charged), asks you to confirm, creates one job for all
target locales, shows live progress, then downloads the bundle and writes each PO back. Only the
msgstr values change on write: the API returns repo-shaped gettext, so headers, order,
comments, and flags round-trip untouched. Rebuild your runtime catalog afterwards with
echo-lingua-i18n compile.
If the wait stops before the job finishes (a slow provider, a dropped connection, Ctrl-C), the
job keeps running on the server. Resume it with echo-lingua pull <jobId>.
Config
The CLI reads echo-lingua.config.{mjs,js,cjs,json} from the current directory. If that file is
absent it falls back to i18n.config.{mjs,js,cjs,json} for the shared fields, so a JS project that
already has an i18n.config can keep a single file. When both exist, the dedicated file wins per
field.
// echo-lingua.config.js
export default {
sourceLocale: 'en', // skipped; holds the msgids, not a translation
locales: ['de', 'es', 'fr'], // target locales to translate
poDir: 'translations/{locale}/messages.po', // PO path pattern; {locale} is substituted
// optional translation defaults (env and flags override these):
quality: 'standard',
contextMode: 'none',
// optional API settings (env overrides these):
baseUrl: 'https://app.echo-lingua.com/v1',
apiKey: process.env.ELK_API_KEY, // read from .env; never commit a raw key
};A non-JS gettext project (PHP, Python, C) can use a JSON config instead. It cannot read
process.env, so leave the key out of it and keep the key in .env (below):
{ "sourceLocale": "en", "locales": ["de", "es", "fr"], "quality": "standard" }Resolution precedence for each setting is flag, then env, then config, then the built-in default.
Environment
ELK_API_KEYthe API key withtranslate:write. Required unless set asapiKeyin the config.ELK_API_URL(optional) the API base including/v1. OverridesbaseUrl. Defaults to production. Point it at a local emulator for dev.ELK_APP_URL(optional) the app origin, used for review deep-links.ELK_QUALITY,ELK_CONTEXT_MODE(optional) defaults for those settings.
A gitignored .env in the current directory or any ancestor (for example the repo root) is
loaded automatically, so the key can live there. It is loaded before the config file is imported,
so a JS config can safely reference process.env.ELK_API_KEY. Keep the raw secret in .env, not
in a committed config.
Client
For custom scripts and integrations. If you are translating a project's catalogs, the CLI already does all of this for you, so most users can skip this section. Use the Client when you want to drive the API from your own code.
import { EchoLingua } from '@echo-lingua/sdk';
const el = new EchoLingua({ apiKey: process.env.ELK_API_KEY! });
const input = {
sourceLanguage: 'en',
contextMode: 'none',
quality: 'standard',
inputs: [
{ filename: 'messages.po', targetLanguage: 'fr', content: poTextFr },
{ filename: 'messages.po', targetLanguage: 'de', content: poTextDe },
],
};
const estimate = await el.estimate(input); // nothing is charged
const job = await el.createJob(input);
const done = await el.waitForJob(job.id, {
onProgress: (j) => console.log(j.status, j.completedEntries, '/', j.totalEntries),
});
const zip = await el.downloadJob(done.id); // Uint8Array of the .po bundle (a zip)Options
new EchoLingua({
apiKey: 'elk_live_…',
baseUrl: 'https://app.echo-lingua.com/v1', // default; point at a local emulator for dev
fetch: customFetch, // optional; defaults to the global fetch
});Methods
| Method | Endpoint | Returns |
| --- | --- | --- |
| estimate(input) | POST /jobs/estimate | size and cost, nothing charged |
| createJob(input) | POST /jobs | the created job |
| getJob(id) | GET /jobs/{id} | the job with live progress |
| waitForJob(id, opts?) | polls GET /jobs/{id} | the terminal job |
| downloadJob(id) | GET /jobs/{id}/download | zip bytes (Uint8Array) |
| getReview(id) | GET /jobs/{id}/review | per-file review, or null when none |
waitForJob
waitForJob polls until the job reaches a terminal state (COMPLETE, ERRORED, CANCELLED).
The job runs on the server regardless of this process, so the wait keeps going while entries are
still landing and only gives up if the job stalls, hits the time cap, the signal aborts, or
polling fails repeatedly. It is resumable: it only needs the job id.
const controller = new AbortController();
const done = await el.waitForJob(id, {
onProgress: (job) => render(job),
pollMs: 3000, // poll interval
stallMs: 240_000, // give up after no progress for this long
timeoutMs: 3_600_000, // absolute cap on the whole wait
signal: controller.signal,
});When it gives up before the job finishes it throws an EchoLinguaWaitError with a reason
(stalled, timeout, poll-failed, or aborted) and the lastJob it saw, so you can resume
by id later. A non-2xx response anywhere else throws an EchoLinguaApiError carrying the HTTP
status, the API code, the message, and retryAfter (seconds) on a 429.
License
MIT
