Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configure Lokalise local development with file sync and hot reload. Use when setting up a development environment, configuring translation sync, or establishing a fast iteration cycle with Lokalise. Trigger with phrases like "lokalise dev setup", "lokalise local development", "lokalise dev environment", "develop with lokalise", "lokalise sync".
.claude/skills/jeremylongshore-lokalise-local-dev-loop/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 20% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 95% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 3% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 109% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 120% | 0% |
Set up a complete local development workflow with Lokalise: project structure for i18n files, CLI push/pull commands, file watching for auto-upload, mock translations for offline development, framework integration (React i18next, Vue i18n), and a pre-commit hook to keep translations synced.
LOKALISE_API_TOKENLOKALISE_PROJECT_IDlokalise2 CLI installedbundle_structure and most i18n frameworks.project-root/
├── src/
│ └── locales/
│ ├── en.json # Base language (source of truth)
│ ├── fr.json # Downloaded from Lokalise
│ ├── de.json
│ ├── es.json
│ └── index.ts # Barrel export + type definitions
├── scripts/
│ ├── i18n-push.sh # Upload source to Lokalise
│ ├── i18n-pull.sh # Download translations from Lokalise
│ └── i18n-mock.ts # Generate mock translations
├── .env.local # LOKALISE_API_TOKEN, LOKALISE_PROJECT_ID
└── package.json # i18n:push, i18n:pull, i18n:sync scriptsBarrel export with type safety (src/locales/index.ts):
typescriptimport en from "./en.json"; // Type derived from base language — all other locales must match this shape export type TranslationKeys = typeof en; export const defaultLocale = "en" as const; export const supportedLocales = ["en", "fr", "de", "es"] as const; export type Locale = (typeof supportedLocales)[number]; export async function loadLocale(locale: Locale): Promise<TranslationKeys> { const mod = await import(`./${locale}.json`); return mod.default; }
Push script (scripts/i18n-push.sh):
bash#!/usr/bin/env bash set -euo pipefail # Upload source language file to Lokalise lokalise2 --token "$LOKALISE_API_TOKEN" file upload \ --project-id "$LOKALISE_PROJECT_ID" \ --file ./src/locales/en.json \ --lang-iso en \ --replace-modified \ --include-path \ --detect-icu-plurals \ --poll \ --tag-inserted-keys \ --tag-updated-keys echo "Source strings pushed to Lokalise"
Pull script (scripts/i18n-pull.sh):
bash#!/usr/bin/env bash set -euo pipefail # Download all translations from Lokalise lokalise2 --token "$LOKALISE_API_TOKEN" file download \ --project-id "$LOKALISE_PROJECT_ID" \ --format json \ --original-filenames=false \ --bundle-structure "%LANG_ISO%.json" \ --export-empty-as base \ --export-sort a_z \ --replace-breaks=false \ --placeholder-format icu \ --unzip-to ./src/locales echo "Translations pulled to ./src/locales/" # Show what changed git diff --stat src/locales/ || true
Package.json scripts:
json{ "scripts": { "i18n:push": "bash scripts/i18n-push.sh", "i18n:pull": "bash scripts/i18n-pull.sh", "i18n:sync": "npm run i18n:push && npm run i18n:pull" } }
Typical workflow:
bash# Edit source strings locally vim src/locales/en.json # Push changes to Lokalise npm run i18n:push # ... translators work in Lokalise UI ... # Pull completed translations npm run i18n:pull # Full round-trip npm run i18n:sync
en.json changes during development.typescript// scripts/i18n-watch.ts — run with: npx tsx scripts/i18n-watch.ts import { watch } from "node:fs"; import { execSync } from "node:child_process"; const SOURCE_FILE = "./src/locales/en.json"; let debounceTimer: ReturnType<typeof setTimeout> | null = null; function pushToLokalise() { console.log(`[${new Date().toISOString()}] Uploading ${SOURCE_FILE}...`); try { execSync("npm run i18n:push", { stdio: "inherit" }); console.log("Upload complete\n"); } catch (err) { console.error("Upload failed:", (err as Error).message); } } watch(SOURCE_FILE, (eventType) => { if (eventType !== "change") return; if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(pushToLokalise, 2000); // 2s debounce }); console.log(`Watching ${SOURCE_FILE} for changes... (Ctrl+C to stop)`);
Add to package.json:
json{ "scripts": { "i18n:watch": "npx tsx scripts/i18n-watch.ts" } }
typescript// scripts/i18n-mock.ts import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; const source: Record<string, string> = JSON.parse( readFileSync("./src/locales/en.json", "utf-8") ); // Pseudo-localization: wraps text in brackets and adds length function pseudoLocalize(text: string): string { // Preserve ICU placeholders like {name}, {count, plural, ...} return text.replace(/([^{}]+)/g, (match) => { const padded = match.replace(/[a-zA-Z]/g, (c) => { const base = c === c.toUpperCase() ? 65 : 97; return String.fromCharCode(((c.charCodeAt(0) - base + 13) % 26) + base); }); return `[${padded}]`; }); } // Generate longer text to test layout overflow function stretchLocalize(text: string): string { return `[${text}${"~".repeat(Math.ceil(text.length * 0.3))}]`; } const pseudo: Record<string, string> = {}; const stretch: Record<string, string> = {}; for (const [key, value] of Object.entries(source)) { pseudo[key] = pseudoLocalize(value); stretch[key] = stretchLocalize(value); } mkdirSync("./src/locales", { recursive: true }); writeFileSync("./src/locales/pseudo.json", JSON.stringify(pseudo, null, 2)); writeFileSync("./src/locales/xx-long.json", JSON.stringify(stretch, null, 2)); console.log("Generated pseudo.json and xx-long.json for testing");
Use in development:
typescript// In your app's locale config, add mock locales for dev only const devLocales = process.env.NODE_ENV === "development" ? { pseudo: () => import("./locales/pseudo.json"), "xx-long": () => import("./locales/xx-long.json") } : {};
typescript// src/i18n.ts import i18n from "i18next"; import { initReactI18next } from "react-i18next"; import en from "./locales/en.json"; i18n.use(initReactI18next).init({ resources: { en: { translation: en }, }, lng: "en", fallbackLng: "en", interpolation: { escapeValue: false }, }); // Lazy-load other languages export async function changeLanguage(lng: string) { if (!i18n.hasResourceBundle(lng, "translation")) { const mod = await import(`./locales/${lng}.json`); i18n.addResourceBundle(lng, "translation", mod.default); } await i18n.changeLanguage(lng); } export default i18n;
typescript// src/i18n.ts import { createI18n } from "vue-i18n"; import en from "./locales/en.json"; const i18n = createI18n({ legacy: false, locale: "en", fallbackLocale: "en", messages: { en }, }); // Lazy-load translations export async function loadLocaleMessages(locale: string) { if (i18n.global.availableLocales.includes(locale)) { i18n.global.locale.value = locale; return; } const messages = await import(`./locales/${locale}.json`); i18n.global.setLocaleMessage(locale, messages.default); i18n.global.locale.value = locale; } export default i18n;
bash#!/usr/bin/env bash # .husky/pre-commit (or .git/hooks/pre-commit) set -euo pipefail # Only run if locale files are staged STAGED_LOCALES=$(git diff --cached --name-only -- 'src/locales/*.json' || true) if [ -z "$STAGED_LOCALES" ]; then exit 0 fi echo "Locale files staged — pulling latest translations from Lokalise..." # Pull latest translations npm run i18n:pull # Check if pull changed any staged files CHANGED=$(git diff --name-only -- 'src/locales/*.json' || true) if [ -n "$CHANGED" ]; then echo "" echo "WARNING: Lokalise has newer translations for:" echo "$CHANGED" echo "" echo "Review the changes, then: git add src/locales/ && git commit" exit 1 fi echo "Translations are up to date"
Install with Husky:
bashset -euo pipefail npx husky add .husky/pre-commit "bash .husky/pre-commit" chmod +x .husky/pre-commit
| Error | Cause | Solution | |-------|-------|----------| | LOKALISE_API_TOKEN not set | Missing env variable | Add to .env.local and source it | | LOKALISE_PROJECT_ID not set | Missing env variable | Get from Lokalise dashboard > Project Settings | | File not found for push | Wrong path in script | Verify --file path matches your project structure | | Rate limit 429 | Watch mode uploading too fast | Increase debounce timeout to 5+ seconds | | Polling timeout | Large file taking too long | Add --poll-timeout 120 to CLI commands | | git diff shows unexpected changes | Lokalise reformatted JSON | Use export_sort: "a_z" and consistent formatting |
bash#!/usr/bin/env bash set -euo pipefail # One-time setup for a new project mkdir -p src/locales scripts # Create base language file if it doesn't exist if [ ! -f src/locales/en.json ]; then echo '{}' > src/locales/en.json echo "Created empty src/locales/en.json" fi # Create push/pull scripts cat > scripts/i18n-push.sh << 'SCRIPT' #!/usr/bin/env bash set -euo pipefail lokalise2 --token "$LOKALISE_API_TOKEN" file upload \ --project-id "$LOKALISE_PROJECT_ID" \ --file ./src/locales/en.json \ --lang-iso en \ --replace-modified \ --detect-icu-plurals \ --poll echo "Pushed source strings" SCRIPT cat > scripts/i18n-pull.sh << 'SCRIPT' #!/usr/bin/env bash 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-empty-as base \ --export-sort a_z \ --unzip-to ./src/locales echo "Pulled translations" SCRIPT chmod +x scripts/i18n-push.sh scripts/i18n-pull.sh # Add npm scripts (requires jq) jq '.scripts += {"i18n:push":"bash scripts/i18n-push.sh","i18n:pull":"bash scripts/i18n-pull.sh","i18n:sync":"npm run i18n:push && npm run i18n:pull"}' \ package.json > package.json.tmp && mv package.json.tmp package.json echo "Setup complete. Add LOKALISE_API_TOKEN and LOKALISE_PROJECT_ID to .env.local"
See lokalise-sdk-patterns for production-ready code patterns and advanced SDK usage.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | pass→pass | 21,809 | 19,838 | -9% | 1 | 1 | 0% | 3,507 | 7,073 | +102% | 0 | 0 | — |
case-01 | fail→pass | 39,900 | 23,520 | -41% | 1 | 1 | 0% | 6,223 | 7,468 | +20% | 0 | 0 | — |
case-02 | fail→fail | 25,429 | 15,649 | -38% | 1 | 1 | 0% | 4,033 | 5,833 | +45% | 0 | 0 | — |
case-03 | fail→pass | 20,391 | 16,182 | -21% | 1 | 1 | 0% | 2,891 | 5,644 | +95% | 0 | 0 | — |
case-04 | fail→pass | 31,710 | 16,123 | -49% | 1 | 1 | 0% | 5,332 | 5,512 | +3% | 0 | 0 | — |
case-05 | pass→pass | 18,349 | 10,996 | -40% | 1 | 1 | 0% | 2,394 | 4,911 | +105% | 0 | 0 | — |
case-06 | fail→pass | 17,862 | 15,167 | -15% | 1 | 1 | 0% | 2,690 | 5,615 | +109% | 0 | 0 | — |
case-07 | pass→pass | 40,380 | 24,944 | -38% | 1 | 1 | 0% | 6,526 | 7,542 | +16% | 0 | 0 | — |
case-08 | pass→pass | 17,168 | 10,834 | -37% | 1 | 1 | 0% | 2,496 | 5,797 | +132% | 0 | 0 | — |
case-09 | pass→pass | 19,677 | 18,790 | -5% | 1 | 1 | 0% | 3,512 | 6,369 | +81% | 0 | 0 | — |
case-11 | fail→pass | 12,528 | 12,980 | +4% | 1 | 1 | 0% | 2,396 | 5,263 | +120% | 0 | 0 | — |
case-12 | pass→pass | 20,410 | 11,922 | -42% | 1 | 1 | 0% | 2,426 | 5,054 | +108% | 0 | 0 | — |
case-13 | fail→pass | 24,443 | 11,821 | -52% | 1 | 1 | 0% | 2,669 | 5,787 | +117% | 0 | 0 | — |
case-14 | fail→fail | 15,642 | 22,149 | +42% | 1 | 1 | 0% | 2,585 | 6,523 | +152% | 0 | 0 | — |
case-15 | pass→pass | 52,055 | 47,770 | -8% | 1 | 1 | 0% | 8,255 | 11,921 | +44% | 0 | 0 | — |
case-16 | pass→pass | 18,320 | 21,353 | +17% | 1 | 1 | 0% | 2,510 | 6,706 | +167% | 0 | 0 | — |
case-17 | fail→pass | 11,792 | 8,792 | -25% | 1 | 1 | 0% | 2,068 | 5,263 | +154% | 0 | 0 | — |
case-18 | pass→pass | 13,367 | 9,279 | -31% | 1 | 1 | 0% | 2,354 | 4,412 | +87% | 0 | 0 | — |
case-19 | pass→fail | 9,902 | 22,610 | +128% | 1 | 1 | 0% | 1,529 | 6,321 | +313% | 0 | 0 | — |
case-20 | fail→fail | 11,345 | 14,157 | +25% | 1 | 1 | 0% | 2,058 | 6,371 | +210% | 0 | 0 | — |
case-21 | fail→fail | 19,811 | 22,638 | +14% | 1 | 1 | 0% | 3,272 | 6,976 | +113% | 0 | 0 | — |
case-22 | fail→fail | 23,501 | 24,721 | +5% | 1 | 1 | 0% | 3,047 | 7,965 | +161% | 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. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 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.