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

broken-translate

v1.0.4

Published

Zero-dependency translation for Node. Auto-detect, batch, and long-text support using the built-in fetch. No axios, no SDK, nothing to install.

Readme

broken-translate

Zero-dependency translation for Node. Auto-detect, batch, and long-text support using the built-in fetch. No axios, no SDK, nothing to install but this.

npm install broken-translate

Why this exists

The usual way to translate in Node is to pull in an HTTP client and a provider SDK, then babysit the versions. This package uses the fetch that already ships with Node 18+, so the dependency count is zero and there is nothing to keep updated.

0 dependencies. Node 18+. Full TypeScript types included.

Quick start

const { translate } = require('broken-translate');

const r = await translate('Hello world', { to: 'fr' });
console.log(r.text); // "Bonjour le monde"

Source language is auto-detected, so you usually only pass to.

await translate('Bonjour mon ami', { to: 'en' }); // "Hello my friend"
await translate('Good morning', { to: 'yo' });    // Yoruba

ESM works too:

import { translate } from 'broken-translate';

API

translate(text, options?)

Returns { text, detectedLanguage, match, from, to }.

| option | default | what it does | |---|---|---| | to | 'en' | target language code | | from | 'autodetect' | source code — leave it and let the API work it out | | email | — | raises your daily quota a lot. Pass your email. | | timeoutMs | 15000 | per-request timeout | | retries | 2 | retries on rate-limit / server / network errors |

detectLanguage(text, options?)

Returns a language code, or null if it couldn't tell.

await detectLanguage('Bonjour mon ami'); // 'fr'

translateBatch(texts, options?)

Translates an array concurrently, preserving input order.

const out = await translateBatch(['one', 'two', 'three'], { to: 'es', concurrency: 3 });
out[1].text; // translation of 'two'

concurrency defaults to 3 and is capped at 10 — be kind to a free API.

translateLong(text, options?)

For text longer than one request allows. Splits on paragraph → sentence → word boundaries, translates the parts, and rejoins them.

const { text, parts } = await translateLong(bigArticle, { to: 'fr' });
console.log(`translated in ${parts} parts`);

splitText(text, max?)

The chunker on its own, if you want to inspect or cache the pieces.

splitText('First para.\n\nSecond para.', 500);
// ['First para.', 'Second para.']

Never returns a chunk longer than max, even for a single unbroken 1000-character token.

LANGUAGES

A frozen map of common codes to names, for building pickers.

LANGUAGES.fr; // 'French'
LANGUAGES.yo; // 'Yoruba'

TranslateError

Every failure throws this, so you branch on .code instead of matching strings.

| code | meaning | |---|---| | EMPTY_INPUT | you passed empty text | | TOO_LONG | over MAX_CHARS — use translateLong | | BAD_TARGET | target was autodetect | | BAD_INPUT | translateBatch got a non-array | | RATE_LIMITED | HTTP 429 — slow down | | QUOTA_EXCEEDED | daily allowance used up | | TIMEOUT | exceeded timeoutMs | | NETWORK | couldn't reach the service | | SERVER_ERROR | 5xx from upstream | | BAD_RESPONSE | body wasn't JSON | | EMPTY_RESULT | upstream returned nothing usable | | API_ERROR | upstream reported a problem | | NO_TRANSLATION | the text came back unchanged even with the source named |

try {
  await translate(text, { to: 'fr' });
} catch (err) {
  if (err.code === 'QUOTA_EXCEEDED') {
    // fall back to a cached translation, or tell the user to retry tomorrow
  }
  if (err.code === 'TOO_LONG') {
    await translateLong(text, { to: 'fr' });
  }
}

Words that are the same in both languages

The upstream API answers an untranslatable request by echoing the input back with 200 and a match of 0.85, which is indistinguishable from a translation unless you check. This library checks.

An echo is not automatically a failure, though: plenty of words are identical in the target language — pizza, hotel, Tokyo, restaurant — and those are correct results, not errors. The API separates the two cases itself. On a pair where the source was auto-detected, an echo always scores 0.85. On a pair where the source was named, a genuine identity translation scores 0.98–1.

So when an auto-detected call comes back unchanged, the library retries once with the detected source, then with en, and returns the result if either resolves it. Only when every attempt echoes does it raise NO_TRANSLATION:

await translate('pizza', { to: 'fr' });   // 'pizza'  — correct, not an error
await translate('Tokyo', { to: 'fr' });   // 'Tokyo'  — correct, not an error
await translate('zzz qqq', { to: 'fr' }); // throws NO_TRANSLATION

Quota

The upstream service is free and rate-limited. Two things help a lot:

  1. Pass your email. The free daily allowance jumps substantially.
  2. Cache. You're billed per request, so don't re-translate the same string.
const cache = new Map();

async function cachedTranslate(text, to) {
  const key = `${to}:${text}`;
  if (cache.has(key)) return cache.get(key);
  const r = await translate(text, { to, email: '[email protected]' });
  cache.set(key, r);
  return r;
}

Common patterns

Translate a whole file

const fs = require('node:fs');
const { translateLong } = require('broken-translate');

const source = fs.readFileSync('README.md', 'utf8');
const { text } = await translateLong(source, { to: 'fr' });
fs.writeFileSync('README.fr.md', text);

Bot command

if (text.startsWith('!tr ')) {
  const [, lang, ...rest] = text.split(' ');
  const r = await translate(rest.join(' '), { to: lang });
  return reply(r.text);
}

Handle quota gracefully

async function safeTranslate(text, to) {
  try {
    return (await translate(text, { to })).text;
  } catch (err) {
    if (err.code === 'QUOTA_EXCEEDED') return '[translation quota reached — try later]';
    if (err.code === 'TOO_LONG') return (await translateLong(text, { to })).text;
    throw err;
  }
}

Tests

npm test          # 21 offline tests, fetch stubbed, no network
npm run test:live # 5 tests against the real API

The default suite is fully offline and deterministic. The live suite is separate so a flaky network never turns CI red.

TypeScript

Types ship with the package.

import { translate, TranslateError, type TranslateOptions } from 'broken-translate';

const opts: TranslateOptions = { to: 'fr', retries: 3 };
const r = await translate('hi', opts);
const s: string = r.text;

Licence

MIT © Akewushola Abdulbakri Temitope (brokenvzn)