@i18n-micro/devtools-ui 1.1.1 → 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/dist/bridge/create-bridge.d.ts +2 -6
- package/dist/composables/useI18nState.d.ts +16 -8
- package/dist/devtools-ui.css +1 -1
- package/dist/index.es.js +190 -193
- 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/util/Translator.d.ts +11 -11
- package/dist/vite/plugin.mjs +11 -11
- package/dist/vite/plugin.mjs.map +1 -1
- package/package.json +2 -2
- package/vite/plugin.ts +37 -37
|
@@ -22,41 +22,41 @@ export declare class Translator {
|
|
|
22
22
|
*/
|
|
23
23
|
translate(text: string, fromLang: string, toLang: string): Promise<string>;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* Cleans text before translation.
|
|
26
|
+
* Replaces special constructs with temporary markers.
|
|
27
27
|
*/
|
|
28
28
|
private clearText;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Restores text after translation.
|
|
31
|
+
* Replaces temporary markers with original values.
|
|
32
32
|
*/
|
|
33
33
|
private revertText;
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* Google Translator (paid API).
|
|
36
36
|
*/
|
|
37
37
|
private googleTranslate;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* Yandex Translator (paid API).
|
|
40
40
|
*/
|
|
41
41
|
private yandexTranslate;
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
43
|
+
* DeepL Translator (paid API).
|
|
44
44
|
*/
|
|
45
45
|
private deeplTranslate;
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* OpenAI Translator (GPT).
|
|
48
48
|
*/
|
|
49
49
|
private openaiTranslate;
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
51
|
+
* DeepSeek Translator.
|
|
52
52
|
*/
|
|
53
53
|
private deepseekTranslate;
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* Google Free Translator (unofficial API).
|
|
56
56
|
*/
|
|
57
57
|
private googleFreeTranslate;
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* Yandex Cloud Translator.
|
|
60
60
|
*/
|
|
61
61
|
private yandexCloudTranslate;
|
|
62
62
|
/**
|
package/dist/vite/plugin.mjs
CHANGED
|
@@ -65,7 +65,7 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
65
65
|
|
|
66
66
|
function hideButton() {
|
|
67
67
|
if (isVisible) {
|
|
68
|
-
//
|
|
68
|
+
// Leave only a small part of the button visible (approximately 40px)
|
|
69
69
|
button.style.transform = 'translateX(calc(100% - 40px))';
|
|
70
70
|
button.style.opacity = '0.3';
|
|
71
71
|
isVisible = false;
|
|
@@ -103,7 +103,7 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
103
103
|
clearTimeout(hideTimeout);
|
|
104
104
|
hideTimeout = null;
|
|
105
105
|
}
|
|
106
|
-
//
|
|
106
|
+
// Show the button fully on hover
|
|
107
107
|
if (!isVisible) {
|
|
108
108
|
showButton();
|
|
109
109
|
}
|
|
@@ -118,12 +118,12 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
button.addEventListener('click', function() {
|
|
121
|
-
//
|
|
121
|
+
// Check if the editor is already open
|
|
122
122
|
if (document.getElementById('i18n-devtools-modal')) {
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
//
|
|
126
|
+
// Open modal window with iframe
|
|
127
127
|
const modal = document.createElement('div');
|
|
128
128
|
modal.id = 'i18n-devtools-modal';
|
|
129
129
|
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;';
|
|
@@ -172,7 +172,7 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
172
172
|
|
|
173
173
|
document.addEventListener('mousemove', checkMouseDistance);
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// Auto-hide after 3 seconds of loading
|
|
176
176
|
setTimeout(() => {
|
|
177
177
|
if (!button.matches(':hover')) {
|
|
178
178
|
hideTimeout = setTimeout(hideButton, 3000);
|
|
@@ -189,7 +189,7 @@ function i18nDevToolsPlugin(options = {}) {
|
|
|
189
189
|
return {
|
|
190
190
|
name: "i18n-micro-devtools-plugin",
|
|
191
191
|
apply: "serve",
|
|
192
|
-
//
|
|
192
|
+
// Works only in dev server
|
|
193
193
|
resolveId(id) {
|
|
194
194
|
if (id === "/@vite-plugin-i18n-devtools/devtools-ui.js") {
|
|
195
195
|
return id;
|
|
@@ -249,14 +249,14 @@ function i18nDevToolsPlugin(options = {}) {
|
|
|
249
249
|
<div class="loading">Loading DevTools...</div>
|
|
250
250
|
</div>
|
|
251
251
|
<script type="module">
|
|
252
|
-
//
|
|
253
|
-
//
|
|
252
|
+
// Import devtools UI
|
|
253
|
+
// Try to import through different paths for compatibility
|
|
254
254
|
let register;
|
|
255
255
|
try {
|
|
256
256
|
const devtoolsModule = await import('/@vite-plugin-i18n-devtools/devtools-ui.js');
|
|
257
257
|
register = devtoolsModule.register;
|
|
258
258
|
} catch (e) {
|
|
259
|
-
// Fallback:
|
|
259
|
+
// Fallback: try to import directly
|
|
260
260
|
try {
|
|
261
261
|
const devtoolsModule = await import('@i18n-micro/devtools-ui');
|
|
262
262
|
register = devtoolsModule.register;
|
|
@@ -267,10 +267,10 @@ function i18nDevToolsPlugin(options = {}) {
|
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
//
|
|
270
|
+
// Register custom element
|
|
271
271
|
register();
|
|
272
272
|
|
|
273
|
-
//
|
|
273
|
+
// Create bridge via API
|
|
274
274
|
const bridge = {
|
|
275
275
|
async getLocalesAndTranslations() {
|
|
276
276
|
try {
|
package/dist/vite/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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// Helper function for safe path resolution\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// Recursive directory scanning to find JSON files\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, '/')) // Normalize for cross-platform compatibility\n }\n }\n } catch (error) {\n // Ignore directory access errors\n console.warn(`[i18n-devtools] Cannot scan directory ${dir}:`, error)\n }\n return files\n}\n\n// Script for button injection\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 // Leave only a small part of the button visible (approximately 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 // Show the button fully on hover\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 // Check if the editor is already open\n if (document.getElementById('i18n-devtools-modal')) {\n return;\n }\n\n // Open modal window with 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 // Auto-hide after 3 seconds of loading\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 // Default is true\n\n return {\n name: 'i18n-micro-devtools-plugin',\n apply: 'serve', // Works only in 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 // Return re-export from devtools-ui package\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 // Inject script before the closing </body> tag\n const scriptTag = `<script>${BUTTON_INJECTION_SCRIPT}</script>`\n if (html.includes('</body>')) {\n return html.replace('</body>', `${scriptTag}</body>`)\n }\n // If no </body>, append to the end\n return html + scriptTag\n },\n\n configureServer(server: ViteDevServer) {\n // Use server.config.root as the source of truth for the project root\n const projectRoot = server.config.root\n\n // HTML page for iframe with devtools UI\n // Use virtual module for proper imports\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 // Import devtools UI\n // Try to import through different paths for compatibility\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: try to import directly\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 // Register custom element\n register();\n\n // Create bridge via 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 for serving the devtools HTML page\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 // Endpoint for getting configuration\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 // Endpoint for getting file list\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 // Verify the path is safe\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 // Build directory structure\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 // Endpoint for loading a specific file\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 // Resolve path relative to 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 // Endpoint for saving a file\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 // Read request body\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 // Resolve path relative to translationDir if path is not absolute\n const fullPath = file.startsWith(translationDir) ? file : path.join(translationDir, file)\n const filePath = safeResolvePath(projectRoot, fullPath)\n\n // Check extension\n if (!filePath.endsWith('.json')) {\n throw new Error('Invalid file: only .json files are allowed')\n }\n\n // Create directory if it doesn't exist\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n // Write file\n fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf-8')\n\n // Successful response\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.
|
|
3
|
+
"version": "1.2.0",
|
|
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.5"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@tailwindcss/vite": "^4.1.17",
|
package/vite/plugin.ts
CHANGED
|
@@ -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
|
-
//
|
|
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 {
|
|
@@ -36,17 +36,17 @@ async function scanTranslationFiles(dir: string, baseDir: string): Promise<strin
|
|
|
36
36
|
files.push(...subFiles)
|
|
37
37
|
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
38
38
|
const relativePath = path.relative(baseDir, fullPath)
|
|
39
|
-
files.push(relativePath.replace(/\\/g, '/')) //
|
|
39
|
+
files.push(relativePath.replace(/\\/g, '/')) // Normalize for cross-platform compatibility
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
} catch (error) {
|
|
43
|
-
//
|
|
43
|
+
// Ignore directory access errors
|
|
44
44
|
console.warn(`[i18n-devtools] Cannot scan directory ${dir}:`, error)
|
|
45
45
|
}
|
|
46
46
|
return files
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// Script for button injection
|
|
50
50
|
const BUTTON_INJECTION_SCRIPT = `
|
|
51
51
|
(function() {
|
|
52
52
|
if (typeof window === 'undefined' || document.getElementById('i18n-devtools-button-container')) {
|
|
@@ -82,7 +82,7 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
82
82
|
|
|
83
83
|
function hideButton() {
|
|
84
84
|
if (isVisible) {
|
|
85
|
-
//
|
|
85
|
+
// Leave only a small part of the button visible (approximately 40px)
|
|
86
86
|
button.style.transform = 'translateX(calc(100% - 40px))';
|
|
87
87
|
button.style.opacity = '0.3';
|
|
88
88
|
isVisible = false;
|
|
@@ -120,7 +120,7 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
120
120
|
clearTimeout(hideTimeout);
|
|
121
121
|
hideTimeout = null;
|
|
122
122
|
}
|
|
123
|
-
//
|
|
123
|
+
// Show the button fully on hover
|
|
124
124
|
if (!isVisible) {
|
|
125
125
|
showButton();
|
|
126
126
|
}
|
|
@@ -135,12 +135,12 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
button.addEventListener('click', function() {
|
|
138
|
-
//
|
|
138
|
+
// Check if the editor is already open
|
|
139
139
|
if (document.getElementById('i18n-devtools-modal')) {
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
//
|
|
143
|
+
// Open modal window with iframe
|
|
144
144
|
const modal = document.createElement('div');
|
|
145
145
|
modal.id = 'i18n-devtools-modal';
|
|
146
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;';
|
|
@@ -189,7 +189,7 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
189
189
|
|
|
190
190
|
document.addEventListener('mousemove', checkMouseDistance);
|
|
191
191
|
|
|
192
|
-
//
|
|
192
|
+
// Auto-hide after 3 seconds of loading
|
|
193
193
|
setTimeout(() => {
|
|
194
194
|
if (!button.matches(':hover')) {
|
|
195
195
|
hideTimeout = setTimeout(hideButton, 3000);
|
|
@@ -203,11 +203,11 @@ const BUTTON_INJECTION_SCRIPT = `
|
|
|
203
203
|
export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginOption {
|
|
204
204
|
const apiBase = options.base || '/__i18n_api'
|
|
205
205
|
const translationDir = options.translationDir || 'src/locales'
|
|
206
|
-
const injectButton = options.injectButton !== false //
|
|
206
|
+
const injectButton = options.injectButton !== false // Default is true
|
|
207
207
|
|
|
208
208
|
return {
|
|
209
209
|
name: 'i18n-micro-devtools-plugin',
|
|
210
|
-
apply: 'serve', //
|
|
210
|
+
apply: 'serve', // Works only in dev server
|
|
211
211
|
|
|
212
212
|
resolveId(id: string) {
|
|
213
213
|
if (id === '/@vite-plugin-i18n-devtools/devtools-ui.js') {
|
|
@@ -218,7 +218,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
218
218
|
|
|
219
219
|
load(id: string) {
|
|
220
220
|
if (id === '/@vite-plugin-i18n-devtools/devtools-ui.js') {
|
|
221
|
-
//
|
|
221
|
+
// Return re-export from devtools-ui package
|
|
222
222
|
return `export * from '@i18n-micro/devtools-ui'`
|
|
223
223
|
}
|
|
224
224
|
return null
|
|
@@ -229,21 +229,21 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
229
229
|
return html
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
//
|
|
232
|
+
// Inject script before the closing </body> tag
|
|
233
233
|
const scriptTag = `<script>${BUTTON_INJECTION_SCRIPT}</script>`
|
|
234
234
|
if (html.includes('</body>')) {
|
|
235
235
|
return html.replace('</body>', `${scriptTag}</body>`)
|
|
236
236
|
}
|
|
237
|
-
//
|
|
237
|
+
// If no </body>, append to the end
|
|
238
238
|
return html + scriptTag
|
|
239
239
|
},
|
|
240
240
|
|
|
241
241
|
configureServer(server: ViteDevServer) {
|
|
242
|
-
//
|
|
242
|
+
// Use server.config.root as the source of truth for the project root
|
|
243
243
|
const projectRoot = server.config.root
|
|
244
244
|
|
|
245
|
-
// HTML
|
|
246
|
-
//
|
|
245
|
+
// HTML page for iframe with devtools UI
|
|
246
|
+
// Use virtual module for proper imports
|
|
247
247
|
const devtoolsHtml = `<!DOCTYPE html>
|
|
248
248
|
<html lang="en">
|
|
249
249
|
<head>
|
|
@@ -279,14 +279,14 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
279
279
|
<div class="loading">Loading DevTools...</div>
|
|
280
280
|
</div>
|
|
281
281
|
<script type="module">
|
|
282
|
-
//
|
|
283
|
-
//
|
|
282
|
+
// Import devtools UI
|
|
283
|
+
// Try to import through different paths for compatibility
|
|
284
284
|
let register;
|
|
285
285
|
try {
|
|
286
286
|
const devtoolsModule = await import('/@vite-plugin-i18n-devtools/devtools-ui.js');
|
|
287
287
|
register = devtoolsModule.register;
|
|
288
288
|
} catch (e) {
|
|
289
|
-
// Fallback:
|
|
289
|
+
// Fallback: try to import directly
|
|
290
290
|
try {
|
|
291
291
|
const devtoolsModule = await import('@i18n-micro/devtools-ui');
|
|
292
292
|
register = devtoolsModule.register;
|
|
@@ -297,10 +297,10 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
//
|
|
300
|
+
// Register custom element
|
|
301
301
|
register();
|
|
302
302
|
|
|
303
|
-
//
|
|
303
|
+
// Create bridge via API
|
|
304
304
|
const bridge = {
|
|
305
305
|
async getLocalesAndTranslations() {
|
|
306
306
|
try {
|
|
@@ -379,14 +379,14 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
379
379
|
</body>
|
|
380
380
|
</html>`
|
|
381
381
|
|
|
382
|
-
// Middleware
|
|
382
|
+
// Middleware for serving the devtools HTML page
|
|
383
383
|
server.middlewares.use('/__i18n_devtools.html', (_req: IncomingMessage, res: ServerResponse) => {
|
|
384
384
|
res.statusCode = 200
|
|
385
385
|
res.setHeader('Content-Type', 'text/html; charset=utf-8')
|
|
386
386
|
res.end(devtoolsHtml)
|
|
387
387
|
})
|
|
388
388
|
|
|
389
|
-
//
|
|
389
|
+
// Endpoint for getting configuration
|
|
390
390
|
server.middlewares.use(`${apiBase}/config`, async (_req: IncomingMessage, res: ServerResponse, next) => {
|
|
391
391
|
if (_req.method !== 'GET') {
|
|
392
392
|
return next()
|
|
@@ -416,7 +416,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
416
416
|
}
|
|
417
417
|
})
|
|
418
418
|
|
|
419
|
-
//
|
|
419
|
+
// Endpoint for getting file list
|
|
420
420
|
server.middlewares.use(`${apiBase}/files`, async (req: IncomingMessage, res: ServerResponse, next) => {
|
|
421
421
|
if (req.method !== 'GET') {
|
|
422
422
|
return next()
|
|
@@ -425,7 +425,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
425
425
|
try {
|
|
426
426
|
const localesPath = path.resolve(projectRoot, translationDir)
|
|
427
427
|
|
|
428
|
-
//
|
|
428
|
+
// Verify the path is safe
|
|
429
429
|
safeResolvePath(projectRoot, translationDir)
|
|
430
430
|
|
|
431
431
|
if (!fs.existsSync(localesPath)) {
|
|
@@ -437,7 +437,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
437
437
|
|
|
438
438
|
const files = await scanTranslationFiles(localesPath, localesPath)
|
|
439
439
|
|
|
440
|
-
//
|
|
440
|
+
// Build directory structure
|
|
441
441
|
const structure: Record<string, unknown> = {}
|
|
442
442
|
for (const file of files) {
|
|
443
443
|
const parts = file.split('/')
|
|
@@ -471,7 +471,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
471
471
|
}
|
|
472
472
|
})
|
|
473
473
|
|
|
474
|
-
//
|
|
474
|
+
// Endpoint for loading a specific file
|
|
475
475
|
server.middlewares.use(`${apiBase}/file`, async (req: IncomingMessage, res: ServerResponse, next) => {
|
|
476
476
|
if (req.method !== 'GET') {
|
|
477
477
|
return next()
|
|
@@ -485,7 +485,7 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
485
485
|
throw new Error('Path parameter is required')
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
//
|
|
488
|
+
// Resolve path relative to translationDir
|
|
489
489
|
const fullPath = path.join(translationDir, filePath)
|
|
490
490
|
const resolvedPath = safeResolvePath(projectRoot, fullPath)
|
|
491
491
|
|
|
@@ -519,14 +519,14 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
519
519
|
}
|
|
520
520
|
})
|
|
521
521
|
|
|
522
|
-
//
|
|
522
|
+
// Endpoint for saving a file
|
|
523
523
|
server.middlewares.use(`${apiBase}/save`, async (req: IncomingMessage, res: ServerResponse, next) => {
|
|
524
524
|
if (req.method !== 'POST') {
|
|
525
525
|
return next()
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
try {
|
|
529
|
-
//
|
|
529
|
+
// Read request body
|
|
530
530
|
const buffers: Buffer[] = []
|
|
531
531
|
for await (const chunk of req) {
|
|
532
532
|
buffers.push(chunk)
|
|
@@ -544,25 +544,25 @@ export function i18nDevToolsPlugin(options: DevToolsPluginOptions = {}): PluginO
|
|
|
544
544
|
throw new Error('Invalid data: file and content are required')
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
//
|
|
547
|
+
// Resolve path relative to translationDir if path is not absolute
|
|
548
548
|
const fullPath = file.startsWith(translationDir) ? file : path.join(translationDir, file)
|
|
549
549
|
const filePath = safeResolvePath(projectRoot, fullPath)
|
|
550
550
|
|
|
551
|
-
//
|
|
551
|
+
// Check extension
|
|
552
552
|
if (!filePath.endsWith('.json')) {
|
|
553
553
|
throw new Error('Invalid file: only .json files are allowed')
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
//
|
|
556
|
+
// Create directory if it doesn't exist
|
|
557
557
|
const dir = path.dirname(filePath)
|
|
558
558
|
if (!fs.existsSync(dir)) {
|
|
559
559
|
fs.mkdirSync(dir, { recursive: true })
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
//
|
|
562
|
+
// Write file
|
|
563
563
|
fs.writeFileSync(filePath, JSON.stringify(content, null, 2), 'utf-8')
|
|
564
564
|
|
|
565
|
-
//
|
|
565
|
+
// Successful response
|
|
566
566
|
res.statusCode = 200
|
|
567
567
|
res.setHeader('Content-Type', 'application/json')
|
|
568
568
|
res.end(JSON.stringify({ success: true }))
|