Docs
eloqnt/studio is an AI-based toolchain that helps you ship high-quality translations. Its primary entry point is the command line interface, which is configured through local project files.
Project files
All relevant configuration is kept in a .eloqnt folder at your project root:
.eloqnt/config.tsstyleguide.mdstyleguide.es.md
.eloqnt/config.{ts,mts,js,mjs}
Keeps your configuration.
.eloqnt/styleguide.md
(optional)A global styleguide with rules that apply to every locale (plain markdown).
.eloqnt/styleguide.<locale>.md
(optional)A per-locale styleguide (e.g. styleguide.es.md) that's layered on top of the global one and takes precedence for that locale.
Configuration
defineConfig describes your project: where source code and messages live, and optionally which model to translate with.
.eloqnt/config.ts
import {defineConfig} from '@eloqnt/cli';export default defineConfig({srcPath: './src',messages: {path: './messages',locales: 'infer',sourceLocale: 'en',format: 'json'}});
srcPath
string | Array<string> (optional)
Relative path(s) to the source code that uses the messages (optional, next-intl only).
Enables source code analysis that feeds AST-level lint rules (orphan-message and undefined-key), as well as contextually enriched translations.
In a monorepo, pass an array of relative source roots, including sibling or installed packages (see also the next-intl docs).
messages
object
pathstring | Array<string>
Relative path(s) to the directories containing messages. To support custom paths for messages for frameworks other than next-intl, you can use {locale} and {namespace} placeholders.
locales'infer' | Array<string>
Locales to translate between, either an explicit array or 'infer' to detect them.
sourceLocalestring
The primary locale that serves as the source for translations.
formatMessageFormat
How catalogs are stored: either the built-in 'po' (recommended), 'json' or a custom format.
model
LanguageModel (optional)
If you're on the "Bring your own model" plan, you can configure a custom provider and model that the AI SDK supports. By doing this, all inference will run through the provided model and no messages or source code will ever touch our backend.
Note that you may need to set environment parameters like ANTHROPIC_API_KEY.
.eloqnt/config.ts
import {anthropic} from '@ai-sdk/anthropic';import {defineConfig} from '@eloqnt/cli';export default defineConfig({// Pick any model supported by the AI SDKmodel: anthropic('claude-opus-4-8'),// ...});
Using a coding agent subscription (Claude Code, Codex, …)
If you already subscribe to a coding agent, 3rd-party providers let you use it as your model (examples: Claude Code, Codex). No separate API key needed.
.eloqnt/config.ts
import {claudeCode} from 'ai-sdk-provider-claude-code';import {defineConfig} from '@eloqnt/cli';export default defineConfig({model: claudeCode('opus'),// ...});
Note: These providers are community-maintained, and whether your subscription allows this kind of use is up to the vendor. That can change at any time, so check that it's in line with the plan you're using.
Styleguides
A styleguide is free-form markdown that steers aspects like tone, terminology, and consistency. You write what is specific to your project, and eloqnt/engine layers on translation best practices automatically.
styleguide.md
Applies to every locale and contains overall guidance.
.eloqnt/styleguide.md
# Styleguide## AudienceA few sentences about who your users are and how your site/app serves them.## Voice and tone- Friendly and concise.- Address the user as "you", and prefer active voice.## Do not translate- eloqnt- API key
Styleguides are self-contained. eloqnt/engine doesn't follow local or external links inside them, so any guidance the engine needs must live in the file itself.
If you want to share styleguides across packages and apps—for example in a monorepo—you can symlink them where relevant.
styleguide.<locale>.md
Layers on top of the global styleguide and takes precedence wherever the two conflict.
.eloqnt/styleguide.es.md
# `es` styleguide## Tone- Use informal "tú", never formal "usted".- Use straight quotation marks (`"…"`), not Spanish guillemets.## Glossary- "dashboard": "panel"
Command-line interface
Install @eloqnt/cli and run commands from your project root.
eloqnt lint
Statically analyze your source code and messages for common problems.
Errors fail the command with a non-zero exit code, while warnings don’t. Pass --strict if you want to bail on warnings as well.
| missing-translation | A message in the source locale has no translation in a target locale. |
| superfluous-key | A target locale defines a key that the source locale doesn't. |
| inconsistent-args | A translation's ICU arguments don't match the source message. |
| duplicate-id | A message id is defined in more than one messages folder (when messages.path lists several). |
| orphan-message | A catalog message is never referenced in your source code (next-intl only). |
| undefined-key | Your source code references a key that no catalog defines (next-intl only). |
eloqnt review
Requires a planReview source strings for errors and fix them in place (defaults to strings with missing translations). When using srcPath, changes will sync to your source code as well.
By default, source strings are only reviewed for objective language errors (spelling and grammar), as well as glossary consistency based on your global styleguide. Intentional wording is protected from unwanted corrections.
If you want to enforce additional rules like typographic preferences, you can mention them in styleguide.<sourceLocale>.md. It's recommended to restrict yourself to mechanical rules in this file and avoid tonality instructions that could cause the review command to rewrite source strings unexpectedly.
| --id | string | (Re-)review these message ids (comma-separated) |
eloqnt translate
Requires a planTranslate strings to target locales (defaults to missing ones).
| --id | string | (Re-)translate these message ids (comma-separated) |
| --locale | string | Restrict to these target locales (comma-separated) |
eloqnt login
Authenticates the CLI by storing an auth token locally.
Your login stays active as long as you keep using the CLI. After 14 days without activity, you will be signed out. Run eloqnt login again to reconnect.
For usage in CI, you can set up an API key and provide it as ELOQNT_TOKEN in your environment to authenticate without the interactive login.
eloqnt logout
Logs out of eloqnt studio by removing a local auth token.
eloqnt whoami
Shows the logged-in user account.
eloqnt crowdin
Coming soonSync your translations with Crowdin.
Global flags
Accepted by every command.
| --config | string | Path to the eloqnt config file (defaults to .eloqnt/config.{ts,mts,js,mjs}) |
| --json | boolean | Output the result as JSON |
Guides
Walkthroughs for advanced setups.
GitHub Actions
By authenticating the CLI with an API key, you can automatically invoke it as part of your continuous integration workflow (e.g. on GitHub Actions).
Here's an example workflow that:
- Checks for lint issues on every push
- Translates any missing strings once you switch from "Draft" to "Ready for review"
.github/workflows/i18n.yml
name: i18non:pull_request:types: [opened, synchronize, reopened, ready_for_review]jobs:i18n:runs-on: ubuntu-latestpermissions:contents: writesteps:- uses: actions/checkout@v4with:ref: ${{ github.event.pull_request.head.ref }}- run: npm ci# Issues should be fixed also in draft mode- run: npx eloqnt lint# Once ready for review, translate missing strings- if: ${{ !github.event.pull_request.draft }}env:ELOQNT_TOKEN: ${{ secrets.ELOQNT_TOKEN }}run: |npx eloqnt reviewnpx eloqnt translategit config user.name "$GITHUB_ACTOR"git config user.email "$GITHUB_ACTOR_ID+$GITHUB_ACTOR@users.noreply.github.com"git add -Agit diff --quiet --cached || (git commit -m "Translate missing strings" && git push)
Note that commits pushed with the default GITHUB_TOKEN don't run workflows. To have workflows run automatically on pushed commits, create a fine-grained personal access token with read and write access, and pass it to the checkout step:
.github/workflows/i18n.yml
# ...- uses: actions/checkout@v4with:ref: ${{ github.event.pull_request.head.ref }}token: ${{ secrets.GH_TOKEN }}# ...
Custom formats
Beyond the built-in json and po formats, you can configure custom formats. These can handle special cases, like for example storing message descriptions in JSON files:
messages/en.json
{"HomePage.title": {"message": "Welcome, {name}!","description": ["Greeting for the user when the app starts."]}}
To configure a custom format, you need to specify a codec along with an extension.
The codec can be created via defineCodec at an arbitrary place in your project:
.eloqnt/StructuredJSONCodec.ts
import {defineCodec} from '@eloqnt/cli';type Entry = {message: string; description?: Array<string>};export default defineCodec(() => ({decode(content) {const data = JSON.parse(content) as Record<string, Entry>;return Object.entries(data).map(([id, entry]) => ({id,message: entry.message,description: entry.description ?? [],references: []}));},encode(messages) {const data: Record<string, Entry> = {};for (const message of messages) {data[message.id] = {message: message.message,description: message.description};}return JSON.stringify(data, null, 2);}}));
Then, reference it in your configuration along with an extension:
.eloqnt/config.ts
import {defineConfig} from '@eloqnt/cli';export default defineConfig({messages: {format: {codec: './.eloqnt/StructuredJSONCodec.ts',extension: '.json'},// ...}});
Custom paths for messages
By default, messages.path is a directory that holds one file per locale.
For example, path: './messages' corresponds to:
./messages/en.json./messages/es.json- etc.
To support custom folder structures (typically for frameworks other than next-intl), you can use the following placeholders:
{locale}: Resolves to e.g.en,es, etc.{namespace}: Optionally split messages per namespace (the first part of a message id)
For example:
.eloqnt/config.ts
import {defineConfig} from '@eloqnt/cli';export default defineConfig({messages: {path: './locales/{locale}/{namespace}',locales: 'infer',sourceLocale: 'en',format: 'json'}});
Will match:
./messages/en/auth.json./messages/es/auth.json- etc.