LingoSeal CLI
Command-line tool for pushing and pulling translations between your project and LingoSeal.
Installation
The CLI is published on the public npm registry as lingoseal.
# Add it to your project
npm install --save-dev lingoseal
# Or run it without installing anything
npx lingoseal downloadInstalled locally, the lingoseal command is available through npx lingoseal and in your package.json scripts.
Quick Start
# 1. Initialize a config file in your project root
npx lingoseal init --url https://app.lingoseal.com --project <project-id>
# 2. Put your API token in the environment β never in the config file
export LINGOSEAL_TOKEN=lsk_...
# 3. Download translations (pull)
npx lingoseal download
# 4. Push local translation files to the server
npx lingoseal push --include-baseGetting Your API Token
- Open your project in LingoSeal
- Go to the Settings tab
- Under Public API Token, click Generate Token
- Export it as
LINGOSEAL_TOKEN, or pass it via--token. Never commit it to your repository.
Commands
All commands discover the config file automatically. Resolution order is --flag> environment variable > config file.
lingoseal init
Creates a lingoseal.config.mjs configuration file in the current directory. The API token is not written to the file β the generated config reads it from LINGOSEAL_TOKEN.
npx lingoseal init
npx lingoseal init --url https://app.lingoseal.com --project abc-123| Option | Description |
|---|---|
--url <url> | Base URL of the LingoSeal server |
--project <id> | Project ID (find it in your project URL) |
--token <token> | API token; printed as an export line, never written to the config |
lingoseal download
Downloads translations from the server and writes them as JSON files.
# Using the discovered config file
lingoseal download
# Override config values via CLI
lingoseal download \
--url https://app.lingoseal.com \
--project abc-123 \
--token my-secret-token \
--output ./src/locales \
--format nested-json
# Download specific languages only
lingoseal download --languages en,de,fr
# Include the base language too
lingoseal download --include-base| Option | Description | Default |
|---|---|---|
-c, --config <path> | Path to config file | auto-discovered |
-u, --url <url> | Base URL of translation server | - |
-p, --project <id> | Project ID | - |
-t, --token <token> | API token | - |
-o, --output <path> | Output directory | ./locales |
-f, --format <format> | Output format | flat-json |
-l, --languages <langs> | Comma-separated languages | all |
--include-base | Include base language | false |
lingoseal push
Pushes local translation files to the server. Reads files from your output directory, parses them based on the configured format, and uploads them via the public API.
# Push all translation files (excluding base language by default)
lingoseal push
# Preview what would change without applying
lingoseal push --dry-run
# Push including the base (source) language
lingoseal push --include-base
# Push specific languages only
lingoseal push --languages de,fr
# Overwrite existing translations on the server
lingoseal push --conflict-strategy overwrite
# Only fill in empty translations
lingoseal push --conflict-strategy overwrite-empty| Option | Description | Default |
|---|---|---|
-c, --config <path> | Path to config file | auto-discovered |
-u, --url <url> | Base URL of translation server | - |
-p, --project <id> | Project ID | - |
-t, --token <token> | API token | - |
-i, --input <path> | Input directory (overrides outputPath) | ./locales |
-f, --format <format> | File format: flat-json, nested-json, i18next, csv, qt-ts, android-xml, xcstrings | flat-json |
-l, --languages <langs> | Comma-separated languages to push | all found |
--conflict-strategy <s> | How to handle existing keys (see below) | sync |
--dry-run | Preview changes without writing to the server | false |
--include-base | Include the base language | false |
Conflict Strategies
| Strategy | Behavior |
|---|---|
sync | Default. Add new keys, update existing, and delete keys missing from any language file. |
overwrite | Create new keys and update all existing translations. No deletions. |
skip | Only create new keys. Donβt touch existing translations. |
overwrite-empty | Create new keys. Only update translations that are currently empty on the server. |
Dry Run Output
When using --dry-run, the CLI shows what would change without applying anything:
π€ LingoSeal Translation Push
β Project: My App
Base language: en
Supported languages: en, de, fr, cs-CZ
Conflict strategy: overwrite
Mode: DRY RUN (no changes will be made)
Pushing 2 language(s)...
Pushing de... β 150 keys (+3 new, ~12 updated, 135 unchanged)
+ settings.notifications: "Benachrichtigungen"
+ settings.theme: "Design"
~ home.title: "Startseite" β "Willkommen"
~ nav.login: "Anmelden" β "Einloggen"
... and 10 more changed keys
Pushing fr... β 150 keys (+3 new, ~8 updated, 139 unchanged)
β Dry run complete (no changes applied)
Summary:
New keys created: 6
Keys updated: 20
Keys skipped: 0
Keys unchanged: 274Configuration File
Create a lingoseal.config.mjs in your project root (or use lingoseal init). It is a real module, so anything you can compute you can use:
/** @type {import('lingoseal').LingoSealConfig} */
export default {
// Required
url: "https://app.lingoseal.com",
projectId: "your-project-id",
token: process.env.LINGOSEAL_TOKEN,
// Optional
outputPath: "./src/locales", // Where to read/write translation files
format: "flat-json", // flat-json | nested-json | i18next | csv | qt-ts | android-xml | xcstrings
filePattern: "{lang}.json", // Overrides the format's own default
languages: [], // Empty = all project languages
includeBase: true, // Include the base (source) language
namespace: "translation", // Namespace for i18next format
};LINGOSEAL_TOKEN.Where the Config Is Found
The first of these that exists wins. --config <path> skips the search and loads the file you name, by its extension.
| File | Loaded as |
|---|---|
lingoseal.config.mjs | ES module, export default |
lingoseal.config.js | ES module in an ESM project, CommonJS otherwise |
lingoseal.config.cjs | CommonJS, module.exports |
lingoseal.config.json | JSON |
Environment Variables
Three settings can come from the environment instead of the file, so a CI job needs three secrets and no config file at all:
LINGOSEAL_TOKEN # the API token
LINGOSEAL_URL # the server URL
LINGOSEAL_PROJECT # the project idResolution order is --flag> environment variable > config file, for both download and push.
Upgrading a 2.x Config
Before 3.0 the config file was read as text and pattern-matched into JSON β it was never executed. If your lingoseal.config.js uses module.exports and your package.json says "type": "module", it will no longer load. Either rename it to lingoseal.config.cjs, or change module.exports = { to export default {. Configs in CommonJS projects, and .json configs, are unaffected.
File Patterns
| Placeholder | Description | Example |
|---|---|---|
{lang} | Language code | en, cs-CZ |
{ns} | Namespace | translation |
| Pattern | Output |
|---|---|
{lang}.json | en.json, cs-CZ.json |
{lang}/{ns}.json | en/translation.json |
locales/{lang}/translation.json | locales/en/translation.json |
The same file pattern is used by both download (to write files) and push (to discover files to upload).
Output Formats
flat-json (default)
Flat key-value pairs:
{
"home.title": "Welcome",
"home.description": "This is the homepage",
"nav.login": "Login"
}nested-json
Nested objects from dot-separated keys:
{
"home": {
"title": "Welcome",
"description": "This is the homepage"
},
"nav": {
"login": "Login"
}
}i18next
Wrapped in a namespace for i18next:
{
"translation": {
"home.title": "Welcome",
"home.description": "This is the homepage"
}
}android-xml
Android strings.xml, one directory per language. Key names are sanitized to valid resource names (dots become underscores). The directory is the language's Android resource qualifier, which is not the language tag β de becomes values-de, de-DE becomes values-de-rDE, and zh-Hans becomes values-b+zh+Hans. The format supplies that pattern itself, so leave filePattern unset:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="home_title">Welcome</string>
<string name="nav_login">Login</string>
</resources>xcstrings
Xcode String Catalog (Xcode 15+) β a single Localizable.xcstrings file containing every language, written to outputPath. The file pattern is ignored for this format unless it contains no {lang} placeholder.
CI/CD Integration
The recommended CI/CD setup uses two workflows: push source strings when they change, and pull the latest translations on a schedule.
Push on Merge
When source strings change on main, push them to LingoSeal so translators can start working immediately.
name: Push source translations
on:
push:
branches: [main]
paths:
- 'src/locales/en.json' # adjust to your base language file
jobs:
push-source:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Push base language to LingoSeal
run: >
npx --yes lingoseal push
--include-base
--languages en
--conflict-strategy overwrite
env:
LINGOSEAL_TOKEN: ${{ secrets.LINGOSEAL_TOKEN }}
LINGOSEAL_URL: ${{ secrets.LINGOSEAL_URL }}
LINGOSEAL_PROJECT: ${{ secrets.LINGOSEAL_PROJECT }}Pull on Schedule
Periodically pull the latest translations and open a PR for review.
name: Pull translations
on:
schedule:
- cron: '0 6 * * 1-5' # Weekdays at 6am UTC
workflow_dispatch: # Allow manual trigger
jobs:
pull-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Download translations
run: npx --yes lingoseal download
env:
LINGOSEAL_TOKEN: ${{ secrets.LINGOSEAL_TOKEN }}
LINGOSEAL_URL: ${{ secrets.LINGOSEAL_URL }}
LINGOSEAL_PROJECT: ${{ secrets.LINGOSEAL_PROJECT }}
- name: Create PR with updates
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: update translations from LingoSeal'
title: 'chore: update translations from LingoSeal'
body: Automated translation update from LingoSeal.
branch: translations/update
delete-branch: truePreview in PRs
Add a CI check that previews what a push would do, without applying changes:
name: Preview translation changes
on:
pull_request:
paths:
- 'src/locales/**'
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Preview changes
run: >
npx --yes lingoseal push --dry-run
--include-base
--conflict-strategy overwrite
env:
LINGOSEAL_TOKEN: ${{ secrets.LINGOSEAL_TOKEN }}
LINGOSEAL_URL: ${{ secrets.LINGOSEAL_URL }}
LINGOSEAL_PROJECT: ${{ secrets.LINGOSEAL_PROJECT }}Package Scripts
{
"scripts": {
"translations:pull": "lingoseal download",
"translations:push": "lingoseal push --include-base --conflict-strategy overwrite",
"translations:preview": "lingoseal push --dry-run --include-base --conflict-strategy overwrite",
"prebuild": "npm run translations:pull"
}
}Framework Examples
Example configurations for popular i18n libraries.
react-i18next
export default {
url: "https://app.lingoseal.com",
projectId: "your-project-id",
token: process.env.LINGOSEAL_TOKEN,
outputPath: "./src/locales",
format: "flat-json",
filePattern: "{lang}/translation.json",
includeBase: true,
};import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './locales/en/translation.json';
import de from './locales/de/translation.json';
i18n.use(initReactI18next).init({
resources: {
en: { translation: en },
de: { translation: de },
},
lng: 'en',
fallbackLng: 'en',
});next-intl
export default {
url: "https://app.lingoseal.com",
projectId: "your-project-id",
token: process.env.LINGOSEAL_TOKEN,
outputPath: "./messages",
format: "nested-json",
filePattern: "{lang}.json",
includeBase: true,
};Requirements
- Node.js >= 20.10.0