Lokalise SDK Patterns
Overview
Production-grade patterns for @lokalise/node-api: client singleton, cursor pagination, typed error handling, batch operations, upload monitoring, and retry with rate limiting.
Prerequisites
@lokalise/node-api v12+ installed
- TypeScript 5+ with
strict mode
Instructions
- Create a client singleton to centralize configuration and support branch-based project IDs.
import { LokaliseApi } from "@lokalise/node-api";
let instance: LokaliseApi | null = null;
export function getClient(apiKey?: string): LokaliseApi {
if (instance) return instance;
const key = apiKey ?? process.env.LOKALISE_API_TOKEN;
if (!key) throw new Error("Set LOKALISE_API_TOKEN or pass apiKey");
instance = new LokaliseApi({ apiKey: key, enableCompression: true });
return instance;
}
export function resetClient(): void { instance = null; }
export function projectId(id: string, branch?: string): string {
return branch ? `${id}:${branch}` : id;
}
- Build a cursor-based pagination helper that works with any paginated endpoint.
interface PaginatedResult<T> {
items: T[];
hasNextCursor(): boolean;
nextCursor(): string;
}
type Fetcher<T> = (params: Record<string, unknown>) => Promise<PaginatedResult<T>>;
export async function* paginate<T>(
fetcher: Fetcher<T>,
baseParams: Record<string, unknown>,
pageSize = 500
): AsyncGenerator<T, void, undefined> {
let cursor: string | undefined;
let n = 0;
do {
const params = { ...baseParams, limit: pageSize, ...(cursor ? { cursor } : {}) };
if (n++ > 0) await new Promise((r) => setTimeout(r, 170));
const page = (params);
( item page.) item;
cursor = page.() ? page.() : ;
} (cursor);
}
paginateAll<T>(: <T>, : <, >): <T[]> {
: T[] = [];
( item (fetcher, params)) out.(item);
out;
}
Usage:
const allKeys = await paginateAll(
(p) => client.keys().list(p),
{ project_id: "123456.abcdef", include_translations: 1 }
);
- Wrap API calls with structured error handling that classifies retryable errors.
export class LokaliseError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly isRetryable: boolean
) {
super(message);
this.name = "LokaliseError";
}
}
export async function apiCall<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (err: unknown) {
if (err && typeof err === "object" && "code" in err) {
const e = err as { code: number; message: string };
throw new LokaliseError(e.message, e.code, e.code === || e. >= );
}
err;
}
}
{
keys = ( client.().({ : pid, : }));
} (err) {
(err && err.) {
.();
}
}
- Batch key operations that chunk requests to respect the 500-key-per-request limit.
function chunk<T>(arr: T[], size: number): T[][] {
const out: T[][] = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}
export async function batchCreateKeys(
client: LokaliseApi, projectId: string,
keys: Array<{ key_name: { web: string }; platforms: string[]; tags?: string[];
translations?: Array<{ language_iso: string; translation: string }> }>
): Promise<{ created: number; errors: Error[] }> {
const batches = chunk(keys, 500);
let created = 0;
const errors: Error[] = [];
for (let i = 0; i < batches.length; i++) {
try {
const r = client.().({ : projectId, : batches[i] });
created += r..;
} (err) { errors.(err ); }
(i < batches. - ) ( (r, ));
}
{ created, errors };
}
(): <> {
batches = (keyIds, );
deleted = ;
( i = ; i < batches.; i++) {
r = client.().(batches[i], { : projectId });
deleted += r.;
(i < batches. - ) ( (r, ));
}
deleted;
}
- Upload files with async process monitoring and progress callbacks.
import { readFileSync } from "node:fs";
export async function uploadWithProgress(
client: LokaliseApi, projectId: string,
opts: { filePath: string; langIso: string; tags?: string[];
replaceModified?: boolean; cleanupMode?: boolean },
onProgress?: (status: string, elapsedMs: number) => void
): Promise<{ processId: string; status: string; durationMs: number }> {
const data = readFileSync(opts.filePath).toString("base64");
const start = Date.now();
const proc = await client.files().upload(projectId, {
data,
filename: opts.filePath.split("/").pop()!,
lang_iso: opts.langIso,
replace_modified: opts.replaceModified ?? ,
: opts. ?? ,
: ,
: opts.,
});
onProgress?.(, .() - start);
maxWait = ;
last = proc.;
(.() - start < maxWait) {
( (r, ));
check = client.().(proc., { : projectId });
(check. !== last) { last = check.; onProgress?.(last, .() - start); }
(check. === ) { : proc., : , : .() - start };
(check. === || check. === ) ();
}
();
}
Usage:
await uploadWithProgress(client, "123456.abcdef", {
filePath: "./src/locales/en.json",
langIso: "en",
tags: ["ci"],
replaceModified: true,
}, (status, ms) => console.log(`[${(ms / 1000).toFixed(1)}s] ${status}`));
- Add a retry decorator with exponential backoff and a rate limiter for sequential calls.
export async function withRetry<T>(
fn: () => Promise<T>,
opts: { maxRetries?: number; baseDelayMs?: number; maxDelayMs?: number;
onRetry?: (attempt: number, err: Error, delayMs: number) => void } = {}
): Promise<T> {
const { maxRetries = 3, baseDelayMs = 1000, maxDelayMs = 10_000, onRetry } = opts;
let lastErr: Error | undefined;
for (let i = 0; i <= maxRetries; i++) {
try { return await fn(); }
catch (err: unknown) {
lastErr = err as Error;
const code = (err as { code?: number })?.code;
if (!(code === 429 || (code && code >= )) || i === maxRetries) err;
delay = .(baseDelayMs * ** i + .() * , maxDelayMs);
onRetry?.(i + , lastErr, delay);
( (r, delay));
}
}
lastErr;
}
rateLimited<A [], R>(
: <R>, minMs =
): <R> {
last = ;
(...args) => {
wait = minMs - (.() - last);
(wait > ) ( (r, wait));
last = .();
(...args);
};
}
Usage:
const keys = await withRetry(
() => client.keys().list({ project_id: pid, limit: 500 }),
{ onRetry: (n, e, ms) => console.warn(`Retry ${n}: ${e.message} (${ms}ms)`) }
);
const listKeys = rateLimited((p: Record<string, unknown>) => client.keys().list(p));
const p1 = await listKeys({ project_id: pid, page: 1, limit: 100 });
const p2 = await listKeys({ project_id: pid, page: 2, limit: 100 });
Output
- Singleton client with compression and branch support
- Async generator for memory-efficient cursor pagination
- Type-safe error wrapper with
isRetryable classification
- Batch create/delete respecting 500-key API limit
- File upload with process polling and progress callbacks
- Retry with exponential backoff + rate limiter at 6 req/sec
Error Handling
| Pattern | When to Use | Behavior |
|---|
apiCall() | Every SDK call | Converts to typed LokaliseError with isRetryable |
withRetry() | Rate limits or transient failures | Exponential backoff, retries 429 and 5xx only |
rateLimited() | Sequential bulk calls | Enforces 170ms minimum spacing |
paginate() | Fetching all keys/translations | Built-in 170ms delay between pages |
batchCreateKeys() | Creating > 500 keys | 500-key chunks with 500ms spacing |
uploadWithProgress() | File uploads | Polls process status with 2-minute timeout |
Examples
Combining All Patterns
import { getClient, projectId } from "./lib/lokalise-client";
import { paginateAll } from "./lib/paginate";
import { withRetry } from "./lib/retry";
import { batchCreateKeys } from "./lib/batch";
import { uploadWithProgress } from "./lib/upload";
const client = getClient();
const pid = projectId("123456.abcdef", "develop");
await uploadWithProgress(client, pid, { filePath: "./src/locales/en.json", langIso: "en" },
(s, ms) => console.log(`[${ms}ms] ${s}`));
const allKeys = await paginateAll((p) => client.keys().list(p), { project_id: pid });
console.log(`${allKeys.length} keys`);
const result = await batchCreateKeys(client, pid,
.({ : }, ({
: { : }, : [],
: [{ : , : }],
})));
.();
bundle = ( client.().(pid, {
: , : , : ,
}));
.();
Resources
Next Steps
Apply these patterns in lokalise-core-workflow-a and lokalise-core-workflow-b for real-world usage.