LingoSeal

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.

Terminal
# Add it to your project
npm install --save-dev lingoseal

# Or run it without installing anything
npx lingoseal download

Installed locally, the lingoseal command is available through npx lingoseal and in your package.json scripts.

Quick Start

Terminal
# 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-base

Getting Your API Token

  1. Open your project in LingoSeal
  2. Go to the Settings tab
  3. Under Public API Token, click Generate Token
  4. 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.

Terminal
npx lingoseal init
npx lingoseal init --url https://app.lingoseal.com --project abc-123
OptionDescription
--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.

Terminal
# 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
OptionDescriptionDefault
-c, --config <path>Path to config fileauto-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 formatflat-json
-l, --languages <langs>Comma-separated languagesall
--include-baseInclude base languagefalse

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.

Terminal
# 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
OptionDescriptionDefault
-c, --config <path>Path to config fileauto-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, xcstringsflat-json
-l, --languages <langs>Comma-separated languages to pushall found
--conflict-strategy <s>How to handle existing keys (see below)sync
--dry-runPreview changes without writing to the serverfalse
--include-baseInclude the base languagefalse

Conflict Strategies

StrategyBehavior
syncDefault. Add new keys, update existing, and delete keys missing from any language file.
overwriteCreate new keys and update all existing translations. No deletions.
skipOnly create new keys. Don’t touch existing translations.
overwrite-emptyCreate 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:

Output
πŸ“€ 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:    274

Configuration 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:

lingoseal.config.mjs
/** @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
};

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.

FileLoaded as
lingoseal.config.mjsES module, export default
lingoseal.config.jsES module in an ESM project, CommonJS otherwise
lingoseal.config.cjsCommonJS, module.exports
lingoseal.config.jsonJSON

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:

Terminal
LINGOSEAL_TOKEN     # the API token
LINGOSEAL_URL       # the server URL
LINGOSEAL_PROJECT   # the project id

Resolution 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

PlaceholderDescriptionExample
{lang}Language codeen, cs-CZ
{ns}Namespacetranslation
PatternOutput
{lang}.jsonen.json, cs-CZ.json
{lang}/{ns}.jsonen/translation.json
locales/{lang}/translation.jsonlocales/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:

JSON
{
  "home.title": "Welcome",
  "home.description": "This is the homepage",
  "nav.login": "Login"
}

nested-json

Nested objects from dot-separated keys:

JSON
{
  "home": {
    "title": "Welcome",
    "description": "This is the homepage"
  },
  "nav": {
    "login": "Login"
  }
}

i18next

Wrapped in a namespace for i18next:

JSON
{
  "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
<?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.

.github/workflows/translations-push.yml
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.

.github/workflows/translations-pull.yml
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: true

Preview in PRs

Add a CI check that previews what a push would do, without applying changes:

.github/workflows/translations-preview.yml
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

package.json
{
  "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

lingoseal.config.mjs
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,
};
i18n.ts
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

lingoseal.config.mjs
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