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

transly

v1.0.0

Published

Cache-driven incremental translation (via LLM, GoogleTranslate, etc.)

Downloads

186

Readme

npm downloads license

transly

Cache-driven i18n translation CLI. Translates your JSON locale files incrementally — only strings that actually changed since the last run are sent for translation. The translator is fully configurable: use any LLM, any free translation service, or implement your own.

What it does

On each run transly scans your source locale directory, computes a SHA-256 hash of every string, compares it against a local cache, and translates only the keys that are new or changed. Translated strings are written to the target language directories and the cache is updated. If a batch fails mid-run, everything translated so far is already saved — just re-run to continue.

Why transly

  • Only translates what changed, based on content hashes — not git history, not timestamps
  • Cache is written after every chunk, so partial failures never lose work
  • Translator backend is pluggable: LLM, free API, or your own function
  • Works without any LLM config at all (falls back to a free translation service)
  • Concurrent translation of multiple namespace × language pairs

Quick start

Install:

npm install -D transly

Create transly.config.ts in your project root:

import 'dotenv/config';
import { defineConfig } from 'transly';

export default defineConfig({
  sourceLang: 'en',
  targetLangs: ['de', 'fr', 'ja'],
  localesDir: './src/locales',

  llm: {
    model: 'openai/gpt-4o-mini',
    apiKey: process.env.OPENAI_API_KEY,
    baseUrl: 'https://openrouter.ai/api/v1',
    contextPrompt: 'This is a web application for managing personal finances.',
  },
});

Put your source strings in src/locales/en/:

src/locales/en/example.json

{
  "title": "Dashboard",
  "greeting": "Hello, {{name}}!"
}

Run:

npx transly translate

Transly writes the translated files to src/locales/de/, src/locales/fr/, etc., and keeps a cache under src/locales/.transly/ by default.

If you already have translated files from a previous setup, seed the cache before running so transly does not re-translate everything:

npx transly cache hydrate
npx transly translate

Add OPENAI_API_KEY=sk-... to a .env file and use import 'dotenv/config' at the top of your config. Never hardcode secrets.

Free translation (no LLM)

If you omit the llm block entirely, transly falls back to Microsoft Translator via the anylang package — no API key required. This is convenient for quick tests or non-critical projects, but the translation quality is noticeably lower than what a well-prompted LLM produces.

import { defineConfig } from 'transly';

export default defineConfig({
  sourceLang: 'en',
  targetLangs: ['de', 'fr', 'ja'],
  localesDir: './src/locales',
});

Cache management

The cache lives at <localesDir>/.transly/ by default. You can override this with the cacheDir config option.

Strategy 1 — commit the cache. The cache directory is the source of truth for what has been translated. Committing it means CI and teammates never pay to re-translate strings that are already done.

Strategy 2 — gitignore the cache, seed on demand. Add .transly/ to .gitignore. On a fresh clone, seed the cache from the existing translated files before running translation:

npx transly cache hydrate
npx transly translate

This keeps the repository clean at the cost of an extra step per fresh clone.

Dropping the cache. To force a full re-translation:

npx transly cache drop

Config reference

import { defineConfig } from 'transly';

export default defineConfig({
  // Required
  sourceLang: 'en',
  targetLangs: ['de', 'fr'],
  localesDir: './src/locales',

  // Optional
  cacheDir: './.transly',       // default: <localesDir>/.transly
  maxBatchSize: 50,              // keys per translation request, default: 50
  concurrency: 10,               // parallel tasks, default: 10
  debug: false,

  // LLM translator (optional — omit to use the free fallback)
  llm: {
    model: 'openai/gpt-4o-mini',
    apiKey: process.env.OPENAI_API_KEY,
    baseUrl: 'https://openrouter.ai/api/v1', // any OpenAI-compatible endpoint
    systemPrompt: '...',   // string or (targetLang: string) => string
    contextPrompt: '...',  // string or (targetLang: string) => string
  },

  // Custom translator function (overrides llm and the free fallback)
  translateChunk: async (items, targetLang, config) => {
    // items: Array<{ key: string; value: string }>
    // return: Record<string, string>  (key → translated value)
  },
});

Config files are validated with Zod at startup. Invalid configs exit with code 1 and list every failing field.

CLI reference

transly translate [-c <path>] [-j <n>]

Translates all namespaces into all target languages.

| Option | Description | |---|---| | -c, --config <path> | Path to config file (default: auto-discover transly.config.ts / .js) | | -j, --concurrency <n> | Number of parallel translation tasks |

transly cache hydrate [-c <path>]
transly cache restore [-c <path>]   # alias
transly cache seed    [-c <path>]   # alias

Populates the cache from existing translated files without modifying them. Use this when migrating from another tool or after a cache drop.

transly cache drop [-c <path>]

Deletes the cache directory.

Exit codes: 0 — success. 1 — config error or translation failure.

Custom translators

The translateChunk option lets you plug in any translation backend. It receives a batch of { key, value } pairs and must return a Record<string, string> mapping each key to its translation.

A minimal example:

import { defineConfig } from 'transly';

export default defineConfig({
  sourceLang: 'en',
  targetLangs: ['de', 'fr'],
  localesDir: './src/locales',

  async translateChunk(items, targetLang) {
    // call your translation API here
    return Object.fromEntries(
      items.map((item) => [item.key, myTranslate(item.value, targetLang)]),
    );
  },
});

To use any of the services supported by the anylang package (Google Translate, DeepL, Yandex, etc.), use the anylangAdapter helper:

import { GoogleTranslator } from 'anylang/translators';
import { defineConfig, anylangAdapter } from 'transly';

export default defineConfig({
  sourceLang: 'en',
  targetLangs: ['de', 'fr'],
  localesDir: './src/locales',
  translateChunk: anylangAdapter(new GoogleTranslator()),
});

See the packages/transly/examples/ directory for more usage patterns.

Programmatic API

import { runTranslation, type ProgressEvent } from 'transly/runner';
import { defineConfig, anylangAdapter } from 'transly';

runTranslation(config, fs?, translateFn?, onProgress?) — the main pipeline. Processes all namespace × language pairs respecting concurrency settings.

await runTranslation(config, undefined, undefined, (event: ProgressEvent) => {
  if (event.type === 'task_start') {
    console.log(`${event.namespace} → ${event.targetLang}: ${event.changedKeys} keys to translate`);
  }
});

Progress event types:

type ProgressEvent =
  | { type: 'scan_complete'; namespaces: number; targetLangs: number; totalTasks: number; totalKeys: number }
  | { type: 'task_start';    namespace: string; targetLang: string; totalKeys: number; changedKeys: number }
  | { type: 'chunk_done';    namespace: string; targetLang: string; chunkIndex: number; totalChunks: number; chunkSize: number }
  | { type: 'task_done';     namespace: string; targetLang: string }
  | { type: 'task_skip';     namespace: string; targetLang: string };

defineConfig(config) — identity helper that provides TypeScript types when writing .ts config files.

anylangAdapter(translator) — wraps any anylang-compatible translator instance into a translateChunk function.

Comparison

| Feature | transly | i18next-scanner | @lingui/cli | @formatjs/cli | i18n-auto-translation | |---|---|---|---|---|---| | Incremental (hash-based) | ✅ | ❌ | ❌ | ❌ | ❌ | | LLM support | ✅ | ❌ | ❌ | ❌ | ✅ | | Custom translator | ✅ | ❌ | ❌ | ❌ | ❌ | | Free translation fallback | ✅ | ❌ | ❌ | ❌ | ✅ | | Partial-failure safety | ✅ | n/a | n/a | n/a | ❌ | | Extracts keys from source code | ❌ | ✅ | ✅ | ✅ | ❌ | | Format support | JSON | many | many | many | JSON |

transly is not a key extractor — it translates existing JSON locale files. Pair it with i18next-scanner or a similar tool if you also need to extract keys from source code.

❤️ Support & Contribute

This project is open-source under the Apache 2.0 License — it not only allows you to use and modify the code freely, but also protects contributors by explicitly granting patent rights and requiring proper attribution.

Contributions are very welcome — whether it’s code, ideas, bug reports, or improvements.

If you find this tool useful:

  • ⭐ Star the repository
  • 📢 Share it with others
  • 💡 Help shape it with your feedback

Good tools get better when people care.

Want to help?

  • Fix something
  • Suggest something
  • Break something (and report it)

And if you like it, a ⭐ on GitHub goes a long way.