@speranto/speranto
v0.4.3
Published
A quick and simple machine translation tool for i18n in webapps. Translate JSON, JS/TS, Markdown files and database content using OpenAI, Mistral, or Ollama.
Readme
Speranto
A quick and simple machine translation tool for i18n in web apps. Named after Esperanto, the universal European language, Speranto helps you translate content across multiple languages.
The published CLI runs on Node.js 22.13 or newer.
Installation
npm install @speranto/speranto
# or
yarn add @speranto/speranto
# or
pnpm add @speranto/sperantoConfiguration types are also available on JSR:
npx jsr add @speranto/speranto
# or
deno add jsr:@speranto/sperantoimport type { Config } from '@speranto/speranto'Agent documentation
The npm package installs current Speranto guidance for coding agents into the consuming project:
.agents/
└── speranto/
├── guide.md
└── manifest.jsondocs/agent-guide.md in the Speranto repository is the canonical source. During installation
and package upgrades, the postinstall hook copies that guide to
.agents/speranto/guide.md when Speranto is a direct dependency. It also adds a small managed
reference to the project's existing AGENTS.md, CLAUDE.md, or .claude/CLAUDE.md file. When
none of those files exist, it creates a minimal AGENTS.md. Existing content outside the
following markers is never replaced:
<!-- speranto-agent-docs:start -->
## Speranto
When working with localization, Speranto configuration, or translated content, read and follow
@.agents/speranto/guide.md.
<!-- speranto-agent-docs:end -->In a workspace, the hook verifies the dependency against the package where Speranto was
installed, then places the documentation at the nearest workspace root. It recognizes
pnpm-workspace.yaml and the package.json workspaces formats used by npm, Yarn, Bun, and
workspace-based build tools. Previously managed files in the package directory are removed during
this migration without deleting user-authored content. Set SPERANTO_PROJECT_ROOT to override the
documentation destination explicitly.
If CLAUDE.md already imports AGENTS.md using Claude's @AGENTS.md syntax, only AGENTS.md
is updated. Repeated installation is idempotent, and updating the package replaces the managed
guide with the canonical guide from the installed version.
Current package managers generally block unapproved dependency lifecycle scripts. The install still succeeds, but the agent guide is not synchronized until the hook is approved or the setup command is run directly.
| Package manager | Default behavior | Approve and run the hook |
| --------------- | -------------------------------------------- | --------------------------------------------------------------------------------------- |
| npm | Reports and skips unapproved install scripts | npm install-scripts approve @speranto/speranto, then npm rebuild @speranto/speranto |
| pnpm | Reports dependencies awaiting build approval | pnpm approve-builds @speranto/speranto |
| Bun | Blocks untrusted dependency lifecycle scripts | bun pm trust @speranto/speranto |
| Yarn 4.14+ | Disables third-party postinstall scripts | Set dependenciesMeta["@speranto/speranto"].built to true, then run yarn install |
For Yarn, add the approval to the consuming project's top-level package.json:
{
"dependenciesMeta": {
"@speranto/speranto": {
"built": true
}
}
}npm pins script approval to the currently installed package version by default, so an upgrade may require approval again. pnpm and Yarn store package-level approval in the consuming project. Older package-manager versions or projects with a broader script policy may run the hook without an approval step.
Regardless of package-manager policy, install or repair the documentation directly with:
speranto setup-agentsThis command performs the same synchronization as the postinstall hook and remains available when
the hook was blocked. In CI, run it explicitly or use --check to fail when committed agent
documentation is stale.
Check whether the installed copy is current, or remove all Speranto-managed references and files:
speranto setup-agents --check
speranto setup-agents --removeSet SPERANTO_SKIP_AGENT_DOCS=1 to disable automatic installation. The generated
.agents/speranto/guide.md and its manifest may be committed so agents can use them before
dependencies are installed. Do not edit the generated guide directly; update project-specific
instructions in AGENTS.md or CLAUDE.md instead.
Development
Install pnpm using the current official Node.js installer, then install dependencies, type-check, test, and build:
npx get-pnpm
pnpm install
pnpm exec tsc --noEmit
pnpm test
pnpm buildpnpm test starts the PostgreSQL 16 test container and runs the complete suite with
LLM_API_KEY=test. Use pnpm test:unit to run the suite without PostgreSQL.
Agent-facing documentation is maintained in docs/agent-guide.md. Update it in the same change
as CLI flags, configuration, generated-file behavior, or recommended translation workflows
that affect coding agents.
Versioning
Keep package.json as the source of truth. The sync step updates shared package metadata in
jsr.json (name, version, license, description) while leaving JSR-specific fields like
exports and publish intact:
pnpm bump:version patch
pnpm bump:version minor
pnpm bump:version majorYou can also create prereleases or set an explicit version:
pnpm bump:version prerelease beta
pnpm bump:version 1.0.0If package.json was edited manually, resync jsr.json with:
pnpm sync:versionUsage
Run Speranto from your workspace directory:
sperantoConfiguration
Creating a configuration file is strongly recommended. Speranto looks for a configuration file in your workspace:
speranto.config.ts(TypeScript, recommended)speranto.config.js(JavaScript)
Example Configuration
// speranto.config.ts
import type { Config } from '@speranto/speranto'
const config: Config = {
model: 'gpt-4o-mini',
sourceLang: 'en',
targetLangs: ['es', 'fr', 'de', 'it'],
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
files: {
sourceDir: './content',
targetDir: './content/[lang]',
useLangCodeAsFilename: false,
maxStringsPerGroup: 200,
},
}
export default config// speranto.config.js
const config = {
model: 'mistral-large-latest',
sourceLang: 'en',
targetLangs: ['nl'],
provider: 'mistral',
apiKey: process.env.MISTRAL_API_KEY,
files: {
sourceDir: './i18n/languages',
targetDir: './i18n/languages',
useLangCodeAsFilename: true,
},
}
export default configConfiguration Options
| Option | Type | Description |
| ----------------- | ---------- | ----------------------------------------------------------------------------- |
| model | string | The AI model to use for translation |
| sourceLang | string | Source language code (e.g., 'en' for English) |
| targetLangs | string[] | Array of target language codes |
| provider | string | LLM provider: 'openai', 'ollama', 'mistral', or any OpenAI-compatible |
| apiKey | string | API key for the LLM provider |
| baseUrl | string | Provider base URL (overrides the provider default) |
| ollama | object | Ollama model lifecycle and inference settings (see below) |
| concurrency | number | Global LLM call limit across languages and sources (default: 5, local: 1) |
| timeout | number | Request timeout in milliseconds (default: 600000 / 10 minutes) |
| verbose | boolean | Print the resolved configuration with secrets redacted |
| retranslate | boolean | Force retranslation of all values, even if already translated |
| init | boolean | Build state from existing translations without translating |
| dryRun | boolean | Report pending work and approximate source tokens without making changes |
| instructionsDir | string | Directory containing language-specific instruction files (see below) |
File Translation Options (files)
| Option | Type | Description |
| ----------------------- | ---------- | ------------------------------------------------------------------- |
| sourceDir | string | Directory containing source files |
| targetDir | string | Output directory pattern (use [lang] as placeholder) |
| useLangCodeAsFilename | boolean | Use language code as filename (e.g., en.json → es.json) |
| maxStringsPerGroup | number | Maximum strings per translation batch (helps with large files) |
| excludeKeys | string[] | Field names to exclude from translation (matched against leaf keys) |
Speranto keeps file translation state in a sidecar .speranto/ directory so it can use
hash-based change detection. That lets it skip unchanged files quickly and only retranslate changed
groups/chunks on later runs.
Translation failures are reported as errors and cause a non-zero CLI exit. Speranto validates group responses before updating output or sidecar state, so a failed file is not partially written. Other files that completed successfully are committed before the command exits.
Target languages, files, and database tables are prepared concurrently. All LLM requests pass
through one global FIFO queue, so increasing parallelism never multiplies concurrency by the
number of languages. Local Ollama and localhost endpoints default to one active request; set
concurrency explicitly when the local server supports continuous batching.
Before translation starts, Speranto reports the complete plan: file and table targets, pending semantic translation jobs, cached work, and database rows. Interactive terminals show overall and per-language progress plus the currently active file groups or database rows. Redirected and CI output uses stable planning, periodic progress, language completion, rate-limit, and final summary lines instead of terminal redraws.
Ollama
Ollama runs translations locally without an API key. Install and start Ollama, then configure an instruction-following model:
const config: Config = {
provider: 'ollama',
model: 'gemma3:4b',
sourceLang: 'en',
targetLangs: ['nl'],
instructionsDir: './instructions',
ollama: {
autoPull: false,
keepAlive: '10m',
contextLength: 8192,
temperature: 0.2,
},
files: {
sourceDir: './content',
targetDir: './content/[lang]',
},
}Speranto checks that the Ollama server is reachable and that the configured model is installed.
When autoPull is false (the default), a missing model produces the exact ollama pull command
to run. Set it to true to let Speranto download a missing model automatically. JSON translation
groups use Ollama's native JSON output mode.
| Ollama option | Type | Description |
| --------------- | ------------------ | --------------------------------------------------------- |
| autoPull | boolean | Download a missing model automatically (default: false) |
| keepAlive | string \| number | How long Ollama keeps the model loaded |
| contextLength | number | Context window passed as Ollama's num_ctx |
| temperature | number | Sampling temperature passed to Ollama |
Use baseUrl for a remote server or a Docker hostname, for example
http://ollama:11434. Set apiKey only when an authenticated proxy or hosted Ollama endpoint
requires it.
Language-Specific Instructions
You can provide custom translation instructions for each target language by creating Markdown files in an instructions directory:
instructions/
├── es.md # Spanish-specific instructions
├── fr.md # French-specific instructions
└── nl.md # Dutch-specific instructionsThen reference it in your config:
const config: Config = {
// ...
instructionsDir: './instructions',
}Example instruction file (instructions/nl.md):
# Instructions for Dutch Translation
- Use informal "je/jij" instead of formal "u"
- Keep technical terms in English when commonly used
- Use short, direct sentencesCommand Line Options
You can override configuration with command line flags:
# Specify a custom config file
speranto --config ./custom-config.js
# Override specific options
speranto --model gpt-4o-mini --source-lang en --target-langs es,fr,de
# Use a custom OpenAI-compatible provider
speranto --provider custom --base-url https://my-llm.example.com/v1
# Force retranslation of all values
speranto --retranslate
# Build state from existing translations without translating
speranto --init
# Preview pending work and approximate source-token usage
speranto --dry-run
# All available options
speranto \
-c, --config <path> # Path to config file (auto-detects .ts or .js when omitted)
-m, --model <model> # Model to use for translation
-s, --source-lang <lang> # Source language code
-l, --target-langs <langs> # Target language codes (comma-separated)
-p, --provider <provider> # LLM provider (openai, ollama, mistral, or any OpenAI-compatible)
-k, --api-key <key> # API key for LLM provider
-b, --base-url <url> # Base URL for OpenAI-compatible API
-i, --instructions-dir <path> # Directory containing language instruction files
-n, --concurrency <number> # Global LLM call limit (default 5, local 1)
-v, --verbose # Enable verbose output for debugging
-r, --retranslate # Force retranslation of all values
--init # Build state from existing translations
--dry-run # Report pending work without translating or writingDatabase Translation
Speranto can also translate content stored in database tables. This is useful for CMS systems or applications that store translatable content in a database.
Simply add a database section to your config file alongside or instead of files:
// speranto.config.ts
import type { Config } from '@speranto/speranto'
const config: Config = {
model: 'gpt-4o-mini',
sourceLang: 'en',
targetLangs: ['es', 'fr', 'de'],
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
database: {
type: 'postgres', // 'sqlite' or 'postgres'
connection: process.env.DATABASE_URL!,
tables: [
{
name: 'articles',
columns: ['title', 'body', 'summary'],
idColumn: 'id', // optional, defaults to 'id'
langColumn: 'lang', // optional, use row language instead of global sourceLang
},
{
name: 'products',
columns: ['name', 'description'],
},
],
translationTableSuffix: '_translations', // optional, defaults to '_translations'
concurrency: 10, // optional, number of concurrent translations
},
}
export default configSQLite Example
const config: Config = {
// ... other options
database: {
type: 'sqlite',
connection: './data/content.db',
tables: [
{
name: 'posts',
columns: ['title', 'content'],
},
],
},
}Database Configuration Options (database)
| Option | Type | Description |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------- |
| type | string | Database type: 'sqlite' or 'postgres' |
| connection | string | Connection string (file path for SQLite, URL for PostgreSQL) |
| tables | array | Array of tables to translate (see below) |
| translationTableSuffix | string | Suffix for translation tables (default: '_translations') |
| concurrency | number | Positive integer limiting active database row jobs (default: top-level value or 10); the global LLM limit still applies |
Table Configuration
| Option | Type | Description |
| ------------ | ---------- | ---------------------------------------------------------------- |
| name | string | Table name |
| schema | string | Schema name (PostgreSQL only, default: 'public') |
| columns | string[] | Array of column names to translate |
| idColumn | string | Primary key column (default: 'id') |
| langColumn | string | Optional source-language column for row-level language detection |
How It Works
For each source table, Speranto creates a translation table (e.g., articles_translations) with
the following structure:
| Column | Description |
| --------------------- | -------------------------------------------------------------------- |
| id | Auto-incrementing primary key |
| source_id | Reference to the source row |
| lang | Language code for the stored row, including the base/source language |
| source_lang | Source language used to generate this row |
| row_source_hash | Hash of the current source content for fast skip checks |
| field_source_hashes | JSON map of per-field hashes for partial retranslations |
| <column> | Stored content for each specified column |
| created_at | Timestamp of creation |
| updated_at | Timestamp of last update |
The translation table is now the canonical read model for all languages. Speranto upserts the base/source language row into that table as well as translated rows, so consumers can query a single table regardless of language.
Database change detection is hash-based:
- a row-level hash skips unchanged rows quickly
- per-field hashes allow Speranto to retranslate only changed fields instead of the full row
If langColumn is configured, Speranto uses the row value as the source language for that record;
otherwise it falls back to the global sourceLang.
Cleaning Up Database Translations
Speranto does not delete translation rows during normal translation runs. Use the dedicated cleanup command to remove rows for deleted source records and languages that are no longer the source language or one of the configured target languages:
speranto cleanupThe command displays the number of stale rows per table and asks for confirmation before deleting
anything. Enter y or yes to approve. For non-interactive environments, pass --yes to approve
the displayed cleanup automatically:
speranto cleanup --yesUse --config <path> when the configuration is not in the current directory.
Database Test Commands
The full test script starts PostgreSQL and runs all tests:
pnpm testTo run the database suites individually:
LLM_API_KEY=test pnpm exec vitest run tests/database/sqlite.test.ts
pnpm docker:up
LLM_API_KEY=test pnpm exec vitest run tests/database/postgres.test.tsStop the PostgreSQL test container afterward with:
docker compose -p speranto -f tests/docker-compose.yml downCombining Files and Database
You can translate both files and database content in a single run by including both files and
database in your config:
const config: Config = {
model: 'gpt-4o-mini',
sourceLang: 'en',
targetLangs: ['es', 'fr'],
provider: 'openai',
files: {
sourceDir: './content',
targetDir: './content/[lang]',
},
database: {
type: 'postgres',
connection: process.env.DATABASE_URL!,
tables: [{ name: 'posts', columns: ['title', 'body'] }],
},
}