Claude-code-plugins-plus-skills lokalise-deploy-integration
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-deploy-integration" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-lokalise-deploy-integration && rm -rf "$T"
plugins/saas-packs/lokalise-pack/skills/lokalise-deploy-integration/SKILL.mdLokalise Deploy Integration
Overview
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.
Prerequisites
- Lokalise API token with download permissions (read-only token recommended for CI)
andLOKALISE_API_TOKEN
stored as CI secretsLOKALISE_PROJECT_ID
andcurl
available in CI environment (standard on GitHub Actions runners)unzip- For OTA: Lokalise OTA SDK token (separate from API token, generated in Lokalise dashboard)
Instructions
1. Download Translations in the Build Step
Add a pre-build script that pulls translations from Lokalise before your framework compiles:
#!/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:
{ "scripts": { "prebuild": "./scripts/download-translations.sh ./src/locales", "build": "next build" } }
2. GitHub Actions Workflow
Full workflow that downloads translations, builds, and deploys:
# .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:
# 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"}'
3. Vercel Build Integration
For Vercel, translations download during the build phase. Configure the token as an environment variable:
# 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:
{ "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:
// 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"] } );
4. Netlify Build Integration
Netlify uses build plugins or the
prebuild command. The simplest approach uses netlify.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:
netlify 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:
// 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"); } }, };
5. OTA Updates for Mobile (iOS/Android)
Over-the-air updates let you push translation changes without an app store release. Lokalise provides native SDKs for this.
iOS (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):
// 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.
6. Environment-Specific Translation Bundles
Use tags in Lokalise to manage environment-specific content:
# 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:
# 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:
- production — fully reviewed, stable translations
- staging — includes new translations under review
- beta — experimental copy for A/B testing
Output
- CI/CD pipeline downloading fresh translations before every build
- GitHub Actions workflow with translation validation and deployment
- Vercel/Netlify configured with Lokalise secrets and build commands
- Mobile apps receiving OTA translation updates without app store releases
- Environment-specific bundles controlled by Lokalise tags
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Missing translations in build | failed silently | Use and check bundle URL |
| Secret not found in CI | Env var not configured | Add via / / GitHub Secrets |
| Build timeout | Large project with many languages | Filter with and |
| OTA fails on device | Network blocked or token invalid | SDKs fall back to bundled translations automatically |
| Stale translations in production | Cache not invalidated | Use webhook to trigger rebuild |
| Empty JSON files | No translations match tag filter | Verify tag names match between Lokalise and script |
Examples
Minimal GitHub Action (Translation Sync Only)
# .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
Docker Build with Translations
# 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 .
Resources
- Lokalise Files API — Download
- Lokalise OTA — iOS SDK
- Lokalise OTA — Android SDK
- Lokalise CI/CD Guide
- GitHub Actions — Repository Dispatch
Next Steps
For handling errors during API calls in your pipeline, see
lokalise-common-errors. For managing translation data formats and encoding, see lokalise-data-handling.