For developers

The nlit CLI

Pull approved translations from the nlit platform straight into your repository — in the format and file layout your app already expects.

$brew install nlit-app/tap/nlit

macOS & Linux · upgrade with brew upgrade nlit

Overview#

nlit is a small command-line tool, written in Go, that turns approved translations from the nlit platform into files committed to your repository.

The CLI is read-only by design. It never pushes back to the platform — translators and reviewers work in the web app, and the CLI is how those translations reach your build. That keeps the platform the single source of truth and avoids accidental overwrites from local edits.

  1. 1Install

    One-time per developer or CI runner. Homebrew on macOS and Linux.

  2. 2nlit init

    Run in the project root to write a .nlit.yaml config file.

  3. 3nlit pull

    Run locally or in CI to write translation files into your output directory.

Step 1

Install#

Homebrew (macOS, Linux)

Terminal
sh
brew install nlit-app/tap/nlit

Verify the install

Terminal
sh
nlit --help

Upgrade to a newer version

Homebrew caches tap metadata, so a plain brew upgrade nlitwon't pick up a fresh release until you refresh the tap first. Run both:

Terminal
sh
brew update
brew upgrade nlit

Confirm the new version with nlit --version.

60-second tour

Quick start#

From the root of your application repository:

Terminal
sh
# 1. Sign in and write .nlit.yaml in the current directory
nlit init

# 2. Pull every language for every module
nlit pull

The output directory and file format are chosen during init based on your target platform — e.g. src/locales/ for web, the repo root (.) for Android and iOS. You can change either at any time by editing .nlit.yaml.

Command

nlit init#

Authenticates against the API and writes a .nlit.yaml config file in the current directory. Every prompt has a matching flag, so the same command works interactively for humans and non-interactively for CI bootstrap.

Interactive walkthrough

Just run it — the wizard walks you through login, project selection, output directory, and platform configuration in three steps:

terminal — nlit init
$

You will be prompted for:

  • Email + password to sign in
  • Project — an interactive picker lists every project you can access (or pass --project-id to skip it)
  • Target platform (sets the format default and path layout)
  • File format (skipped on platforms with a single canonical format, e.g. Android — added in v0.1.3)
  • Root output directory — platform-dependent default (src/locales for web, the repo root . for Android/iOS)
  • Languages — the wizard pulls the project's existing languages from the API and offers a deselect-by-number prompt. Pass --languages sv,de,fr to skip it.

Non-interactive

Terminal
sh
nlit init \
  --email you@example.com \
  --password "$NLIT_PASSWORD" \
  --project-id 00000000-0000-0000-0000-000000000000 \
  --languages sv,de,fr \
  --format json \
  --platform web

Flags

FlagDescription
--emailEmail address used to sign in
--passwordPassword (or use env vars in CI — never commit it)
--project-idProject UUID from the platform
--output-dirWhere to write translation files (default: translations)
--formatjson, json-nested, strings, stringsdict, xcstrings, xml, xliff, po
--languagesComma-separated BCP 47 tags, e.g. sv,de,fr
--platformandroid, ios, web — drives format default and pull filter

Already signed in?

If you ran nlit auth login earlier (or saved an API key — see below), initreuses the saved token and skips the email/password step. Skip the project-ID flag too and you'll get an interactive picker listing every project you have access to.

Tip

Add .nlit.yaml to .gitignore if your team prefers to keep the project ID out of source control.

Command

nlit pull#

Downloads translations for every module in your project and writes one file per module per language. The file extension is derived from the chosen format.

Default output

With format: json and languages: [sv, de]:

translations/
translations/
  sv/
    auth.json
    core.json
    payments.json
  de/
    auth.json
    core.json
    payments.json

With platform: android or platform: ios set, pull writes to the native localisation layout instead — Android puts each language under res/values-<lang>/strings.xml, iOS uses <lang>.lproj/Localizable.strings. The exact paths can be customised per-module via the modules: map in .nlit.yaml (see Configuration).

Flags

FlagShortDescription
--languageLanguage code; repeatable. Overrides .nlit.yaml.
--formatOverride output format for this run.
--modulePull a single module by name instead of every module.
--platform-pPlatform filter (android | ios | web). Overrides .nlit.yaml.

Platform filtering

When a platform is set, the CLI only includes keys tagged for that platform. Keys with no platform tag are always included — those are cross-platform strings shared by every target. This is how the same project can serve a web app and a mobile app without duplicating shared keys.

Examples

Terminal
sh
# Pull only Swedish
nlit pull --language sv

# Pull a single module, in two languages
nlit pull --module checkout --language sv --language de

# Pull Android XML for an iOS project (e.g. for an Android sibling app)
nlit pull --platform android --format xml

Command

nlit diff#

Compares the translation files on disk against what's currently on the platform. Use it before a release to confirm your repo is up to date — or in code review to spot a stale checked-in translation.

Terminal
sh
nlit diff

# Or scope to a single module
nlit diff --module checkout

Each file is reported as one of:

  • up to date — the local file matches the platform.
  • OUTDATED — the platform has changed since your last nlit pull.
  • NEW — the platform has content you have never pulled.

diffalso prints a one-line warning for any language that exists on the platform but isn't listed in your .nlit.yaml— useful for catching new target languages a translator added that you haven't pulled yet.

Run nlit pull to bring everything back in sync.

Command

nlit status#

Prints translation coverage per language for the configured project — handy for spotting which languages still need work before a release.

Terminal
sh
nlit status
Output
Language      Keys   Translated  Missing  Progress
──────────────────────────────────────────────────
de             120          108       12   90% [█████████░]
fr             120          114        6   95% [█████████░]
ja             120           72       48   60% [██████░░░░]

Run 'nlit pull' to sync the latest translations locally.

When everything is fully translated, the trailer flips to ✓ All languages fully translated. instead.

No flags — it always queries the live project.

Command

nlit auth — login & API keys#

nlit auth login saves credentials so the other commands (pull, diff, status) can talk to the platform. Two ways to do it.

1. API key (recommended for CI & long-lived setups)

Generate an API key in the web app under Project settings → API keys → Create. Keys are individually revocable from that same page, don't expire on a password change, and never need to be re-entered.

Terminal
sh
# Pass via --api-key flag
nlit auth login --api-key nlit_xxxxxxxxxxxxxxxxxxxxxxxx

# …or as a positional argument
nlit auth login nlit_xxxxxxxxxxxxxxxxxxxxxxxx

# Now run init or pull as usual
nlit init
nlit pull

API keys are scoped to a single project, so the key you save here only grants access to the project it was created in. nlit initwill use that project automatically if its UUID matches what's in .nlit.yaml.

2. Email & password (interactive)

Useful when you want the same login the web app uses — for example after a token expires locally and you're working from your own machine.

Terminal
sh
nlit auth login \
  --email you@example.com \
  --password "$NLIT_PASSWORD"

Omit --passwordand you'll be prompted for it securely (no echo).

Where credentials are stored

Both flows write to ~/.nlit-token with 0600 permissions. The same file is reused by every nlit invocation.

For CI and ephemeral environments you can skip the file entirely by setting NLIT_TOKEN in the environment — it overrides the token file (with a one-line stderr notice so unexpected overrides are visible). See Use in CI for the recommended pattern.

nlit auth logout

Removes the saved credentials. For email/password tokens the CLI also asks the API to revoke the token server-side, so a leaked credential can't outlive the logout. API keys are not revoked by logout— delete them from the platform's API keys page instead.

Terminal
sh
nlit auth logout

Reference

Configuration#

nlit init generates a .nlit.yaml in the project root. You can hand-edit it at any time; the format is intentionally tiny.

.nlit.yaml
yaml
# nlit project configuration
# Generated by 'nlit init' — edit as needed.

project_id: 00000000-0000-0000-0000-000000000000
source_language: en

# Translation file format: json | json-nested | strings |
# stringsdict | xcstrings | xml | xliff | po
format: json

# Where translation files are written
output_dir: src/locales

# Languages to pull (init prepends the source language automatically)
languages:
  - sv
  - de
  - fr

# Default platform filter for pull (android | ios | web)
platform: web

# Auto-populated per-module anchor paths (see below)
modules:
  auth: src/locales/en/auth.json
  checkout: src/locales/en/checkout.json

Where the config is found

nlit searches for .nlit.yaml starting from the current directory and walking up toward the filesystem root, so you can run commands from any subdirectory of your project — e.g. a build script in scripts/ or from inside a monorepo package.

The modules: map

init auto-populates a modules: section mapping each module name to an anchor path — the file location for the source-language version. pull derives sibling locale paths from the anchor (e.g. res/values/strings.xmlres/values-de/strings.xml on Android). You can hand-edit anchors to point individual modules at non-standard locations in an existing repo without changing the global output_dir.

Reference

File formats#

Every format is selectable on every project, regardless of platform. init just picks a sensible default — an iOS app might output xliff for an agency handoff and xcstrings for modern Xcode targets.

FormatExtensionUse case
json.jsonWeb, React, generic i18n libs (flat key map)
json-nested.jsoni18next, vue-i18n (dot-notation expands to nested objects)
strings.stringsApple iOS/macOS — classic single-language string table
stringsdict.stringsdictApple iOS/macOS — plural-rule strings
xcstrings.xcstringsiOS 17+ / Xcode 15+ — modern String Catalog
xml.xmlAndroid (res/values/strings.xml)
xliff.xliffXLIFF 1.2 — industry-standard interchange for translation agencies

Parameter syntax is rewritten automatically based on the platform tag on each key — e.g. {{name}} in nlit becomes %1$s on Android and %1$@ on iOS, so you never need to convert placeholders by hand.

Wiring it up

Use in CI#

Running nlit pull before your build keeps your bundle in sync with whatever translators have approved. The CLI is small, statically linked, and exits with a non-zero status on failure, so it slots into any CI runner.

Pattern

  1. Check .nlit.yaml into your repo at the root, with the output_dir you want files written to.
  2. Generate an API key in the web app (Project settings → API keys → Create) and store it as a CI secret.
  3. Expose the secret as NLIT_TOKEN when running nlit pull. No auth loginstep is needed — the env var is read directly and never written to the runner's filesystem.

The output path comes from .nlit.yaml — not a flag

nlit pull has no --output-dir flag. The committed output_dir in .nlit.yamlis the source of truth, and it's resolved relative to wherever you run the command from.

That means two things in CI: run nlit pull from the repo root (or set working-directory: on the step), and if you need a different path per environment, edit .nlit.yaml rather than passing flags.

Example — GitHub Actions

.github/workflows/build.yml
yaml
- name: Install nlit
  run: brew install nlit-app/tap/nlit

- name: Pull translations
  # .nlit.yaml at the repo root drives the output path (output_dir).
  # Run from the repo root so relative paths resolve correctly.
  working-directory: ${{ github.workspace }}
  env:
    NLIT_TOKEN: ${{ secrets.NLIT_API_KEY }}
  run: nlit pull

Why API keys for CI

They're individually revocable from the platform's API keys page, scoped to one project, and don't break if a team member rotates their password.

Multi-step jobs?

If you need to call several nlit commands in sequence (e.g. pull followed by diff), run nlit auth login --api-key "$NLIT_API_KEY" once at the start of the job — the token lands in ~/.nlit-token on the runner and subsequent commands pick it up without needing the env var on every step. Use the env-var pattern above for single-shot pulls.

Caching tip

If you commit translation files to source control, only run nlit pull on release builds and let pull requests rely on whatever is checked in. That makes diffs easy to review and pins the strings used in any given release.

When things go wrong

Troubleshooting#

401 Unauthorized

Your token expired or the credentials are wrong. Run nlit auth login with fresh credentials, then retry nlit pull.

403 Forbidden

Your account doesn't have access to the project ID in .nlit.yaml. Check the project ID, and confirm with a project admin that your account is a member.

Empty output / fewer files than expected

If you're using --platform or have it set in .nlit.yaml, only keys tagged for that platform (plus untagged keys) are pulled. Remove the platform filter to confirm whether keys exist but are being filtered out.

Not sure what's out of date

Run nlit diff for a per-file up to date / OUTDATED / NEW report, or nlit status for per-language coverage.


Ready to wire it up?

Spin up a project, generate an API key, and pull your first translation file in under five minutes.