{"_id":"@3sln/bab-extract","name":"@3sln/bab-extract","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@3sln/bab-extract","//":"The version is stamped from the root package.json when a release is published; the two packages release in lockstep. Do not bump it by hand.","version":"0.2.0","description":"Extract translatable messages from source for @3sln/bab.","main":"index.js","type":"module","author":{"name":"Ray Stubbs"},"repository":{"type":"git","url":"git+https://github.com/3sln/bab.git","directory":"extract"},"license":"MIT","bin":{"bab-extract":"cli.js"},"exports":{".":"./index.js","./package.json":"./package.json"},"keywords":["i18n","l10n","localization","translation","extract","gettext","pot"],"engines":{"node":">=18.3"},"publishConfig":{"access":"public"},"dependencies":{"acorn":"^8.15.0","acorn-jsx":"^5.3.2","tinyglobby":"^0.2.15"},"_id":"@3sln/bab-extract@0.2.0","gitHead":"9252aad7bae60c48f2130fd6155f5ee7d600935b","bugs":{"url":"https://github.com/3sln/bab/issues"},"homepage":"https://github.com/3sln/bab#readme","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-4uko83GN4fuDd0UTn1Ll41jnnyoZRXTJu9cqe+g5dMArSP+VSwYVM6vipsSx9/eNIi068+X6bPBxW7XEdU/hPw==","shasum":"6f5a9645e137adf0842c0f4c2af36e9023dfa662","tarball":"https://registry.npmjs.org/@3sln/bab-extract/-/bab-extract-0.2.0.tgz","fileCount":7,"unpackedSize":51669,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2fbab-extract@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC015X6hIxTZ51RP3Hd6htn67a124ov8vNaeAKyhF3rUwIhAMV69asUEdj3d2oMM0s7CySikiVuMBvzzzBHhOixLhlT"}]},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"directories":{},"maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/bab-extract_0.2.0_1786077979743_0.980294764713711"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-07T04:46:19.516Z","0.2.0":"2026-08-07T04:46:19.907Z","modified":"2026-08-07T04:46:20.307Z"},"maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"description":"Extract translatable messages from source for @3sln/bab.","homepage":"https://github.com/3sln/bab#readme","keywords":["i18n","l10n","localization","translation","extract","gettext","pot"],"repository":{"type":"git","url":"git+https://github.com/3sln/bab.git","directory":"extract"},"author":{"name":"Ray Stubbs"},"bugs":{"url":"https://github.com/3sln/bab/issues"},"license":"MIT","readme":"# @3sln/bab-extract\n\nPull the translatable messages out of a codebase that uses\n[`@3sln/bab`](https://github.com/3sln/bab), with a parser rather than a\npattern.\n\n```sh\nnpx @3sln/bab-extract 'src/**/*.{js,jsx}' -x '**/*.test.js' -o messages.json\n```\n\n`@3sln/bab` itself has no dependencies and never will — this is a separate\npackage so that an application that only renders strings never installs a\nJavaScript parser.\n\n## Why a parser\n\nbab's msgid is the source text, so an untranslated build already works and\nextraction is never a step you have to run before the app renders. What it is\nfor is finding out what there is to translate — and getting that wrong is\nexpensive, because a string that quietly fails to be extracted is a string\nnobody ever translates.\n\nA regex over `tr\\(` cannot tell a translator from a variable that happens to be\ncalled `tr`, cannot see that `t` in one file is the same translator as `tr` in\nanother, and cannot read a scope that was fixed three modules away. This reads\nthe source with [acorn](https://github.com/acornjs/acorn) and follows the\nactual bindings:\n\n```javascript\n// src/i18n.js\nimport { createTranslator } from '@3sln/bab';\nexport const tr = createTranslator({ locale });\nexport const player = tr.scope('player');\n\n// src/components/Header.jsx\nimport { player } from '../lib/i18n.js';\nconst stats = player.scope('stats');\n\nstats('Goals'); // extracted under the scope `player.stats`\n```\n\nImports, re-exports (`export {tr as t} from …`, `export *`), namespace imports,\ndefault exports, destructuring and CommonJS `require`/`module.exports` are all\nfollowed, across as many modules as it takes, as long as those modules are in\nthe include set. Shadowing is respected: a parameter named `tr` is not the\ntranslator, and is not extracted.\n\nAnything it cannot prove is a warning rather than a silent omission:\n\n```\nsrc/components/Row.jsx:5:46: message id is not a literal; it cannot be extracted\n```\n\n## Output\n\n`--format json` (the default) is a bab catalogue — the object\n`ObjectCatalogue` takes:\n\n```json\n{\n  \"\": { \"Sign out\": null },\n  \"player\": { \"{#} days\": { \"one\": null, \"other\": null } }\n}\n```\n\nUntranslated entries are `null` and not `\"\"`, because `null` is what\n`ObjectCatalogue` reads as \"no translation\" and falls back to source text\nfrom. An empty string is a translation *to* nothing, and would blank the\nstring wherever it is used. The plural categories are the ones the locale\nactually has, so `--locale ru` writes `one` / `few` / `many` / `other`.\n\n`--format pot` is gettext, for the translation platforms that speak it. bab\nkeys a plural message on its *plural* form, which is the reverse of gettext's\nconvention, so `msgid` carries the singular given to `.singular()` and\n`msgid_plural` carries the catalogue key. The header says as much\n(`X-Bab-Plural-Key`) for whatever puts the PO back into a catalogue.\n\n```\n#. the button that ends a session, a verb\n#: src/components/Header.jsx:7\nmsgid \"Sign out\"\nmsgstr \"\"\n```\n\nMessages come out sorted by scope and id, and references sorted by file, so\nre-running over an unchanged tree produces an unchanged file.\n\n## Notes to translators\n\nA comment tagged `translators:` on the line above a call is carried through to\nthe output — `#.` in a POT.\n\n```javascript\n// translators: the button that ends a session, a verb\ntr('Sign out');\n```\n\n`--comment-tag` changes the tag.\n\n## The command\n\n```\nbab-extract [options] [include-glob...]\n\n  -i, --include <glob>   Files to read. Repeatable; also accepted positionally.\n                         Default: **/*.{js,mjs,cjs,jsx}\n  -x, --exclude <glob>   Files to skip. Repeatable. node_modules is always skipped.\n  -f, --format <name>    json (a bab catalogue) or pot (gettext). Default: json\n  -o, --out <file>       Write here instead of stdout.\n  -l, --locale <tag>     Locale whose plural categories the json template gets.\n      --fill <mode>      What untranslated entries hold: null (default), source\n                         or empty.\n  -m, --module <spec>    Specifier that means bab itself. Repeatable.\n  -k, --keyword <name>   Treat a call to this bare name as a translator call\n                         even when it cannot be traced to one. Repeatable.\n  -c, --cwd <dir>        Resolve globs and report paths relative to here.\n      --comment-tag <s>  Prefix marking a comment as a note to translators.\n      --strict           Exit non-zero if anything was warned about.\n  -q, --quiet            Do not print warnings.\n```\n\nTwo of those are worth expanding on.\n\n**`--keyword`** is for a translator that arrives as a parameter or a prop —\n`function Row({tr}) { return tr('Hello') }`. Nothing static can trace that back\nto where it was made, so `-k tr` says \"trust me, a call to `tr` is a message\".\nIts scope is unknowable, so such messages are extracted unscoped, which is also\nthe bucket every scope falls back to at lookup time.\n\n**`--module`** is the set of specifiers that mean bab itself, `@3sln/bab` and\n`bab` by default. Add to it if your build aliases bab to something else.\n\nGlobs are matched by [tinyglobby](https://github.com/SuperchupuDev/tinyglobby).\n`node_modules` and `.git` are always excluded.\n\nExit codes: `0` fine, `1` warnings under `--strict`, `2` bad usage.\n\n## As a library\n\n```javascript\nimport { extract, format, formatJSON, formatPOT } from '@3sln/bab-extract';\n\nconst { messages, warnings, files } = await extract({\n  include: ['src/**/*.js'],\n  exclude: ['**/*.test.js'],\n  cwd: process.cwd(),\n  keywords: ['tr'],\n});\n\nawait writeFile('messages.json', formatJSON(messages, { locale: 'es' }));\n```\n\nEach message is\n\n```javascript\n{\n  scope: 'player',\n  id: '{#} days',\n  plural: true,\n  singular: '{#} day',            // or null\n  comments: ['a note to translators'],\n  references: [{file: 'src/x.js', line: 12, column: 3}],\n}\n```\n\n`extractSources([{file, code}], options)` runs the same analysis over sources\nalready in hand — a bundler plugin, an editor, a test — resolving imports\nbetween them without touching the filesystem. `extractSource(code, {file})` is\nthe one-module version; nothing can be followed across an import there, so the\ntranslator has to be created in that module or named with `keywords`.\n\n## What it does not do\n\nTypeScript. Acorn parses JavaScript and JSX; a `.ts` file needs a different\nparser, and pretending otherwise would mean falling back to guessing. Run it\nover build output, or over the `.js`/`.jsx` part of a mixed codebase.\n\nA module that is not in the include set is not read, so a translator imported\nfrom one is not followed — put the module that calls `createTranslator` in the\nglobs.\n\nWriting translations back. This extracts; merging a returned PO or JSON into a\ncatalogue is a separate job with different opinions in it.\n","readmeFilename":"README.md","_rev":"1-e3315dfd85c4d6a95b0df133fe933397"}