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/i18n-cli

v0.2.0

Published

Project-agnostic extractor and compiler for Echo Lingua translations: scan source (JS/TS + PHP/.phtml) for marked strings, hydrate gettext POs, and compile to JS, binary .mo, or Laravel JSON.

Readme

@echo-lingua/i18n-cli

The project-agnostic extractor and compiler for Echo Lingua translations. The echo-lingua-i18n command (plus a programmatic API) scans your source for marked strings, hydrates gettext .po catalogs, and, after the Echo Lingua API fills those in, compiles them into the output your project consumes: a JS runtime catalog, a binary .mo for native gettext(), or flat JSON for Laravel.

It is dev-only and framework-neutral. It pairs with:

  • @echo-lingua/i18n: the optional JS/TS runtime (_()). Not needed for PHP/Laravel projects.
  • @echo-lingua/i18n-core: the shared flag/plural/catalog primitives (installed transitively).

Translation is not part of this package: hydrated POs are filled by the Echo Lingua API, then compiled here.

npm i -D @echo-lingua/i18n-cli

Markers

JS/TS uses the _() convention from @echo-lingua/i18n:

_('Save changes');
_('Hello {name}', { name });
_('There is a file', 'There are {count} files', { count });     // plural
_('Order', {}, { context: 'verb' });                            // msgctxt
_('Legal notice', {}, { flags: [EL.QUALITY_PRO], comment: '…' });

PHP / .phtml uses the built-in gettext/Laravel functions, recognised by argument position per kind:

<?php
_('Account settings');                               // singular
gettext('Account settings');                         // singular (alias)
__('Welcome back');                                  // singular (Laravel string-key)
ngettext('one invoice', '{count} invoices', $n);     // plural → msgid + msgid_plural
pgettext('nav', 'Home');                             // context → msgctxt + msgid
npgettext('mail', 'one message', '{count} messages', $n);  // context + plural

Defaults: singular _, gettext, __, t, trans, translate; plural ngettext; context pgettext; context + plural npgettext. .phtml (PHP-in-HTML) uses the same markers inside <?php … ?> / <?= … ?>. Method calls ($obj->trans('…')) and non-literal/interpolated msgids are skipped; a file that fails to parse warns rather than aborting the run. JS and PHP extract into the same POs, so identical strings across .tsx and .php merge into one entry.

Only string literals extract. A non-literal argument (a variable, concatenation, or interpolated template) is skipped with a warning; pass the dynamic part as a {placeholder}.

Config

This is the single config for the whole toolchain, and this CLI is the only thing that reads it. The runtime (@echo-lingua/i18n) takes no config of its own, so installing both packages still means one i18n.config.*. Everything here drives extraction and compilation; the only field with a runtime counterpart is split (emit chunks here, load with initLazyI18n there).

i18n.config.{js,mjs,cjs,json} in your project root:

export default {
  sourceLocale: 'en',                        // the source language (default 'en')
  locales: ['fr', 'de', 'pl'],               // targets; no need to list the source
  include: ['src/**/*.{ts,tsx,php,phtml}'],  // globs to scan for markers
  exclude: ['**/node_modules/**'],           // globs to skip (has sensible defaults)
  poDir: 'translations/{locale}/messages.po',// PO path pattern; {locale} is substituted
  catalogOut: 'src/locales/generated.ts',    // where the compiled JS catalog is written
  split: false,                              // true: per-locale chunks + lazy loading
  functionNames: ['_'],                      // the JS names the scanner treats as markers

  // Compile output. Default ['js']. Add 'mo' (PHP gettext) and/or 'laravel' (Laravel JSON).
  compileTargets: ['js'],
  moDir: 'translations/{locale}/LC_MESSAGES/messages.mo',  // 'mo' target path; gettext layout
  laravelJsonDir: 'lang/{locale}.json',                    // 'laravel' target path
};

functionNames is the list of JS marker names (default ['_']); set it if you alias the marker. PHP markers are a separate, built-in set (above). compileTargets selects which formats compile writes; all targets render from the same translated POs, so you can emit several at once.

CLI

echo-lingua-i18n extract            # scan source, hydrate each locale's PO (no translation)
echo-lingua-i18n compile            # compile the translated POs into the configured target(s)
echo-lingua-i18n sync               # extract, then compile
echo-lingua-i18n extract --cleanup  # drop entries no longer in source (else kept as #~)

A typical run is extract, then the Echo Lingua API translation over the POs, then compile. extract never writes a msgstr. It preserves existing translations and Echo Lingua written flags (el-processed, el-tier-*, fuzzy), and refreshes the #: references, the #. comment, and the applied #, flags from source.

What a generated PO looks like

extract writes entries with a file:line reference, any #. comment and #, flags you set, and (for a plural call) msgid_plural with one msgstr[n] per form. msgstr is left empty for the API:

#: src/Example.tsx:7
msgid "Hello {name}"
msgstr ""

#: src/Example.tsx:12
#. Footer link
#, el-quality-pro el-context-legal
msgid "Legal notice"
msgstr ""

#: src/Example.tsx:8
msgid "There is a file"
msgid_plural "There are {count} files"
msgstr[0] ""
msgstr[1] ""

msgid plus msgctxt is the entry key (standard gettext), so identical strings share one entry and a real msgctxt keeps two translations of the same text apart. The POs round-trip through the Echo Lingua pipeline and any gettext tool (Poedit, Weblate, msgmerge).

Compile targets

All targets render from the same translated POs. Select them with compileTargets.

js (the default): the runtime catalog

Writes catalogOut for @echo-lingua/i18n to load, keyed by msgid with message / plurals entries. In split: true mode it writes a loaders index plus one code-split chunk per locale. See the @echo-lingua/i18n README for the shapes and initI18n / initLazyI18n usage.

mo: native PHP gettext

// i18n.config.mjs
export default {
  sourceLocale: 'en',
  locales: ['de', 'fr'],
  include: ['src/**/*.{php,phtml}'],
  compileTargets: ['mo'],   // drop 'js' if you don't need the JS catalog
};

sync hydrates the POs and, after translation, writes translations/{locale}/LC_MESSAGES/messages.mo (the layout gettext expects). Load it natively; bindtextdomain's second argument is the directory that contains the {locale}/LC_MESSAGES tree:

<?php
setlocale(LC_ALL, 'de_DE.UTF-8');
bindtextdomain('messages', __DIR__ . '/translations');
bind_textdomain_codeset('messages', 'UTF-8');
textdomain('messages');

echo _('Account settings');                        // Kontoeinstellungen
echo ngettext('one invoice', '{count} invoices', 3);// {count} Rechnungen

The .mo is a faithful re-encoding of the PO: msgctxt, plural forms, and the Plural-Forms header all round-trip, so context and plural selection resolve correctly. The path is set by moDir.

laravel: flat string-key JSON

Laravel's string-key mode (__('Welcome back'), not the dotted __('messages.welcome') key mode) is source-as-key, exactly like gettext, so it extracts the same way. Compile to Laravel's flat JSON:

// i18n.config.mjs
export default {
  sourceLocale: 'en',
  locales: ['de', 'fr'],
  include: ['app/**/*.php', 'resources/views/**/*.php'],
  compileTargets: ['laravel'],   // writes lang/{locale}.json
};
// lang/de.json
{
  "Account settings": "Kontoeinstellungen",
  "one invoice": "eine Rechnung|{count} Rechnungen"
}

Then __('Account settings') and trans_choice('one invoice', $n) resolve natively. Caveats of the flat-JSON format (not PO limitations):

  • No msgctxt. Flat JSON is keyed by the source string alone, so context-qualified entries can't be represented; compile skips them and prints a warning naming each. Use the mo target if you need context.
  • Plurals use Laravel's singular|plural pipe convention, for trans_choice.
  • Placeholders pass through verbatim. The compiler does not rewrite {name} into Laravel's :name, so write placeholders in the style your runtime expects.

Programmatic API

The same steps are available as functions (used, for example, by the inline editor's dev server):

import { loadConfig, runExtract, runCompile } from '@echo-lingua/i18n-cli';

const config = await loadConfig(projectRoot);
await runExtract(config, { cleanup: false });
await runCompile(config);

Lower-level building blocks are exported too: extractMessages / extractFromSource, mergePo, compileCatalog / renderCatalogModule, and the compile-target renderers renderMoFile, renderLaravelJson, and flattenCatalog.

Adding a source language

Extraction is pluggable: each file extension maps to a parser (JS/TS via Babel, .php/.phtml via php-parser), and every parser emits the same normalised message shape. Adding a new source type is a registry entry, not a change to the extract/compile core.