@tuhama/translation-manager 0.7.0 → 0.7.2
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/package.json +1 -1
- package/src/core/Scanner.js +11 -1
- package/src/core/TranslatorManager.js +5 -0
- package/src/core/services/AITranslator.js +16 -2
- package/src/server.js +59 -62
- package/web/dist/assets/index-Dk1Y6pBW.css +1 -0
- package/web/dist/assets/index-vFJA1ghv.js +21 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-5lMTDHig.css +0 -1
- package/web/dist/assets/index-MTmK2fr8.js +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuhama/translation-manager",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "AI-powered i18n management with context-aware scanning and support for OpenAI, Gemini, and Google Translate.",
|
|
5
5
|
"author": "Tuhama <tuhama.gh.qlyshi@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
package/src/core/Scanner.js
CHANGED
|
@@ -106,7 +106,7 @@ class Scanner {
|
|
|
106
106
|
// 4. <Trans i18nKey="key">
|
|
107
107
|
// 5. useTranslation(['namespace']) -> t('key')
|
|
108
108
|
const patterns = [
|
|
109
|
-
/(?:\bt\(|i18n\.t\(|i18nKey=)\s*['"\`]([
|
|
109
|
+
/(?:\bt\(|i18n\.t\(|i18nKey=)\s*['"\`]([a-zA-Z0-9._-]+)['"\`]/g
|
|
110
110
|
];
|
|
111
111
|
|
|
112
112
|
for (const file of files) {
|
|
@@ -119,6 +119,16 @@ class Scanner {
|
|
|
119
119
|
regex.lastIndex = 0;
|
|
120
120
|
while ((match = regex.exec(content)) !== null) {
|
|
121
121
|
const key = match[1];
|
|
122
|
+
|
|
123
|
+
// VALIDATION: Skip dynamic keys
|
|
124
|
+
// 1. Skip if it contains template literal placeholders ${...}
|
|
125
|
+
// 2. Skip if it's just the placeholder prefix ${
|
|
126
|
+
// 3. Skip if it looks like a variable (no dots, no spaces, starts with lowercase and followed by camelCase etc)
|
|
127
|
+
// - actually, dots are good, but ${ is the killer.
|
|
128
|
+
if (key.includes('${') || key.includes('`') || key.startsWith('$')) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
122
132
|
// basic validation to avoid random strings and ensure it's not a translation file path
|
|
123
133
|
if (key && !existingKeysSet.has(key) && !key.includes('/') && !key.includes('\\')) {
|
|
124
134
|
if (includeContext) {
|
|
@@ -69,6 +69,9 @@ class TranslatorManager {
|
|
|
69
69
|
* Saves a translation key across all language files.
|
|
70
70
|
*/
|
|
71
71
|
async saveTranslation(key, values, options = {}) {
|
|
72
|
+
if (!key) throw new Error('Key is required');
|
|
73
|
+
if (!values || typeof values !== 'object') throw new Error('Values must be an object');
|
|
74
|
+
|
|
72
75
|
const translations = await this.storage.readAll();
|
|
73
76
|
const languages = Object.keys(translations);
|
|
74
77
|
|
|
@@ -84,6 +87,8 @@ class TranslatorManager {
|
|
|
84
87
|
* Deletes one or more translation keys across all language files.
|
|
85
88
|
*/
|
|
86
89
|
async deleteTranslations(keys) {
|
|
90
|
+
if (!keys || (Array.isArray(keys) && keys.length === 0)) return;
|
|
91
|
+
|
|
87
92
|
const keysToDelete = Array.isArray(keys) ? keys : [keys];
|
|
88
93
|
const translations = await this.storage.readAll();
|
|
89
94
|
const languages = Object.keys(translations);
|
|
@@ -87,8 +87,15 @@ ${textsToTranslate.map(t => `- "${t}"`).join('\n')}
|
|
|
87
87
|
})
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
const errorData = await response.json().catch(() => ({}));
|
|
92
|
+
throw new Error(errorData.error?.message || `OpenAI API returned status ${response.status}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
90
95
|
const data = await response.json();
|
|
91
|
-
if (data.
|
|
96
|
+
if (!data.choices || data.choices.length === 0) {
|
|
97
|
+
throw new Error('OpenAI API returned no results');
|
|
98
|
+
}
|
|
92
99
|
return data.choices[0].message.content;
|
|
93
100
|
}
|
|
94
101
|
|
|
@@ -102,8 +109,15 @@ ${textsToTranslate.map(t => `- "${t}"`).join('\n')}
|
|
|
102
109
|
})
|
|
103
110
|
});
|
|
104
111
|
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const errorData = await response.json().catch(() => ({}));
|
|
114
|
+
throw new Error(errorData.error?.message || `Gemini API returned status ${response.status}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
const data = await response.json();
|
|
106
|
-
if (data.
|
|
118
|
+
if (!data.candidates || data.candidates.length === 0 || !data.candidates[0].content?.parts?.length) {
|
|
119
|
+
throw new Error('Gemini API returned no results');
|
|
120
|
+
}
|
|
107
121
|
return data.candidates[0].content.parts[0].text;
|
|
108
122
|
}
|
|
109
123
|
}
|
package/src/server.js
CHANGED
|
@@ -15,131 +15,113 @@ function startServer(targetDir, port = 3000, config = {}) {
|
|
|
15
15
|
app.use(express.json());
|
|
16
16
|
|
|
17
17
|
// API endpoints
|
|
18
|
-
app.get('/api/translations', async (req, res) => {
|
|
18
|
+
app.get('/api/translations', async (req, res, next) => {
|
|
19
19
|
try {
|
|
20
20
|
const data = await manager.scan();
|
|
21
21
|
res.json(data);
|
|
22
22
|
} catch (err) {
|
|
23
|
-
|
|
23
|
+
next(err);
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
app.post('/api/translations', async (req, res) => {
|
|
27
|
+
app.post('/api/translations', async (req, res, next) => {
|
|
28
28
|
try {
|
|
29
29
|
const { key, values, format = true } = req.body;
|
|
30
30
|
const options = { sort: format };
|
|
31
31
|
await manager.saveTranslation(key, values, options);
|
|
32
32
|
res.json({ success: true });
|
|
33
33
|
} catch (err) {
|
|
34
|
-
|
|
34
|
+
next(err);
|
|
35
35
|
}
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
app.delete('/api/translations', async (req, res) => {
|
|
38
|
+
app.delete('/api/translations', async (req, res, next) => {
|
|
39
39
|
try {
|
|
40
40
|
const { key } = req.body;
|
|
41
41
|
await manager.deleteTranslations(key);
|
|
42
42
|
res.json({ success: true });
|
|
43
43
|
} catch (err) {
|
|
44
|
-
|
|
44
|
+
next(err);
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
app.post('/api/delete-keys', async (req, res) => {
|
|
48
|
+
app.post('/api/delete-keys', async (req, res, next) => {
|
|
49
49
|
try {
|
|
50
50
|
const { keys } = req.body;
|
|
51
51
|
await manager.deleteTranslations(keys);
|
|
52
52
|
res.json({ success: true });
|
|
53
53
|
} catch (err) {
|
|
54
|
-
|
|
54
|
+
next(err);
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
app.post('/api/normalize', async (req, res) => {
|
|
58
|
+
app.post('/api/normalize', async (req, res, next) => {
|
|
59
59
|
try {
|
|
60
60
|
await manager.normalize();
|
|
61
61
|
res.json({ success: true });
|
|
62
62
|
} catch (err) {
|
|
63
|
-
|
|
63
|
+
next(err);
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
const checkConfig = (res) => {
|
|
68
|
+
const hasAI = manager.config.aiTranslate && manager.config.aiTranslate.apiKey;
|
|
69
|
+
const hasGoogle = manager.config.googleTranslate && manager.config.googleTranslate.projectId;
|
|
70
|
+
|
|
71
|
+
if (!hasAI && !hasGoogle) {
|
|
72
|
+
res.status(400).json({
|
|
73
|
+
error: 'No translation service configured. Please add OpenAI, Gemini, or Google Translate settings.',
|
|
74
|
+
configurationRequired: true
|
|
75
|
+
});
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
app.post('/api/translate', async (req, res, next) => {
|
|
82
|
+
try {
|
|
83
|
+
if (!checkConfig(res)) return;
|
|
84
|
+
const { text, targetLang, sourceLang, key } = req.body;
|
|
85
|
+
const translatedText = await manager.translateSingle(text, targetLang, sourceLang, key);
|
|
80
86
|
res.json({ translatedText });
|
|
81
87
|
} catch (err) {
|
|
82
|
-
|
|
83
|
-
if (err.message.includes('Google Cloud Project ID is required') ||
|
|
84
|
-
err.message.includes('Google Translate configuration is missing')) {
|
|
85
|
-
return res.status(400).json({
|
|
86
|
-
error: err.message,
|
|
87
|
-
configurationRequired: true
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
res.status(500).json({ error: err.message });
|
|
88
|
+
next(err);
|
|
91
89
|
}
|
|
92
90
|
});
|
|
93
91
|
|
|
94
|
-
app.get('/api/bulk-translate/scan', async (req, res) => {
|
|
92
|
+
app.get('/api/bulk-translate/scan', async (req, res, next) => {
|
|
95
93
|
try {
|
|
96
94
|
const { sourceLang } = req.query;
|
|
97
95
|
const report = await manager.getBulkTranslateReport(sourceLang || 'en');
|
|
98
96
|
res.json(report);
|
|
99
97
|
} catch (err) {
|
|
100
|
-
|
|
98
|
+
next(err);
|
|
101
99
|
}
|
|
102
100
|
});
|
|
103
101
|
|
|
104
|
-
app.post('/api/bulk-translate/execute', async (req, res) => {
|
|
102
|
+
app.post('/api/bulk-translate/execute', async (req, res, next) => {
|
|
105
103
|
try {
|
|
104
|
+
if (!checkConfig(res)) return;
|
|
106
105
|
const { sourceLang } = req.body;
|
|
107
|
-
|
|
108
|
-
// Check if Google Translate is configured
|
|
109
|
-
if (!manager.config.googleTranslate || !manager.config.googleTranslate.projectId) {
|
|
110
|
-
return res.status(400).json({
|
|
111
|
-
error: 'Google Translate is not configured. Please add your Google Cloud Project ID and key file in Settings.',
|
|
112
|
-
configurationRequired: true
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
106
|
const preview = await manager.bulkTranslate(sourceLang || 'en');
|
|
117
107
|
res.json(preview);
|
|
118
108
|
} catch (err) {
|
|
119
|
-
|
|
120
|
-
if (err.message.includes('Google Cloud Project ID is required') ||
|
|
121
|
-
err.message.includes('Google Translate configuration is missing')) {
|
|
122
|
-
return res.status(400).json({
|
|
123
|
-
error: err.message,
|
|
124
|
-
configurationRequired: true
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
res.status(500).json({ error: err.message });
|
|
109
|
+
next(err);
|
|
128
110
|
}
|
|
129
111
|
});
|
|
130
112
|
|
|
131
|
-
app.post('/api/bulk-save', async (req, res) => {
|
|
113
|
+
app.post('/api/bulk-save', async (req, res, next) => {
|
|
132
114
|
try {
|
|
133
115
|
const { data, format = true } = req.body;
|
|
134
116
|
const options = { sort: format };
|
|
135
117
|
await manager.saveBulkTranslations(data, options);
|
|
136
118
|
res.json({ success: true });
|
|
137
119
|
} catch (err) {
|
|
138
|
-
|
|
120
|
+
next(err);
|
|
139
121
|
}
|
|
140
122
|
});
|
|
141
123
|
|
|
142
|
-
app.get('/api/export-missing', async (req, res) => {
|
|
124
|
+
app.get('/api/export-missing', async (req, res, next) => {
|
|
143
125
|
try {
|
|
144
126
|
const exportResult = await manager.exportMissingFromFiles();
|
|
145
127
|
|
|
@@ -149,11 +131,11 @@ function startServer(targetDir, port = 3000, config = {}) {
|
|
|
149
131
|
|
|
150
132
|
res.json(exportResult.exportData);
|
|
151
133
|
} catch (err) {
|
|
152
|
-
|
|
134
|
+
next(err);
|
|
153
135
|
}
|
|
154
136
|
});
|
|
155
137
|
|
|
156
|
-
app.post('/api/import-translations', async (req, res) => {
|
|
138
|
+
app.post('/api/import-translations', async (req, res, next) => {
|
|
157
139
|
try {
|
|
158
140
|
const { data, options = {} } = req.body;
|
|
159
141
|
|
|
@@ -164,16 +146,16 @@ function startServer(targetDir, port = 3000, config = {}) {
|
|
|
164
146
|
const importStats = await manager.importTranslations(data, options);
|
|
165
147
|
res.json({ success: true, stats: importStats });
|
|
166
148
|
} catch (err) {
|
|
167
|
-
|
|
149
|
+
next(err);
|
|
168
150
|
}
|
|
169
151
|
});
|
|
170
152
|
|
|
171
|
-
app.get('/api/export-missing/preview', async (req, res) => {
|
|
153
|
+
app.get('/api/export-missing/preview', async (req, res, next) => {
|
|
172
154
|
try {
|
|
173
155
|
const exportResult = await manager.exportMissingFromFiles();
|
|
174
156
|
res.json(exportResult);
|
|
175
157
|
} catch (err) {
|
|
176
|
-
|
|
158
|
+
next(err);
|
|
177
159
|
}
|
|
178
160
|
});
|
|
179
161
|
|
|
@@ -181,13 +163,13 @@ function startServer(targetDir, port = 3000, config = {}) {
|
|
|
181
163
|
res.json(manager.config);
|
|
182
164
|
});
|
|
183
165
|
|
|
184
|
-
app.post('/api/settings', async (req, res) => {
|
|
166
|
+
app.post('/api/settings', async (req, res, next) => {
|
|
185
167
|
try {
|
|
186
168
|
const { settings } = req.body;
|
|
187
169
|
await manager.saveConfig(settings);
|
|
188
170
|
res.json({ success: true, config: manager.config });
|
|
189
171
|
} catch (err) {
|
|
190
|
-
|
|
172
|
+
next(err);
|
|
191
173
|
}
|
|
192
174
|
});
|
|
193
175
|
|
|
@@ -196,6 +178,21 @@ function startServer(targetDir, port = 3000, config = {}) {
|
|
|
196
178
|
app.use(express.static(buildPath));
|
|
197
179
|
app.use(history('index.html', { root: buildPath }));
|
|
198
180
|
|
|
181
|
+
// Global error handler
|
|
182
|
+
app.use((err, req, res, next) => {
|
|
183
|
+
console.error('\x1b[31mError:\x1b[0m', err.message);
|
|
184
|
+
|
|
185
|
+
// Check for specific error types
|
|
186
|
+
const isConfigError = err.message.includes('not configured') ||
|
|
187
|
+
err.message.includes('API Key is required') ||
|
|
188
|
+
err.message.includes('Project ID is required');
|
|
189
|
+
|
|
190
|
+
res.status(isConfigError ? 400 : 500).json({
|
|
191
|
+
error: err.message,
|
|
192
|
+
configurationRequired: isConfigError
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
199
196
|
app.listen(port, () => {
|
|
200
197
|
console.log(`\x1b[32m✔\x1b[0m Translation Manager UI is running at http://localhost:${port}`);
|
|
201
198
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg-color:#0b0e14;--sidebar-bg:#111827cc;--accent-color:#7c3aed;--accent-hover:#6d28d9;--text-color:#f3f4f6;--text-muted:#9ca3af;--border-color:#ffffff14;--glass-bg:#ffffff08;--glass-blur:blur(20px);--green:#10b981;--red:#ef4444;--transition:all .25s ease}*{box-sizing:border-box;margin:0;padding:0}body{background-color:var(--bg-color);color:var(--text-color);-webkit-font-smoothing:antialiased;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.6}#root{height:100vh;display:flex}.app-container{flex-direction:column;width:100%;height:100vh;display:flex}.app-body{flex:1;grid-template-columns:320px 1fr;display:grid;position:relative;overflow:hidden}@media (width<=1024px){.app-body{grid-template-columns:280px 1fr}}@media (width<=768px){.app-body{flex-direction:column;display:flex;overflow-y:auto}}.app-header{background:var(--sidebar-bg);-webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur);border-bottom:1px solid var(--border-color);z-index:100;justify-content:space-between;align-items:center;min-height:70px;padding:16px 24px;display:flex}.header-left{align-items:center;display:flex}.brand-section{align-items:center;gap:16px;display:flex}.header-logo{filter:drop-shadow(0 0 8px #7c3aed66);border-radius:8px;width:36px;height:36px}.app-title{background:linear-gradient(135deg,#a78bfa 0%,#3b82f6 100%);-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text;margin:0;font-size:1.4rem;font-weight:800}.header-right{align-items:center;display:flex}.logo-container{align-items:center;display:flex;position:relative}.logo-glow{background:var(--accent-color);filter:blur(15px);opacity:.2;z-index:-1;border-radius:50%;width:40px;height:40px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.title-stack{flex-direction:column;display:flex}.title-row{align-items:center;gap:10px;display:flex}.version-badge{color:var(--accent-color);text-transform:uppercase;letter-spacing:.5px;background:#7c3aed1a;border:1px solid #7c3aed33;border-radius:4px;padding:2px 6px;font-size:.65rem;font-weight:700}.header-stats{align-items:center;gap:12px;margin-top:2px;display:flex}.stat-item{align-items:center;gap:4px;display:flex}.stat-value{color:#fff;font-size:.75rem;font-weight:800}.stat-label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;font-size:.7rem}.stat-divider{background:var(--border-color);width:1px;height:10px}@media (width<=768px){.header-stats{display:none}}.header-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.header-primary-btn{background:linear-gradient(135deg, var(--accent-color) 0%, var(--accent-hover) 100%);color:#fff;cursor:pointer;transition:var(--transition);border:none;border-radius:8px;align-items:center;gap:8px;padding:10px 16px;font-size:.9rem;font-weight:600;display:flex}.header-primary-btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px #7c3aed4d}.header-btn{border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;transition:var(--transition);white-space:nowrap;background:#ffffff0d;border-radius:6px;align-items:center;gap:6px;padding:8px 12px;font-size:.8rem;font-weight:500;display:flex}.header-btn:hover{border-color:var(--accent-color);color:var(--accent-color);background:#ffffff14;transform:translateY(-1px)}.mobile-menu-toggle{cursor:pointer;z-index:101;background:0 0;border:none;flex-direction:column;gap:4px;padding:8px;display:none}.mobile-menu-toggle span{background:var(--text-color);width:24px;height:2px;transition:var(--transition);border-radius:2px;display:block}@media (width<=768px){.mobile-menu-toggle{display:flex}.brand-section{gap:8px}.app-title{font-size:1.1rem}.header-logo{width:28px;height:28px}}.sidebar{background:var(--sidebar-bg);-webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur);border-right:1px solid var(--border-color);flex-direction:column;transition:transform .3s cubic-bezier(.4,0,.2,1);display:flex;overflow:hidden}@media (width<=768px){.sidebar{z-index:1000;background:#111827;width:300px;position:fixed;top:0;bottom:0;left:0;transform:translate(-100%);box-shadow:20px 0 50px #00000080}.sidebar.open{transform:translate(0)}.sidebar-overlay{-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:999;background:#0009;position:fixed;inset:0}}.sidebar-header-mobile{border-bottom:1px solid var(--border-color);justify-content:space-between;align-items:center;padding:20px;display:none}@media (width<=768px){.sidebar-header-mobile{display:flex}}.sidebar-title{text-transform:uppercase;letter-spacing:1px;color:var(--accent-color);font-size:.85rem;font-weight:700}.sidebar-close{color:var(--text-muted);cursor:pointer;background:0 0;border:none;font-size:1.5rem}.sidebar-search{border-bottom:1px solid var(--border-color);padding:20px 16px}.search-input{border:1px solid var(--border-color);color:#fff;width:100%;transition:var(--transition);background:#0003;border-radius:10px;padding:12px 16px;font-size:.95rem}.search-input:focus{border-color:var(--accent-color);outline:none;box-shadow:0 0 0 2px #7c3aed1a}.search-input::placeholder{color:var(--text-muted)}.tree-view{flex:1;padding:16px;overflow-y:auto}.loading-state{color:var(--text-muted);justify-content:center;align-items:center;padding:40px 20px;font-style:italic;display:flex}.dropdown-container{position:relative}.dropdown-container:hover .dropdown-menu{opacity:1;pointer-events:auto;display:block;transform:translateY(0)}.dropdown-trigger{align-items:center;gap:8px;display:flex}.dropdown-menu{border:1px solid var(--border-color);z-index:1000;-webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur);opacity:0;pointer-events:none;background:#111827;border-radius:12px;min-width:220px;margin-top:8px;padding:8px;transition:all .2s cubic-bezier(.4,0,.2,1);display:none;position:absolute;top:100%;right:0;transform:translateY(10px);box-shadow:0 20px 40px #0009}.dropdown-item{width:100%;color:var(--text-color);cursor:pointer;transition:var(--transition);text-align:left;background:0 0;border:none;border-radius:8px;align-items:center;gap:12px;padding:10px 12px;font-size:.9rem;display:flex}.dropdown-item:hover{color:#fff;background:#7c3aed26}.item-icon{text-align:center;width:16px;font-size:1rem}.dropdown-divider{background:var(--border-color);height:1px;margin:8px 16px}.dropdown-arrow{color:var(--text-muted);transition:var(--transition);font-size:.8rem}.btn-icon{font-size:1rem}.key-tree{list-style:none}.tree-node{margin-bottom:2px}.tree-children{margin:0;padding:0;list-style:none}.tree-branch-header{cursor:pointer;transition:var(--transition);color:var(--text-color);border-radius:6px;align-items:center;padding:8px 14px;font-size:.9rem;font-weight:500;display:flex}.tree-branch-header:hover{background:#ffffff0d}.tree-branch-header.has-selected-child{color:#a78bfa;background:#7c3aed14}.tree-expand-icon{color:var(--text-muted);transition:var(--transition);text-align:center;width:12px;margin-right:8px;font-size:.8rem}.tree-branch-text{flex:1;align-items:center;gap:8px;display:flex}.tree-branch-count{color:var(--text-muted);font-size:.75rem;font-weight:400}.branch-indicator{margin-left:4px;font-size:.8rem}.branch-indicator.missing{filter:drop-shadow(0 0 3px #ef44444d)}.branch-indicator.unused{opacity:.6}.tree-leaf{cursor:pointer;transition:var(--transition);color:var(--text-muted);border-radius:6px;justify-content:space-between;align-items:center;margin-bottom:2px;padding:8px 14px;font-size:.85rem;display:flex}.tree-leaf.unused{opacity:.5;font-style:italic}.tree-leaf.unused .tree-leaf-text:after{content:" (unused)";opacity:.7;margin-left:4px;font-size:.7rem}.tree-leaf-content{flex:1;align-items:center;display:flex}.tree-leaf-text{white-space:nowrap;text-overflow:ellipsis;flex:1;overflow:hidden}.tree-leaf:hover{color:#fff;background:#ffffff0d}.tree-leaf.active{color:#a78bfa;background:#7c3aed26;font-weight:600}.tree-leaf.incomplete .tree-leaf-text{color:var(--text-muted)}.missing-translation-dot{filter:drop-shadow(0 0 5px #ef444480);margin-left:8px;font-size:.8rem}.delete-icon{opacity:0;color:var(--red);cursor:pointer;transition:var(--transition);background:0 0;border:none;border-radius:4px;padding:2px 4px;font-size:1.1rem}.tree-leaf:hover .delete-icon{opacity:1}.delete-icon:hover{background:#ef44441a}.editor{background:radial-gradient(circle at 100% 0,#7c3aed0d,#0000 400px),radial-gradient(circle at 0 100%,#3b82f60d,#0000 400px);justify-content:center;align-items:flex-start;padding:24px;display:flex;overflow-y:auto}@media (width<=768px){.editor{background:0 0;padding:16px}}.editor-form{width:100%;max-width:1100px;-webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur);border:1px solid var(--border-color);background:#1f293766;border-radius:24px;flex-direction:column;height:fit-content;max-height:calc(100vh - 120px);display:flex;box-shadow:0 25px 50px -12px #00000080}@media (width<=768px){.editor-form{border-radius:16px;max-height:none}}.editor-main-form{flex-direction:column;flex:1;display:flex;overflow:hidden}.form-scroll-area{padding:32px 40px;overflow-y:auto}@media (width<=768px){.form-scroll-area{padding:24px 20px}}.form-group{text-align:left;margin-bottom:24px}.form-group.missing label{color:var(--red)}.form-group.missing input,.form-group.missing textarea{border-color:#ef44444d}.missing-label{margin-left:4px;font-size:.75rem;font-style:italic;font-weight:400}.form-group input,.form-group textarea{border:1px solid var(--border-color);color:#fff;background:#0000004d;border-radius:10px;width:100%;padding:14px;font-family:inherit;font-size:1rem}.form-group textarea{resize:vertical;min-height:100px}.form-actions-sticky{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);border-top:1px solid var(--border-color);background:#111827cc;border-bottom-right-radius:24px;border-bottom-left-radius:24px;margin-top:auto;padding:20px 40px}@media (width<=768px){.form-actions-sticky{z-index:10;padding:16px 20px;position:sticky;bottom:0}}.form-actions-content{justify-content:flex-end;gap:16px;display:flex}.primary-btn{background:var(--accent-color);color:#fff;cursor:pointer;transition:var(--transition);border:none;border-radius:10px;padding:14px 28px;font-weight:600}.primary-btn:hover{background:var(--accent-hover);transform:translateY(-1px)}.secondary-btn{border:1px solid var(--border-color);color:var(--text-muted);cursor:pointer;background:0 0;border-radius:10px;padding:14px 28px;font-weight:600}.empty-state{text-align:center;max-width:500px}.hero-icon{filter:drop-shadow(0 0 20px #8b5cf64d);margin-bottom:24px;font-size:4rem}.empty-state h2{margin-bottom:12px;font-size:2rem}.empty-state p{color:var(--text-muted);margin-bottom:32px}.modal-overlay{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);z-index:1000;background:#000000b3;justify-content:center;align-items:center;padding:20px;display:flex;position:fixed;inset:0}.modal-content{border:1px solid var(--border-color);background:#1f2937;border-radius:20px;flex-direction:column;width:100%;max-width:600px;max-height:80vh;display:flex;overflow:hidden;box-shadow:0 25px 50px -12px #00000080}.modal-header{border-bottom:1px solid var(--border-color);justify-content:space-between;align-items:center;padding:24px;display:flex}.modal-header h2,.modal-header h3{margin:0;font-size:1.25rem;font-weight:600}.settings-divider{border-bottom:1px solid var(--border-color);color:var(--primary-color);text-transform:uppercase;letter-spacing:.05em;margin:24px 0 16px;padding-bottom:8px;font-size:.9rem;font-weight:600}.close-btn{color:var(--text-muted);cursor:pointer;transition:var(--transition);background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;width:36px;height:36px;padding:0;font-size:1.8rem;line-height:1;display:flex}.close-btn:hover{color:#fff;background:#ffffff0d}.modal-body{flex:1;padding:24px;overflow-y:auto}.cleanup-item{cursor:pointer;background:#ffffff08;border-radius:10px;align-items:center;gap:12px;margin-bottom:8px;padding:12px;display:flex}.cleanup-item:hover{background:#ffffff0f}.cleanup-item input{accent-color:var(--accent-color)}.maybe-used-warning{color:#f59e0b;align-items:center;gap:4px;margin-left:auto;font-size:.8rem;font-style:italic;display:flex}.modal-footer{border-top:1px solid var(--border-color);justify-content:flex-end;gap:12px;padding:20px;display:flex}.editor-header-row{border-bottom:1px solid var(--border-color);justify-content:space-between;align-items:center;padding:24px 40px;display:flex}@media (width<=768px){.editor-header-row{flex-direction:column;align-items:flex-start;gap:16px;padding:20px}}.editor-title-container{flex-direction:column;gap:4px;display:flex}.editor-subtitle{text-transform:uppercase;letter-spacing:.1em;color:var(--accent-color);font-size:.75rem;font-weight:700}.key-breadcrumbs{flex-wrap:wrap;align-items:center;gap:4px;display:flex}.breadcrumb-part{color:#fff}.breadcrumb-separator{color:var(--text-muted);opacity:.5}.languages-grid{grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:24px;display:grid}@media (width<=1200px){.languages-grid{grid-template-columns:1fr}}.full-width-field{grid-column:1/-1}.magic-btn{color:#a78bfa;background:#7c3aed1a;border-color:#7c3aed4d;padding:10px 16px;font-size:.85rem}.magic-btn:hover{background:var(--accent-color);color:#fff}.label-row{justify-content:space-between;align-items:center;margin-bottom:8px;display:flex}.label-row label{margin-bottom:0}.icon-btn{cursor:pointer;transition:var(--transition);background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:4px;display:flex}.icon-btn:hover{background:#ffffff1a}.magic-wand{opacity:.6;font-size:1.1rem}.form-group:hover .magic-wand{opacity:1}.alert{border-radius:10px;margin-bottom:20px;padding:12px 16px;font-size:.9rem}.warning-badge{color:#fbbf24;cursor:pointer;transition:var(--transition);background:#f59e0b1a;border:1px solid #f59e0b33;border-radius:8px;align-items:center;gap:8px;padding:8px 14px;font-size:.8rem;font-weight:600;display:flex}.warning-badge:hover{background:#f59e0b33;transform:translateY(-1px)}.alert-success{color:#34d399;background:#10b9811a;border:1px solid #10b9814d}.alert-error{color:#f87171;background:#ef44441a;border:1px solid #ef44444d}.wizard-step{margin-bottom:24px}.wizard-step label{color:var(--text-color);margin-bottom:12px;font-weight:600;display:block}.source-select{border:1px solid var(--border-color);color:#fff;background:#0000004d;border-radius:10px;width:100%;padding:12px;font-size:1rem}.report-summary{border:1px solid var(--border-color);background:#0003;border-radius:12px;padding:16px}.report-summary ul{color:var(--text-muted);margin-top:12px;padding-left:20px}.preview-container{border:1px solid var(--border-color);background:#0003;border-radius:12px;max-height:400px;padding:16px;overflow-y:auto}.lang-preview{margin-bottom:24px}.lang-preview h3{color:var(--accent-color);border-bottom:1px solid var(--border-color);margin-bottom:12px;padding-bottom:4px;font-size:.85rem}.preview-list{flex-direction:column;gap:8px;display:flex}.preview-item{gap:8px;font-size:.85rem;display:flex}.preview-key{color:var(--text-muted);font-weight:500}.preview-value{color:var(--text-color)}.settings-input{letter-spacing:2px}.help-text{color:var(--text-muted);margin-top:8px;font-size:.8rem}.help-text a{color:var(--accent-color);text-decoration:none}.help-text a:hover{text-decoration:underline}.success-text{color:var(--green);font-weight:500}.approve-btn{background:var(--green)}.approve-btn:hover{background:#059669}.tool-btn.warning{color:#f59e0b;background:#f59e0b1a;border-color:#f59e0b4d}.tool-btn.warning:hover{color:#fff;background:#f59e0b}.missing-keys-list{margin-top:16px;list-style:none}.missing-key-item{border:1px solid var(--border-color);background:#ffffff08;border-radius:12px;justify-content:space-between;align-items:center;margin-bottom:8px;padding:14px;display:flex}.key-path{color:#a78bfa;font-family:JetBrains Mono,Fira Code,monospace;font-size:.85rem}.warning-box{color:#f59e0b;background:#f59e0b14;border:1px solid #f59e0b33;border-radius:12px;margin-bottom:24px;padding:16px;font-size:.85rem}.sm{padding:8px 14px;font-size:.8rem}.tab-container{margin-top:24px}.tab-buttons{border-bottom:1px solid var(--border-color);gap:12px;margin-bottom:24px;padding-bottom:8px;display:flex}.tab-button{color:var(--text-muted);cursor:pointer;transition:var(--transition);background:0 0;border:none;border-radius:8px;padding:8px 16px;font-size:.95rem;font-weight:600;position:relative}.tab-button:hover{color:var(--text-color);background:#ffffff0d}.tab-button.active{color:var(--accent-color);background:#7c3aed1a}.tab-button.active:after{content:"";background:var(--accent-color);border-radius:2px 2px 0 0;height:2px;position:absolute;bottom:-9px;left:0;right:0}.export-tab,.import-tab{padding:8px 0}.export-info{border-left:4px solid var(--accent-color);background:#7c3aed0d;border-radius:4px 12px 12px 4px;margin-bottom:24px;padding:12px 16px}.export-info p{color:var(--text-color);margin:0;font-size:.9rem}.export-preview h3,.import-options h3{color:var(--text-color);margin-bottom:16px;font-size:1.1rem;font-weight:700}.preview-stats{grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin-bottom:20px;display:grid}.preview-stats p{border:1px solid var(--border-color);background:#ffffff08;border-radius:12px;margin:0;padding:12px 16px;font-size:.9rem}.preview-sample h4{color:var(--text-muted);margin-bottom:12px;font-size:.9rem}.import-options{border-top:1px solid var(--border-color);margin-top:24px;padding-top:24px}.import-options label{cursor:pointer;color:var(--text-color);transition:var(--transition);align-items:center;gap:10px;margin-bottom:12px;font-size:.9rem;display:flex}.import-options label:hover{color:var(--accent-color)}.import-options input[type=checkbox]{accent-color:var(--accent-color);cursor:pointer;width:18px;height:18px}.export-import-modal .modal-content{max-width:600px}
|