@lexique/cli
v0.2.0
Published
Official CLI for pulling and pushing translations between developer codebases and Lexique.
Downloads
32
Readme
Lexique CLI
Official CLI for pulling and pushing translation keys and locale files between developer codebases and Lexique.
Status
This package is the first npm-distributable CLI for Lexique. It uses the machine-facing API implemented by the Lexique server:
GET /api/projects/{id}/translations/pullPOST /api/projects/{id}/translations/push
Authentication uses account-based CLI auth for humans and account-owned machine tokens for CI. Project-scoped API tokens are legacy compatibility only.
Installation
During development:
npm test
node src/bin/lexique.js --helpPublished usage:
npx @lexique/cli --helpor, after global/project installation:
lexique pull --helpCommand-specific help:
lexique login --help
lexique config --help
lexique projects --help
lexique tokens --help
lexique pull --help
lexique push --help
lexique check --helpAuthentication
Sign in with your Lexique account:
lexique loginlogin opens Lexique in your browser, starts a localhost callback, and completes a PKCE authorization flow. The CLI never asks for your password and does not reuse web cookies. For another Lexique deployment:
lexique login --base-url http://127.0.0.1:8000Account credentials are stored outside the repository. On macOS, the refresh token is stored in Keychain when available; otherwise the CLI falls back to:
~/.config/lexique/auth.jsonThe fallback file is written with 0600 permissions. Refresh tokens are rotated by the server. lexique logout revokes the saved CLI session and removes the local credential.
For CI and agents, create account-owned machine tokens:
lexique tokens create --name CI --project 1 --scope projects:read --scope translations:read --scope translations:write
export LEXIQUE_TOKEN="lxq_mch_<selector>_<secret>"Machine tokens are scoped to explicit projects and permissions. They are intended for LEXIQUE_TOKEN; they are not written to lexique.config.json.
When a token is supplied through LEXIQUE_TOKEN, --token, or a legacy
repository token, the CLI does not trust a repository-controlled baseUrl. It
uses https://lexique.app or an explicit LEXIQUE_BASE_URL/--base-url.
Self-hosted CI must therefore pin both values:
export LEXIQUE_BASE_URL="https://lexique.example"
export LEXIQUE_TOKEN="lxq_mch_<selector>_<secret>"Legacy project-token login remains temporarily available during migration:
lexique login --project-id 1 --token 'lxq_pjt_<selector>_<secret>'Authentication precedence is intentionally migration-safe:
--tokenorLEXIQUE_TOKEN- legacy
tokenvalues already present inlexique.config.json - saved account sessions from
lexique login - legacy saved project-token logins
Configuration
Every project command needs:
- account auth from
lexique loginor aLEXIQUE_TOKEN - a numeric project ID from
--project,LEXIQUE_PROJECT_ID, orlexique.config.json
baseUrl defaults to https://lexique.app. You only need --base-url for local development, self-hosting, staging, or another deployment. HTTPS is required except for loopback development URLs using localhost, 127.0.0.1, or ::1.
The server does not expose project slug discovery yet, so project IDs must be numeric.
Configuration priority:
- CLI flags
- environment variables
- repo config file
- saved account auth
- legacy saved project-token login config
- default base URL
Environment variables:
export LEXIQUE_BASE_URL="https://lexique.app"
export LEXIQUE_PROJECT_ID="1"
export LEXIQUE_TOKEN="lxq_mch_<selector>_<secret>"Config file names searched from the current directory upward:
lexique.config.json.lexiquerc.json
lexique.config.json stores project and sync configuration only: base URL, project ID, locales, adapter, and paths. Do not put auth secrets in it. Existing legacy token values are still read for migration compatibility before saved account sessions, but every use emits a security warning and lexique config removes them when it rewrites the file.
.lexique/state.json stores the local sync baseline used by changed-only push. Add it to .gitignore; it is per working copy.
Configured translation paths are resolved relative to the directory containing
lexique.config.json. They must remain inside that directory tree and must not
traverse symbolic links. Explicit --file and --output paths remain under the
caller's control. Catalog files are limited to 10 MiB, and API responses are
limited to 20 MiB with a 30-second request timeout.
Version 0.2 uses an adapter-aware version 2 baseline. If a working copy still has
a 0.1 baseline, run lexique pull --all once before the next changed-only push.
Example:
{
"baseUrl": "https://lexique.app",
"projectId": 1,
"defaultLocale": "en",
"supportedLocales": ["en", "fr"],
"translationAdapter": "flat-json",
"translationAdapterConfig": {
"pathPattern": "locales/{locale}.json"
},
"sync": {
"format": "json",
"path": "locales/{locale}.json",
"locales": ["en", "fr"]
}
}Do not commit tokens to repo config files.
Local Sync Baseline
Configured pull and push workflows use .lexique/state.json to avoid uploading every configured file on each push. The baseline is deliberately conservative: it is only advanced when the CLI can trust that local files match what Lexique returned or accepted.
The baseline is updated after:
- a full configured pull, such as
lexique pullorlexique pull --all - a configured push whose server summary confirms that no values, keys, entries, or locales were skipped
The baseline is not updated after:
- filtered pulls using
--locale,--key,--tag,--match, or--only-completed - pushes using
--since, because they compare against Git instead of the local baseline - pushes using an explicit
--match, because only part of the local changes may have been uploaded - pushes where local inputs were skipped, for example with
--skip-invalid - pushes where the server summary is missing or reports skipped values, missing keys, invalid entries, empty values, or unsupported locales
When the baseline is not updated, the CLI prints a message explaining why. This keeps unresolved local changes visible to the next lexique push instead of marking them as synchronized too early.
License
Lexique CLI is proprietary software. You may install and use it to interact with Lexique services. See LICENSE for details.
lexique login
Authenticate the current machine with a Lexique account.
Usage:
lexique login
lexique login --base-url http://127.0.0.1:8000Flags:
--base-url <url>
--name <device-name>Deprecated migration form:
lexique login --project-id 1 --token 'lxq_pjt_<selector>_<secret>'lexique projects
List account projects or save the current repository's project selection.
lexique projects
lexique projects use 1projects use writes only project metadata to lexique.config.json. Run lexique config when you also want adapter/path sync settings.
lexique config
Configure a repository for repeatable pull and push workflows.
Usage:
lexique config --project-id <id> [options]
lexique config printconfig reads the project adapter from GET /api/projects/{id}/meta and writes a matching local sync config when the adapter is supported. Supported adapter-aware config targets are:
flat-jsonlocalized-jsonsymfony-yamli18next-json
XLIFF projects are detected from server metadata but rejected with a clear unsupported-capability error until the Lexique server can import and export XLIFF.
Interactive config uses existing lexique.config.json values as editable defaults when rerun. It asks for:
- translation format and location
Authenticate first with lexique login, or set LEXIQUE_TOKEN to an account-owned machine token for CI.
Non-interactive examples:
lexique config --project-id 1 --translations-format json --path 'locales/{locale}.json'
lexique config --project-id 1 --translations-format yaml --path 'translations/{locale}.yaml'
lexique config --project-id 1 --translations-format localized-json --path translations.json
lexique config --base-url http://127.0.0.1:8000 --project-id 1 --translations-format json --path localesFlags:
--base-url <url>
--project-id <id>
--project <id>
--token <token> Deprecated project token override
--translations-format <json|yaml|localized-json>
--path <path>
--domain <domain>
--namespace <namespace>
--printconfig writes lexique.config.json in the current repository. It does not store auth secrets there.
Use print or --print to display the effective project setup as JSON without changing files:
lexique config printExample output:
{
"baseUrl": "http://127.0.0.1:8000",
"projectId": "11",
"configPath": "/path/to/lexique.config.json",
"defaultLocale": "en",
"supportedLocales": ["en", "fr"],
"translationAdapter": "symfony-yaml",
"translationAdapterConfig": {
"pathPattern": "translations/{domain}.{locale}.yaml",
"defaultDomain": "messages"
},
"sync": {
"format": "yaml",
"path": "translations/messages.{locale}.yaml",
"locales": ["en", "fr"],
"domain": "messages"
}
}Supported path styles:
locales/{locale}.json: explicit per-locale file patterntranslations/{locale}.yaml: explicit per-locale YAML file patterntranslations/{domain}.{locale}.yaml: Symfony YAML, with{domain}resolved during configtranslations: Symfony YAML directory mode, scans{domain}.{locale}.ymland skips locales outsidesync.localeslocales/{locale}/{namespace}.json: i18next JSON, with{namespace}resolved during configlocales: directory mode, writesen.json,fr.json, etc.translations.json: single multi-locale JSON file forlocalized-json
lexique setup remains as a deprecated alias for lexique config.
After config, basic sync commands are short:
lexique pull
lexique pushlexique pull
Download translations from Lexique.
Usage:
lexique pull [--project <id>] [options]lexique pull --project-id 1 --locale fr --key checkout.payAfter lexique login and lexique config, no connection flags are needed:
lexique pullUseful examples:
lexique pull --project-id 1 --locale en --locale fr --match '^checkout\.'
lexique pull --project-id 1 --locale fr --format yaml --output locales/fr.yaml
lexique pull --project-id 1 --locale en --locale fr --output localesFlags:
--base-url <url>
--project-id <id>
--project <id>
--token <token> Machine token or deprecated project token
--locale <locale> Repeatable
--key <key> Repeatable
--tag <tag> Repeatable
--match <regex>
--format <json|yaml>
--output <path>
--only-completedPull behavior:
- after
lexique config, runninglexique pullwith no flags writes configured translation files - full configured pulls update
.lexique/state.json, which becomes the baseline for changed-only push - filtered configured pulls, such as
--locale,--key,--tag,--match, or--only-completed, do not update the baseline - first pull has no local baseline, so it is a full configured pull
--allis accepted as an explicit full refresh flag- repeated
--keyvalues are OR filters - repeated
--tagvalues are OR filters --matchfilters translation keys- response envelopes are JSON from the server
- file output uses
files[*].contentfrom the server response localized-jsonconfig writes a single JSON file from the responseentries
If one file is returned and no --output is passed, the file content is printed to stdout. If multiple files are returned and no --output is passed, the full JSON envelope is printed.
When multiple files are returned with --output, the output path is treated as a directory and server-provided filenames are used. When one file is returned and --output has an extension, it is treated as the exact target file.
lexique push
Upload translations to Lexique.
Usage:
lexique push [options]
lexique push --project <id> --locale <locale> --file <path> [options]
lexique push --project <id> --locale <locale> --key <key> --value <value> [options]Configured mode:
lexique pushFile mode:
lexique push --project-id 1 --locale fr --file locales/fr.jsonSingle-key mode:
lexique push --project-id 1 --locale fr --key checkout.pay --value "Payer"Useful examples:
lexique push
lexique push --all --dry-run
lexique push --since main --dry-run
lexique push --project-id 1 --locale fr --file locales/fr.json --tag billing
lexique push --project-id 1 --locale fr --file locales/fr.yaml --match '^checkout\.'
lexique push --project-id 1 --locale fr --file locales/fr.json --overwrite
lexique push --project-id 1 --locale fr --file locales/fr.json --skip-existing
lexique push --project-id 1 --locale fr --file locales/fr.json --no-create-missing
lexique push --since main
lexique push --all
lexique push --skip-invalid
lexique push --skip-invalid --batch-size 50
lexique push --skip-invalid --batch-max-bytes 4mb
lexique push --project-id 1 --locale fr --key checkout.pay --value "Payer"Flags:
--base-url <url>
--project-id <id>
--project <id>
--token <token> Machine token or deprecated project token
--locale <locale>
--key <key>
--value <value>
--file <path>
--tag <tag> Repeatable
--match <regex>
--overwrite
--skip-existing
--create-missing
--no-create-missing
--all
--since <ref>
--dry-run
--skip-invalid
--batch-size <count>
--batch-max-bytes <bytes>Push behavior:
- after
lexique config, runninglexique pushwith no--fileor--keysends configured translation keys changed since.lexique/state.json - if
.lexique/state.jsondoes not exist, default push fails safely and asks forlexique pull --allorlexique push --all --dry-run --since <ref>keeps the legacy Git comparison mode for explicit one-off comparisons- use
--allto send every configured translation file - successful configured pushes update
.lexique/state.jsononly when the server summary confirms that no values or keys were skipped - pushes using
--sinceor an explicit--matchdo not update.lexique/state.json - file mode sends raw file content to the server using the API
inputspayload - supported input extensions are
.json,.yaml, and.yml - parsing, validation, regex filtering, and merge behavior remain server-owned
- default write behavior is conservative:
overwrite=false,createMissing=true --overwriteand--skip-existingare mutually exclusive--create-missingand--no-create-missingare mutually exclusive--filecannot be combined with--keyor--value--dry-runbuilds and prints the exact payload inputs without calling Lexique- changed-key filtering uses adapter-normalized keys, including nested i18next JSON and nested Symfony YAML
--skip-invalidomits locally invalid JSON/YAML files before uploading and reports them in--dry-run--batch-sizesplits configured file uploads into multiple API requests and aggregates the final summary--batch-max-bytesqueues files until adding another file would exceed the serialized request size, then sends the batch--batch-max-bytesaccepts raw bytes orkb,kib,mb, andmibsuffixes, such as4000000,512kb, or4mb- batched pushes print per-batch progress and the final summary aggregates all batch responses
lexique tokens
Manage account-owned machine tokens for CI and agents.
lexique tokens create --name CI --project 1 --scope projects:read --scope translations:read --scope translations:write
lexique tokens list
lexique tokens revoke 4Usage:
lexique tokens list
lexique tokens create --name <name> --project <id>[,<id>] [options]
lexique tokens revoke <token-id>Flags:
--base-url <url>
--name <name>
--project <ids> Comma-separated numeric project IDs
--scope <scope> Repeatable; defaults to projects:read and translations:read
--expires-in-days <n> Optional; must be 30, 90, 180, or 365Machine tokens are returned once on creation. Use them through LEXIQUE_TOKEN. Invalid project IDs or unsupported expiration windows are rejected before the CLI calls Lexique.
lexique status and lexique diff
lexique status
lexique diffstatus prints the current project, auth source, config path, sync settings, and local baseline status. diff renders the same changed-input view as push --dry-run without uploading.
lexique check --staged
Validate exactly the translation catalogs staged in Git:
lexique check --stagedThe check is offline and read-only. It reads lexique.config.json and catalog
content from the Git index, ignores unstaged edits, and never contacts or updates
Lexique. It reports added keys and updated locale values, requires a non-empty
default-locale value for every new key, and warns when secondary locales are
missing.
Exit codes:
0: no changes, or staged changes are valid1: invalid staged catalog, missing default-locale value/file, or Git conflict2: invalid usage or project configuration
Install @lexique/cli as a project dependency so hooks can run without downloading
packages during a commit. A native Git hook can call:
#!/bin/sh
npx --no-install @lexique/cli check --stagedThe same command can be placed in .husky/pre-commit. For the pre-commit framework:
repos:
- repo: local
hooks:
- id: lexique-staged-translations
name: Lexique staged translations
entry: npx --no-install @lexique/cli check --staged
language: system
pass_filenames: falseRecommended human workflow:
lexique pull
# edit translation catalogs
git add lexique.config.json locales translations
lexique check --staged
git commit
lexique pushPushing remains explicit. Pre-commit checks do not require account credentials;
CI jobs that pull or push should continue using a project-scoped account machine
token through LEXIQUE_TOKEN.
Development
Run checks:
npm test
npm run checkRun the opt-in read-only contract test against a real Lexique deployment and a disposable project:
LEXIQUE_CONTRACT_BASE_URL=https://lexique.example \
LEXIQUE_CONTRACT_TOKEN="$LEXIQUE_TOKEN" \
LEXIQUE_CONTRACT_PROJECT_ID=42 \
npm run test:contractThe live push contract remains skipped unless LEXIQUE_CONTRACT_ALLOW_WRITE=true
and LEXIQUE_CONTRACT_PUSH_LOCALE, LEXIQUE_CONTRACT_PUSH_KEY, and
LEXIQUE_CONTRACT_PUSH_VALUE are all explicitly provided. Only enable it for a
disposable test project.
Create a local npm package tarball:
npm packDistribution Plan
Primary target:
- npm package:
@lexique/cli - executable:
lexique - one-off usage:
npx @lexique/cli ...
Release
Releases are published by GitHub Actions after changes land on main.
Required npm setup:
@lexique/climust exist on npm. Publish the first version manually with 2FA.- npm Trusted Publishing must trust GitHub Actions for
Lyro1/lexique-cli, workflow filenpm-release.yml, and environmentnpm. - Create a protected GitHub environment named
npmand require maintainer approval for deployments. - In npm package settings, require 2FA and disallow traditional token publishing after Trusted Publishing is verified.
Pull requests targeting main run:
npm ci --ignore-scripts
npm run release:checkPushes to main run the same checks, then publish package.json's version to npm if that exact version is not already published. If the version already exists, the publish job exits successfully without publishing again.
Prepare a release by bumping the package version in a pull request:
npm version patchUse minor, major, or an explicit version when appropriate. After the pull request is merged into main, GitHub Actions publishes the new version through npm Trusted Publishing.
Follow-ups:
- Homebrew formula or tap
- shell completions
- server-side XLIFF import/export and sync support
