Command-line interface for the Localess headless CMS. Handles authentication, translation sync, TypeScript type generation, and schema pull/diff/push.
Requires: Node.js >= 24.0.0.
npm install -g @localess/cli
# or without installing
npx @localess/cli [command]localess login \
--origin https://my-localess.web.app \
--space YOUR_SPACE_ID \
--token YOUR_API_TOKEN| Flag | Description |
|---|---|
-o, --origin <url> |
Localess instance URL |
-s, --space <id> |
Space ID (from Space settings) |
-t, --token <token> |
API token (masked input) |
-v, --verbose |
Print verbose debug output |
What it does:
- Checks for existing credentials (env vars or file) — if found, prints
Already logged in.and exits (runlogoutfirst to switch) - Prompts interactively for any missing options
- Validates credentials against the API (
GET /spaces/{spaceId}); exits1without saving on failure - Saves credentials to
.localess/credentials.json(mode0o600) - Appends
.localessto.gitignoreautomatically (creates the file if absent)
localess logoutClears .localess/credentials.json (overwrites it with {}). If authenticated via environment variables, instructs you to unset them manually.
Credentials are resolved in this priority order:
- Environment variables (highest priority — recommended for CI/CD; all three must be set)
.localess/credentials.jsonin the current working directory (file-based — for local development)
Every command that talks to the API accepts -v, --verbose to print client debug output (request URLs, statuses). Commands that need credentials exit 1 with Not logged in when neither source is available. After any command, the CLI checks the npm registry (3-second timeout, failures ignored) and prints an "Update available" box if a newer @localess/cli exists (pre-release installs also check the dev tag; a newer stable release wins).
The CLI and the platform are released in lockstep: a 4.x.x CLI is designed against a 4.x.x platform. Before running a command, the CLI reads the platform's version from <origin>/assets/version.json and enforces two rules:
- The majors must match. A
4.x.xCLI refuses a3.x.xor5.x.xplatform. - The platform must meet the command's minimum, from the compatibility matrix below.
A confirmed incompatibility blocks the command — every command, not only the ones that write — and exits 1 before any API call, because an unmet requirement means the command cannot work correctly.
| Command | Minimum platform |
|---|---|
login |
— (no platform call) |
logout |
— (no platform call) |
schema validate |
— (reads local files only) |
schema pull |
4.0.0 |
schema diff |
4.0.0 |
schema push |
4.0.0 |
translation pull |
4.0.0 |
translation push |
4.0.0 |
translation diff |
4.0.0 |
type generate |
4.0.0 |
Entries are minimums, not ranges. A future platform that breaks a command can't be predicted from the CLI, so that direction is covered by rule 1 instead. Every command must appear in the matrix — a test walks the command tree and fails on a missing or stale entry, so adding a command forces a deliberate choice.
| Situation | Behaviour |
|---|---|
| Both rules satisfied | Runs normally, prints nothing |
| Command makes no platform call | Skipped, so it works offline and against any platform |
| No credentials configured | Skipped — the command reports the auth problem itself |
| Version undeterminable | Skipped silently and the command proceeds |
LOCALESS_SKIP_VERSION_CHECK set |
Skipped entirely; the version is not even fetched |
"Undeterminable" covers a non-2xx response, an unreachable host, a malformed body, and a 3-second timeout. A network problem can never stop a command — only a confirmed incompatibility does.
That version file is emitted by the platform's Angular build and served by Firebase Hosting, so it is present on every normally deployed instance. It is not served by an API-only or emulator-only deployment, where the check reports "unknown" and proceeds.
export LOCALESS_ORIGIN=https://my-localess.web.app
export LOCALESS_SPACE=YOUR_SPACE_ID
export LOCALESS_TOKEN=YOUR_API_TOKEN
# Optional: bypass the platform compatibility check (any value)
export LOCALESS_SKIP_VERSION_CHECK=1// .localess/credentials.json
{
"origin": "https://my-localess.web.app",
"space": "YOUR_SPACE_ID",
"token": "YOUR_API_TOKEN"
}Never commit
.localess/credentials.json.localess loginadds.localessto.gitignoreautomatically.
localess translationwas previouslylocaless translations, andlocaless typewaslocaless types. The old plural names still work as aliases.
Upload a local JSON translation file to Localess. Prints only the keys the selected --type acts on, as +/~/- lines under a summary such as Added 1 translation in locale "en". or, with --dry-run, Dry run: would add 1 translation in locale "en": (No translations to add for locale "en". when there is nothing to do). It does not print a full diff; use translation diff for that. update-existing, delete-missing-key and delete-missing-value first ask the server for a dry run, list the affected keys and prompt for confirmation (skippable with -y, --yes, automatically skipped under --dry-run, and nothing is pushed when the dry run reports no keys); add-missing never prompts since it is additive-only. Declining the prompt prints Aborted. and exits 1.
The server responds with { message, ids, dryRun? }, where ids lists only the keys the push type wrote (or would write).
localess translation push <locale> --path <file> [options]| Flag | Default | Description |
|---|---|---|
-p, --path <path> |
required | Path to the translations JSON file |
-f, --format <format> |
flat |
File format: flat or nested |
-t, --type <type> |
add-missing |
Update strategy (see below) |
--dry-run |
false |
Preview changes without applying (also skips confirmation) |
-y, --yes |
false |
Skip the confirmation prompt |
-v, --verbose |
false |
Print verbose debug output |
| Strategy | Behaviour | Confirmation |
|---|---|---|
add-missing |
Only adds keys absent from Localess | Never |
update-existing |
Only updates keys already in Localess — overwrites edits made in Localess since your last pull | Prompted |
delete-missing-key |
Deletes keys absent from the local file in every locale — run it with a complete file (normally the source locale) | Prompted |
delete-missing-value |
Removes only <locale>'s value of keys absent from the local file; other locales keep theirs |
Prompted |
The two delete strategies differ in scope: delete-missing-key deletes keys from every locale — the pushed <locale> only selects the file — so run it with a complete file, normally the source locale; delete-missing-value removes only <locale>'s value and leaves other languages untouched. The preview, prompt and summary state the scope (… in every locale / the "de" value of … (other locales keep theirs)).
Flat (default):
{ "common.submit": "Submit", "nav.home": "Home" }Nested (flattened automatically before uploading):
{ "common": { "submit": "Submit" }, "nav": { "home": "Home" } }localess translation push en --path ./locales/en.json
localess translation push de --path ./locales/de.json --type update-existing
localess translation push de --path ./locales/de.json --type delete-missing-value
localess translation push en --path ./locales/en.json --type delete-missing-key
localess translation push fr --path ./locales/fr.json --dry-run
localess translation push de --path ./locales/de.json --format nestedDownload translations from Localess to a local JSON file. Keys are sorted alphabetically (recursively for nested) for stable, diff-friendly output. In nested, a key that is also the parent of other keys (button beside button.save) keeps its child keys; its own value is left out and listed in a warning. Use flat to keep every key.
By default the file is what the app is served: any key with no value in <locale> holds the fallback locale's text. That suits bundling translations into an app, but not files you edit and push back — push --type update-existing would save that fallback text as <locale> translations. Use --raw for those: it writes only the values actually stored for the locale, leaving gaps as gaps (needs a platform newer than 4.0.0, and the Development Tools permission). A locale the space doesn't have is refused rather than silently served as the fallback locale (checked when the token can read the space).
localess translation pull <locale> --path <file> [options]| Flag | Default | Description |
|---|---|---|
-p, --path <path> |
required | Output file path |
-f, --format <format> |
flat |
File format: flat or nested |
--draft |
false |
Pull the draft (unpublished) version |
--raw |
false |
Pull only the values stored for the locale, without fallback filling — for files you edit and push back. Cannot be combined with --draft |
-v, --verbose |
false |
Print verbose debug output |
localess translation pull en --path ./locales/en.json
localess translation pull de --path ./locales/de.json --format nested
localess translation pull en --path ./locales/en.json --draft
localess translation pull de --path ./locales/de.json --rawRead-only comparison between a local translations file and the space — published by default, the draft with --draft, or the values stored for the locale with --raw (exactly what push acts on). Groups keys into Only in file/Different/Only in Localess sections (color-coded, git-diff style +/~/- symbols; direction-neutral labels); unchanged keys collapse into a count by default. Exits 1 on any drift — use as a CI gate. A locale the space doesn't have is refused (checked when the token can read the space).
localess translation diff <locale> --path <file> [options]| Flag | Default | Description |
|---|---|---|
-p, --path <path> |
required | Path to the local translations file |
-f, --format <format> |
flat |
File format: flat or nested |
--draft |
false |
Compare against the draft version |
--raw |
false |
Compare against stored values, without fallback filling — what push acts on. Cannot be combined with --draft |
-a, --all |
false |
Also print unchanged keys |
-v, --verbose |
false |
Print verbose debug output |
localess translation diff en --path ./locales/en.json
localess translation diff en --path ./locales/en.json --all
localess translation diff de --path ./locales/de.json --rawReport layout: Only in file (n) / Different (n) / Only in Localess (n) sections (empty ones omitted), an N unchanged (use --all to show) line, then either N translation(s) differ: a only in file, b different, c only in Localess. or In sync..
The translation commands are stateless building blocks: each has a fixed direction, and you choose the direction per locale and per step.
Source (pull, diff) |
Flag |
|---|---|
| published, fallback-filled | (default) |
| draft, fallback-filled | --draft |
| stored values, no fallback filling | --raw |
pullreplaces the file with Localess's version — Localess wins.push --type …applies one operation from the file — the file wins:add-missing,update-existing,delete-missing-key(every locale),delete-missing-value(this locale only).diffshowsOnly in file/Different/Only in Localessagainst the same sourcepullwould use and exits 1 on drift;diff --rawpredicts exactly whatpushwould act on.
Typical setups:
- Code owns everything:
push --type add-missingandupdate-existingper locale;diff --rawas a CI gate. - Localess owns everything:
pullper locale at build time; translators edit in Localess. - Code owns keys and the source language, translators own the rest:
push en --type add-missing/update-existing/delete-missing-keyfrom the source file;pull <locale>for the other locales.
Generate TypeScript type definitions from your Localess space's schemas.
localess type generate [--path <output>] [--prefix <prefix>]| Flag | Default | Description |
|---|---|---|
-p, --path <path> |
.localess/localess.d.ts |
Output file path |
--prefix <prefix> |
'' |
Prefix prepended to every generated type name (including the helper types and ContentData) |
-v, --verbose |
false |
Print verbose debug output |
Prerequisite: The API token must have the Development Tools permission in Localess Space settings.
What it does:
- Fetches the schema definitions from your space (
GET /schemas) - Generates a single
.d.tsfile:ContentAsset/ContentLink/ContentReference/ContentRichTexthelper interfaces, oneinterfaceperROOT/NODEschema (fields sorted by name; optional unlessrequired: true;_id: stringplus a literal_schema: '<schemaId>'), one string-literal union perENUMschema, and aContentDataunion of allROOTtypes
localess type generate
localess type generate --path src/types/localess.d.ts
localess type generate --prefix Localess # LocalessPage, LocalessContentAsset, LocalessContentData, ...Generated output:
/**
* Generated by Localess CLI
* Do not edit manually.
*/
export interface ContentAsset { kind: 'ASSET'; uri: string; }
// ... ContentLink, ContentReference, ContentRichText
export interface Page {
/** Unique identifier of a component in a content. */
_id: string;
/** Unique identifier for the Schema object. */
_schema: 'page';
body?: (HeroSection | CardGrid | RichTextBlock)[];
title: string;
}
export type ContentData = Page;Using generated types:
import type { Page } from './.localess/localess';
const content = await client.getContentBySlug<Page>('home');Define schemas in TypeScript with @localess/schema (defineSchema/defineEnum/defineConfig) and sync them with a space. See docs/schema.md for the authoring API.
Entry file: a TS/JS file exporting the result of defineConfig() (convention: schemas/index.ts).
Prerequisite: The token needs the Development Tools permission (
DEV_TOOLS) — same astype generate. No dedicated schema permission exists.
Offline — no login, no network. Loads the entry with jiti (default export first, then named exports; first value shaped like a defineConfig() result wins), prints each validate() issue as ERROR|WARNING <code> <path> — <message>; exits 1 on any error-severity issue or if no config is found. --format json prints the full ValidationResult instead.
localess schema validate ./schemas/index.ts
localess schema validate ./schemas/index.ts --format json(Re)generates one TS definition file per schema (kebab-case name, e.g. HeroBlock → hero-block.ts) plus index.ts (export const config = defineConfig({ schemas: [...] })) from the space, into --path (default schemas). Repeatable: only overwrites/deletes files it previously generated (marked with a header comment); a same-named hand-written file without that marker is skipped and reported, never overwritten. Each field is emitted wrapped in defineField(...) rather than as a bare object literal (import { defineField, defineSchema } from '@localess/schema'; defineEnum for ENUMs). References to other pulled schemas (source, schemas) become import { X } from './x' statements and by-value refs; unknown ids stay strings. A defineField(...) call wraps to one property per line once it would exceed --print-width columns (default 80, Prettier's own default — set it to match your own project's .prettierrc, since that's a per-project preference this CLI doesn't assume). Output is deterministic for a given input and --print-width.
localess schema pull
localess schema pull --path src/schemas
localess schema pull --print-width 100Read-only comparison, same grouped/colored report layout as translation diff, with schema-specific Create/Update/Stale sections; unchanged schemas collapsed into a count by default. Equality is key-sorted JSON of each SchemaExport, matching the server's change detection. Exits 1 on any drift — use as a CI gate. Does not run validate() first (unlike push).
localess schema diff ./schemas/index.ts
localess schema diff ./schemas/index.ts --all # also list unchanged schemasValidates, diffs, then pushes. The pre-push diff uses the same grouped/colored report as schema diff (-a, --all to also list unchanged schemas).
localess schema push ./schemas/index.ts --dry-run # preview
localess schema push ./schemas/index.ts # upsert: create/update only
localess schema push ./schemas/index.ts --delete # sync: also delete schemas absent from code (confirms unless -y)Aborts (exit 1) without pushing if validation reports any error. Default is upsert (type: 'upsert' — stale server schemas are kept and listed in a warning); --delete sends type: 'sync' and prompts with the exact list unless -y, --dry-run, or nothing is stale. Prints the server's created/updated/deleted/unchanged counts (prefixed [DryRun] under --dry-run), then reconciles the pre-push diff against the returned ids and prints a ⚠ Prediction mismatch warning (without failing the command) for any schema id whose predicted status didn't match what the server actually did — e.g. a concurrent change made between the preview and the push. stale entries are only reconciled in sync mode.
Use environment variables — no localess login step required.
# .github/workflows/sync-translations.yml
name: Sync translations
on:
push:
paths: ['locales/**']
jobs:
push-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '24' }
- run: npm install -g @localess/cli
- run: localess translation push en --path ./locales/en.json
env:
LOCALESS_ORIGIN: ${{ secrets.LOCALESS_ORIGIN }}
LOCALESS_SPACE: ${{ secrets.LOCALESS_SPACE_ID }}
LOCALESS_TOKEN: ${{ secrets.LOCALESS_TOKEN }}Generate types in CI:
- run: localess type generate --path src/types/localess.d.ts
env:
LOCALESS_ORIGIN: ${{ secrets.LOCALESS_ORIGIN }}
LOCALESS_SPACE: ${{ secrets.LOCALESS_SPACE_ID }}
LOCALESS_TOKEN: ${{ secrets.LOCALESS_TOKEN }}Gate merges on schema drift:
- run: localess schema validate ./schemas/index.ts
- run: localess schema diff ./schemas/index.ts
env:
LOCALESS_ORIGIN: ${{ secrets.LOCALESS_ORIGIN }}
LOCALESS_SPACE: ${{ secrets.LOCALESS_SPACE_ID }}
LOCALESS_TOKEN: ${{ secrets.LOCALESS_TOKEN }}# 1. Authenticate once
localess login
# 2. Generate types after schema changes in Localess CMS
localess type generate
# 3. Pull latest translations
localess translation pull en --path ./locales/en.json
# 4. Edit locally, then push (dry-run first)
localess translation push en --path ./locales/en.json --dry-run
localess translation push en --path ./locales/en.json| File | Created by | Permissions | Purpose |
|---|---|---|---|
.localess/credentials.json |
localess login |
0o600 (owner only) |
Persisted auth credentials |
.localess/localess.d.ts |
localess type generate |
Standard | Generated TypeScript types (path via -p, --path) |
schemas/<kebab-case-id>.ts, schemas/index.ts |
localess schema pull |
Standard | Generated @localess/schema definitions (dir via -p, --path; each file starts with the pull marker comment) |
localess login automatically appends .localess to .gitignore. To commit generated types while protecting credentials:
.localess/credentials.json
# .localess/localess.d.ts ← uncomment if regenerating in CI instead of committing