@lionrapid/cli
v0.1.2
Published
LionRapid CLI — sync translation files (iOS, Android, Flutter, JSON, YAML, PO) with the LionRapid backend
Maintainers
Readme
@lionrapid/cli
The LionRapid command-line tool. Syncs translation files in many formats
(iOS .strings/.xcstrings, Android strings.xml, Flutter .arb, generic
JSON, YAML, PO) with the LionRapid backend, and generates type-safe TypeScript
keys from your translations.
This is the non-web integration wedge: it reaches platforms the web
surfaces (WordPress, edge proxy, JS snippet, React SDK) can't — mobile apps,
backend services, static-site generators, and CI/CD — by speaking the existing
/api/integrations/* REST surface instead of the DOM.
Status
Phase 1 (file-sync dev-loop) is implemented. Command surface:
| Command | Status | What it does |
| ----------------- | ------ | ------------------------------------------------------------------- |
| init | ✅ | Scaffold a lionrapid.yml (detects Flutter/iOS/Android/Rails/JS) |
| push | ✅ | Upload source strings to the backend (server handles MT) |
| pull [--types] | ✅ | Download translations into target files; optionally regen types |
| validate [--ci] | ✅ | Check local files vs the backend for missing/orphan/empty keys |
| types generate | ✅ | Generate a typed .d.ts from your source files (offline) |
| types check | ✅ | Report ICU issues in your source translations |
| scan [--json] | ✅ | Find user-facing strings in source → inventory keyed by auto_key |
| scan --push | ✅ | Seed the safe wrap candidates to the backend under their auto_key |
| snapshot | ✅ | Write offline-resilience bundles (raw ContentUnit) per locale/ns |
| doctor | ✅ | Preflight: config, credentials, server connectivity, source files |
Formats: JSON, YAML, PO (incl. plurals + msgctxt), Flutter .arb, iOS
.strings + .xcstrings, Android strings.xml, XLIFF (1.2/2.0, CAT-tool
interchange).
Multiple namespaces: give any files entry its own namespace to map
different file trees to different backend namespaces (defaults to the top-level
namespace). Each is pushed/pulled/validated independently, so the same key name
in two namespaces never collides:
namespace: app
files:
- { format: json, source: common/en.json, target: common/{locale}.json, namespace: common }
- { format: json, source: app/en.json, target: app/{locale}.json } # → namespace: appReliability & automation
Built for CI and AI agents — every networked command is resilient and scriptable.
--json(all commands): emit one result envelope{ ok, command, … }on stdout; human prose goes to stderr. On failure, a structured error envelope{ ok: false, error: { type, message, status? }, exitCode }is written to stdout. Parse stdout, read stderr.--dry-run(push,pull): report exactly what would happen — counts, keys, target files — without any network mutation (push) or file write (pull).pull --dry-runstill does the read-only fetch so the counts are real.- Exit codes (stable, gate CI on them):
0ok ·1unexpected ·2validation/config ·3auth ·4network/5xx ·5partial. - Automatic retries: transient failures (network error,
5xx,429) are retried with exponential backoff + jitter; a429honorsRetry-After. Tune withLIONRAPID_RETRIES(default3;0disables). Writes are safe to retry — the integration endpoints upsert by composite key. - Concurrent
pull: namespace fetches fan out with bounded concurrency (LIONRAPID_CONCURRENCY, default6) and show interactive progress on a TTY. Output stays deterministic — only the network fan-out is parallelized. doctor: one command to verify config validity, credential resolution, backend reachability/auth, and that declared source files exist — each checked independently so you get an actionable list, not a stack trace.
Offline resilience (snapshot)
lionrapid snapshot writes the raw ContentUnit bundle ({ meta, translations })
per locale/namespace, byte-shaped like the API response — bypassing the format
adapters (which flatten to display strings and drop the styles map). Bundle
these with your app and load them via the runtime so translations keep working
with the server down:
import bundle from './snapshots/es.app.json' assert { type: 'json' };
import {
LionRapidBuilder,
MemoryRepository,
StaticRepository,
} from '@lionrapid/core';
const core = await LionRapidBuilder.init({
defaultLocale: 'es',
namespace: 'app',
})
.use(new MemoryRepository({ enabled: true }))
.useStaticBundle({ es: { app: bundle.translations } }) // offline layer (memory → static → network)
.build();
core.t('welcome'); // works offline, styles intactNode apps can read a snapshot directory at runtime via
new FileRepository('./snapshots') from @lionrapid/storage/node.
No-library localization (scan)
For a codebase with no i18n library (hardcoded strings, no t()), scan
finds user-facing strings via the TypeScript AST over a finite allowlist of UI
sinks (JSX text + placeholder/alt/aria-label/…), classifies them, and keys
each by content-hash auto_key — the same key the web edge proxy / DOM
auto-detect produces, so a string already translated on your website is reused
in your app.
action: wrap— static text/attributes (deterministic, safe).action: review— interpolation / inline markup / dynamic values (left for a human or AI agent to author correctly).
scan --json emits a machine-readable inventory on stdout (human prose on
stderr) so an AI coding agent can drive the judgment-heavy wrapping while the CLI
owns the correctness-critical parts (the auto_key hash, file writes, sync).
scan --push seeds the safe candidates into the translation memory.
Wrapping (done by you / your agent)
The CLI intentionally does not rewrite your source. It hands you the
inventory — each candidate carries its autoKey, and the result carries the
namespace — and you (or your AI coding agent, e.g. Claude Code / Cursor) wrap
using the SDK, which also lets the agent handle the review cases (interpolation,
plurals) a deterministic codemod can't:
// inventory: { namespace: "app", autoKey: "auto_2d0f6b8300be", text: "Add to cart", sink: "jsx-text" }
<Text>Add to cart</Text>
// →
<Text><Trans namespace="app" i18nKey="auto_2d0f6b8300be">Add to cart</Trans></Text>
// inventory: { autoKey: "auto_13348442cc6a", text: "Search", sink: "jsx-attr:placeholder" }
placeholder="Search" → placeholder={t('app:auto_13348442cc6a', 'Search')}Because the key is the same content-hash the web produces, any of these strings already translated on your website are translated in the app for free.
Usage
lionrapid init --project <id> --targets es,fr
lionrapid push
lionrapid pull --types
lionrapid validate --ci
# no-library localization
lionrapid scan --json # inventory of discovered strings (auto_key-keyed)
lionrapid scan --push # seed the safe candidates into the TMThe API key is read from LIONRAPID_API_KEY (or ~/.lionrapid/credentials),
never from the committed lionrapid.yml.
Development
npm run build -w packages/cli
npm test -w packages/cli