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.
Sixty seconds
|
# the point of the whole thing:
|
./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 grep — 0 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
42is a number and a TOML date is a date - the untyped formats are the exception: INI and
.envhave no types, soPORT=8080yields the string8080 - values are trimmed; empty ones are dropped
- duplicates are kept, in document order.
--dedupeis 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
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.
- pixelcoords — Freeze your screen, mark regions, get pixel-exact coordinates and crops pixelcoords.dev · crates.io · docs.rs
- pixelactions — Consume human-verified coordinates, perform the interaction, confirm it landed pixelactions.dev · crates.io · docs.rs
- paths-le — Find every path in a codebase and report whether it still points at anything crates.io
- secrets-le — Find hardcoded credentials, and never print one crates.io
- urls-le — Extract every URL from a codebase, with its protocol and exact position crates.io
- regex-le — Find every regex in a codebase and report which can be driven into catastrophic backtracking crates.io
- numbers-le — Find every hardcoded number in a codebase so a person can check them crates.io
- envsync-le — Compare the dotenv files in a tree and say which keys are missing from which crates.io
- colors-le — Find every colour in a codebase, and say which are not in your palette crates.io
- scrape-le — Check whether a page is scrapeable before the scraper is written crates.io
Contact Developer — nolindnaidoo.com · GitHub · LinkedIn
License
MIT — see LICENSE.