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

v0.3.0

Published

A tiny JS/TS runtime for Echo Lingua translations: the _() marker with placeholders, plurals, and rich text. Extraction and compilation live in @echo-lingua/i18n-cli.

Readme

@echo-lingua/i18n

The JS/TS runtime for Echo Lingua translations. You wrap UI text in _(); at run time _() renders the active locale's translation, with placeholders, plurals, and rich text. Zero heavy dependencies, browser-safe, and tree-shakeable.

This package is the runtime only, and it is optional. It is designed to plug into and sit alongside established translation runtimes rather than replace them, so outside JS/TS you keep your own (gettext(), Laravel __(), and the like). The other pieces of the toolchain:

  • @echo-lingua/i18n-cli: the project-agnostic extractor and compiler (the echo-lingua-i18n command). It scans source (JS/TS and PHP/.phtml), hydrates gettext POs, and compiles them to the output your project consumes: this JS catalog, a binary .mo for native gettext, or Laravel JSON.
  • @echo-lingua/i18n-core: the shared, dependency-free primitives (flag vocabulary, CLDR plural tables, catalog key + types) both halves agree on.
  • Translation itself is the Echo Lingua API, run between extract and compile.

So the pipeline is echo-lingua-i18n extract → Echo Lingua API → echo-lingua-i18n compile, which writes the catalog this runtime loads. For extraction, config, the PO format, and the .mo/Laravel targets, see @echo-lingua/i18n-cli.

One config, owned by the CLI. There is a single i18n.config.* file for the whole toolchain, and @echo-lingua/i18n-cli is the only thing that reads it. This runtime takes no configuration: it loads the catalog the CLI generated plus whatever you pass to initI18n. So installing both packages still means one config file. The only setting with a runtime counterpart is split: when it is on, load with initLazyI18n instead of initI18n (see Runtime setup).

The _() marker

import { _, EL } from '@echo-lingua/i18n';

_('Save changes');                                   // "Save changes"
_('Hello {name}', { name: 'Tom' });                  // "Hello Tom"
_('Click {link}', { link: <a href="/x">here</a> });  // ["Click ", <a/>]  (rich text)

// Plurals. The count key selects the form (CLDR, via Intl.PluralRules).
_('There is a file', 'There are {count} files', { count: 5 });

// Options tail: { flags, context, comment }. Use {} for values when there are none.
_('Save', {}, { flags: [EL.CONTEXT_UI_LABEL] });
_('Order', {}, { context: 'verb' });   // real gettext msgctxt, for disambiguation
_('There is a file', 'There are {count} files', { count }, {
  flags: [EL.QUALITY_PRO, EL.CONTEXT_LEGAL],
  context: 'file list',
  comment: 'Shown under the section title',   // #. guidance for the model
});

_() calls nest. A placeholder value can itself be a _() call, so you can translate the label inside a link without splitting the sentence:

_('Click {link} to continue', { link: <a href="/docs">{_('here')}</a> });

The extractor finds both calls, so Click {link} to continue and here each become their own entry. At run time the inner _('here') resolves first and its text renders inside the <a>, while the outer call keeps {link} as a placeholder.

Notes:

  • The return type is string, or (string | node)[] when a placeholder value is a node.
  • The message must be a string literal (single or double quotes, or a backtick with no interpolation). A non-literal argument (a variable, a concatenation, or a ${...} template) cannot be extracted, so the extractor skips it with a warning instead of failing the build. Pass the dynamic part as a {placeholder}, or provide the literal elsewhere if you look it up at run time.
  • The values keys are type-checked against the {tokens} in the message.
  • EL exposes only applied flags (context, quality, cache, lock, ignore, translate-quotes, translate-attrs). Two flags from the same group fail the build. The flag vocabulary lives in @echo-lingua/i18n-core and is re-exported here, so you import EL from @echo-lingua/i18n.

What the runtime loads (the compiled catalog)

echo-lingua-i18n compile (its default js target) writes the catalog keyed by msgid, with a message for a singular entry and plurals (in CLDR form order) for a plural one.

Single file (the CLI's default), one module for every locale:

// src/locales/generated.ts
import type { Catalog } from '@echo-lingua/i18n';

export const translations: Catalog = {
  "fr": {
    "Hello {name}": { "message": "Bonjour {name}" },
    "Save changes": { "message": "Enregistrer les modifications" },
    "There is a file": { "plurals": ["Il y a un fichier", "Il y a {count} fichiers"] }
  }
};

Per-locale (when the CLI is configured with split: true), a loaders index plus one code-split chunk per locale:

// src/locales/generated.ts (index)
import type { LocaleCatalog } from '@echo-lingua/i18n';

export const locales = ["fr"] as const;

export const loaders: Record<string, () => Promise<{ default: LocaleCatalog }>> = {
  "fr": () => import("./fr.js"),
};

Runtime setup

Single file (default). One module bundles every locale. Simplest, and fine for small to medium catalogs:

import { initI18n, detectLocale } from '@echo-lingua/i18n';
import { translations } from './locales/generated';

initI18n(translations, { locale: detectLocale(Object.keys(translations)) ?? 'en' });

Per-locale with lazy loading (from the CLI's split: true output). The index gives you loaders and locales, and only the active locale's chunk is fetched. Use this when catalogs grow large or you have many locales:

import { initLazyI18n, loadLocale, detectLocale } from '@echo-lingua/i18n';
import { loaders, locales } from './locales/generated';

initLazyI18n({ loaders, locale: 'en', sourceLocale: 'en' });
await loadLocale(detectLocale(locales) ?? 'en');   // dynamically imports one chunk

An untranslated or not-yet-loaded string falls back to the source text, so a missing entry renders the English rather than a blank or a raw key.

Reactivity

setLocale(locale) switches language and notifies subscribers; subscribe(listener) lets a framework re-render on a switch (and on the inline editor's live preview). getLocale / getSourceLocale read the current state. These keep _() a plain function with no per-call context.