Lokalise
"Lokalise: translation management, API, SDKs, OTA updates, branching, screenshots, plural forms, CLI"
Lokalise is a translation management platform built around developer-centric workflows. Keys and translations live in Lokalise's cloud, synced to your repository via CLI or API. The platform supports branching that mirrors your Git branches, so feature work and translations stay aligned. OTA (Over-The-Air) updates deliver translation changes without app redeployment. Screenshots can be attached to keys so translators see exactly where strings appear. Lokalise's API is RESTful and comprehensive, covering keys, translations, files, contributors, and webhooks, enabling full automation of the localization pipeline. ## Key Points - Use `distinguish_by_file: true` on upload so keys with the same name in different files do not collide. - Attach screenshots to keys so translators see the exact UI context — automate this with E2E test runs. - Use Lokalise branches that mirror Git branches; merge them together when the feature PR merges. - Set `apply_tm: true` on uploads to auto-fill translations from translation memory. - Export with `export_empty_as: "skip"` so untranslated keys fall back to the source language at runtime. - Use tags to organize keys by feature, release, or priority, and filter exports by tag. - Set up webhooks to trigger CI pipelines when translations reach 100% for a language. - Use the `replace_modified` flag to update changed source strings without losing existing translations. - Paginate API calls with `limit: 500` to stay within rate limits on large projects. - Use `placeholder_format: "icu"` and `plural_format: "icu"` on export for consistency with i18n frameworks. - Do not store the Lokalise API token in source code — use environment variables or a secrets manager. - Avoid uploading binary files (images, PDFs) as source files; Lokalise is for string-based content.
skilldb get i18n-services-skills/LokaliseFull skill: 355 linesLokalise
Core Philosophy
Lokalise is a translation management platform built around developer-centric workflows. Keys and translations live in Lokalise's cloud, synced to your repository via CLI or API. The platform supports branching that mirrors your Git branches, so feature work and translations stay aligned. OTA (Over-The-Air) updates deliver translation changes without app redeployment. Screenshots can be attached to keys so translators see exactly where strings appear. Lokalise's API is RESTful and comprehensive, covering keys, translations, files, contributors, and webhooks, enabling full automation of the localization pipeline.
Setup
CLI installation and project initialization
// Install Lokalise CLI
// npm install -g @lokalise/cli
// Environment setup
// export LOKALISE_API_TOKEN=your_api_token
// export LOKALISE_PROJECT_ID=123456789abcdef.01
// lokalise.yml equivalent — CLI flags:
// lokalise2 file upload \
// --project-id $LOKALISE_PROJECT_ID \
// --file src/locales/en.json \
// --lang-iso en \
// --replace-modified \
// --include-path \
// --distinguish-by-file \
// --poll
API client setup
// npm install @lokalise/node-api
import { LokaliseApi } from "@lokalise/node-api";
const lokaliseApi = new LokaliseApi({
apiKey: process.env.LOKALISE_API_TOKEN!,
});
const PROJECT_ID = process.env.LOKALISE_PROJECT_ID!;
OTA SDK setup for web applications
// npm install @lokalise/sdk-web
import { LokaliseOta } from "@lokalise/sdk-web";
const otaClient = new LokaliseOta({
projectId: process.env.LOKALISE_OTA_PROJECT_ID!,
token: process.env.LOKALISE_OTA_TOKEN!,
});
async function loadTranslations(
lang: string
): Promise<Record<string, string>> {
const bundle = await otaClient.getTranslations(lang);
return bundle;
}
// Preload multiple languages
async function preloadLanguages(langs: string[]): Promise<void> {
await Promise.all(langs.map((lang) => otaClient.getTranslations(lang)));
}
Key Techniques
Managing keys via API
interface TranslationKey {
key_name: string;
description: string;
platforms: string[];
translations: Array<{
language_iso: string;
translation: string;
}>;
tags: string[];
}
async function createKeys(keys: TranslationKey[]): Promise<void> {
const payload = keys.map((key) => ({
key_name: key.key_name,
description: key.description,
platforms: key.platforms,
translations: key.translations,
tags: key.tags,
}));
await lokaliseApi.keys().create(payload, { project_id: PROJECT_ID });
}
async function listKeys(
page: number = 1,
limit: number = 500
): Promise<void> {
const keys = await lokaliseApi.keys().list({
project_id: PROJECT_ID,
page,
limit,
include_translations: 1,
filter_platforms: "web",
});
for (const key of keys.items) {
console.log(`${key.key_name}: ${key.translations.length} translations`);
}
}
async function updateKey(
keyId: number,
updates: { description?: string; tags?: string[] }
): Promise<void> {
await lokaliseApi.keys().update(keyId, updates, {
project_id: PROJECT_ID,
});
}
File upload and download
import * as fs from "fs";
import * as path from "path";
async function uploadFile(
filePath: string,
langIso: string
): Promise<string> {
const fileContent = fs.readFileSync(filePath, "base64");
const process = await lokaliseApi.files().upload(PROJECT_ID, {
data: fileContent,
filename: path.basename(filePath),
lang_iso: langIso,
replace_modified: true,
distinguish_by_file: true,
apply_tm: true,
cleanup_mode: false,
tags: ["auto-upload"],
});
// Upload is async — returns a process ID
return process.process_id;
}
async function downloadFiles(lang: string): Promise<string> {
const response = await lokaliseApi.files().download(PROJECT_ID, {
format: "json",
filter_langs: [lang],
original_filenames: true,
directory_prefix: "%LANG_ISO%/",
placeholder_format: "icu",
plural_format: "icu",
export_empty_as: "skip",
include_tags: ["production"],
});
// Returns a URL to a ZIP archive
return response.bundle_url;
}
async function downloadAndExtract(outputDir: string): Promise<void> {
const langs = ["en", "fr", "de", "ja"];
for (const lang of langs) {
const bundleUrl = await downloadFiles(lang);
const response = await fetch(bundleUrl);
const buffer = Buffer.from(await response.arrayBuffer());
const zipPath = path.join(outputDir, `${lang}.zip`);
fs.writeFileSync(zipPath, buffer);
// Extract with your preferred zip library
}
}
Branching for feature work
async function createBranch(branchName: string): Promise<void> {
await lokaliseApi.branches().create({ name: branchName }, {
project_id: PROJECT_ID,
});
}
async function mergeBranch(branchId: number): Promise<void> {
await lokaliseApi.branches().merge(branchId, {
project_id: PROJECT_ID,
}, {
force_current_branch_conflict_resolution: false,
target_branch_id: 0, // 0 = main branch
});
}
// CI workflow: create a branch per feature PR
async function syncFeatureBranch(gitBranch: string): Promise<void> {
const branches = await lokaliseApi.branches().list({
project_id: PROJECT_ID,
});
const existing = branches.items.find((b) => b.name === gitBranch);
if (!existing) {
await createBranch(gitBranch);
}
// Upload source file to the branch
// Branch-scoped project ID format: PROJECT_ID:branch_name
const branchProjectId = `${PROJECT_ID}:${gitBranch}`;
const fileContent = fs.readFileSync("src/locales/en.json", "base64");
await lokaliseApi.files().upload(branchProjectId, {
data: fileContent,
filename: "en.json",
lang_iso: "en",
replace_modified: true,
});
}
Plural forms and platform keys
// Lokalise supports ICU plural syntax natively.
// When uploading JSON with ICU plurals:
{
"cart_items": "{count, plural, =0 {Your cart is empty} one {# item in cart} other {# items in cart}}"
}
// For platform-specific keys, Lokalise tracks separate values per platform:
async function createPlatformKey(): Promise<void> {
await lokaliseApi.keys().create(
[
{
key_name: {
web: "btn.submit",
ios: "btn_submit",
android: "btn_submit",
},
platforms: ["web", "ios", "android"],
translations: [
{ language_iso: "en", translation: "Submit" },
{ language_iso: "fr", translation: "Soumettre" },
],
},
],
{ project_id: PROJECT_ID }
);
}
Screenshots for translator context
async function uploadScreenshot(
imagePath: string,
keyIds: number[]
): Promise<void> {
const imageData = fs.readFileSync(imagePath, "base64");
await lokaliseApi.screenshots().create(
[
{
data: imageData,
title: path.basename(imagePath, path.extname(imagePath)),
description: "Captured from staging environment",
key_ids: keyIds,
},
],
{ project_id: PROJECT_ID }
);
}
// Automate screenshot capture during E2E tests
async function captureAndUpload(
page: any, // Playwright Page
keyIds: number[],
name: string
): Promise<void> {
const screenshotBuffer = await page.screenshot({ fullPage: false });
const base64 = screenshotBuffer.toString("base64");
await lokaliseApi.screenshots().create(
[{ data: base64, title: name, key_ids: keyIds }],
{ project_id: PROJECT_ID }
);
}
Webhooks for automation
import type { Request, Response } from "express";
interface LokaliseWebhookPayload {
event: string;
project: { id: string; name: string };
language: { id: number; iso: string; name: string };
user: { email: string; full_name: string };
key?: { id: number; name: string };
translation?: { id: number; value: string };
}
function handleLokaliseWebhook(req: Request, res: Response): void {
const payload = req.body as LokaliseWebhookPayload;
switch (payload.event) {
case "project.translation.updated":
console.log(
`Translation updated: ${payload.key?.name} in ${payload.language.iso}`
);
// Trigger OTA bundle rebuild or CI pipeline
break;
case "project.exported":
console.log("Export completed — downloading fresh translations");
break;
case "project.branch.merged":
console.log("Branch merged — syncing to main");
break;
}
res.status(200).json({ status: "ok" });
}
Best Practices
- Use
distinguish_by_file: trueon upload so keys with the same name in different files do not collide. - Attach screenshots to keys so translators see the exact UI context — automate this with E2E test runs.
- Use Lokalise branches that mirror Git branches; merge them together when the feature PR merges.
- Set
apply_tm: trueon uploads to auto-fill translations from translation memory. - Export with
export_empty_as: "skip"so untranslated keys fall back to the source language at runtime. - Use tags to organize keys by feature, release, or priority, and filter exports by tag.
- Set up webhooks to trigger CI pipelines when translations reach 100% for a language.
- Use the
replace_modifiedflag to update changed source strings without losing existing translations. - Paginate API calls with
limit: 500to stay within rate limits on large projects. - Use
placeholder_format: "icu"andplural_format: "icu"on export for consistency with i18n frameworks.
Anti-Patterns
- Do not store the Lokalise API token in source code — use environment variables or a secrets manager.
- Avoid uploading binary files (images, PDFs) as source files; Lokalise is for string-based content.
- Do not manually edit exported files — they will be overwritten on the next download cycle.
- Avoid creating duplicate keys by ignoring the
distinguish_by_fileoption on projects with multiple source files. - Do not skip the
pollflag on CLI uploads — without it, the CLI returns before the async processing finishes. - Avoid exporting all languages in a single call on large projects; batch by language to reduce timeout risk.
- Do not ignore Lokalise's QA warnings about missing placeholders or inconsistent plural forms.
- Avoid using the same project for web and mobile without platform-scoped key names — platform keys prevent conflicts.
Install this skill directly: skilldb add i18n-services-skills
Related Skills
Crowdin
"Crowdin: translation management, OTA content delivery, API, CLI, GitHub integration, in-context editing, glossaries"
I18next
"i18next: internationalization framework, React (react-i18next), namespaces, interpolation, plurals, backends, language detection"
Next Intl
"next-intl: Next.js internationalization, message catalogs, routing, middleware, server components, formatters, pluralization"
Phrase
"Phrase (formerly PhraseApp): translation management, API, CLI, OTA, in-context editor, branching, webhooks, glossaries"
Pontoon
"Mozilla Pontoon: open-source translation management, Fluent/FTL support, in-place editing, community localization, VCS sync"
Transifex
"Transifex: translation management, Native SDK, OTA delivery, API v3, CLI, GitHub/GitLab integration, ICU plurals"