Claude
Skills
Sign in
Back

lokalise-local-dev-loop

Included with Lifetime
$97 forever

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".

Generalsaaslokaliselokalise-local

What this skill does

# 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
- `lokalise2` CLI installed
- Git (for pre-commit hook)

## Instructions

1. Set up the project directory structure for i18n files, compatible with Lokalise's `bundle_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 scripts
```

**Barrel export with type safety (`src/locales/index.ts`):**

```typescript
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;
}
```

1. Create CLI push/pull scripts for the upload-translate-download cycle.

**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
```

1. Set up watch mode to auto-upload source strings when `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"
  }
}
```

1. Generate mock translations for offline development and layout testing.

```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") }
  : {};
```

1. Integrate with React i18next (or skip to step 6 for Vue i18n).

```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;
```

1. Integrate with Vue i18n (alternative to step 5).

```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;
```

1. Add a pre-commit hook to prevent committing stale translations.

```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/

Related in General