@i18n-micro/devtools-ui 1.1.0 → 1.1.1
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/dist/bridge/create-bridge.d.ts +1 -1
- package/dist/components/ui/Loader.vue.d.ts +1 -1
- package/dist/composables/useI18nState.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.es.js +1148 -1141
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +6 -6
- package/dist/index.umd.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/vite/plugin.mjs +33 -23
- package/dist/vite/plugin.mjs.map +1 -1
- package/package.json +2 -2
- package/vite/plugin.ts +41 -37
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { JSONValue, TranslationContent, LocaleData, I18nDevToolsBridge, } from '../bridge/interface';
|
|
2
1
|
export type { ModuleOptions } from '@i18n-micro/types';
|
|
2
|
+
export type { I18nDevToolsBridge, JSONValue, LocaleData, TranslationContent, } from '../bridge/interface';
|
|
3
3
|
export interface TreeNode {
|
|
4
4
|
name: string;
|
|
5
5
|
fullPath: string;
|
package/dist/vite/plugin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
2
|
import { readdir } from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
4
|
function safeResolvePath(projectRoot, filePath) {
|
|
5
5
|
const normalizedFile = filePath.replace(/^\/+/, "").replace(/\/+/g, "/");
|
|
6
6
|
const resolvedPath = path.resolve(projectRoot, normalizedFile);
|
|
@@ -360,20 +360,24 @@ function i18nDevToolsPlugin(options = {}) {
|
|
|
360
360
|
try {
|
|
361
361
|
res.statusCode = 200;
|
|
362
362
|
res.setHeader("Content-Type", "application/json");
|
|
363
|
-
res.end(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
363
|
+
res.end(
|
|
364
|
+
JSON.stringify({
|
|
365
|
+
defaultLocale: "en",
|
|
366
|
+
fallbackLocale: "en",
|
|
367
|
+
locales: [],
|
|
368
|
+
translationDir
|
|
369
|
+
})
|
|
370
|
+
);
|
|
369
371
|
} catch (e) {
|
|
370
372
|
console.error("[i18n-devtools] Config error:", e);
|
|
371
373
|
res.statusCode = 500;
|
|
372
374
|
res.setHeader("Content-Type", "application/json");
|
|
373
|
-
res.end(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
375
|
+
res.end(
|
|
376
|
+
JSON.stringify({
|
|
377
|
+
success: false,
|
|
378
|
+
error: e instanceof Error ? e.message : String(e)
|
|
379
|
+
})
|
|
380
|
+
);
|
|
377
381
|
}
|
|
378
382
|
});
|
|
379
383
|
server.middlewares.use(`${apiBase}/files`, async (req, res, next) => {
|
|
@@ -413,10 +417,12 @@ function i18nDevToolsPlugin(options = {}) {
|
|
|
413
417
|
console.error("[i18n-devtools] Files list error:", e);
|
|
414
418
|
res.statusCode = 500;
|
|
415
419
|
res.setHeader("Content-Type", "application/json");
|
|
416
|
-
res.end(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
+
res.end(
|
|
421
|
+
JSON.stringify({
|
|
422
|
+
success: false,
|
|
423
|
+
error: e instanceof Error ? e.message : String(e)
|
|
424
|
+
})
|
|
425
|
+
);
|
|
420
426
|
}
|
|
421
427
|
});
|
|
422
428
|
server.middlewares.use(`${apiBase}/file`, async (req, res, next) => {
|
|
@@ -449,10 +455,12 @@ function i18nDevToolsPlugin(options = {}) {
|
|
|
449
455
|
console.error("[i18n-devtools] File read error:", e);
|
|
450
456
|
res.statusCode = 500;
|
|
451
457
|
res.setHeader("Content-Type", "application/json");
|
|
452
|
-
res.end(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
458
|
+
res.end(
|
|
459
|
+
JSON.stringify({
|
|
460
|
+
success: false,
|
|
461
|
+
error: e instanceof Error ? e.message : String(e)
|
|
462
|
+
})
|
|
463
|
+
);
|
|
456
464
|
}
|
|
457
465
|
});
|
|
458
466
|
server.middlewares.use(`${apiBase}/save`, async (req, res, next) => {
|
|
@@ -490,10 +498,12 @@ function i18nDevToolsPlugin(options = {}) {
|
|
|
490
498
|
console.error("[i18n-devtools] Save error:", e);
|
|
491
499
|
res.statusCode = 500;
|
|
492
500
|
res.setHeader("Content-Type", "application/json");
|
|
493
|
-
res.end(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
501
|
+
res.end(
|
|
502
|
+
JSON.stringify({
|
|
503
|
+
success: false,
|
|
504
|
+
error: e instanceof Error ? e.message : String(e)
|
|
505
|
+
})
|
|
506
|
+
);
|
|
497
507
|
}
|
|
498
508
|
});
|
|
499
509
|
}
|
package/dist/vite/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","sources":["../../vite/plugin.ts"],"sourcesContent":["import type { PluginOption, ViteDevServer, IndexHtmlTransformResult } from 'vite'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { readdir } from 'node:fs/promises'\n\nexport interface DevToolsPluginOptions {\n base?: string\n translationDir?: string\n injectButton?: boolean\n}\n\n// Вспомогательная функция для безопасного резолва пути\nfunction safeResolvePath(projectRoot: string, filePath: string): string {\n const normalizedFile = filePath.replace(/^\\/+/, '').replace(/\\/+/g, '/')\n const resolvedPath = path.resolve(projectRoot, normalizedFile)\n const normalizedRoot = path.resolve(projectRoot)\n const normalizedFilePath = path.resolve(resolvedPath)\n\n if (!normalizedFilePath.startsWith(normalizedRoot)) {\n throw new Error(`Access denied: Path ${resolvedPath} is outside project root`)\n }\n\n return resolvedPath\n}\n\n// Рекурсивное сканирование директории для поиска JSON файлов\nasync function scanTranslationFiles(dir: string, baseDir: string): Promise<string[]> {\n const files: string[] = []\n try {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n const subFiles = await scanTranslationFiles(fullPath, baseDir)\n files.push(...subFiles)\n }\n else if (entry.isFile() && entry.name.endsWith('.json')) {\n const relativePath = path.relative(baseDir, fullPath)\n files.push(relativePath.replace(/\\\\/g, '/')) // Нормализуем для кроссплатформенности\n }\n }\n }\n catch (error) {\n // Игнорируем ошибки доступа к директориям\n console.warn(`[i18n-devtools] Cannot scan directory ${dir}:`, error)\n }\n return files\n}\n\n// Скрипт для инжекции кнопки\nconst BUTTON_INJECTION_SCRIPT = `\n(function() {\n if (typeof window === 'undefined' || document.getElementById('i18n-devtools-button-container')) {\n return;\n }\n\n const container = document.createElement('div');\n container.id = 'i18n-devtools-button-container';\n container.style.cssText = 'position: fixed; bottom: 20px; right: 20px; z-index: 99999; pointer-events: none;';\n document.body.appendChild(container);\n\n const button = document.createElement('button');\n button.id = 'i18n-devtools-button';\n button.type = 'button';\n button.innerHTML = '<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" style=\"display: inline-block; vertical-align: middle; margin-right: 8px;\"><circle cx=\"12\" cy=\"12\" r=\"10\" /><line x1=\"2\" y1=\"12\" x2=\"22\" y2=\"12\" /><path d=\"M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\" /></svg>i18n';\n button.style.cssText = 'cursor: pointer; background: #1e1e1e; color: white; padding: 8px 16px; border-radius: 9999px; display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border: 1px solid #333; font-size: 14px; font-weight: 600; transition: all 0.3s ease; opacity: 0.3; pointer-events: auto; font-family: system-ui, sans-serif;';\n\n let hideTimeout = null;\n let isVisible = true;\n let lastMouseY = window.innerHeight;\n\n function showButton() {\n if (!isVisible) {\n button.style.transform = 'translateX(0)';\n button.style.opacity = '0.3';\n isVisible = true;\n }\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n hideTimeout = null;\n }\n }\n\n function hideButton() {\n if (isVisible) {\n // Оставляем видимой только небольшую часть кнопки (примерно 40px)\n button.style.transform = 'translateX(calc(100% - 40px))';\n button.style.opacity = '0.3';\n isVisible = false;\n }\n }\n\n function checkMouseDistance(e) {\n const buttonRect = button.getBoundingClientRect();\n const buttonCenterX = buttonRect.left + buttonRect.width / 2;\n const buttonCenterY = buttonRect.top + buttonRect.height / 2;\n const distanceX = Math.abs(e.clientX - buttonCenterX);\n const distanceY = Math.abs(e.clientY - buttonCenterY);\n const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);\n\n if (distance > 200 && !button.matches(':hover')) {\n if (!hideTimeout) {\n hideTimeout = setTimeout(hideButton, 1000);\n }\n } else {\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n hideTimeout = null;\n }\n if (!isVisible) {\n showButton();\n }\n }\n lastMouseY = e.clientY;\n }\n\n button.addEventListener('mouseenter', function() {\n this.style.opacity = '1';\n this.style.transform = 'translateX(0) translateY(-2px) scale(1.05)';\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n hideTimeout = null;\n }\n // Показываем кнопку полностью при наведении\n if (!isVisible) {\n showButton();\n }\n });\n\n button.addEventListener('mouseleave', function() {\n if (isVisible) {\n this.style.opacity = '0.3';\n this.style.transform = 'translateX(0) scale(1)';\n hideTimeout = setTimeout(hideButton, 3000);\n }\n });\n\n button.addEventListener('click', function() {\n // Проверяем, не открыт ли уже редактор\n if (document.getElementById('i18n-devtools-modal')) {\n return;\n }\n\n // Открываем модальное окно с iframe\n const modal = document.createElement('div');\n modal.id = 'i18n-devtools-modal';\n 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;';\n modal.addEventListener('click', function(e) {\n if (e.target === modal) {\n document.body.removeChild(modal);\n }\n });\n\n const content = document.createElement('div');\n content.style.cssText = 'background: white; width: 90vw; max-width: 1200px; height: 85vh; border-radius: 12px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; overflow: hidden; border: 1px solid #e5e7eb;';\n content.addEventListener('click', function(e) {\n e.stopPropagation();\n });\n\n const header = document.createElement('div');\n header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #e2e8f0; background: #f8fafc; flex-shrink: 0;';\n header.innerHTML = '<div style=\"font-weight: 600; color: #334155; display: flex; align-items: center; gap: 8px;\"><svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\" /><line x1=\"2\" y1=\"12\" x2=\"22\" y2=\"12\" /><path d=\"M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\" /></svg> i18n DevTools</div><button id=\"i18n-devtools-close\" style=\"background: transparent; border: none; cursor: pointer; padding: 4px; border-radius: 4px; display: flex; color: #64748b; transition: background 0.2s;\">✕</button>';\n\n const closeBtn = header.querySelector('#i18n-devtools-close');\n closeBtn.addEventListener('mouseenter', function() {\n this.style.background = '#e2e8f0';\n });\n closeBtn.addEventListener('mouseleave', function() {\n this.style.background = 'transparent';\n });\n closeBtn.addEventListener('click', function() {\n document.body.removeChild(modal);\n });\n\n const iframeContainer = document.createElement('div');\n iframeContainer.style.cssText = 'flex: 1; position: relative; overflow: hidden; background: white;';\n\n const iframe = document.createElement('iframe');\n iframe.id = 'i18n-devtools-iframe';\n iframe.style.cssText = 'width: 100%; height: 100%; border: none; background: white;';\n iframe.src = '/__i18n_devtools.html';\n iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');\n\n iframeContainer.appendChild(iframe);\n content.appendChild(header);\n content.appendChild(iframeContainer);\n modal.appendChild(content);\n document.body.appendChild(modal);\n });\n\n document.addEventListener('mousemove', checkMouseDistance);\n\n // Автоматически скрываем через 3 секунды после загрузки\n setTimeout(() => {\n if (!button.matches(':hover')) {\n hideTimeout = setTimeout(hideButton, 3000);\n }\n }, 3000);\n\n container.appendChild(button);\n})();\n`\n\nexport function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginOption {\n const apiBase = options.base || '/__i18n_api'\n const translationDir = options.translationDir || 'src/locales'\n const injectButton = options.injectButton !== false // По умолчанию true\n\n return {\n name: 'i18n-micro-devtools-plugin',\n apply: 'serve', // Работает только в dev server\n\n resolveId(id: string) {\n if (id === '/@vite-plugin-i18n-devtools/devtools-ui.js') {\n return id\n }\n return null\n },\n\n load(id: string) {\n if (id === '/@vite-plugin-i18n-devtools/devtools-ui.js') {\n // Возвращаем реэкспорт из devtools-ui пакета\n return `export * from '@i18n-micro/devtools-ui'`\n }\n return null\n },\n\n transformIndexHtml(html: string): IndexHtmlTransformResult {\n if (!injectButton) {\n return html\n }\n\n // Инжектируем скрипт перед закрывающим тегом </body>\n const scriptTag = `<script>${BUTTON_INJECTION_SCRIPT}</script>`\n if (html.includes('</body>')) {\n return html.replace('</body>', `${scriptTag}</body>`)\n }\n // Если нет </body>, добавляем в конец\n return html + scriptTag\n },\n\n configureServer(server: ViteDevServer) {\n // Используем server.config.root как источник правды для корня проекта\n const projectRoot = server.config.root\n\n // HTML страница для iframe с devtools UI\n // Используем виртуальный модуль для правильного импорта\n const devtoolsHtml = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>i18n DevTools</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n body {\n font-family: system-ui, -apple-system, sans-serif;\n overflow: hidden;\n height: 100vh;\n }\n #app {\n width: 100%;\n height: 100%;\n }\n .loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #64748b;\n }\n </style>\n</head>\n<body>\n <div id=\"app\">\n <div class=\"loading\">Loading DevTools...</div>\n </div>\n <script type=\"module\">\n // Импортируем devtools UI\n // Пытаемся импортировать через разные пути для совместимости\n let register;\n try {\n const devtoolsModule = await import('/@vite-plugin-i18n-devtools/devtools-ui.js');\n register = devtoolsModule.register;\n } catch (e) {\n // Fallback: пытаемся импортировать напрямую\n try {\n const devtoolsModule = await import('@i18n-micro/devtools-ui');\n register = devtoolsModule.register;\n } catch (e2) {\n console.error('Failed to load devtools UI:', e2);\n document.getElementById('app').innerHTML = '<div class=\"loading\" style=\"color: #ef4444;\">Failed to load DevTools. Please ensure @i18n-micro/devtools-ui is installed.</div>';\n throw e2;\n }\n }\n\n // Регистрируем custom element\n register();\n\n // Создаем bridge через API\n const bridge = {\n async getLocalesAndTranslations() {\n try {\n const response = await fetch('/__i18n_api/files');\n const data = await response.json();\n const result = {};\n for (const file of data.files || []) {\n try {\n const fileResponse = await fetch('/__i18n_api/file?path=' + encodeURIComponent(file));\n const fileData = await fileResponse.json();\n if (fileData.success && fileData.content) {\n result[file] = fileData.content;\n }\n } catch (e) {\n console.warn('Failed to load file:', file, e);\n }\n }\n return result;\n } catch (e) {\n console.error('Failed to get locales:', e);\n return {};\n }\n },\n\n async getConfigs() {\n try {\n const response = await fetch('/__i18n_api/config');\n return await response.json();\n } catch (e) {\n return {\n defaultLocale: 'en',\n fallbackLocale: 'en',\n locales: [],\n translationDir: '${translationDir}',\n };\n }\n },\n\n async saveTranslation(filePath, content) {\n const response = await fetch('/__i18n_api/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ file: filePath, content }),\n });\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Unknown error' }));\n throw new Error(error.error || 'Failed to save');\n }\n },\n\n onLocalesUpdate(callback) {\n let interval = null;\n interval = setInterval(async () => {\n try {\n const data = await bridge.getLocalesAndTranslations();\n callback(data);\n } catch (e) {\n console.error('Failed to update locales:', e);\n }\n }, 2000);\n return () => {\n if (interval) {\n clearInterval(interval);\n }\n };\n },\n };\n\n const app = document.getElementById('app');\n app.innerHTML = '';\n const element = document.createElement('i18n-devtools-ui');\n element.bridge = bridge;\n element.style.cssText = 'width: 100%; height: 100%; display: block;';\n app.appendChild(element);\n </script>\n</body>\n</html>`\n\n // Middleware для обслуживания HTML страницы devtools\n server.middlewares.use('/__i18n_devtools.html', (_req: IncomingMessage, res: ServerResponse) => {\n res.statusCode = 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(devtoolsHtml)\n })\n\n // Эндпоинт для получения конфигурации\n server.middlewares.use(`${apiBase}/config`, async (_req: IncomingMessage, res: ServerResponse, next) => {\n if (_req.method !== 'GET') {\n return next()\n }\n\n try {\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({\n defaultLocale: 'en',\n fallbackLocale: 'en',\n locales: [],\n translationDir,\n }))\n }\n catch (e) {\n console.error('[i18n-devtools] Config error:', e)\n res.statusCode = 500\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({\n success: false,\n error: e instanceof Error ? e.message : String(e),\n }))\n }\n })\n\n // Эндпоинт для получения списка файлов\n server.middlewares.use(`${apiBase}/files`, async (req: IncomingMessage, res: ServerResponse, next) => {\n if (req.method !== 'GET') {\n return next()\n }\n\n try {\n const localesPath = path.resolve(projectRoot, translationDir)\n\n // Проверяем, что путь безопасен\n safeResolvePath(projectRoot, translationDir)\n\n if (!fs.existsSync(localesPath)) {\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ files: [], structure: {} }))\n return\n }\n\n const files = await scanTranslationFiles(localesPath, localesPath)\n\n // Строим структуру директорий\n const structure: Record<string, unknown> = {}\n for (const file of files) {\n const parts = file.split('/')\n let current = structure\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]!\n if (!current[part]) {\n current[part] = {}\n }\n current = current[part] as Record<string, unknown>\n }\n const last = parts[parts.length - 1]\n if (last !== undefined) {\n current[last] = file\n }\n }\n\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ files, structure }))\n }\n catch (e) {\n console.error('[i18n-devtools] Files list error:', e)\n res.statusCode = 500\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({\n success: false,\n error: e instanceof Error ? e.message : String(e),\n }))\n }\n })\n\n // Эндпоинт для загрузки конкретного файла\n server.middlewares.use(`${apiBase}/file`, async (req: IncomingMessage, res: ServerResponse, next) => {\n if (req.method !== 'GET') {\n return next()\n }\n\n try {\n const url = new URL(req.url || '', `http://${req.headers.host}`)\n const filePath = url.searchParams.get('path')\n\n if (!filePath) {\n throw new Error('Path parameter is required')\n }\n\n // Резолвим путь относительно translationDir\n const fullPath = path.join(translationDir, filePath)\n const resolvedPath = safeResolvePath(projectRoot, fullPath)\n\n if (!fs.existsSync(resolvedPath)) {\n res.statusCode = 404\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ success: false, error: 'File not found' }))\n return\n }\n\n if (!resolvedPath.endsWith('.json')) {\n throw new Error('Invalid file: only .json files are allowed')\n }\n\n const content = fs.readFileSync(resolvedPath, 'utf-8')\n const parsed = JSON.parse(content)\n\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ success: true, content: parsed, path: filePath }))\n }\n catch (e) {\n console.error('[i18n-devtools] File read error:', e)\n res.statusCode = 500\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({\n success: false,\n error: e instanceof Error ? e.message : String(e),\n }))\n }\n })\n\n // Эндпоинт для сохранения файла\n server.middlewares.use(`${apiBase}/save`, async (req: IncomingMessage, res: ServerResponse, next) => {\n if (req.method !== 'POST') {\n return next()\n }\n\n try {\n // Чтение тела запроса\n const buffers: Buffer[] = []\n for await (const chunk of req) {\n buffers.push(chunk)\n }\n const bodyData = Buffer.concat(buffers).toString()\n\n if (!bodyData) {\n throw new Error('Empty request body')\n }\n\n const body = JSON.parse(bodyData)\n const { file, content } = body\n\n if (!file || !content) {\n throw new Error('Invalid data: file and content are required')\n }\n\n // Резолвим путь относительно translationDir, если путь не абсолютный\n const fullPath = file.startsWith(translationDir) ? file : path.join(translationDir, file)\n const filePath = safeResolvePath(projectRoot, fullPath)\n\n // Проверка расширения\n if (!filePath.endsWith('.json')) {\n throw new Error('Invalid file: only .json files are allowed')\n }\n\n // Создаем директорию, если её нет\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n // Записываем файл\n fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf-8')\n\n // Успешный ответ\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ success: true }))\n }\n catch (e) {\n console.error('[i18n-devtools] Save error:', e)\n res.statusCode = 500\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({\n success: false,\n error: e instanceof Error ? e.message : String(e),\n }))\n }\n })\n },\n } as PluginOption\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.mjs","sources":["../../vite/plugin.ts"],"sourcesContent":["import * as fs from 'node:fs'\nimport { readdir } from 'node:fs/promises'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport * as path from 'node:path'\nimport type { IndexHtmlTransformResult, PluginOption, ViteDevServer } from 'vite'\n\nexport interface DevToolsPluginOptions {\n base?: string\n translationDir?: string\n injectButton?: boolean\n}\n\n// Вспомогательная функция для безопасного резолва пути\nfunction safeResolvePath(projectRoot: string, filePath: string): string {\n const normalizedFile = filePath.replace(/^\\/+/, '').replace(/\\/+/g, '/')\n const resolvedPath = path.resolve(projectRoot, normalizedFile)\n const normalizedRoot = path.resolve(projectRoot)\n const normalizedFilePath = path.resolve(resolvedPath)\n\n if (!normalizedFilePath.startsWith(normalizedRoot)) {\n throw new Error(`Access denied: Path ${resolvedPath} is outside project root`)\n }\n\n return resolvedPath\n}\n\n// Рекурсивное сканирование директории для поиска JSON файлов\nasync function scanTranslationFiles(dir: string, baseDir: string): Promise<string[]> {\n const files: string[] = []\n try {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n const subFiles = await scanTranslationFiles(fullPath, baseDir)\n files.push(...subFiles)\n } else if (entry.isFile() && entry.name.endsWith('.json')) {\n const relativePath = path.relative(baseDir, fullPath)\n files.push(relativePath.replace(/\\\\/g, '/')) // Нормализуем для кроссплатформенности\n }\n }\n } catch (error) {\n // Игнорируем ошибки доступа к директориям\n console.warn(`[i18n-devtools] Cannot scan directory ${dir}:`, error)\n }\n return files\n}\n\n// Скрипт для инжекции кнопки\nconst BUTTON_INJECTION_SCRIPT = `\n(function() {\n if (typeof window === 'undefined' || document.getElementById('i18n-devtools-button-container')) {\n return;\n }\n\n const container = document.createElement('div');\n container.id = 'i18n-devtools-button-container';\n container.style.cssText = 'position: fixed; bottom: 20px; right: 20px; z-index: 99999; pointer-events: none;';\n document.body.appendChild(container);\n\n const button = document.createElement('button');\n button.id = 'i18n-devtools-button';\n button.type = 'button';\n button.innerHTML = '<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" style=\"display: inline-block; vertical-align: middle; margin-right: 8px;\"><circle cx=\"12\" cy=\"12\" r=\"10\" /><line x1=\"2\" y1=\"12\" x2=\"22\" y2=\"12\" /><path d=\"M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\" /></svg>i18n';\n button.style.cssText = 'cursor: pointer; background: #1e1e1e; color: white; padding: 8px 16px; border-radius: 9999px; display: flex; align-items: center; gap: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border: 1px solid #333; font-size: 14px; font-weight: 600; transition: all 0.3s ease; opacity: 0.3; pointer-events: auto; font-family: system-ui, sans-serif;';\n\n let hideTimeout = null;\n let isVisible = true;\n let lastMouseY = window.innerHeight;\n\n function showButton() {\n if (!isVisible) {\n button.style.transform = 'translateX(0)';\n button.style.opacity = '0.3';\n isVisible = true;\n }\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n hideTimeout = null;\n }\n }\n\n function hideButton() {\n if (isVisible) {\n // Оставляем видимой только небольшую часть кнопки (примерно 40px)\n button.style.transform = 'translateX(calc(100% - 40px))';\n button.style.opacity = '0.3';\n isVisible = false;\n }\n }\n\n function checkMouseDistance(e) {\n const buttonRect = button.getBoundingClientRect();\n const buttonCenterX = buttonRect.left + buttonRect.width / 2;\n const buttonCenterY = buttonRect.top + buttonRect.height / 2;\n const distanceX = Math.abs(e.clientX - buttonCenterX);\n const distanceY = Math.abs(e.clientY - buttonCenterY);\n const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);\n\n if (distance > 200 && !button.matches(':hover')) {\n if (!hideTimeout) {\n hideTimeout = setTimeout(hideButton, 1000);\n }\n } else {\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n hideTimeout = null;\n }\n if (!isVisible) {\n showButton();\n }\n }\n lastMouseY = e.clientY;\n }\n\n button.addEventListener('mouseenter', function() {\n this.style.opacity = '1';\n this.style.transform = 'translateX(0) translateY(-2px) scale(1.05)';\n if (hideTimeout) {\n clearTimeout(hideTimeout);\n hideTimeout = null;\n }\n // Показываем кнопку полностью при наведении\n if (!isVisible) {\n showButton();\n }\n });\n\n button.addEventListener('mouseleave', function() {\n if (isVisible) {\n this.style.opacity = '0.3';\n this.style.transform = 'translateX(0) scale(1)';\n hideTimeout = setTimeout(hideButton, 3000);\n }\n });\n\n button.addEventListener('click', function() {\n // Проверяем, не открыт ли уже редактор\n if (document.getElementById('i18n-devtools-modal')) {\n return;\n }\n\n // Открываем модальное окно с iframe\n const modal = document.createElement('div');\n modal.id = 'i18n-devtools-modal';\n 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;';\n modal.addEventListener('click', function(e) {\n if (e.target === modal) {\n document.body.removeChild(modal);\n }\n });\n\n const content = document.createElement('div');\n content.style.cssText = 'background: white; width: 90vw; max-width: 1200px; height: 85vh; border-radius: 12px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; overflow: hidden; border: 1px solid #e5e7eb;';\n content.addEventListener('click', function(e) {\n e.stopPropagation();\n });\n\n const header = document.createElement('div');\n header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #e2e8f0; background: #f8fafc; flex-shrink: 0;';\n header.innerHTML = '<div style=\"font-weight: 600; color: #334155; display: flex; align-items: center; gap: 8px;\"><svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\" /><line x1=\"2\" y1=\"12\" x2=\"22\" y2=\"12\" /><path d=\"M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z\" /></svg> i18n DevTools</div><button id=\"i18n-devtools-close\" style=\"background: transparent; border: none; cursor: pointer; padding: 4px; border-radius: 4px; display: flex; color: #64748b; transition: background 0.2s;\">✕</button>';\n\n const closeBtn = header.querySelector('#i18n-devtools-close');\n closeBtn.addEventListener('mouseenter', function() {\n this.style.background = '#e2e8f0';\n });\n closeBtn.addEventListener('mouseleave', function() {\n this.style.background = 'transparent';\n });\n closeBtn.addEventListener('click', function() {\n document.body.removeChild(modal);\n });\n\n const iframeContainer = document.createElement('div');\n iframeContainer.style.cssText = 'flex: 1; position: relative; overflow: hidden; background: white;';\n\n const iframe = document.createElement('iframe');\n iframe.id = 'i18n-devtools-iframe';\n iframe.style.cssText = 'width: 100%; height: 100%; border: none; background: white;';\n iframe.src = '/__i18n_devtools.html';\n iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-popups');\n\n iframeContainer.appendChild(iframe);\n content.appendChild(header);\n content.appendChild(iframeContainer);\n modal.appendChild(content);\n document.body.appendChild(modal);\n });\n\n document.addEventListener('mousemove', checkMouseDistance);\n\n // Автоматически скрываем через 3 секунды после загрузки\n setTimeout(() => {\n if (!button.matches(':hover')) {\n hideTimeout = setTimeout(hideButton, 3000);\n }\n }, 3000);\n\n container.appendChild(button);\n})();\n`\n\nexport function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginOption {\n const apiBase = options.base || '/__i18n_api'\n const translationDir = options.translationDir || 'src/locales'\n const injectButton = options.injectButton !== false // По умолчанию true\n\n return {\n name: 'i18n-micro-devtools-plugin',\n apply: 'serve', // Работает только в dev server\n\n resolveId(id: string) {\n if (id === '/@vite-plugin-i18n-devtools/devtools-ui.js') {\n return id\n }\n return null\n },\n\n load(id: string) {\n if (id === '/@vite-plugin-i18n-devtools/devtools-ui.js') {\n // Возвращаем реэкспорт из devtools-ui пакета\n return `export * from '@i18n-micro/devtools-ui'`\n }\n return null\n },\n\n transformIndexHtml(html: string): IndexHtmlTransformResult {\n if (!injectButton) {\n return html\n }\n\n // Инжектируем скрипт перед закрывающим тегом </body>\n const scriptTag = `<script>${BUTTON_INJECTION_SCRIPT}</script>`\n if (html.includes('</body>')) {\n return html.replace('</body>', `${scriptTag}</body>`)\n }\n // Если нет </body>, добавляем в конец\n return html + scriptTag\n },\n\n configureServer(server: ViteDevServer) {\n // Используем server.config.root как источник правды для корня проекта\n const projectRoot = server.config.root\n\n // HTML страница для iframe с devtools UI\n // Используем виртуальный модуль для правильного импорта\n const devtoolsHtml = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>i18n DevTools</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n body {\n font-family: system-ui, -apple-system, sans-serif;\n overflow: hidden;\n height: 100vh;\n }\n #app {\n width: 100%;\n height: 100%;\n }\n .loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: #64748b;\n }\n </style>\n</head>\n<body>\n <div id=\"app\">\n <div class=\"loading\">Loading DevTools...</div>\n </div>\n <script type=\"module\">\n // Импортируем devtools UI\n // Пытаемся импортировать через разные пути для совместимости\n let register;\n try {\n const devtoolsModule = await import('/@vite-plugin-i18n-devtools/devtools-ui.js');\n register = devtoolsModule.register;\n } catch (e) {\n // Fallback: пытаемся импортировать напрямую\n try {\n const devtoolsModule = await import('@i18n-micro/devtools-ui');\n register = devtoolsModule.register;\n } catch (e2) {\n console.error('Failed to load devtools UI:', e2);\n document.getElementById('app').innerHTML = '<div class=\"loading\" style=\"color: #ef4444;\">Failed to load DevTools. Please ensure @i18n-micro/devtools-ui is installed.</div>';\n throw e2;\n }\n }\n\n // Регистрируем custom element\n register();\n\n // Создаем bridge через API\n const bridge = {\n async getLocalesAndTranslations() {\n try {\n const response = await fetch('/__i18n_api/files');\n const data = await response.json();\n const result = {};\n for (const file of data.files || []) {\n try {\n const fileResponse = await fetch('/__i18n_api/file?path=' + encodeURIComponent(file));\n const fileData = await fileResponse.json();\n if (fileData.success && fileData.content) {\n result[file] = fileData.content;\n }\n } catch (e) {\n console.warn('Failed to load file:', file, e);\n }\n }\n return result;\n } catch (e) {\n console.error('Failed to get locales:', e);\n return {};\n }\n },\n\n async getConfigs() {\n try {\n const response = await fetch('/__i18n_api/config');\n return await response.json();\n } catch (e) {\n return {\n defaultLocale: 'en',\n fallbackLocale: 'en',\n locales: [],\n translationDir: '${translationDir}',\n };\n }\n },\n\n async saveTranslation(filePath, content) {\n const response = await fetch('/__i18n_api/save', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ file: filePath, content }),\n });\n if (!response.ok) {\n const error = await response.json().catch(() => ({ error: 'Unknown error' }));\n throw new Error(error.error || 'Failed to save');\n }\n },\n\n onLocalesUpdate(callback) {\n let interval = null;\n interval = setInterval(async () => {\n try {\n const data = await bridge.getLocalesAndTranslations();\n callback(data);\n } catch (e) {\n console.error('Failed to update locales:', e);\n }\n }, 2000);\n return () => {\n if (interval) {\n clearInterval(interval);\n }\n };\n },\n };\n\n const app = document.getElementById('app');\n app.innerHTML = '';\n const element = document.createElement('i18n-devtools-ui');\n element.bridge = bridge;\n element.style.cssText = 'width: 100%; height: 100%; display: block;';\n app.appendChild(element);\n </script>\n</body>\n</html>`\n\n // Middleware для обслуживания HTML страницы devtools\n server.middlewares.use('/__i18n_devtools.html', (_req: IncomingMessage, res: ServerResponse) => {\n res.statusCode = 200\n res.setHeader('Content-Type', 'text/html; charset=utf-8')\n res.end(devtoolsHtml)\n })\n\n // Эндпоинт для получения конфигурации\n server.middlewares.use(`${apiBase}/config`, async (_req: IncomingMessage, res: ServerResponse, next) => {\n if (_req.method !== 'GET') {\n return next()\n }\n\n try {\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(\n JSON.stringify({\n defaultLocale: 'en',\n fallbackLocale: 'en',\n locales: [],\n translationDir,\n }),\n )\n } catch (e) {\n console.error('[i18n-devtools] Config error:', e)\n res.statusCode = 500\n res.setHeader('Content-Type', 'application/json')\n res.end(\n JSON.stringify({\n success: false,\n error: e instanceof Error ? e.message : String(e),\n }),\n )\n }\n })\n\n // Эндпоинт для получения списка файлов\n server.middlewares.use(`${apiBase}/files`, async (req: IncomingMessage, res: ServerResponse, next) => {\n if (req.method !== 'GET') {\n return next()\n }\n\n try {\n const localesPath = path.resolve(projectRoot, translationDir)\n\n // Проверяем, что путь безопасен\n safeResolvePath(projectRoot, translationDir)\n\n if (!fs.existsSync(localesPath)) {\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ files: [], structure: {} }))\n return\n }\n\n const files = await scanTranslationFiles(localesPath, localesPath)\n\n // Строим структуру директорий\n const structure: Record<string, unknown> = {}\n for (const file of files) {\n const parts = file.split('/')\n let current = structure\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]!\n if (!current[part]) {\n current[part] = {}\n }\n current = current[part] as Record<string, unknown>\n }\n const last = parts[parts.length - 1]\n if (last !== undefined) {\n current[last] = file\n }\n }\n\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ files, structure }))\n } catch (e) {\n console.error('[i18n-devtools] Files list error:', e)\n res.statusCode = 500\n res.setHeader('Content-Type', 'application/json')\n res.end(\n JSON.stringify({\n success: false,\n error: e instanceof Error ? e.message : String(e),\n }),\n )\n }\n })\n\n // Эндпоинт для загрузки конкретного файла\n server.middlewares.use(`${apiBase}/file`, async (req: IncomingMessage, res: ServerResponse, next) => {\n if (req.method !== 'GET') {\n return next()\n }\n\n try {\n const url = new URL(req.url || '', `http://${req.headers.host}`)\n const filePath = url.searchParams.get('path')\n\n if (!filePath) {\n throw new Error('Path parameter is required')\n }\n\n // Резолвим путь относительно translationDir\n const fullPath = path.join(translationDir, filePath)\n const resolvedPath = safeResolvePath(projectRoot, fullPath)\n\n if (!fs.existsSync(resolvedPath)) {\n res.statusCode = 404\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ success: false, error: 'File not found' }))\n return\n }\n\n if (!resolvedPath.endsWith('.json')) {\n throw new Error('Invalid file: only .json files are allowed')\n }\n\n const content = fs.readFileSync(resolvedPath, 'utf-8')\n const parsed = JSON.parse(content)\n\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ success: true, content: parsed, path: filePath }))\n } catch (e) {\n console.error('[i18n-devtools] File read error:', e)\n res.statusCode = 500\n res.setHeader('Content-Type', 'application/json')\n res.end(\n JSON.stringify({\n success: false,\n error: e instanceof Error ? e.message : String(e),\n }),\n )\n }\n })\n\n // Эндпоинт для сохранения файла\n server.middlewares.use(`${apiBase}/save`, async (req: IncomingMessage, res: ServerResponse, next) => {\n if (req.method !== 'POST') {\n return next()\n }\n\n try {\n // Чтение тела запроса\n const buffers: Buffer[] = []\n for await (const chunk of req) {\n buffers.push(chunk)\n }\n const bodyData = Buffer.concat(buffers).toString()\n\n if (!bodyData) {\n throw new Error('Empty request body')\n }\n\n const body = JSON.parse(bodyData)\n const { file, content } = body\n\n if (!file || !content) {\n throw new Error('Invalid data: file and content are required')\n }\n\n // Резолвим путь относительно translationDir, если путь не абсолютный\n const fullPath = file.startsWith(translationDir) ? file : path.join(translationDir, file)\n const filePath = safeResolvePath(projectRoot, fullPath)\n\n // Проверка расширения\n if (!filePath.endsWith('.json')) {\n throw new Error('Invalid file: only .json files are allowed')\n }\n\n // Создаем директорию, если её нет\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n // Записываем файл\n fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf-8')\n\n // Успешный ответ\n res.statusCode = 200\n res.setHeader('Content-Type', 'application/json')\n res.end(JSON.stringify({ success: true }))\n } catch (e) {\n console.error('[i18n-devtools] Save error:', e)\n res.statusCode = 500\n res.setHeader('Content-Type', 'application/json')\n res.end(\n JSON.stringify({\n success: false,\n error: e instanceof Error ? e.message : String(e),\n }),\n )\n }\n })\n },\n } as PluginOption\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@i18n-micro/devtools-ui",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.umd.js",
|
|
6
6
|
"module": "./dist/index.es.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"vue": "^3.5.25",
|
|
36
|
-
"@i18n-micro/types": "1.1.
|
|
36
|
+
"@i18n-micro/types": "1.1.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@tailwindcss/vite": "^4.1.17",
|
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
|
|
@@ -34,14 +34,12 @@ 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
39
|
files.push(relativePath.replace(/\\/g, '/')) // Нормализуем для кроссплатформенности
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
42
|
+
} catch (error) {
|
|
45
43
|
// Игнорируем ошибки доступа к директориям
|
|
46
44
|
console.warn(`[i18n-devtools] Cannot scan directory ${dir}:`, error)
|
|
47
45
|
}
|
|
@@ -397,21 +395,24 @@ 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(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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(
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
|
@@ -457,15 +458,16 @@ 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(
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
|
@@ -504,15 +506,16 @@ 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(
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
|
@@ -563,15 +566,16 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
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(
|
|
572
|
-
|
|
573
|
-
|
|
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
|
},
|