Lokalise Local Dev Loop
Overview
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.
Prerequisites
- Lokalise API token exported as
LOKALISE_API_TOKEN - Lokalise project ID exported as
LOKALISE_PROJECT_ID - Node.js 18+ with npm or pnpm
lokalise2CLI installed- Git (for pre-commit hook)
Instructions
- Set up the project directory structure for i18n files, compatible with Lokalise's
bundle_structureand 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 scripts
Barrel export with type safety (src/locales/index.ts):
import 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;
}
- Create CLI push/pull scripts for the upload-translate-download cycle.
Push script (scripts/i18n-push.sh):
#!/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):
#!/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:
{
"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:
# 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
- Set up watch mode to auto-upload source strings when
en.jsonchanges during development.
// 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:
{
"scripts": {
"i18n:watch": "npx tsx scripts/i18n-watch.ts"
}
}
- Generate mock translations for offline development and layout testing.
// 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:
// 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") }
: {};
- Integrate with React i18next (or skip to step 6 for Vue i18n).
// 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;
- Integrate with Vue i18n (alternative to step 5).
// 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;
- Add a pre-commit hook to prevent committing stale translations.
#!/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:
set -euo pipefail
npx husky add .husky/pre-commit "bash .husky/pre-commit"
chmod +x .husky/pre-commit
Output
- Project directory structured for i18n with typed locale imports
- Push/pull/sync npm scripts for CLI workflow
- File watcher for automatic source string uploads
- Mock translation files for offline UI testing
- Framework integration (React i18next or Vue i18n) with lazy loading
- Pre-commit hook preventing stale translations
Error Handling
| 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 |
Examples
Complete Setup Script
#!/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"
Resources
- Lokalise CLI Documentation
- File Upload API
- File Download API
- react-i18next Setup
- Vue i18n Guide
- Bundle Structure Placeholders
Next Steps
See lokalise-sdk-patterns for production-ready code patterns and advanced SDK usage.