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

@3sln/bab-extract

v0.2.0

Published

Extract translatable messages from source for @3sln/bab.

Readme

@3sln/bab-extract

Pull the translatable messages out of a codebase that uses @3sln/bab, with a parser rather than a pattern.

npx @3sln/bab-extract 'src/**/*.{js,jsx}' -x '**/*.test.js' -o messages.json

@3sln/bab itself has no dependencies and never will — this is a separate package so that an application that only renders strings never installs a JavaScript parser.

Why a parser

bab's msgid is the source text, so an untranslated build already works and extraction is never a step you have to run before the app renders. What it is for is finding out what there is to translate — and getting that wrong is expensive, because a string that quietly fails to be extracted is a string nobody ever translates.

A regex over tr\( cannot tell a translator from a variable that happens to be called tr, cannot see that t in one file is the same translator as tr in another, and cannot read a scope that was fixed three modules away. This reads the source with acorn and follows the actual bindings:

// src/i18n.js
import { createTranslator } from '@3sln/bab';
export const tr = createTranslator({ locale });
export const player = tr.scope('player');

// src/components/Header.jsx
import { player } from '../lib/i18n.js';
const stats = player.scope('stats');

stats('Goals'); // extracted under the scope `player.stats`

Imports, re-exports (export {tr as t} from …, export *), namespace imports, default exports, destructuring and CommonJS require/module.exports are all followed, across as many modules as it takes, as long as those modules are in the include set. Shadowing is respected: a parameter named tr is not the translator, and is not extracted.

Anything it cannot prove is a warning rather than a silent omission:

src/components/Row.jsx:5:46: message id is not a literal; it cannot be extracted

Output

--format json (the default) is a bab catalogue — the object ObjectCatalogue takes:

{
  "": { "Sign out": null },
  "player": { "{#} days": { "one": null, "other": null } }
}

Untranslated entries are null and not "", because null is what ObjectCatalogue reads as "no translation" and falls back to source text from. An empty string is a translation to nothing, and would blank the string wherever it is used. The plural categories are the ones the locale actually has, so --locale ru writes one / few / many / other.

--format pot is gettext, for the translation platforms that speak it. bab keys a plural message on its plural form, which is the reverse of gettext's convention, so msgid carries the singular given to .singular() and msgid_plural carries the catalogue key. The header says as much (X-Bab-Plural-Key) for whatever puts the PO back into a catalogue.

#. the button that ends a session, a verb
#: src/components/Header.jsx:7
msgid "Sign out"
msgstr ""

Messages come out sorted by scope and id, and references sorted by file, so re-running over an unchanged tree produces an unchanged file.

Notes to translators

A comment tagged translators: on the line above a call is carried through to the output — #. in a POT.

// translators: the button that ends a session, a verb
tr('Sign out');

--comment-tag changes the tag.

The command

bab-extract [options] [include-glob...]

  -i, --include <glob>   Files to read. Repeatable; also accepted positionally.
                         Default: **/*.{js,mjs,cjs,jsx}
  -x, --exclude <glob>   Files to skip. Repeatable. node_modules is always skipped.
  -f, --format <name>    json (a bab catalogue) or pot (gettext). Default: json
  -o, --out <file>       Write here instead of stdout.
  -l, --locale <tag>     Locale whose plural categories the json template gets.
      --fill <mode>      What untranslated entries hold: null (default), source
                         or empty.
  -m, --module <spec>    Specifier that means bab itself. Repeatable.
  -k, --keyword <name>   Treat a call to this bare name as a translator call
                         even when it cannot be traced to one. Repeatable.
  -c, --cwd <dir>        Resolve globs and report paths relative to here.
      --comment-tag <s>  Prefix marking a comment as a note to translators.
      --strict           Exit non-zero if anything was warned about.
  -q, --quiet            Do not print warnings.

Two of those are worth expanding on.

--keyword is for a translator that arrives as a parameter or a prop — function Row({tr}) { return tr('Hello') }. Nothing static can trace that back to where it was made, so -k tr says "trust me, a call to tr is a message". Its scope is unknowable, so such messages are extracted unscoped, which is also the bucket every scope falls back to at lookup time.

--module is the set of specifiers that mean bab itself, @3sln/bab and bab by default. Add to it if your build aliases bab to something else.

Globs are matched by tinyglobby. node_modules and .git are always excluded.

Exit codes: 0 fine, 1 warnings under --strict, 2 bad usage.

As a library

import { extract, format, formatJSON, formatPOT } from '@3sln/bab-extract';

const { messages, warnings, files } = await extract({
  include: ['src/**/*.js'],
  exclude: ['**/*.test.js'],
  cwd: process.cwd(),
  keywords: ['tr'],
});

await writeFile('messages.json', formatJSON(messages, { locale: 'es' }));

Each message is

{
  scope: 'player',
  id: '{#} days',
  plural: true,
  singular: '{#} day',            // or null
  comments: ['a note to translators'],
  references: [{file: 'src/x.js', line: 12, column: 3}],
}

extractSources([{file, code}], options) runs the same analysis over sources already in hand — a bundler plugin, an editor, a test — resolving imports between them without touching the filesystem. extractSource(code, {file}) is the one-module version; nothing can be followed across an import there, so the translator has to be created in that module or named with keywords.

What it does not do

TypeScript. Acorn parses JavaScript and JSX; a .ts file needs a different parser, and pretending otherwise would mean falling back to guessing. Run it over build output, or over the .js/.jsx part of a mixed codebase.

A module that is not in the include set is not read, so a translator imported from one is not followed — put the module that calls createTranslator in the globs.

Writing translations back. This extracts; merging a returned PO or JSON into a catalogue is a separate job with different opinions in it.