npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@echo-lingua/sdk

v0.0.1

Published

Echo Lingua API client and translate CLI: send gettext PO files, get translated PO files back.

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 catalog

Install

npm install @echo-lingua/sdk

Authentication

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 progress

Options for translate:

  • --yes, -y skip the estimate confirmation.
  • --quality <tier> one of draft, standard, pro.
  • --context <mode> one of none, 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_KEY the API key with translate:write. Required unless set as apiKey in the config.
  • ELK_API_URL (optional) the API base including /v1. Overrides baseUrl. 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