{"id":50301950,"url":"https://github.com/mrkhachaturov/otranslator-cli","last_synced_at":"2026-05-28T13:09:01.747Z","repository":{"id":355485011,"uuid":"1228254583","full_name":"mrkhachaturov/otranslator-cli","owner":"mrkhachaturov","description":"Unofficial TypeScript SDK and otcli CLI for the OTranslator AI translation API — 100+ languages, 50+ formats, all 15 v1 endpoints verified live.","archived":false,"fork":false,"pushed_at":"2026-05-03T21:27:06.000Z","size":116,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-03T22:21:36.893Z","etag":null,"topics":["ai-translation","api-client","cli","document-translation","openapi","otranslator","sdk","translation","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/otranslator-cli","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mrkhachaturov.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-03T19:49:13.000Z","updated_at":"2026-05-03T21:27:09.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mrkhachaturov/otranslator-cli","commit_stats":null,"previous_names":["mrkhachaturov/otranslator-cli"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/mrkhachaturov/otranslator-cli","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkhachaturov%2Fotranslator-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkhachaturov%2Fotranslator-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkhachaturov%2Fotranslator-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkhachaturov%2Fotranslator-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mrkhachaturov","download_url":"https://codeload.github.com/mrkhachaturov/otranslator-cli/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mrkhachaturov%2Fotranslator-cli/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33609472,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-translation","api-client","cli","document-translation","openapi","otranslator","sdk","translation","typescript"],"created_at":"2026-05-28T13:08:58.212Z","updated_at":"2026-05-28T13:09:01.736Z","avatar_url":"https://github.com/mrkhachaturov.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# otranslator-cli\n\n[![npm version](https://img.shields.io/npm/v/otranslator-cli?logo=npm\u0026label=npm)](https://www.npmjs.com/package/otranslator-cli)\n[![Downloads](https://img.shields.io/npm/dm/otranslator-cli?logo=npm\u0026color=blue)](https://www.npmjs.com/package/otranslator-cli)\n[![CI](https://github.com/mrkhachaturov/otranslator-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/mrkhachaturov/otranslator-cli/actions/workflows/ci.yml)\n[![Node](https://img.shields.io/node/v/otranslator-cli?logo=nodedotjs)](https://nodejs.org)\n[![License: MIT](https://img.shields.io/npm/l/otranslator-cli?color=success)](./LICENSE)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript\u0026logoColor=white)](./tsconfig.json)\n\nUnofficial Node.js SDK and CLI for the [OTranslator](https://otranslator.com) API. Document, subtitle, audio, and plain text translation across 100+ languages with the original layout preserved.\n\n\u003e Not affiliated with OTranslator. Reverse-engineered from the public developer docs at https://otranslator.com/en/developer and verified against the live API. Every endpoint, request shape, and response shape documented here was exercised by the e2e suite under `test/e2e/` on 2026-05-03.\n\n## First run\n\n```bash\nnpm install -g otranslator-cli   # adds `otcli` to your $PATH\notcli login                      # interactive prompt, validates against /v1/me\notcli whoami                     # → { \"source\": \"config\", \"balance\": \u003ccredits\u003e }\notcli translate \"Hello, world.\" --from English --to Spanish\n```\n\nPrefer not to install globally? `npx otranslator-cli \u003ccommand\u003e` works the same.\n\n## Install\n\n```bash\nnpm install otranslator-cli      # as a dependency in your project\n# or\nnpm install -g otranslator-cli   # as a system-wide CLI\n```\n\nThe npm package is `otranslator-cli`. It installs a single CLI binary on your `$PATH` called `otcli`. All examples in this README use `otcli`.\n\nNode 20+ required. The SDK uses built-in `fetch`, `FormData`, and `File`, so it also works in browsers without polyfills.\n\n## How translation works\n\nOTranslator follows a preview-then-pay model. You upload a document, the API generates a free preview of roughly the first 2,000 words, and only when you're happy with the preview do you pay credits to translate the rest.\n\nThe full flow:\n\n1. `createTask` uploads the file. Set `preview: true` for a sample, or `preview: false` to translate everything immediately (deducts credits up front).\n2. `queryTask` returns status, progress, and — once `Completed` — `translatedFileUrl` plus a `price` field showing what a full translation would cost.\n3. `restartTask` with `payWithCredits: true` and a `model` from one of the paid tiers converts the preview into a full translation.\n\nFree preview credits replenish whenever you complete a paid full translation, so the cost model rewards finishing what you start.\n\n### Model tiers\n\nThe product UI groups the eleven models from `/v1/models` into three tiers. Pricing applies to **document full translation** (the `restartTask` step). Synchronous `translateTexts` costs 2 credits regardless of tier — at least on the short strings we measured.\n\n- **Basic**: `gpt-5-mini`, `claude-4.5-haiku`, `gemini-3.1-flash`, `deepseek-3.2`\n- **Advanced**: `gpt-5.4`, `claude-4.6-sonnet`, `gemini-3.1-pro`, `deepseek-3.2-thinking`\n- **Inference / Thinking**: `gpt-5.4-thinking`, `claude-4.6-sonnet-thinking`, `gemini-3.1-thinking`\n\nThe API takes a raw model id — the SDK doesn't gate which tier you can use. Run `node --env-file=.env --import tsx scripts/verify-models.ts` to probe one model per tier and see live credit costs.\n\n## Authentication\n\nThree ways to provide your key, resolved in this order:\n\n1. `--api-key \u003ckey\u003e` flag on any command\n2. `OTRANSLATOR_API_KEY` environment variable\n3. The stored config file written by `otcli login` at `~/.config/otranslator-cli/config.json` (mode 0600)\n\nThe interactive flow mirrors `gh`, `stripe`, `vercel`, and friends:\n\n```bash\notcli login         # prompts for the key, hides echo, validates against /v1/me, persists on success\notcli whoami        # shows which source the active key comes from + current balance\notcli logout        # deletes the stored config file\n```\n\n`whoami` is also handy when handing the CLI to another agent — it confirms the key is wired up before any real work starts.\n\n## CLI quick start\n\n```bash\notcli login                  # one-time setup; or `export OTRANSLATOR_API_KEY=sk-…`\n\n# Discover the surface\notcli filetypes\notcli languages\notcli models\notcli me                     # → { \"balance\": 421 }\n\n# Synchronous text translation\notcli translate \"Hello, world.\" --from English --to Spanish\n\n# Document workflow — preview → wait → download\notcli create -f contract.pdf --from English --to French --preview\notcli wait \u003ctaskId\u003e                              # blocks until Completed\notcli download \u003ctaskId\u003e -o translated.pdf        # writes the file to disk\n\n# Or in one chained call\nTASK=$(otcli create -f contract.pdf --from English --to French --preview | jq -r .taskId)\notcli download \"$TASK\" --wait -o translated.pdf\n\n# Pay credits to convert a preview into a full document translation\notcli start \u003ctaskId\u003e --pay-with-credits --model gpt-5.4\n```\n\nEvery command prints JSON to stdout and exits non-zero on failure with the API's error body on stderr. Pipe into `jq` to filter, or chain commands together.\n\n## SDK quick start\n\n```ts\nimport { OTranslatorClient } from 'otranslator-cli';\nimport { readFile, writeFile } from 'node:fs/promises';\n\nconst client = new OTranslatorClient({\n  apiKey: process.env.OTRANSLATOR_API_KEY!,\n});\n\n// Upload, get a preview, wait for completion\nconst buffer = await readFile('contract.pdf');\nconst file = new File([buffer], 'contract.pdf');\n\nconst { taskId } = await client.createTask({\n  file,\n  fromLang: 'English',\n  toLang: 'French',\n  preview: true,\n});\n\n// Wait for the preview to finish, then download it\nconst task = await client.waitForTask(taskId);\nconst { blob, filename } = await client.downloadTranslated(taskId);\nawait writeFile(filename, Buffer.from(await blob.arrayBuffer()));\nconsole.log(`saved ${filename} — costs ${task.price} credits to convert to a full translation`);\n\n// Convert preview → full translation\nif (task.price !== undefined) {\n  await client.restartTask({ taskId, payWithCredits: true, model: 'gpt-5.4' });\n}\n```\n\n## What's covered\n\n15 raw endpoints (one SDK method each, all exercised against the live API) plus two composed helpers — `waitForTask` and `downloadTranslated` — that turn the preview-then-pay flow into one call each.\n\n| SDK method       | HTTP                                  | Notes                                                                                        |\n| ---------------- | ------------------------------------- | -------------------------------------------------------------------------------------------- |\n| `createTask`     | `POST /v1/translation/create`         | `multipart/form-data`. Returns `{ taskId }`.                                                 |\n| `queryTask`      | `POST /v1/translation/query`          | Returns the full task object — status, progress, file URLs, credit costs, model used.        |\n| `deleteTask`     | `POST /v1/translation/delete`         | Returns `{ success: true }`.                                                                 |\n| `restartTask`    | `POST /v1/translation/start`          | `{ taskId, payWithCredits?, model? }` → `{ success: true }`.                                 |\n| `queryTexts`     | `POST /v1/translation/queryTexts`     | Returns `{ texts: { [src]: tgt }, revisedTexts: { [src]: tgt } }`. Source string is the key. |\n| `updateTexts`    | `POST /v1/translation/updateTexts`    | Submit a `{ [sourceSegment]: revisedTranslation }` map. Returns `{ success: true }`.         |\n| `translateTexts` | `POST /v1/translation/translateTexts` | Synchronous. Returns `{ translatedTexts, price, usedCredits }`.                              |\n| `createGlossary` | `POST /v1/glossary/create`            | Returns `{ glossaryId }`.                                                                    |\n| `queryGlossary`  | `POST /v1/glossary/query`             | SDK parses `keys` and `translated` from JSON-encoded strings to native types.                |\n| `updateGlossary` | `POST /v1/glossary/update`            | Same parsing as `queryGlossary`.                                                             |\n| `deleteGlossary` | `POST /v1/glossary/delete`            | Returns `{ success: true }`.                                                                 |\n| `filetypes`      | `POST /v1/filetypes`                  | 60+ formats: pdf, docx, epub, srt, mp3, cbz, etc.                                            |\n| `languages`      | `POST /v1/languages`                  | 80+ languages including `Any Language` as a wildcard.                                        |\n| `models`         | `POST /v1/models`                     | Returns 11 model ids (see Model tiers above).                                                |\n| `me`             | `POST /v1/me`                         | Returns `{ balance: number }` — credit balance and nothing else.                             |\n\nComposed helpers (no extra endpoints — built on top of the rows above):\n\n| SDK method           | What it does                                                                                                             |\n| -------------------- | ------------------------------------------------------------------------------------------------------------------------ |\n| `waitForTask`        | Polls `queryTask` until `Completed` / `Terminated` / `Cancelled`. Configurable `intervalMs` and `maxMs`.                 |\n| `downloadTranslated` | Queries the task, fetches the pre-signed GCS URL, returns `{ blob, filename, contentType, task }`. Supports `bilingual`. |\n\nFor full request and response shapes, look at the generated TypeScript types in `dist/index.d.ts` or open [`openapi.json`](./openapi.json).\n\n## Behavioural notes from real responses\n\nA few things worth flagging because the official docs don't mention them:\n\n- **Auth.** The `Authorization` header takes the raw secret key. No `Bearer` prefix. This is the single most common stumbling block when porting from another translation API.\n- **Preview is normally free.** Each account ships with a pool of free preview credits, and the pool refills every time you complete a paid full translation. The 2-credit cost the docs mention only kicks in after the pool runs out.\n- **Glossary `keys` and `translated` are doubly encoded on the wire.** Both fields are sent as JSON-encoded strings inside the JSON request body, and they come back the same way. The SDK encodes on the way out and parses on the way in, so your code works with native arrays and objects.\n- **`TranslationTask` returns more fields than documented.** `fileTitle`, `fileUrl`, `wordNums`, `forceOCR`, and an `errorMsg` that may be `null` are all present. The SDK types include them.\n- **Default model.** When `createTask` omits `model`, the server picks `gpt-4.1-mini` for previews. That id is not in the `/v1/models` list, so don't hard-code your model lookups against it.\n- **Status transitions.** The state machine we observed: `Waiting → Processing → Completed`. `Terminated` and `Cancelled` exist in the type but only show up on errors or explicit deletes.\n\n## Configuration\n\n| Env var                  | CLI flag     | Default                       |\n| ------------------------ | ------------ | ----------------------------- |\n| `OTRANSLATOR_API_KEY`    | `--api-key`  | (required)                    |\n| `OTRANSLATOR_BASE_URL`   | `--base-url` | `https://otranslator.com/api` |\n| `OTRANSLATOR_TIMEOUT_MS` | `--timeout`  | `60000`                       |\n\n## Tests\n\nTwo suites.\n\n`npm test` runs unit tests with a mocked fetch. Fast, no network, no key needed.\n\n`npm run test:e2e` hits the real API. It loads `OTRANSLATOR_API_KEY` from `.env` via dotenv and auto-skips when the key is missing. The default e2e suite is free to run. It covers `languages`, `filetypes`, `models`, `me`, and the full glossary create-query-update-delete lifecycle.\n\nThe paid suite is opt-in:\n\n```bash\nnpm run test:e2e:paid\n```\n\nThat run exercises `translateTexts`, `createTask` in preview mode against `test/fixtures/sample.md`, `queryTask` polling, `queryTexts`, `updateTexts`, `restartTask` with `payWithCredits: true`, and `deleteTask` cleanup. With the bundled 351-byte fixture it costs around 2 credits per run because `price` for the doc itself is `0`. To test against a larger document, point at it:\n\n```bash\nOTRANSLATOR_E2E_FIXTURE=\"/path/to/your/document.md\" npm run test:e2e:paid\n```\n\nThe e2e tests also log every response we found undocumented (`/me`, `/models`, `/glossary/query`, `/translation/queryTexts`, etc.) so future API changes show up as test diffs.\n\n## Errors\n\nThe SDK throws `OTranslatorError` for everything that isn't a 2xx with a JSON body. Inspect `code` to branch:\n\n```ts\nimport { OTranslatorError } from 'otranslator-cli/errors';\n\ntry {\n  await client.me();\n} catch (err) {\n  if (err instanceof OTranslatorError \u0026\u0026 err.code === 'HTTP_ERROR' \u0026\u0026 err.status === 401) {\n    // bad API key — re-prompt the user\n  }\n  throw err;\n}\n```\n\n`code` values: `MISSING_API_KEY`, `INVALID_INPUT`, `NETWORK_ERROR`, `TIMEOUT`, `HTTP_ERROR`, `INVALID_RESPONSE`.\n\n## Development\n\n```bash\nnpm install\nnpm run typecheck\nnpm test\nnpm run build      # tsup → dist/ (ESM + CJS + .d.ts)\n```\n\nRun the CLI against the source directly without rebuilding:\n\n```bash\nnpm run cli -- task \u003ctaskId\u003e\n```\n\n## Releasing\n\nThe first publish is manual and **does not include provenance** — OIDC tokens can only be minted inside GitHub Actions, not on a laptop. Every subsequent release is published from CI on tag push, with provenance signed against the source commit.\n\n```bash\n# One-time first publish\nnpm login\nnpm publish --access public\n\n# Then on https://www.npmjs.com/package/otranslator-cli →\n#   Settings → Trusted Publishers → Add Publisher\n#   - Repository: mrkhachaturov/otranslator-cli\n#   - Workflow:   publish.yml\n#   - Environment: (leave blank)\n\n# Subsequent releases\n# 1. Bump version in package.json\n# 2. Add a new [X.Y.Z] - \u003cdate\u003e heading in CHANGELOG.md (move items from\n#    [Unreleased]). Keep a Changelog format.\n# 3. Commit, tag, push\ngit commit -am 'release: v0.1.1'\ngit tag v0.1.1\ngit push --follow-tags\n# Actions publishes to npm with provenance and creates a GitHub Release whose\n# body is the matching CHANGELOG section.\n```\n\n### Verifying provenance\n\nOnce a release is published from CI, anyone can verify the build trail:\n\n```bash\n# Visual: https://www.npmjs.com/package/otranslator-cli shows a Provenance badge\n# Programmatic:\nnpm view otranslator-cli --json | jq '.dist.attestations'\nnpm install otranslator-cli \u0026\u0026 npm audit signatures\n```\n\nCryptographic verification independent of npm uses Sigstore's transparency log — see https://search.sigstore.dev for the public Rekor entries.\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrkhachaturov%2Fotranslator-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmrkhachaturov%2Fotranslator-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmrkhachaturov%2Fotranslator-cli/lists"}