string-le 0.1.0

Extract every string value from a codebase so a person can read them
string-le-0.1.0 is not a library.

Useful? A star is how other developers find it — ★ GitHub · letools.dev/tools/string-le

Someone has to read every user-visible string before a release. A QA lead checking the messages. A compliance reviewer looking for a claim the product is not allowed to make. A localisation owner finding what never reached a catalog. None of them has the editor open, and several of them cannot be handed a checkout at all.

string-le . puts every string in the repository into one file they can read.

string-le --values --dedupe src/ > strings.txt

Sixty seconds

string-le .                      # every string in the tree, as JSON
string-le --values src/          # just the values, one per line
string-le --dedupe --values .    # each distinct string once
cat config.toml | string-le --stdin --format toml

# the point of the whole thing:
string-le --values --dedupe src/ | sort > after.txt
diff before.txt after.txt        # what changed in the copy this release
./config.json:2:14  Settings
./src/messages.ts:2:12  Delete this permanently?
./src/messages.ts:3:11  Never mind
3 strings in 3 files

Exit codes follow grep0 strings found, 1 none found, 2 the question was malformed. Finding none is an answer, not an error.

Install

Route Command Worth knowing
cargo cargo install string-le Any platform, needs Rust 1.88+. (publishing shortly)
From source git clone https://github.com/nolindnaidoo/string-lecd string-le/crate && cargo build --release The same build CI runs.

No runtime, no network, nothing written.

The fallback is the main event

Six formats are parsed — JSON, YAML, CSV, TOML, INI and dotenv — and anything else falls back to quoted runs: single, double or backtick.

That fallback is not a consolation prize. A .ts, .py, .go or .java file is not a format with a parser here, so it lands exactly there — and the quoted strings in a source file are the user-facing copy the reviewer came for. An unrecognised format is a normal, useful answer, which is why it is not an error.

The trade is honest: unquoted prose yields nothing. Point this at a README and you get nothing back, correctly.

What it reads, and what it drops

One rule, applied to every parsed format:

  • keys are never extracted, only values
  • non-string primitives are dropped — in a typed format a bare 42 is a number and a TOML date is a date
  • the untyped formats are the exception: INI and .env have no types, so PORT=8080 yields the string 8080
  • values are trimmed; empty ones are dropped
  • duplicates are kept, in document order. --dedupe is opt-in, because a string that appears forty times is a different finding from one that appears once, and which of those matters is your call

A directory is walked the way ripgrep walks one: .gitignore honoured, hidden files skipped, --no-ignore and --hidden to reach the rest. A file named explicitly is always read.

Positions, and where they stop

Each value is reported with its file and, where it can be found in the source, a 1-based line and column in UTF-16 units — the number your editor shows.

JSON is placed by its parser; the other six by a forward search over the source. Where a parser hands back real ranges they win, so a JSON value is always located even when its escapes mean it appears nowhere literally.

The rest can miss, and the report says how many. A parser resolves escapes and folds scalars, so a YAML block scalar and a CSV cell with an escaped quote are correct values that never appear literally in the file. Those get no position and summary.unlocated counts them. Reporting a nearby guess would be worse than reporting nothing; a count you can see is the difference between a limitation and a lie.

Where it goes further than the extension

--multiline. JavaScript's . does not match a newline, so the extension's quoted-run pattern cannot span lines and a multi-line template literal is invisible to it. That is an email body, a help paragraph, a consent notice — the copy an audit least wants to miss, and a terminal has no reason to inherit a limit that exists because a regex in an editor did not set a flag.

It is off by default, so the default answer is the extension's answer, and the shared corpus keeps the two honest.

It has no opinions

No spell check. No banned-word list. No reading-level score. No guess at which strings are "user-facing".

Which strings matter is the reviewer's call, and a tool that pre-filtered would decide the audit before the auditor saw it. What you do with the list — grep it, diff it against last release, feed it to a translation memory, hand it to legal — is the part this deliberately stays out of. A contract test asserts no flag asks for a judgment.

Options

--dedupe             collapse repeated values to their first occurrence
--format <format>    force a format instead of inferring from the name;
                     an unknown name falls back rather than failing
--values             print only the values, one per line, for piping
--multiline          let a quoted run span lines, so a multi-line
                     template literal is read too
--csv-header         skip the first CSV row
--csv-column <n>     take only this 0-based CSV column
--stdin              read one document from stdin
--hidden             walk hidden files and directories too
--no-ignore          walk files that .gitignore excludes

As an MCP server

string-le mcp

Two tools, both returning { ok, data, diagnostics, meta }:

  • extract_strings — content in, values out, no positions. Touches no filesystem. The npm server ships the same tool with byte-identical output; one corpus runs against both.
  • string_le_scan — files or directories in, the same reports the CLI writes, positions included.

ok means the scan ran, never that it found something. A file with no strings is a result, not an error.

The other four ways to run it

Where What you get Install
VS Code The same extraction, in your editor, on a keystroke Marketplace
Cursor, VSCodium, Windsurf The same extension Open VSX
Any MCP agent, via Node extract_strings over stdio npx string-le-mcp · npm
Zed The MCP server as a context server add it by hand (no listing yet)

All ten LE tools are on letools.dev.

Also by nolindnaidoo

Rust — pixelcoords and pixelactions are one loop: pixelcoords answers where, pixelactions acts there. The six LE crates are the terminal half of the extensions they sit in — the same extraction, held to the extension's own corpus, and an exit code instead of a results editor.

Contact Developernolindnaidoo.com · GitHub · LinkedIn

License

MIT — see LICENSE.