Lokalise Multi-Environment Setup
Overview
Configure Lokalise for isolated development, staging, and production environments. Two strategies are covered: separate Lokalise projects per environment (strongest isolation) and Lokalise branching within a single project (simpler management). Both approaches include secret management, environment-aware configuration, and a promotion workflow that moves translations through the pipeline from dev to production without cross-contamination.
Prerequisites
- Lokalise Team or Enterprise plan (branching requires Team plan or higher)
- One Lokalise API token per environment, each scoped to minimum required permissions
- Secret management system: GitHub Secrets, AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault
- Node.js 18+ with
@lokalise/node-api SDK installed
- Environment variable
NODE_ENV (or equivalent) set in each deployment target
Instructions
Step 1: Choose Your Strategy
Option A — Separate projects per environment (recommended for teams > 5 translators or strict compliance):
| Environment | Lokalise Project | Purpose |
|---|
| Development | MyApp (Dev) | Rapid iteration, machine translations OK |
| Staging | MyApp (Staging) | QA review, translator proofing |
| Production | MyApp (Prod) | Approved translations only |
Option B — Single project with Lokalise branching (simpler for small teams):
| Branch | Purpose |
|---|
main | Production translations |
staging | QA translations under review |
dev | Work-in-progress translations |
Step 2: Environment-Aware Configuration
Create a configuration module that selects the correct Lokalise project and credentials based on the runtime environment:
interface LokaliseEnvConfig {
environment: string;
apiToken: string;
projectId: string;
branch?: string;
cacheTtlMs: number;
enableOta: boolean;
fallbackLocale: string;
rateLimitPerSec: number;
}
const ENV_CONFIGS: Record<string, Omit<LokaliseEnvConfig, 'apiToken' | 'projectId'>> = {
development: {
environment: 'development',
cacheTtlMs: 0,
enableOta: false,
fallbackLocale: 'en',
rateLimitPerSec: 6,
},
staging: {
environment: 'staging',
cacheTtlMs: 5 * 60_000,
enableOta: true,
fallbackLocale: 'en',
rateLimitPerSec: 6,
},
production: {
environment: 'production',
cacheTtlMs: 30 * 60_000,
enableOta: true,
fallbackLocale: 'en',
rateLimitPerSec: 4,
},
};
export function getLokaliseConfig(): LokaliseEnvConfig {
const env = process.env.NODE_ENV || 'development';
const base = ENV_CONFIGS[env];
if (!base) {
throw new Error(`Unknown environment: ${env}. Expected: ${Object.keys(ENV_CONFIGS).join(', ')}`);
}
const apiToken = process.env.LOKALISE_API_TOKEN;
const projectId = process.env.LOKALISE_PROJECT_ID;
if (!apiToken) {
throw new Error('LOKALISE_API_TOKEN is not set');
}
if (!projectId) {
throw new Error('LOKALISE_PROJECT_ID is not set');
}
return {
...base,
apiToken,
projectId,
branch: process.env.LOKALISE_BRANCH,
};
}
Step 3: Secret Management
Store API tokens securely in each environment. Never commit tokens to source control.
GitHub Actions (CI/CD):
jobs:
deploy-staging:
environment: staging
env:
LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN_STAGING }}
LOKALISE_PROJECT_ID: ${{ vars.LOKALISE_PROJECT_ID_STAGING }}
steps:
- run: npm run build
deploy-production:
environment: production
env:
LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN_PROD }}
LOKALISE_PROJECT_ID: ${{ vars.LOKALISE_PROJECT_ID_PROD }}
steps:
- run: npm run build
AWS Secrets Manager:
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
async function getLokaliseToken(environment: string): Promise<string> {
const client = new SecretsManagerClient({ region: 'us-east-1' });
const command = new GetSecretValueCommand({
SecretId: `lokalise/${environment}/api-token`,
});
const response = await client.send(command);
return response.SecretString!;
}
GCP Secret Manager:
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
async function getLokaliseToken(environment: string): Promise<string> {
const client = new SecretManagerServiceClient();
const [version] = await client.accessSecretVersion({
name: `projects/my-project/secrets/lokalise-token-${environment}/versions/latest`,
});
return version.payload!.data!.toString();
}
HashiCorp Vault:
vault kv get -field=api_token secret/lokalise/production
Step 4: Lokalise Branching (Option B Alternative)
If using a single project with branching instead of separate projects:
import { LokaliseApi } from '@lokalise/node-api';
const lokalise = new LokaliseApi({ apiKey: process.env.LOKALISE_API_TOKEN! });
const projectId = process.env.LOKALISE_PROJECT_ID!;
async function createBranch(branchName: string): Promise<void> {
await lokalise.branches().create({ name: branchName }, { project_id: projectId });
console.log(`Created branch: ${branchName}`);
}
async function downloadFromBranch(branchName: string, outputDir: string): Promise<void> {
const response = await lokalise.files().download(`${projectId}:${branchName}`, {
format: 'json',
: ,
: ,
: ,
});
.();
}
(): <> {
lokalise.().(
{ : projectId },
{
: sourceBranch,
: targetBranch,
: ,
}
);
.();
}
Step 5: Promotion Workflow (Dev to Staging to Production)
Promote translations through environments with validation at each gate:
#!/bin/bash
set -euo pipefail
TARGET_ENV="${1:?Usage: promote-translations.sh <staging|production>}"
case "$TARGET_ENV" in
staging)
SOURCE_TOKEN="$LOKALISE_API_TOKEN_DEV"
SOURCE_PROJECT="$LOKALISE_PROJECT_ID_DEV"
TARGET_TOKEN="$LOKALISE_API_TOKEN_STAGING"
TARGET_PROJECT="$LOKALISE_PROJECT_ID_STAGING"
;;
production)
SOURCE_TOKEN="$LOKALISE_API_TOKEN_STAGING"
SOURCE_PROJECT="$LOKALISE_PROJECT_ID_STAGING"
TARGET_TOKEN="$LOKALISE_API_TOKEN_PROD"
TARGET_PROJECT="$LOKALISE_PROJECT_ID_PROD"
;;
*)
echo "Invalid target: $TARGET_ENV (expected staging or production)"
exit 1
;;
esac
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
echo "=== Step 1: Download from source ==="
lokalise2 file download \
--token "$SOURCE_TOKEN" \
--project-id "$SOURCE_PROJECT" \
--format json \
--original-filenames=true \
--directory-prefix="" \
--export-empty-as=skip \
--unzip-to
SOURCE_FILE=
[[ ! -f ]];
1
SOURCE_KEY_COUNT=$(jq )
locale_file /*.json;
locale=$( .json)
key_count=$(jq )
coverage=$((key_count * / SOURCE_KEY_COUNT))
[[ == && -lt 100 ]];
1
[[ == && -lt 80 ]];
locale_file /*.json;
locale=$( .json)
lokalise2 file upload \
--token \
--project-id \
--file \
--lang-iso \
--replace-modified \
--poll \
--poll-timeout 120s
0.2
Output
After applying this skill, the project will have:
- Environment-aware Lokalise configuration module (
src/config/lokalise.ts)
- Per-environment API tokens stored in the chosen secret manager
- GitHub Actions workflows with environment-specific secrets
- Promotion script for moving translations through the dev/staging/prod pipeline
- (If using branching) Branch management utilities for the single-project approach
Error Handling
| Issue | Cause | Solution |
|---|
LOKALISE_API_TOKEN is not set | Missing environment variable | Verify secret injection in deployment config |
| Wrong translations in production | Using dev project ID | Audit LOKALISE_PROJECT_ID per environment; never share project IDs across environments |
| Cross-env data leak | Shared API token with write access to multiple projects | Create separate tokens per environment with project-scoped permissions |
| Secret rotation breaks CI | Old token in GitHub Secrets | Rotate in Lokalise first, update GitHub Secret, verify CI run |
| Branch merge conflict | Same key edited in multiple branches | Resolve in Lokalise UI or use force_conflict_resolve_using |
| Promotion blocked at 80% | Coverage gate in staging | Expected — translate remaining keys in dev before promoting |
| Rate limit during promotion | Uploading many files sequentially | Add sleep 0.2 between uploads; batch files if possible |
Examples
Quick Environment Check
import { getLokaliseConfig } from './config/lokalise';
const config = getLokaliseConfig();
console.log(`Environment: ${config.environment}`);
console.log(`Project ID: ${config.projectId}`);
console.log(`Cache TTL: ${config.cacheTtlMs}ms`);
console.log(`OTA enabled: ${config.enableOta}`);
Startup Validation with Zod
import { z } from 'zod';
import { getLokaliseConfig } from './config/lokalise';
const configSchema = z.object({
environment: z.enum(['development', 'staging', 'production']),
apiToken: z.string().min(30, 'LOKALISE_API_TOKEN looks too short — check the value'),
projectId: z.string().regex(/^\w+\.\w+$/, 'LOKALISE_PROJECT_ID should be in format: projectId.branchSuffix'),
cacheTtlMs: z.number().min(0),
enableOta: z.boolean(),
fallbackLocale: z.string().min(2),
rateLimitPerSec: z.number().min(1).max(6),
});
const config = configSchema.parse(getLokaliseConfig());
Environment Matrix for .env Files
LOKALISE_API_TOKEN=dev-token-here
LOKALISE_PROJECT_ID=123456789.dev
LOKALISE_BRANCH=dev
LOKALISE_API_TOKEN=staging-token-here
LOKALISE_PROJECT_ID=123456789.staging
LOKALISE_BRANCH=staging
LOKALISE_API_TOKEN=prod-token-here
LOKALISE_PROJECT_ID=987654321.prod
Add .env.* to .gitignore. Never commit tokens.
Resources
Next Steps
- Set up
lokalise-ci-integration for automated upload/download in CI
- Run
lokalise-prod-checklist before your first production deployment
- Use
lokalise-reference-architecture to establish the full i18n project structure