Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Deploy Lokalise integrations to Vercel, Netlify, and Cloud Run platforms. Use when deploying apps with Lokalise translations to production, configuring platform-specific secrets, or setting up deployment pipelines. Trigger with phrases like "deploy lokalise", "lokalise Vercel", "lokalise production deploy", "lokalise Netlify", "lokalise Cloud Run".
.claude/skills/jeremylongshore-lokalise-deploy-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 103% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 148% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 346% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 162% | 0% |
Translations must be downloaded fresh during CI/CD builds to ensure production always ships the latest reviewed content. This skill covers downloading translations as a build step, GitHub Actions workflows for translation sync, Vercel and Netlify build plugin integration, OTA (over-the-air) updates for mobile apps via Lokalise's iOS and Android SDKs, and environment-specific translation bundles.
LOKALISE_API_TOKEN and LOKALISE_PROJECT_ID stored as CI secretscurl and unzip available in CI environment (standard on GitHub Actions runners)Add a pre-build script that pulls translations from Lokalise before your framework compiles:
bash#!/bin/bash # scripts/download-translations.sh set -euo pipefail PROJECT_ID="${LOKALISE_PROJECT_ID:?Missing LOKALISE_PROJECT_ID}" API_TOKEN="${LOKALISE_API_TOKEN:?Missing LOKALISE_API_TOKEN}" DEST_DIR="${1:-./src/locales}" echo "Downloading translations for project $PROJECT_ID..." BUNDLE_URL=$(curl -sf -X POST \ "https://api.lokalise.com/api2/projects/${PROJECT_ID}/files/download" \ -H "X-Api-Token: ${API_TOKEN}" \ -H "Content-Type: application/json" \ -d "{ \"format\": \"json\", \"original_filenames\": false, \"bundle_structure\": \"%LANG_ISO%.json\", \"export_empty_as\": \"base\", \"json_unescaped_slashes\": true, \"include_tags\": [\"production\"], \"filter_data\": [\"translated\", \"reviewed\"] }" | jq -r '.bundle_url') if [ -z "$BUNDLE_URL" ] || [ "$BUNDLE_URL" = "null" ]; then echo "ERROR: Failed to get bundle URL from Lokalise" exit 1 fi mkdir -p "$DEST_DIR" curl -sfL "$BUNDLE_URL" -o /tmp/translations.zip unzip -o /tmp/translations.zip -d "$DEST_DIR" rm /tmp/translations.zip FILE_COUNT=$(ls -1 "$DEST_DIR"/*.json 2>/dev/null | wc -l) echo "Downloaded $FILE_COUNT translation files to $DEST_DIR"
Wire it into package.json:
json{ "scripts": { "prebuild": "./scripts/download-translations.sh ./src/locales", "build": "next build" } }
Full workflow that downloads translations, builds, and deploys:
yaml# .github/workflows/deploy.yml name: Build & Deploy on: push: branches: [main] # Trigger from Lokalise webhook (via repository_dispatch) repository_dispatch: types: [translations_updated] jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - name: Install dependencies run: npm ci - name: Download translations from Lokalise env: LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }} LOKALISE_PROJECT_ID: ${{ secrets.LOKALISE_PROJECT_ID }} run: | chmod +x ./scripts/download-translations.sh ./scripts/download-translations.sh ./src/locales - name: Verify translation integrity run: | # Ensure all expected languages are present EXPECTED_LANGS="en fr de ja es" for lang in $EXPECTED_LANGS; do if [ ! -f "./src/locales/${lang}.json" ]; then echo "ERROR: Missing translation file for ${lang}" exit 1 fi # Validate JSON jq empty "./src/locales/${lang}.json" || { echo "ERROR: Invalid JSON in ${lang}.json" exit 1 } done echo "All translation files present and valid" - name: Build run: npm run build - name: Deploy to Vercel uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} vercel-args: --prod
To trigger builds when translations change, set up a Lokalise webhook that fires a GitHub repository_dispatch:
bash# In your webhook handler (see lokalise-webhooks-events) curl -X POST \ "https://api.github.com/repos/OWNER/REPO/dispatches" \ -H "Authorization: token ${GITHUB_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"event_type": "translations_updated"}'
For Vercel, translations download during the build phase. Configure the token as an environment variable:
bash# Set Lokalise secrets in Vercel vercel env add LOKALISE_API_TOKEN production preview vercel env add LOKALISE_PROJECT_ID production preview
In vercel.json, ensure the build command runs the translation download:
json{ "buildCommand": "./scripts/download-translations.sh ./src/locales && next build", "outputDirectory": ".next" }
For ISR/SSR apps that need translations at runtime (not just build time), cache translations in a KV store or download on cold start:
typescript// lib/translations.ts (Next.js example) import { unstable_cache } from "next/cache"; export const getTranslations = unstable_cache( async (locale: string) => { const res = await fetch( `https://api.lokalise.com/api2/projects/${process.env.LOKALISE_PROJECT_ID}/translations`, { headers: { "X-Api-Token": process.env.LOKALISE_API_TOKEN! }, } ); const data = await res.json(); return data.translations .filter((t: any) => t.language_iso === locale) .reduce( (acc: Record<string, string>, t: any) => ({ ...acc, [t.key_name]: t.translation, }), {} ); }, ["translations"], { revalidate: 3600, tags: ["translations"] } );
Netlify uses build plugins or the prebuild command. The simplest approach uses netlify.toml:
toml# netlify.toml [build] command = "./scripts/download-translations.sh ./src/locales && npm run build" publish = "dist" [build.environment] NODE_VERSION = "20"
Set secrets via Netlify CLI:
bashnetlify env:set LOKALISE_API_TOKEN "your-token" --scope builds netlify env:set LOKALISE_PROJECT_ID "123456789.abcdefgh" --scope builds
For a custom Netlify Build Plugin that integrates more deeply:
javascript// plugins/netlify-plugin-lokalise/index.js module.exports = { async onPreBuild({ utils, constants }) { const { execSync } = require("child_process"); try { console.log("Downloading translations from Lokalise..."); execSync("./scripts/download-translations.sh ./src/locales", { stdio: "inherit", env: process.env, }); } catch (error) { utils.build.failBuild("Failed to download translations from Lokalise"); } }, };
Over-the-air updates let you push translation changes without an app store release. Lokalise provides native SDKs for this.
iOS (Swift):
swift// AppDelegate.swift import Lokalise func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize with OTA SDK token and project ID Lokalise.shared.setProjectID( "123456789.abcdefgh", token: "ota-sdk-token-from-lokalise-dashboard" ) // Preemptively check for updates Lokalise.shared.checkForUpdates { updated, error in if let error = error { print("OTA update check failed: \(error.localizedDescription)") return } if updated { print("Translations updated OTA") } } return true } // Usage — works with NSLocalizedString automatically let welcome = NSLocalizedString("welcome.title", comment: "Welcome screen title")
Android (Kotlin):
kotlin// Application.kt import com.lokalise.sdk.Lokalise import com.lokalise.sdk.LokaliseCallback class MyApp : Application() { override fun onCreate() { super.onCreate() Lokalise.init(this) Lokalise.updateTranslations() // Optional: listen for update completion Lokalise.setUpdateCallback(object : LokaliseCallback { override fun onUpdated(oldBundleId: Long, newBundleId: Long) { Log.d("Lokalise", "Translations updated: $oldBundleId -> $newBundleId") } override fun onErrorOccurred(e: LokaliseException) { Log.e("Lokalise", "OTA update failed", e) } }) } } // Usage — strings.xml values are overridden by OTA bundles val welcome = getString(R.string.welcome_title)
Both SDKs fall back to the bundled translations if OTA download fails, so the app always has working strings.
Use tags in Lokalise to manage environment-specific content:
bash# Download only production-tagged translations ./scripts/download-translations.sh ./src/locales # uses "production" tag filter # For staging: modify the script or use an env var LOKALISE_TAGS="staging,beta" ./scripts/download-translations.sh ./src/locales
Update download-translations.sh to support dynamic tags:
bash# Add near the top of download-translations.sh TAGS="${LOKALISE_TAGS:-production}" TAG_JSON=$(echo "$TAGS" | jq -R 'split(",")' ) # Use in the curl payload: # "include_tags": $TAG_JSON
This lets you maintain separate translation sets:
| Issue | Cause | Solution | |-------|-------|----------| | Missing translations in build | download-translations.sh failed silently | Use set -euo pipefail and check bundle URL | | Secret not found in CI | Env var not configured | Add via vercel env add / netlify env:set / GitHub Secrets | | Build timeout | Large project with many languages | Filter with filter_langs and include_tags | | OTA fails on device | Network blocked or token invalid | SDKs fall back to bundled translations automatically | | Stale translations in production | Cache not invalidated | Use repository_dispatch webhook to trigger rebuild | | Empty JSON files | No translations match tag filter | Verify tag names match between Lokalise and script |
yaml# .github/workflows/sync-translations.yml name: Sync Translations on: schedule: - cron: "0 */6 * * *" # Every 6 hours workflow_dispatch: jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download translations env: LOKALISE_API_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }} LOKALISE_PROJECT_ID: ${{ secrets.LOKALISE_PROJECT_ID }} run: | chmod +x ./scripts/download-translations.sh ./scripts/download-translations.sh ./src/locales - name: Commit if changed run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add ./src/locales/ git diff --cached --quiet || git commit -m "chore: sync translations from Lokalise" git push
dockerfile# Dockerfile FROM node:20-slim AS builder WORKDIR /app COPY package*.json ./ RUN npm ci ARG LOKALISE_API_TOKEN ARG LOKALISE_PROJECT_ID COPY . . RUN chmod +x ./scripts/download-translations.sh \ && ./scripts/download-translations.sh ./src/locales \ && npm run build FROM node:20-slim WORKDIR /app COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public COPY --from=builder /app/package*.json ./ RUN npm ci --production EXPOSE 3000 CMD ["npm", "start"]
Build with: docker build --build-arg LOKALISE_API_TOKEN=$TOKEN --build-arg LOKALISE_PROJECT_ID=$PID .
For handling errors during API calls in your pipeline, see lokalise-common-errors. For managing translation data formats and encoding, see lokalise-data-handling.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 26,310 | 16,636 | -37% | 1 | 1 | 0% | 4,386 | 5,981 | +36% | 0 | 0 | — |
case-02 | fail→fail | 20,013 | 15,924 | -20% | 1 | 1 | 0% | 3,124 | 5,931 | +90% | 0 | 0 | — |
case-03 | fail→pass | 22,003 | 20,024 | -9% | 1 | 1 | 0% | 3,150 | 6,382 | +103% | 0 | 0 | — |
case-04 | fail→fail | 20,379 | 19,417 | -5% | 1 | 1 | 0% | 2,538 | 6,146 | +142% | 0 | 0 | — |
case-05 | pass→pass | 18,253 | 15,229 | -17% | 1 | 1 | 0% | 2,300 | 5,490 | +139% | 0 | 0 | — |
case-06 | pass→pass | 20,267 | 14,098 | -30% | 1 | 1 | 0% | 2,060 | 4,919 | +139% | 0 | 0 | — |
case-07 | fail→pass | 17,758 | 10,510 | -41% | 1 | 1 | 0% | 1,751 | 4,337 | +148% | 0 | 0 | — |
case-08 | fail→pass | 9,914 | 9,698 | -2% | 1 | 1 | 0% | 940 | 4,191 | +346% | 0 | 0 | — |
case-09 | pass→pass | 7,677 | 9,875 | +29% | 1 | 1 | 0% | 1,128 | 4,265 | +278% | 0 | 0 | — |
case-10 | pass→pass | 18,958 | 21,585 | +14% | 1 | 1 | 0% | 2,367 | 5,968 | +152% | 0 | 0 | — |
case-11 | pass→pass | 9,195 | 4,819 | -48% | 1 | 1 | 0% | 1,590 | 4,303 | +171% | 0 | 0 | — |
case-12 | fail→fail | 28,048 | 17,545 | -37% | 1 | 1 | 0% | 3,202 | 6,641 | +107% | 0 | 0 | — |
case-13 | fail→fail | 9,991 | 4,183 | -58% | 1 | 1 | 0% | 1,432 | 4,273 | +198% | 0 | 0 | — |
case-14 | pass→pass | 4,961 | 4,708 | -5% | 1 | 1 | 0% | 886 | 4,357 | +392% | 0 | 0 | — |
case-15 | pass→pass | 11,468 | 9,185 | -20% | 1 | 1 | 0% | 1,125 | 4,173 | +271% | 0 | 0 | — |
case-16 | pass→pass | 18,159 | 13,645 | -25% | 1 | 1 | 0% | 2,369 | 6,061 | +156% | 0 | 0 | — |
case-17 | fail→fail | 22,860 | 10,428 | -54% | 1 | 1 | 0% | 2,470 | 5,247 | +112% | 0 | 0 | — |
case-18 | fail→pass | 14,760 | 13,810 | -6% | 1 | 1 | 0% | 2,064 | 5,414 | +162% | 0 | 0 | — |
case-19 | fail→pass | 21,685 | 21,252 | -2% | 1 | 1 | 0% | 2,511 | 6,038 | +140% | 0 | 0 | — |
case-20 | pass→pass | 9,109 | 5,227 | -43% | 1 | 1 | 0% | 1,596 | 4,541 | +185% | 0 | 0 | — |
case-21 | pass→fail | 19,894 | 12,948 | -35% | 1 | 1 | 0% | 2,768 | 6,081 | +120% | 0 | 0 | — |
case-22 | pass→pass | 12,037 | 6,308 | -48% | 1 | 1 | 0% | 1,204 | 4,776 | +297% | 0 | 0 | — |
case-23 | pass→pass | 16,096 | 11,243 | -30% | 1 | 1 | 0% | 1,960 | 5,191 | +165% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.