Claude-code-plugins-plus-skills lokalise-local-dev-loop
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/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-local-dev-loop" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-lokalise-local-dev-loop && rm -rf "$T"
manifest:
plugins/saas-packs/lokalise-pack/skills/lokalise-local-dev-loop/SKILL.mdsource content
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
CLI installedlokalise2- Git (for pre-commit hook)
Instructions
- Set up the project directory structure for i18n files, compatible with Lokalise's
and most i18n frameworks.bundle_structure
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
changes during development.en.json
// 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 |
|---|---|---|
| Missing env variable | Add to and source it |
| Missing env variable | Get from Lokalise dashboard > Project Settings |
for push | Wrong path in script | Verify path matches your project structure |
| Watch mode uploading too fast | Increase debounce timeout to 5+ seconds |
| Large file taking too long | Add to CLI commands |
shows unexpected changes | Lokalise reformatted JSON | Use 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.