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

@mihairo/loqui

v2.0.0

Published

loqui — i18n translation engine for software projects.

Readme

npx @mihairo/loqui --input en.json --from en --to fr,de,es --output ./i18n/{locale}.json

Features

  • Three LLM engines — Gemini, OpenAI, Anthropic (bring your own API key)
  • Any model — not locked to a specific version; pass any model string
  • Incremental translation — only re-translate keys that changed since the last run (recommended)
  • Placeholder protection{{mustache}}, ${template}, {icu}, ICU plural/select blocks, HTML tags, and custom patterns are never mutated
  • Custom prompts — override system/user prompt templates with your own
  • Programmatic APIimport { translate } from '@mihairo/loqui'
  • CLI — pipe-friendly, stdin/stdout support
  • Zero runtime dependencies — only the TypeScript compiler for dev

Installation

npm install @mihairo/loqui
# or
npm install -g @mihairo/loqui   # for the CLI globally

Requires Node.js ≥ 22.


Quick start

1. Create a config file interactively:

npx @mihairo/loqui init

This walks you through choosing an engine, model, source locale, and target locales, then writes .loqui.json to your project root.

2. Set your API key:

export GEMINI_API_KEY=your-key-here
# or OPENAI_API_KEY / ANTHROPIC_API_KEY

3. Translate:

loqui --input en.json --output ./i18n/{locale}.json --incremental

Configuration

All fields are optional. CLI flags always override the config file.

| Field | Type | Default | Description | | --------------------- | ----------------------------------- | ------------------ | ------------------------------------ | | engine | gemini | openai | anthropic | gemini | LLM provider | | model | string | gemini-2.5-flash | Model name (any string) | | from | string | — | Source locale (e.g. en) | | to | string[] | — | Target locales (e.g. ["fr","de"]) | | temperature | 0–2 | 0.1 | Sampling temperature | | topP | 0–1 | 1 | Nucleus sampling | | concurrency | 1–32 | 8 | Parallel API requests | | splitToken | 500–32000 | 4000 | Max tokens per chunk | | context | string | — | Domain context injected into prompts | | prompts | { system?, user? } | — | Custom prompt templates | | placeholderPatterns | string[] | — | Extra regex patterns to protect |

Config file is auto-discovered as .loqui.json in the current directory.


CLI

loqui init                    Interactive setup — creates .loqui.json in the current directory

loqui [input] [options]

[input] — one of:
  --input <file>         read from a JSON file
  --input '<json>'       pass a JSON string inline
  first positional arg   loqui en.json --from en --to fr
  stdin                  cat en.json | loqui --from en --to fr

Options:
  --config <path>        Config file or directory (default: .loqui.json in cwd)
  --from <locale>        Source locale — overrides config.from
  --to <locale,...>      Target locale(s), comma-separated — overrides config.to
  --engine <name>        Engine: gemini | openai | anthropic — overrides config.engine
  --model <name>         Model name — overrides config.model
  --context <text>       Domain context injected into prompts — overrides config.context
  --output <path>        Output path. Use {locale} token: ./i18n/{locale}.json
                         Or a plain directory: writes {dir}/{locale}.json
  --namespace <name>     Namespace label injected into translation prompts
  --incremental          Only translate new/changed keys (uses a hash sidecar)
  --hash-file <path>     Hash sidecar path (implies --incremental)
  --dry-run              Preview without calling the API or writing files
  --force                Re-translate all keys regardless of existing translations

Examples

# Translate a file, write per-locale files
loqui --input src/i18n/en.json --from en --to fr,de --output src/i18n/{locale}.json

# Pipe JSON through stdin, get JSON on stdout
cat en.json | loqui --from en --to ja

# Inline JSON as a positional arg
loqui '{"hello":"Hello"}' --from en --to fr

# Incremental — only re-translate changed keys
loqui --input en.json --from en --to fr,de --output ./i18n/{locale}.json --incremental

# Dry run — preview without any API calls or file writes
loqui --input en.json --from en --to fr --dry-run

# Use a different engine and model
loqui --input en.json --from en --to fr --engine anthropic --model claude-opus-4-6

Programmatic API

import { translate } from "@mihairo/loqui";

const result = await translate({
  input: "./en.json", // file path or raw JSON string
  from: "en",
  to: ["de", "es", "fr", "pt"],
  output: "./i18n/{locale}.json",
});

// result: { de: '{"hello":"Hallo"}', fr: '{"hello":"Bonjour"}', ... }

TranslateOptions

interface TranslateOptions {
  input: string; // file path or raw JSON string
  from?: string; // source locale
  to?: string | string[]; // target locale(s)
  output?: string | Record<string, string>; // {locale} template, dir, or locale→path map
  namespace?: string; // label injected into prompts
  incremental?: boolean; // hash-based change detection
  hashFile?: string; // custom hash sidecar path
  force?: boolean; // re-translate all keys
  dryRun?: boolean; // no API calls or writes
  engine?: EngineAdapter; // custom engine instance
  config?: Partial<LoquiConfig>; // inline config overrides
  configPath?: string; // path to config file or directory
}

Return value

translate() returns Promise<Record<string, string>> — a map from locale to serialised JSON string.

If output is specified, files are written to disk and the same map is still returned.


Batch workflow

Translate multiple namespaces with a simple script:

import { translate } from "@mihairo/loqui";
import { readdirSync } from "fs";
import { join } from "path";

const I18N_DIR = "src/assets/i18n";
const FROM = "en";
const TO = ["fr", "de", "es"];

const namespaces = readdirSync(I18N_DIR, { withFileTypes: true })
  .filter((d) => d.isDirectory())
  .map((d) => d.name);

for (const ns of namespaces) {
  await translate({
    input: join(I18N_DIR, ns, `${FROM}.json`),
    from: FROM,
    to: TO,
    output: join(I18N_DIR, ns, "{locale}.json"),
    namespace: ns,
    incremental: true,
  });
}

Placeholder protection

Tokens that must not be translated are automatically masked before the LLM call and restored afterward.

| Pattern | Example | | ------------------- | ----------------------------------------------- | | Double mustache | {{userName}}, {{count}} | | Template literal | ${firstName} | | ICU plural/select | {count, plural, one {# item} other {# items}} | | Simple ICU variable | {name} | | HTML tags | <strong>, </p>, <br/> |

Custom patterns

Add extra patterns in your config:

{
  "placeholderPatterns": ["%{variable}", "__VAR__"]
}

Patterns are regex strings. Custom patterns are applied before the built-ins.


Incremental translation

When --incremental is set (or incremental: true in the API), loqui stores a hash of each source value next to the input file as .{name}.loqui-hash.json. On subsequent runs, only keys whose source text changed (or that are missing from the target) are sent to the LLM.

loqui --input en.json --from en --to fr,de --output ./i18n/{locale}.json --incremental

The hash file path can be customised:

loqui --input en.json --incremental --hash-file .cache/en.hash.json ...

Custom prompts

Override the system or user prompt with your own template. Available variables:

| Variable | Description | | ------------------- | ----------------------------------- | | {{sourceLocale}} | Source locale code (e.g. en) | | {{targetLocales}} | Comma-separated target locales | | {{namespace}} | Namespace label (if provided) | | {{context}} | Domain context string (if provided) | | {{json}} | The JSON chunk to translate |

{
  "prompts": {
    "system": "You are a professional translator for a SaaS product. Translate from {{sourceLocale}} to {{targetLocales}}. Keep all placeholders intact.",
    "user": "Translate this JSON:\n\n{{json}}"
  }
}

Custom engines

Pass any object implementing EngineAdapter via the engine option to bypass the built-in providers entirely:

import {
  translate,
  EngineAdapter,
  TranslationChunk,
  TranslationResult,
} from "@mihairo/loqui";

const myEngine: EngineAdapter = {
  async translateChunk(
    chunk: TranslationChunk,
    targetLocales: string[],
    sourceLocale: string,
    namespace: string,
  ) {
    const result: Record<string, TranslationResult> = {};
    for (const locale of targetLocales) {
      // call your own LLM or translation service here
      result[locale] = {
        keys: {
          /* translated flat key/value pairs */
        },
      };
    }
    return result;
  },
};

await translate({ input: "en.json", from: "en", to: ["fr"], engine: myEngine });

Or extend BaseEngine to reuse the built-in prompt builder and JSON response parser:

import { BaseEngine, LoquiConfig, TranslationChunk } from "@mihairo/loqui";

class MyEngine extends BaseEngine {
  constructor(config: LoquiConfig) {
    super(config);
  }

  async translateChunk(
    chunk: TranslationChunk,
    targetLocales: string[],
    sourceLocale: string,
    namespace: string,
  ) {
    const systemPrompt = this.buildSystemPrompt(
      targetLocales,
      sourceLocale,
      namespace,
    );
    const userPrompt = this.buildUserPrompt(chunk, targetLocales, sourceLocale);

    const raw = await callMyLLM(systemPrompt, userPrompt); // your implementation

    return this.parseResponse(raw, Object.keys(chunk.keys), targetLocales);
  }
}

await translate({
  input: "en.json",
  from: "en",
  to: ["fr"],
  engine: new MyEngine(config),
});

Environment variables

| Variable | Required for | | ---------------------- | --------------------- | | GEMINI_API_KEY | engine: "gemini" | | OPENAI_API_KEY | engine: "openai" | | ANTHROPIC_API_KEY | engine: "anthropic" | | ANTHROPIC_API_VERSION | Override Anthropic API version (default: 2023-06-01) |


Troubleshooting

| Symptom | Cause | Fix | |---------|-------|-----| | GEMINI_API_KEY environment variable is not set | Missing env var | Export the correct key for your engine | | 'from' (source locale) is required | No from in options or config | Add from to .loqui.json or pass --from | | Empty translation strings in output | LLM response missing keys | Try reducing splitToken, check model availability | | Engine returned invalid JSON | LLM returned non-JSON | Try a more capable model, or add context to help the LLM | | 429 rate limit errors | Too many concurrent requests | Reduce concurrency in config (default: 8) | | Keys not re-translated after source changes | Hash file has stale values | Run with --force once to reset, or delete the .loqui-hash.json sidecar | | Failed to parse '.loqui.json' | Syntax error in config | Validate the JSON at jsonlint.com or similar |

Performance Tuning

splitToken (default: 4000)

Controls how many source keys are bundled into a single LLM request. Higher = fewer requests (faster, cheaper) but risks hitting model context limits. Lower = safer for models with small context windows.

  • 4000 — Recommended for Flash/GPT-4o-mini tier models
  • 12000+ — Recommended for Pro/GPT-4o tier models

concurrency (default: 8)

Number of simultaneous API requests. Higher = faster for large files but risks rate limits.

  • Reduce to 3–4 if you see frequent 429s
  • Free-tier API keys: use 1–2

--incremental

Always use this flag for repeated runs. Skips unchanged keys entirely. On a 1000-key file where only 10 keys changed, you pay for 10 keys, not 1000.


GitHub Actions

Integrate loqui into your CI pipeline to automatically translate i18n files on push.

Example workflow

name: Translate i18n

on:
  push:
    paths: ['src/i18n/en.json']

jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npx @mihairo/loqui --input src/i18n/en.json --from en --to fr,de,es --output src/i18n/{locale}.json --incremental
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
      - uses: peter-evans/create-pull-request@v6
        with:
          title: 'chore: update i18n translations'
          commit-message: 'chore: update i18n translations'
          branch: i18n/update

Create your own action

A standalone GitHub Action reference is available at .github/actions/loqui/action.yml for use in other repositories.


Contributing

See CONTRIBUTING.md for guidelines on setting up the project, running tests, and submitting pull requests. Please read the Code of Conduct before participating.


License

Apache 2.0 — see LICENSE for details.