The nlit CLI
Pull approved translations from the nlit platform straight into your repository — in the format and file layout your app already expects.
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.
- 1Install
One-time per developer or CI runner. Homebrew on macOS and Linux.
- 2nlit init
Run in the project root to write a .nlit.yaml config file.
- 3nlit pull
Run locally or in CI to write translation files into your output directory.
Step 1
Install#
Homebrew (macOS, Linux)
brew install nlit-app/tap/nlitVerify the install
nlit --helpUpgrade 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:
brew update
brew upgrade nlitConfirm the new version with nlit --version.
60-second tour
Quick start#
From the root of your application repository:
# 1. Sign in and write .nlit.yaml in the current directory
nlit init
# 2. Pull every language for every module
nlit pullThe 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:
You will be prompted for:
- Email + password to sign in
- Project — an interactive picker lists every project you can access (or pass
--project-idto 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/localesfor 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,frto skip it.
Non-interactive
nlit init \
--email you@example.com \
--password "$NLIT_PASSWORD" \
--project-id 00000000-0000-0000-0000-000000000000 \
--languages sv,de,fr \
--format json \
--platform webFlags
| Flag | Description |
|---|---|
--email | Email address used to sign in |
--password | Password (or use env vars in CI — never commit it) |
--project-id | Project UUID from the platform |
--output-dir | Where to write translation files (default: translations) |
--format | json, json-nested, strings, stringsdict, xcstrings, xml, xliff, po |
--languages | Comma-separated BCP 47 tags, e.g. sv,de,fr |
--platform | android, ios, web — drives format default and pull filter |
Already signed in?
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
.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/
sv/
auth.json
core.json
payments.json
de/
auth.json
core.json
payments.jsonWith 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
| Flag | Short | Description |
|---|---|---|
--language | — | Language code; repeatable. Overrides .nlit.yaml. |
--format | — | Override output format for this run. |
--module | — | Pull a single module by name instead of every module. |
--platform | -p | Platform 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
# 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 xmlCommand
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.
nlit diff
# Or scope to a single module
nlit diff --module checkoutEach file is reported as one of:
up to date— the local file matches the platform.OUTDATED— the platform has changed since your lastnlit 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.
nlit statusLanguage 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.
# 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 pullAPI 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.
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.
nlit auth logoutReference
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 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.jsonWhere 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.xml → res/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.
| Format | Extension | Use case |
|---|---|---|
json | .json | Web, React, generic i18n libs (flat key map) |
json-nested | .json | i18next, vue-i18n (dot-notation expands to nested objects) |
strings | .strings | Apple iOS/macOS — classic single-language string table |
stringsdict | .stringsdict | Apple iOS/macOS — plural-rule strings |
xcstrings | .xcstrings | iOS 17+ / Xcode 15+ — modern String Catalog |
xml | .xml | Android (res/values/strings.xml) |
xliff | .xliff | XLIFF 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
- Check
.nlit.yamlinto your repo at the root, with theoutput_diryou want files written to. - Generate an API key in the web app (Project settings → API keys → Create) and store it as a CI secret.
- Expose the secret as
NLIT_TOKENwhen runningnlit pull. Noauth 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
- 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 pullWhy API keys for CI
Multi-step jobs?
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
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.