Claude-code-plugins-plus-skills lokalise-core-workflow-b
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/lokalise-pack/skills/lokalise-core-workflow-b" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-lokalise-core-workflow-b && rm -rf "$T"
plugins/saas-packs/lokalise-pack/skills/lokalise-core-workflow-b/SKILL.mdLokalise Core Workflow B
Overview
Everything on the "Lokalise to app" side: download translated files, manage translations and review status, leverage translation memory, manage contributors and their language access, and handle format differences across JSON, XLIFF, and PO files.
Prerequisites
- Lokalise API token exported as
LOKALISE_API_TOKEN - Lokalise project ID exported as
LOKALISE_PROJECT_ID
installed for SDK examples@lokalise/node-api
CLI installed for CLI exampleslokalise2
available for extracting download bundlesunzip
Instructions
- Download translated files. The download endpoint returns an S3 URL to a zip bundle — request the bundle, download the zip, then extract.
SDK — Download and extract:
import { LokaliseApi } from "@lokalise/node-api"; import { execSync } from "node:child_process"; import { mkdirSync } from "node:fs"; const client = new LokaliseApi({ apiKey: process.env.LOKALISE_API_TOKEN! }); const PROJECT_ID = process.env.LOKALISE_PROJECT_ID!; // Request the download bundle const download = await client.files().download(PROJECT_ID, { format: "json", original_filenames: false, bundle_structure: "%LANG_ISO%.json", // Output: en.json, fr.json, de.json filter_langs: ["en", "fr", "de", "es"], // Only these languages export_empty_as: "base", // Use base language for empty translations include_tags: ["release-3.0"], // Only keys with this tag replace_breaks: false, }); const bundleUrl = download.bundle_url; console.log(`Bundle URL: ${bundleUrl}`); // Download and extract mkdirSync("./locales", { recursive: true }); execSync(`curl -sL "${bundleUrl}" -o /tmp/lokalise-bundle.zip`); execSync(`unzip -o /tmp/lokalise-bundle.zip -d ./locales`); console.log("Translations extracted to ./locales/");
CLI — Download with structure:
set -euo pipefail lokalise2 --token "$LOKALISE_API_TOKEN" file download \ --project-id "$LOKALISE_PROJECT_ID" \ --format json \ --original-filenames=false \ --bundle-structure "locales/%LANG_ISO%.json" \ --filter-langs "en,fr,de,es" \ --export-empty-as base \ --replace-breaks=false \ --unzip-to .
SDK — Download with original file structure preserved:
const download = await client.files().download(PROJECT_ID, { format: "json", original_filenames: true, directory_prefix: "", // No extra prefix export_empty_as: "skip", // Omit untranslated keys include_comments: false, include_description: false, });
- Manage translations — list, update, and mark as reviewed.
SDK — List translations for a language:
const frTranslations = await client.translations().list({ project_id: PROJECT_ID, filter_lang_id: 673, // Language ID for French (find via languages endpoint) filter_is_reviewed: 0, // Only unreviewed limit: 100, }); for (const t of frTranslations.items) { console.log(`[${t.key_id}] ${t.translation} (reviewed: ${t.is_reviewed})`); }
SDK — Update a translation:
const updated = await client.translations().update(TRANSLATION_ID, { project_id: PROJECT_ID, translation: "Nouvelle traduction", is_reviewed: false, // Mark as needing review after edit });
SDK — Mark translations as reviewed (batch):
const unreviewed = await client.translations().list({ project_id: PROJECT_ID, filter_lang_id: LANG_ID, filter_is_reviewed: 0, limit: 500, }); for (const t of unreviewed.items) { await client.translations().update(t.translation_id, { project_id: PROJECT_ID, is_reviewed: true, }); } console.log(`Marked ${unreviewed.items.length} translations as reviewed`);
SDK — List translations with cursor pagination (for large datasets):
async function* paginateTranslations( client: LokaliseApi, projectId: string, langId: number ) { let cursor: string | undefined; do { const params: Record<string, unknown> = { project_id: projectId, filter_lang_id: langId, limit: 500, }; if (cursor) params.cursor = cursor; const page = await client.translations().list(params); yield* page.items; cursor = page.hasNextCursor() ? page.nextCursor() : undefined; } while (cursor); } // Usage for await (const t of paginateTranslations(client, PROJECT_ID, 673)) { console.log(`${t.key_id}: ${t.translation}`); }
- Leverage translation memory (TM) for auto-suggestions based on previously translated segments.
SDK — Use TM during upload:
const tmResults = await client.translationProviders().list({ team_id: TEAM_ID, }); // TM is automatically applied during file upload when `use_automations: true` const upload = await client.files().upload(PROJECT_ID, { data: base64Data, filename: "en.json", lang_iso: "en", use_automations: true, // Apply TM and MT suggestions automatically slashn_to_linebreak: true, });
SDK — Leverage TM during download (pre-translate empty keys):
// Pre-translate uses TM + MT before download // First, trigger pre-translation // Then download with filled translations const download = await client.files().download(PROJECT_ID, { format: "json", original_filenames: false, bundle_structure: "%LANG_ISO%.json", export_empty_as: "base", // Fallback to base language if TM has no match });
- Manage contributors — add translators and configure language access.
SDK — Add a translator with specific language access:
const contributor = await client.contributors().create({ project_id: PROJECT_ID, contributors: [ { email: "translator@example.com", fullname: "Marie Dupont", is_admin: false, is_reviewer: true, languages: [ { lang_iso: "fr", is_writable: true, // Can edit French translations }, { lang_iso: "de", is_writable: false, // Read-only access to German }, ], }, ], }); console.log(`Added contributor: ${contributor.items[0].email}`);
SDK — List all contributors:
const contributors = await client.contributors().list({ project_id: PROJECT_ID, limit: 100, }); for (const c of contributors.items) { const langs = c.languages.map( (l: { lang_iso: string; is_writable: boolean }) => `${l.lang_iso}${l.is_writable ? "(rw)" : "(r)"}` ).join(", "); console.log(`${c.fullname} <${c.email}> — ${langs}`); }
SDK — Update contributor permissions:
await client.contributors().update(CONTRIBUTOR_ID, { project_id: PROJECT_ID, is_reviewer: true, languages: [ { lang_iso: "fr", is_writable: true }, { lang_iso: "es", is_writable: true }, // Grant Spanish write access ], });
- Handle file format differences across JSON flat, JSON nested, XLIFF, and PO.
JSON flat (react-i18next default):
{ "greeting.hello": "Hello", "greeting.goodbye": "Goodbye", "errors.network": "Network error" }
Download config:
const download = await client.files().download(PROJECT_ID, { format: "json", json_unescaped_slashes: true, original_filenames: false, bundle_structure: "%LANG_ISO%.json", placeholder_format: "icu", // {name} style export_sort: "a_z", });
JSON nested (next-intl, vue-i18n):
{ "greeting": { "hello": "Hello", "goodbye": "Goodbye" }, "errors": { "network": "Network error" } }
Download config — use
_ as key separator so Lokalise nests on .:
set -euo pipefail lokalise2 --token "$LOKALISE_API_TOKEN" file download \ --project-id "$LOKALISE_PROJECT_ID" \ --format json \ --original-filenames=false \ --bundle-structure "%LANG_ISO%.json" \ --export-key-name-as "key_name_dot_separated" \ --unzip-to ./locales
XLIFF 1.2 (iOS, Angular):
const download = await client.files().download(PROJECT_ID, { format: "xliff", original_filenames: false, bundle_structure: "%LANG_ISO%.xliff", export_empty_as: "empty", });
PO / GNU gettext:
set -euo pipefail lokalise2 --token "$LOKALISE_API_TOKEN" file download \ --project-id "$LOKALISE_PROJECT_ID" \ --format po \ --original-filenames=false \ --bundle-structure "%LANG_ISO%/LC_MESSAGES/messages.po" \ --unzip-to ./locales
Upload format auto-detection:
// Lokalise detects format from filename extension // Just make sure the filename matches the content format await client.files().upload(PROJECT_ID, { data: base64Data, filename: "messages.xliff", // Triggers XLIFF parser lang_iso: "en", });
Output
- Downloaded translation files extracted to project directory
- Translations updated and review status managed
- Contributors added with appropriate language permissions
- Files exported in the correct format for your i18n framework
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Wrong | Run to verify |
| No translations match filters | Remove / to broaden |
| Unsupported export format string | Use: , , , , , |
| Large project with many languages | Filter to specific languages with |
| Contributor lacks write access | Check contributor language permissions |
| S3 bundle URL expired (valid ~30 min) | Request a fresh download URL |
Examples
Build-Time Translation Fetch
// scripts/fetch-translations.ts — run in CI before build import { LokaliseApi } from "@lokalise/node-api"; import { execSync } from "node:child_process"; import { mkdirSync, readdirSync } from "node:fs"; const client = new LokaliseApi({ apiKey: process.env.LOKALISE_API_TOKEN! }); const PROJECT_ID = process.env.LOKALISE_PROJECT_ID!; const download = await client.files().download(PROJECT_ID, { format: "json", original_filenames: false, bundle_structure: "%LANG_ISO%.json", export_empty_as: "base", export_sort: "a_z", replace_breaks: false, }); mkdirSync("./src/locales", { recursive: true }); execSync(`curl -sL "${download.bundle_url}" -o /tmp/i18n.zip`); execSync("unzip -o /tmp/i18n.zip -d ./src/locales"); const files = readdirSync("./src/locales").filter((f) => f.endsWith(".json")); console.log(`Downloaded ${files.length} locale files: ${files.join(", ")}`);
Contributor Onboarding Script
const translators = [ { email: "hans@example.com", name: "Hans Mueller", langs: ["de"] }, { email: "yuki@example.com", name: "Yuki Tanaka", langs: ["ja"] }, { email: "ana@example.com", name: "Ana Garcia", langs: ["es", "pt"] }, ]; for (const t of translators) { await client.contributors().create({ project_id: PROJECT_ID, contributors: [{ email: t.email, fullname: t.name, is_admin: false, is_reviewer: false, languages: t.langs.map((l) => ({ lang_iso: l, is_writable: true })), }], }); console.log(`Invited ${t.name} for ${t.langs.join(", ")}`); }
Resources
- File Download API
- Translations API
- Contributors API
- Export Options Reference
- Bundle Structure Placeholders
- Supported File Formats
Next Steps
For common errors and troubleshooting, see
lokalise-common-errors. For production SDK patterns, see lokalise-sdk-patterns.