Read JavaScript and TypeScript back in plain human language — British English, Punjabi, Scots, Irish English, Nigerian Pidgin, or a pirate's.
$ npx jstranslator examples/sample.js --locale sco
Mak an auld-farrant variable `boolean`, stertin aff as aye.
Mak an auld-farrant variable `string`, stertin aff as the wirds "string".
Mak an auld-farrant variable `emptylist`, stertin aff as a toom list.
Mak a function cried `func`. It taks the yin thing `number`:
Mak an auld-farrant variable `result`, stertin aff as `number` an 1 thegither.
Gie back `result`.Written in 2013 as a pile of regular expressions. Rebuilt on a real parser, because
the original could not tell the difference between the word if in your code and the
word if inside one of your strings.
The 2013 version ran a list of String.replace calls over the raw source text. It had
no idea what any of it meant, so it rewrote characters wherever it found them — inside
strings, inside comments, inside variable names — and produced output that was still
basically JavaScript with some words swapped:
variable true or false (boolean) #equals# true;
if the following condition is true: (true or false (boolean) #is absolutely equal to# true) {
This version parses the file into a syntax tree with
@babel/parser, walks that tree, and writes
actual sentences:
Make an old-style variable `boolean`, starting as true.
If `boolean` is exactly true:
Log true in the console.
Because it works from structure rather than text, it understands the modern language:
classes, async/await, generators, destructuring, optional chaining, modules, JSX —
and all of TypeScript's type syntax.
npm install jstranslatorNeeds Node 20.19+. Works in the browser too — there is no filesystem access anywhere in the library.
jstranslator file.ts # British English
jstranslator file.ts --locale pa # Punjabi
jstranslator file.ts --json # the phrase tree, not prose
cat file.js | jstranslator - # from stdin
jstranslator --locales # list the dialectsimport { translate, describe, listLocales } from 'jstranslator';
translate('const total = 0;');
// → 'Fix `total` as 0.'
translate('const total = 0;', { locale: 'pcm' });
// → 'Hold `total` as 0, e no go change.'
describe('const total = 0;').phrases;
// → [{ id: 'declare.const', values: { name: …, value: …, type: 'none' } }]describe() is the interesting one. It returns a language-neutral phrase tree —
what the code does, with no human language attached. That is machine-readable output
you can feed to something else, and it is why adding a dialect never touches any code.
npm run devPaste code, pick a dialect, watch it translate as you type.
Three stages, each testable on its own:
source ──parse──► syntax tree ──describe──► phrase tree ──render──► text
@babel/parser no language yet catalogue applied here
A phrase names what was said and supplies the moving parts, never the words:
The catalogue turns that into a sentence:
"declare.const": "Fix {name} as {value}{type, select, none {} other {, of type {type}}}."Messages are ICU MessageFormat, so plurals and optional clauses follow each language's own rules rather than being glued together with string concatenation.
Well-known APIs get their own messages, which is where most of the accuracy comes from.
console.log(x) is not "ask console to log with x" — it is "log x in the
console", and in Scots, "shout x intae the console".
There is more detail in docs/architecture.md.
| Locale | Language | Messages | Inherits |
|---|---|---|---|
en-GB |
British English (simplified) | 230 | — |
pa |
ਪੰਜਾਬੀ — Punjabi, Gurmukhi | 230 | en-GB |
pa-Latn |
Punjabi, romanised | 222 | pa |
sco |
Scots | 73 | en-GB |
pcm |
Naijá — Nigerian Pidgin | 70 | en-GB |
en-IE |
Irish English | 53 | en-GB |
en-x-pirate |
Pirate | 48 | en-GB |
The same line, seven ways:
console.log(list[i]);| Locale | Output |
|---|---|
en-GB |
Log item i of list in the console. |
sco |
Shout thing i oot o list intae the console. |
en-IE |
Stick item i of list up in the console. |
pcm |
Put number i for inside list for console. |
en-x-pirate |
Cry item i of list from the crow's nest. |
pa |
`list` ਦੀ `i` ਵਾਲੀ ਚੀਜ਼ ਨੂੰ ਕੰਸੋਲ ਵਿੱਚ ਲਿਖੋ। |
pa-Latn |
`list` dee `i` vaalee cheez nu console vich likho. |
Only en-GB has to be complete. Everything else sets "extends" and overrides just the
lines worth saying differently — which is why Pirate is 48 messages rather than 230.
Adding your own is one JSON file and no code. See docs/writing-a-dialect.md, and docs/message-keys.md for the full key reference.
Worth answering, since it was the first thing considered. Messagevisor is a genuinely good Git-native i18n system — ICU support, locale inheritance, translation tests, a generated review UI. It was installed and run against this project before the current design was chosen.
It was not adopted because of the authoring shape. Messagevisor stores one file per message key with every locale inside it, which suits a product where translators work key by key. Here the unit of work is a dialect: someone sits down and writes Scots. Under that model, adding Scots would be a diff across ~230 files instead of adding one.
So the catalogues are plain JSON, one file per dialect, with the same ICU messages underneath. If this ever grows real translators and a review workflow, the messages port across as they are — they are ordinary ICU strings, and the inheritance model is the same idea.
npm install
npm test # unit tests, in-source tests and snapshots
npm run typecheck
npm run build # library + type declarations
npm run dev # the playgroundTests are Vitest. Small ones live in the source file next to what they cover:
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest;
it('collapses whitespace in quoted source', () => {
expect(collapse('a\n b')).toBe('a b');
});
}These are stripped from the built output. Anything broader lives in test/.
Two tests keep the catalogues honest: every message the walker can emit must exist in
en-GB, and every message in every dialect must be a key the walker actually emits. A
new phrase cannot ship without wording, and a typo in a dialect override fails the build
rather than silently doing nothing.
- Bitwise operators and JSX bodies are quoted as source rather than described.
- Type annotations are quoted as written. They are already declarative, and prosifying a conditional type helps nobody.
- The non-English dialects are written by hand and would benefit from a native speaker's eye. Corrections very welcome — they are one JSON file.
MIT. See LICENSE.
Originally by Jasdeep Khalsa (@JasdeepKhalsa1).
{ "id": "declare.const", "values": { "name": …, "value": … } }