@i18n-micro/devtools-ui 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/vite/plugin.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { PluginOption, ViteDevServer, IndexHtmlTransformResult } from 'vite'
2
- import type { IncomingMessage, ServerResponse } from 'node:http'
3
1
  import * as fs from 'node:fs'
4
- import * as path from 'node:path'
5
2
  import { readdir } from 'node:fs/promises'
3
+ import type { IncomingMessage, ServerResponse } from 'node:http'
4
+ import * as path from 'node:path'
5
+ import type { IndexHtmlTransformResult, PluginOption, ViteDevServer } from 'vite'
6
6
 
7
7
  export interface DevToolsPluginOptions {
8
8
  base?: string
@@ -10,7 +10,7 @@ export interface DevToolsPluginOptions {
10
10
  injectButton?: boolean
11
11
  }
12
12
 
13
- // Вспомогательная функция для безопасного резолва пути
13
+ // Helper function for safe path resolution
14
14
  function safeResolvePath(projectRoot: string, filePath: string): string {
15
15
  const normalizedFile = filePath.replace(/^\/+/, '').replace(/\/+/g, '/')
16
16
  const resolvedPath = path.resolve(projectRoot, normalizedFile)
@@ -24,7 +24,7 @@ function safeResolvePath(projectRoot: string, filePath: string): string {
24
24
  return resolvedPath
25
25
  }
26
26
 
27
- // Рекурсивное сканирование директории для поиска JSON файлов
27
+ // Recursive directory scanning to find JSON files
28
28
  async function scanTranslationFiles(dir: string, baseDir: string): Promise<string[]> {
29
29
  const files: string[] = []
30
30
  try {
@@ -34,21 +34,19 @@ async function scanTranslationFiles(dir: string, baseDir: string): Promise<strin
34
34
  if (entry.isDirectory()) {
35
35
  const subFiles = await scanTranslationFiles(fullPath, baseDir)
36
36
  files.push(...subFiles)
37
- }
38
- else if (entry.isFile() && entry.name.endsWith('.json')) {
37
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
39
38
  const relativePath = path.relative(baseDir, fullPath)
40
- files.push(relativePath.replace(/\\/g, '/')) // Нормализуем для кроссплатформенности
39
+ files.push(relativePath.replace(/\\/g, '/')) // Normalize for cross-platform compatibility
41
40
  }
42
41
  }
43
- }
44
- catch (error) {
45
- // Игнорируем ошибки доступа к директориям
42
+ } catch (error) {
43
+ // Ignore directory access errors
46
44
  console.warn(`[i18n-devtools] Cannot scan directory ${dir}:`, error)
47
45
  }
48
46
  return files
49
47
  }
50
48
 
51
- // Скрипт для инжекции кнопки
49
+ // Script for button injection
52
50
  const BUTTON_INJECTION_SCRIPT = `
53
51
  (function() {
54
52
  if (typeof window === 'undefined' || document.getElementById('i18n-devtools-button-container')) {
@@ -84,7 +82,7 @@ const BUTTON_INJECTION_SCRIPT = `
84
82
 
85
83
  function hideButton() {
86
84
  if (isVisible) {
87
- // Оставляем видимой только небольшую часть кнопки (примерно 40px)
85
+ // Leave only a small part of the button visible (approximately 40px)
88
86
  button.style.transform = 'translateX(calc(100% - 40px))';
89
87
  button.style.opacity = '0.3';
90
88
  isVisible = false;
@@ -122,7 +120,7 @@ const BUTTON_INJECTION_SCRIPT = `
122
120
  clearTimeout(hideTimeout);
123
121
  hideTimeout = null;
124
122
  }
125
- // Показываем кнопку полностью при наведении
123
+ // Show the button fully on hover
126
124
  if (!isVisible) {
127
125
  showButton();
128
126
  }
@@ -137,12 +135,12 @@ const BUTTON_INJECTION_SCRIPT = `
137
135
  });
138
136
 
139
137
  button.addEventListener('click', function() {
140
- // Проверяем, не открыт ли уже редактор
138
+ // Check if the editor is already open
141
139
  if (document.getElementById('i18n-devtools-modal')) {
142
140
  return;
143
141
  }
144
142
 
145
- // Открываем модальное окно с iframe
143
+ // Open modal window with iframe
146
144
  const modal = document.createElement('div');
147
145
  modal.id = 'i18n-devtools-modal';
148
146
  modal.style.cssText = 'position: fixed; inset: 0; z-index: 999999; background: rgba(0, 0, 0, 0.2); backdrop-filter: blur(2px); display: flex; align-items: center; justify-content: center; padding: 2rem;';
@@ -191,7 +189,7 @@ const BUTTON_INJECTION_SCRIPT = `
191
189
 
192
190
  document.addEventListener('mousemove', checkMouseDistance);
193
191
 
194
- // Автоматически скрываем через 3 секунды после загрузки
192
+ // Auto-hide after 3 seconds of loading
195
193
  setTimeout(() => {
196
194
  if (!button.matches(':hover')) {
197
195
  hideTimeout = setTimeout(hideButton, 3000);
@@ -205,11 +203,11 @@ const BUTTON_INJECTION_SCRIPT = `
205
203
  export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginOption {
206
204
  const apiBase = options.base || '/__i18n_api'
207
205
  const translationDir = options.translationDir || 'src/locales'
208
- const injectButton = options.injectButton !== false // По умолчанию true
206
+ const injectButton = options.injectButton !== false // Default is true
209
207
 
210
208
  return {
211
209
  name: 'i18n-micro-devtools-plugin',
212
- apply: 'serve', // Работает только в dev server
210
+ apply: 'serve', // Works only in dev server
213
211
 
214
212
  resolveId(id: string) {
215
213
  if (id === '/@vite-plugin-i18n-devtools/devtools-ui.js') {
@@ -220,7 +218,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
220
218
 
221
219
  load(id: string) {
222
220
  if (id === '/@vite-plugin-i18n-devtools/devtools-ui.js') {
223
- // Возвращаем реэкспорт из devtools-ui пакета
221
+ // Return re-export from devtools-ui package
224
222
  return `export * from '@i18n-micro/devtools-ui'`
225
223
  }
226
224
  return null
@@ -231,21 +229,21 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
231
229
  return html
232
230
  }
233
231
 
234
- // Инжектируем скрипт перед закрывающим тегом </body>
232
+ // Inject script before the closing </body> tag
235
233
  const scriptTag = `<script>${BUTTON_INJECTION_SCRIPT}</script>`
236
234
  if (html.includes('</body>')) {
237
235
  return html.replace('</body>', `${scriptTag}</body>`)
238
236
  }
239
- // Если нет </body>, добавляем в конец
237
+ // If no </body>, append to the end
240
238
  return html + scriptTag
241
239
  },
242
240
 
243
241
  configureServer(server: ViteDevServer) {
244
- // Используем server.config.root как источник правды для корня проекта
242
+ // Use server.config.root as the source of truth for the project root
245
243
  const projectRoot = server.config.root
246
244
 
247
- // HTML страница для iframe с devtools UI
248
- // Используем виртуальный модуль для правильного импорта
245
+ // HTML page for iframe with devtools UI
246
+ // Use virtual module for proper imports
249
247
  const devtoolsHtml = `<!DOCTYPE html>
250
248
  <html lang="en">
251
249
  <head>
@@ -281,14 +279,14 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
281
279
  <div class="loading">Loading DevTools...</div>
282
280
  </div>
283
281
  <script type="module">
284
- // Импортируем devtools UI
285
- // Пытаемся импортировать через разные пути для совместимости
282
+ // Import devtools UI
283
+ // Try to import through different paths for compatibility
286
284
  let register;
287
285
  try {
288
286
  const devtoolsModule = await import('/@vite-plugin-i18n-devtools/devtools-ui.js');
289
287
  register = devtoolsModule.register;
290
288
  } catch (e) {
291
- // Fallback: пытаемся импортировать напрямую
289
+ // Fallback: try to import directly
292
290
  try {
293
291
  const devtoolsModule = await import('@i18n-micro/devtools-ui');
294
292
  register = devtoolsModule.register;
@@ -299,10 +297,10 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
299
297
  }
300
298
  }
301
299
 
302
- // Регистрируем custom element
300
+ // Register custom element
303
301
  register();
304
302
 
305
- // Создаем bridge через API
303
+ // Create bridge via API
306
304
  const bridge = {
307
305
  async getLocalesAndTranslations() {
308
306
  try {
@@ -381,14 +379,14 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
381
379
  </body>
382
380
  </html>`
383
381
 
384
- // Middleware для обслуживания HTML страницы devtools
382
+ // Middleware for serving the devtools HTML page
385
383
  server.middlewares.use('/__i18n_devtools.html', (_req: IncomingMessage, res: ServerResponse) => {
386
384
  res.statusCode = 200
387
385
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
388
386
  res.end(devtoolsHtml)
389
387
  })
390
388
 
391
- // Эндпоинт для получения конфигурации
389
+ // Endpoint for getting configuration
392
390
  server.middlewares.use(`${apiBase}/config`, async (_req: IncomingMessage, res: ServerResponse, next) => {
393
391
  if (_req.method !== 'GET') {
394
392
  return next()
@@ -397,25 +395,28 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
397
395
  try {
398
396
  res.statusCode = 200
399
397
  res.setHeader('Content-Type', 'application/json')
400
- res.end(JSON.stringify({
401
- defaultLocale: 'en',
402
- fallbackLocale: 'en',
403
- locales: [],
404
- translationDir,
405
- }))
406
- }
407
- catch (e) {
398
+ res.end(
399
+ JSON.stringify({
400
+ defaultLocale: 'en',
401
+ fallbackLocale: 'en',
402
+ locales: [],
403
+ translationDir,
404
+ }),
405
+ )
406
+ } catch (e) {
408
407
  console.error('[i18n-devtools] Config error:', e)
409
408
  res.statusCode = 500
410
409
  res.setHeader('Content-Type', 'application/json')
411
- res.end(JSON.stringify({
412
- success: false,
413
- error: e instanceof Error ? e.message : String(e),
414
- }))
410
+ res.end(
411
+ JSON.stringify({
412
+ success: false,
413
+ error: e instanceof Error ? e.message : String(e),
414
+ }),
415
+ )
415
416
  }
416
417
  })
417
418
 
418
- // Эндпоинт для получения списка файлов
419
+ // Endpoint for getting file list
419
420
  server.middlewares.use(`${apiBase}/files`, async (req: IncomingMessage, res: ServerResponse, next) => {
420
421
  if (req.method !== 'GET') {
421
422
  return next()
@@ -424,7 +425,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
424
425
  try {
425
426
  const localesPath = path.resolve(projectRoot, translationDir)
426
427
 
427
- // Проверяем, что путь безопасен
428
+ // Verify the path is safe
428
429
  safeResolvePath(projectRoot, translationDir)
429
430
 
430
431
  if (!fs.existsSync(localesPath)) {
@@ -436,7 +437,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
436
437
 
437
438
  const files = await scanTranslationFiles(localesPath, localesPath)
438
439
 
439
- // Строим структуру директорий
440
+ // Build directory structure
440
441
  const structure: Record<string, unknown> = {}
441
442
  for (const file of files) {
442
443
  const parts = file.split('/')
@@ -457,19 +458,20 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
457
458
  res.statusCode = 200
458
459
  res.setHeader('Content-Type', 'application/json')
459
460
  res.end(JSON.stringify({ files, structure }))
460
- }
461
- catch (e) {
461
+ } catch (e) {
462
462
  console.error('[i18n-devtools] Files list error:', e)
463
463
  res.statusCode = 500
464
464
  res.setHeader('Content-Type', 'application/json')
465
- res.end(JSON.stringify({
466
- success: false,
467
- error: e instanceof Error ? e.message : String(e),
468
- }))
465
+ res.end(
466
+ JSON.stringify({
467
+ success: false,
468
+ error: e instanceof Error ? e.message : String(e),
469
+ }),
470
+ )
469
471
  }
470
472
  })
471
473
 
472
- // Эндпоинт для загрузки конкретного файла
474
+ // Endpoint for loading a specific file
473
475
  server.middlewares.use(`${apiBase}/file`, async (req: IncomingMessage, res: ServerResponse, next) => {
474
476
  if (req.method !== 'GET') {
475
477
  return next()
@@ -483,7 +485,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
483
485
  throw new Error('Path parameter is required')
484
486
  }
485
487
 
486
- // Резолвим путь относительно translationDir
488
+ // Resolve path relative to translationDir
487
489
  const fullPath = path.join(translationDir, filePath)
488
490
  const resolvedPath = safeResolvePath(projectRoot, fullPath)
489
491
 
@@ -504,26 +506,27 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
504
506
  res.statusCode = 200
505
507
  res.setHeader('Content-Type', 'application/json')
506
508
  res.end(JSON.stringify({ success: true, content: parsed, path: filePath }))
507
- }
508
- catch (e) {
509
+ } catch (e) {
509
510
  console.error('[i18n-devtools] File read error:', e)
510
511
  res.statusCode = 500
511
512
  res.setHeader('Content-Type', 'application/json')
512
- res.end(JSON.stringify({
513
- success: false,
514
- error: e instanceof Error ? e.message : String(e),
515
- }))
513
+ res.end(
514
+ JSON.stringify({
515
+ success: false,
516
+ error: e instanceof Error ? e.message : String(e),
517
+ }),
518
+ )
516
519
  }
517
520
  })
518
521
 
519
- // Эндпоинт для сохранения файла
522
+ // Endpoint for saving a file
520
523
  server.middlewares.use(`${apiBase}/save`, async (req: IncomingMessage, res: ServerResponse, next) => {
521
524
  if (req.method !== 'POST') {
522
525
  return next()
523
526
  }
524
527
 
525
528
  try {
526
- // Чтение тела запроса
529
+ // Read request body
527
530
  const buffers: Buffer[] = []
528
531
  for await (const chunk of req) {
529
532
  buffers.push(chunk)
@@ -541,37 +544,38 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
541
544
  throw new Error('Invalid data: file and content are required')
542
545
  }
543
546
 
544
- // Резолвим путь относительно translationDir, если путь не абсолютный
547
+ // Resolve path relative to translationDir if path is not absolute
545
548
  const fullPath = file.startsWith(translationDir) ? file : path.join(translationDir, file)
546
549
  const filePath = safeResolvePath(projectRoot, fullPath)
547
550
 
548
- // Проверка расширения
551
+ // Check extension
549
552
  if (!filePath.endsWith('.json')) {
550
553
  throw new Error('Invalid file: only .json files are allowed')
551
554
  }
552
555
 
553
- // Создаем директорию, если её нет
556
+ // Create directory if it doesn't exist
554
557
  const dir = path.dirname(filePath)
555
558
  if (!fs.existsSync(dir)) {
556
559
  fs.mkdirSync(dir, { recursive: true })
557
560
  }
558
561
 
559
- // Записываем файл
562
+ // Write file
560
563
  fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf-8')
561
564
 
562
- // Успешный ответ
565
+ // Successful response
563
566
  res.statusCode = 200
564
567
  res.setHeader('Content-Type', 'application/json')
565
568
  res.end(JSON.stringify({ success: true }))
566
- }
567
- catch (e) {
569
+ } catch (e) {
568
570
  console.error('[i18n-devtools] Save error:', e)
569
571
  res.statusCode = 500
570
572
  res.setHeader('Content-Type', 'application/json')
571
- res.end(JSON.stringify({
572
- success: false,
573
- error: e instanceof Error ? e.message : String(e),
574
- }))
573
+ res.end(
574
+ JSON.stringify({
575
+ success: false,
576
+ error: e instanceof Error ? e.message : String(e),
577
+ }),
578
+ )
575
579
  }
576
580
  })
577
581
  },