{"_id":"lokki","_rev":"10-25b0fb12a777522078e4dadb0d225472","name":"lokki","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"lokki","version":"1.0.0","keywords":["i18next","translation","editor","i18n","localization","internationalization","polyglot","translation-mode","vanilla-js","framework-agnostic"],"author":{"name":"Vlad Iliev"},"license":"MIT","_id":"lokki@1.0.0","maintainers":[{"name":"vlad_iliev","email":"orionivvlad@gmail.com"}],"homepage":"https://github.com/orionivv/lokki#readme","bugs":{"url":"https://github.com/orionivv/lokki/issues"},"dist":{"shasum":"d4c5053c87124eade006a2306b7c9fc810e4a673","tarball":"https://registry.npmjs.org/lokki/-/lokki-1.0.0.tgz","fileCount":50,"integrity":"sha512-olkJ1eCTma5Cb4rtn3rsSvArhVv8WC7bVerYdyfzRm84QRxeuDK9QVUn40DTfNcOiU57IS9rx7A2OTvMyncdJA==","signatures":[{"sig":"MEUCIQCeLxkBqMvwnRhqhsU7oaqtjhKeEMBUi9eI/bEsy0W3fwIgT8df2B3h14MjbJzm8innWZTUYYzqFvt5hmahLPlI5L4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":7260138},"main":"dist/index.cjs","type":"module","types":"dist/types/index.d.ts","unpkg":"dist/lokki.umd.min.js","module":"dist/index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"gitHead":"ca02495bad2257f4208d9c78f19adfcfa6ecda0f","scripts":{"dev":"rollup -c -w","lint":"eslint src --ext .ts","test":"vitest","build":"npm run clean && rollup -c","clean":"node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"","format":"prettier --write .","test:run":"vitest run","typecheck":"tsc --noEmit","format:check":"prettier --check .","prepublishOnly":"npm run test:run && npm run build"},"_npmUser":{"name":"vlad_iliev","email":"orionivvlad@gmail.com"},"jsdelivr":"dist/lokki.umd.min.js","repository":{"url":"git+https://github.com/orionivv/lokki.git","type":"git"},"_npmVersion":"11.6.2","description":"Zero-dependency, framework-agnostic translation editor for i18next applications","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tslib":"^2.8.1","eslint":"^9.18.0","rollup":"^4.32.0","vitest":"^3.0.4","i18next":"^25.8.0","prettier":"^3.8.1","typescript":"^5.7.3","@types/node":"^22.10.7","@rollup/plugin-terser":"^0.4.4","@rollup/plugin-commonjs":"^28.0.2","@rollup/plugin-typescript":"^12.1.2","@rollup/plugin-node-resolve":"^16.0.0"},"peerDependencies":{"i18next":">=21.0.0"},"peerDependenciesMeta":{"i18next":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/lokki_1.0.0_1769460159151_0.6899403899656693","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"lokki","version":"1.1.0","description":"Zero-dependency, framework-agnostic translation editor for i18next applications","type":"module","main":"dist/index.cjs","module":"dist/index.mjs","types":"dist/types/index.d.ts","unpkg":"dist/lokki.umd.min.js","jsdelivr":"dist/lokki.umd.min.js","sideEffects":false,"exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.cjs"}},"scripts":{"clean":"node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"","build":"npm run clean && rollup -c","dev":"rollup -c -w","test":"vitest","test:run":"vitest run","lint":"eslint src --ext .ts","format":"prettier --write .","format:check":"prettier --check .","typecheck":"tsc --noEmit","prepublishOnly":"npm run test:run && npm run build"},"keywords":["i18next","translation","editor","i18n","localization","internationalization","polyglot","translation-mode","vanilla-js","framework-agnostic"],"author":{"name":"Vlad Iliev"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/orionivv/lokki.git"},"bugs":{"url":"https://github.com/orionivv/lokki/issues"},"homepage":"https://github.com/orionivv/lokki#readme","engines":{"node":">=18"},"publishConfig":{"access":"public"},"devDependencies":{"@rollup/plugin-commonjs":"^28.0.2","@rollup/plugin-node-resolve":"^16.0.0","@rollup/plugin-terser":"^0.4.4","@rollup/plugin-typescript":"^12.1.2","@types/node":"^22.10.7","eslint":"^9.18.0","i18next":"^25.8.0","prettier":"^3.8.1","rollup":"^4.32.0","tslib":"^2.8.1","typescript":"^5.7.3","vitest":"^3.0.4"},"peerDependencies":{"i18next":">=21.0.0"},"peerDependenciesMeta":{"i18next":{"optional":true}},"gitHead":"994cece2e3d17bcb90e439ea2cfd10f2aa19eba6","_id":"lokki@1.1.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-QnzQu+dX+bGzXwWiQi4vbsTgqNNbsJWdgYjahNBhtpy3rDP4MeHRgGbGfnUXHov04IYXiyy3nYCUzFGERGY89A==","shasum":"ba4593d32afa6a4690bd0d452e579c18d7c2320a","tarball":"https://registry.npmjs.org/lokki/-/lokki-1.1.0.tgz","fileCount":52,"unpackedSize":1535996,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDPdpTCNn17S2LSPla0Cayhxb7GsLgDG4UqH9AFve9lIAIhAMoajNX+Bk+eEUxh4D21DFn/bSMVizI0UZqT4YuLHUwL"}]},"_npmUser":{"name":"vlad_iliev","email":"orionivvlad@gmail.com"},"directories":{},"maintainers":[{"name":"vlad_iliev","email":"orionivvlad@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/lokki_1.1.0_1769538429938_0.18860544987882788"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-26T20:42:39.043Z","modified":"2026-01-27T18:27:10.586Z","0.1.0":"2011-06-25T13:29:07.889Z","1.0.0":"2026-01-26T20:42:39.494Z","1.1.0":"2026-01-27T18:27:10.167Z"},"bugs":{"url":"https://github.com/orionivv/lokki/issues"},"author":{"name":"Vlad Iliev"},"license":"MIT","homepage":"https://github.com/orionivv/lokki#readme","keywords":["i18next","translation","editor","i18n","localization","internationalization","polyglot","translation-mode","vanilla-js","framework-agnostic"],"repository":{"type":"git","url":"git+https://github.com/orionivv/lokki.git"},"description":"Zero-dependency, framework-agnostic translation editor for i18next applications","maintainers":[{"name":"vlad_iliev","email":"orionivvlad@gmail.com"}],"readme":"# Lokki 🌍\n\n> Zero-dependency, framework-agnostic translation editor for i18next applications.\n\n[![npm version](https://badge.fury.io/js/lokki.svg)](https://www.npmjs.com/package/lokki)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n![Lokki demo](./assets/lokki.gif)\n\nLokki enables real-time translation editing directly in your application UI. Edit translations, see live previews, and export untranslated keys - all without leaving your browser.\n\n## Features\n\n- 🚀 **Zero runtime dependencies** - Pure TypeScript/JavaScript\n- 🎨 **Framework agnostic** - Works with React, Vue, Angular, or vanilla HTML\n- 📝 **Live editing** - Edit translations with instant preview\n- 🔍 **Search & filter** - Quickly find translations\n- 📋 **Copy to clipboard** - One-click copy translation keys\n- 📦 **CSV export** - Export untranslated keys for translators\n- ⌨️ **Keyboard shortcuts** - Toggle with `Ctrl+Shift+T`\n- 🎯 **Element highlighting** - See which elements use a translation\n- 🌙 **Theme support** - Dark/light/auto with persistent preference\n- ⚙️ **Panel Controls** - Auto-scan, highlight all, scroll to element toggles\n- 💾 **Persistent Settings** - All preferences saved to localStorage\n\n## Installation\n\n```bash\nnpm install lokki\n# or\nyarn add lokki\n# or\npnpm add lokki\n```\n\n## How It Works\n\nLokki uses a **page reload strategy** for reliable cross-framework support:\n\n1. **Toggle** - User enables translation mode, state saved to localStorage\n2. **Reload** - Page reloads with i18next post-processor active\n3. **Markers** - All `t()` calls return special markers: `^$#^key` instead of values\n4. **Observer** - Lokki finds markers in DOM and wraps them in spans for editing\n5. **Edit** - User edits translations with live preview\n6. **Save** - Your `onSave` callback is called with `(key, value, locale)`\n\n### Why Page Reload?\n\nFrameworks like React, Vue, and Angular render translations during initial mount. Without markers from the start, there's no reliable way to map DOM text back to translation keys.\n\nThe reload approach ensures:\n\n- ✅ Works with any framework or vanilla JS\n- ✅ No timing issues or race conditions\n- ✅ All translations are captured, even in lazy-loaded components\n- ✅ No framework-specific adapters needed\n\n## Quick Start\n\n### 1. Add post-processor to your i18next config\n\n```typescript\nimport { Lokki, createLokkiPostProcessor } from 'lokki';\n\ni18next\n  .use(createLokkiPostProcessor()) // Add this\n  .init({\n    // ... your existing config\n    postProcess: Lokki.isEnabled() ? ['lokki'] : [], // Add this\n  });\n```\n\n### 2. Create Lokki instance\n\n```typescript\nconst lokki = new Lokki({\n  i18next,\n  locale: i18next.language,\n  onSave: async (key, value, locale) => {\n    // Save to your backend\n    console.log(`Save: ${key} = \"${value}\" (${locale})`);\n  },\n});\n```\n\n### 3. Toggle translation mode\n\n```typescript\nlokki.toggle(); // or press Ctrl+Shift+T\n```\n\n---\n\n## Framework Examples\n\n### React (react-i18next)\n\n```tsx\n// i18n.ts - modify your existing config\nimport i18next from 'i18next';\nimport { initReactI18next } from 'react-i18next';\nimport { Lokki, createLokkiPostProcessor } from 'lokki';\n\ni18next\n  .use(initReactI18next)\n  .use(createLokkiPostProcessor()) // Add Lokki post-processor\n  .init({\n    // ... your existing config\n    postProcess: Lokki.isEnabled() ? ['lokki'] : [],\n  });\n\nexport default i18next;\n```\n\n```tsx\n// App.tsx or a dedicated hook\nimport { useEffect, useRef } from 'react';\nimport { Lokki } from 'lokki';\nimport i18next from './i18n';\n\nexport function useLokki() {\n  const lokkiRef = useRef<Lokki | null>(null);\n\n  useEffect(() => {\n    lokkiRef.current = new Lokki({\n      i18next,\n      locale: i18next.language,\n      onSave: async (key, value, locale) => {\n        await fetch('/api/translations', {\n          method: 'POST',\n          body: JSON.stringify({ key, value, locale }),\n        });\n      },\n    });\n    return () => lokkiRef.current?.destroy();\n  }, []);\n\n  return lokkiRef;\n}\n```\n\n### Vue 3 (vue-i18n)\n\n```ts\n// i18n.ts\nimport i18next from 'i18next';\nimport { Lokki, createLokkiPostProcessor } from 'lokki';\n\n// Lokki needs i18next instance\ni18next.use(createLokkiPostProcessor()).init({\n  lng: 'en',\n  // Use same resources as vue-i18n\n  postProcess: Lokki.isEnabled() ? ['lokki'] : [],\n});\n\nexport { i18next };\n```\n\n```vue\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref } from 'vue';\nimport { Lokki } from 'lokki';\nimport { i18next } from './i18n';\n\nconst lokki = ref<Lokki | null>(null);\n\nonMounted(() => {\n  lokki.value = new Lokki({\n    i18next,\n    locale: 'en',\n    onSave: async (key, value, locale) => {\n      await fetch('/api/translations', {\n        method: 'POST',\n        body: JSON.stringify({ key, value, locale }),\n      });\n    },\n  });\n});\n\nonUnmounted(() => lokki.value?.destroy());\n</script>\n```\n\n### Angular\n\n```ts\n// lokki.service.ts\nimport { Injectable, OnDestroy } from '@angular/core';\nimport i18next from 'i18next';\nimport { Lokki, createLokkiPostProcessor } from 'lokki';\n\n@Injectable({ providedIn: 'root' })\nexport class LokkiService implements OnDestroy {\n  lokki: Lokki | null = null;\n\n  async init() {\n    await i18next.use(createLokkiPostProcessor()).init({\n      lng: 'en',\n      // ... your config\n      postProcess: Lokki.isEnabled() ? ['lokki'] : [],\n    });\n\n    this.lokki = new Lokki({\n      i18next,\n      locale: 'en',\n      onSave: async (key, value, locale) => {\n        await fetch('/api/translations', {\n          method: 'POST',\n          body: JSON.stringify({ key, value, locale }),\n        });\n      },\n    });\n  }\n\n  toggle() {\n    this.lokki?.toggle();\n  }\n\n  ngOnDestroy() {\n    this.lokki?.destroy();\n  }\n}\n```\n\n### Vanilla JS (CDN)\n\n```html\n<script src=\"https://unpkg.com/i18next@23/dist/umd/i18next.min.js\"></script>\n<script src=\"https://unpkg.com/lokki/dist/lokki.js\"></script>\n<script>\n  const { Lokki, createLokkiPostProcessor } = LokkiLib;\n\n  i18next.use(createLokkiPostProcessor()).init({\n    lng: 'en',\n    // ... your config\n    postProcess: Lokki.isEnabled() ? ['lokki'] : [],\n  });\n\n  const lokki = new Lokki({\n    i18next,\n    locale: 'en',\n    onSave: (key, value, locale) => {\n      console.log(`Save: ${key} = \"${value}\" (${locale})`);\n    },\n  });\n\n  // Toggle with button or Ctrl+Shift+T\n  document.getElementById('toggleBtn').onclick = () => lokki.toggle();\n</script>\n```\n\n## API Reference\n\n### Static Methods\n\n| Method                    | Description                                              |\n| ------------------------- | -------------------------------------------------------- |\n| `Lokki.isEnabled()`       | Check if translation mode is enabled (from localStorage) |\n| `Lokki.getMarkerPrefix()` | Get the marker prefix used by post-processor             |\n\n### Constructor\n\n```typescript\nnew Lokki(options: LokkiOptions)\n\ninterface LokkiOptions {\n  /** i18next instance */\n  i18next: i18n;\n\n  /** Current locale */\n  locale: string;\n\n  /** Enable/disable Lokki functionality (default: true) */\n  enabled?: boolean;\n\n  /** Callback when a translation is saved */\n  onSave?: (key: string, value: string, locale: string) => void | Promise<void>;\n\n  /** Initial panel position */\n  position?: { x: number; y: number };\n\n  /** Keyboard shortcuts */\n  shortcuts?: {\n    toggle?: string; // default: 'ctrl+shift+t'\n  };\n\n  /** Theme preference: 'light' | 'dark' | 'auto' (default: 'auto') */\n  theme?: 'light' | 'dark' | 'auto';\n\n  /** Enable automatic DOM scanning (default: true) */\n  autoScan?: boolean;\n\n  /** Highlight all translations on the page (default: false) */\n  highlightAll?: boolean;\n\n  /** Scroll to element when selecting translation (default: false) */\n  scrollToElement?: boolean;\n\n  /** Container element (default: document.body) */\n  container?: HTMLElement;\n}\n```\n\n#### Disabling in Production\n\nUse the `enabled` option to hide Lokki in production or restrict access:\n\n```typescript\nconst lokki = new Lokki({\n  i18next,\n  locale: 'en',\n  // Disable in production\n  enabled: process.env.NODE_ENV !== 'production',\n  // Or check user permissions\n  // enabled: currentUser.role === 'translator',\n  onSave: async (key, value, locale) => {\n    /* ... */\n  },\n});\n```\n\nWhen `enabled: false`:\n\n- Lokki won't initialize\n- Keyboard shortcuts are disabled\n- Any existing localStorage state is cleared\n\n### Instance Methods\n\n| Method               | Description                                                     |\n| -------------------- | --------------------------------------------------------------- |\n| `enable()`           | Enable translation mode (reloads page)                          |\n| `disable()`          | Disable translation mode (reloads page)                         |\n| `toggle()`           | Toggle translation mode (reloads page)                          |\n| `show()`             | Show the panel                                                  |\n| `hide()`             | Hide the panel                                                  |\n| `isEnabled()`        | Check if translation mode is enabled                            |\n| `isLokkiAvailable()` | Check if Lokki is available (not disabled via `enabled: false`) |\n| `isVisible()`        | Check if panel is visible                                       |\n| `getLocale()`        | Get current locale                                              |\n| `setLocale(locale)`  | Set current locale                                              |\n| `scan()`             | Manually rescan DOM for translations                            |\n| `getTranslations()`  | Get all translations on the page                                |\n| `destroy()`          | Cleanup and destroy instance                                    |\n\n### Panel Controls\n\nThe panel header includes several toggle buttons for controlling Lokki behavior:\n\n| Control | Icon | Description | Default |\n|---------|------|-------------|---------|\n| **Auto-scan** | 🔄 | Automatically scan DOM for new translations | On |\n| **Highlight All** | ✨ | Highlight all translation elements on page | Off |\n| **Scroll to Element** | 📍 | Scroll to element when selecting in list | Off |\n| **Theme** | ☀️/🌙 | Cycle through light/dark/auto themes | Auto |\n| **Rescan** | 🔍 | Manually trigger full DOM rescan | - |\n\nAll toggle states are persisted to localStorage and restored on page reload.\n\n### Events\n\n```typescript\nlokki.on('enable', () => console.log('Translation mode enabled'));\nlokki.on('disable', () => console.log('Translation mode disabled'));\nlokki.on('show', () => console.log('Panel shown'));\nlokki.on('hide', () => console.log('Panel hidden'));\nlokki.on('save', ({ key, value, locale }) => {\n  console.log('Translation saved:', key, value, locale);\n});\n```\n\n## Backend Integration\n\nLokki calls your `onSave` callback when the user saves a translation. You decide how to persist it:\n\n```typescript\nconst lokki = new Lokki({\n  i18next,\n  locale: 'en',\n  onSave: async (key, value, locale) => {\n    // Option 1: REST API\n    await fetch('/api/translations', {\n      method: 'POST',\n      body: JSON.stringify({ key, value, locale }),\n    });\n\n    // Option 2: GraphQL\n    await client.mutate({\n      mutation: SAVE_TRANSLATION,\n      variables: { key, value, locale },\n    });\n\n    // Option 3: Firebase\n    await db\n      .collection('translations')\n      .doc(locale)\n      .update({\n        [key]: value,\n      });\n\n    // Option 4: Just log it (for demo/testing)\n    console.log(`${key} = \"${value}\" (${locale})`);\n  },\n});\n```\n\n## Persistent Settings\n\nLokki automatically saves user preferences to localStorage:\n\n| Setting | Storage Key | Description |\n|---------|-------------|-------------|\n| Enabled state | `lokki:enabled` | Whether translation mode is active |\n| Theme | `lokki:theme` | Current theme preference |\n| Panel position | `lokki:position` | Dragged panel coordinates |\n| Auto-scan | `lokki:autoScan` | Auto-scan toggle state |\n| Highlight All | `lokki:highlightAll` | Highlight toggle state |\n| Scroll to Element | `lokki:scrollToElement` | Scroll toggle state |\n\nSettings persist across page reloads and browser sessions. To clear all Lokki data:\n\n```typescript\n// Clear all Lokki localStorage data\nlocalStorage.removeItem('lokki:enabled');\nlocalStorage.removeItem('lokki:theme');\nlocalStorage.removeItem('lokki:position');\nlocalStorage.removeItem('lokki:autoScan');\nlocalStorage.removeItem('lokki:highlightAll');\nlocalStorage.removeItem('lokki:scrollToElement');\n```\n\n## Try It Locally\n\nClone the repository and run any example to see Lokki in action:\n\n```bash\n# 1. Clone and build\ngit clone https://github.com/orionivv/lokki.git\ncd lokki\nnpm install\nnpm run build\n\n# 2. Run an example\ncd examples/react\nnpm install\nnpm run dev\n```\n\n### Available Examples\n\n| Example                       | Framework       | Port | Command                       |\n| ----------------------------- | --------------- | ---- | ----------------------------- |\n| [vanilla](./examples/vanilla) | Plain HTML/JS   | 3333 | `python3 -m http.server 3333` |\n| [react](./examples/react)     | React 18 + Vite | 5173 | `npm run dev`                 |\n| [vue](./examples/vue)         | Vue 3 + Vite    | 5173 | `npm run dev`                 |\n| [angular](./examples/angular) | Angular 18      | 4200 | `npm run dev`                 |\n\nEach example demonstrates:\n\n- i18next integration with Lokki post-processor\n- Toggle translation mode (button + `Ctrl+Shift+T`)\n- Language switching (English/German)\n- `onSave` callback for persisting translations\n- Status indicator (enabled/disabled)\n\n## Keyboard Shortcuts\n\n| Shortcut       | Action                   |\n| -------------- | ------------------------ |\n| `Ctrl+Shift+T` | Toggle translation panel |\n\n## Browser Support\n\n- Chrome (latest)\n- Firefox (latest)\n- Safari (latest)\n- Edge (latest)\n\n## Development\n\n```bash\n# Install dependencies\nnpm install\n\n# Build\nnpm run build\n\n# Watch mode\nnpm run dev\n```\n\n## License\n\nMIT © [Vlad Iliev](https://github.com/orionivv)\n\n---\n\n**Lokki** - Making translations easy, one key at a time. 🔑\n","readmeFilename":"README.md"}