@tuhama/translation-manager 0.6.0 → 0.7.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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Service for interacting with AI-based Translation APIs (OpenAI, Gemini, etc.)
3
+ * This service is "context-aware" and uses extracted code snippets to improve translation quality.
4
+ */
5
+ class AITranslator {
6
+ constructor(config) {
7
+ this.provider = config.provider || 'openai'; // 'openai' or 'gemini'
8
+ this.apiKey = config.apiKey;
9
+ this.model = config.model || (this.provider === 'openai' ? 'gpt-4o' : 'gemini-1.5-flash');
10
+
11
+ if (!this.apiKey) {
12
+ throw new Error(`API Key is required for ${this.provider} translation.`);
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Translates text with optional context.
18
+ * @param {string|string[]} text - Text(s) to translate
19
+ * @param {string} targetLang - Target language code
20
+ * @param {string} sourceLang - Source language code
21
+ * @param {Object} context - Optional context mapping keys to snippets
22
+ */
23
+ async translate(text, targetLang, sourceLang = 'en', context = {}) {
24
+ if (!text || (Array.isArray(text) && text.length === 0)) {
25
+ return text;
26
+ }
27
+
28
+ const isArray = Array.isArray(text);
29
+ const textsToTranslate = isArray ? text : [text];
30
+
31
+ // Prepare the prompt
32
+ let prompt = `Translate the following ${sourceLang} strings to ${targetLang}.
33
+ Return only a JSON object where keys are the original strings and values are the translations.
34
+ Maintain any placeholders like {name} or {{count}}.
35
+
36
+ Strings to translate:
37
+ ${textsToTranslate.map(t => `- "${t}"`).join('\n')}
38
+ `;
39
+
40
+ // Add context if available
41
+ const contextEntries = Object.entries(context);
42
+ if (contextEntries.length > 0) {
43
+ prompt += `\nContext for some strings:\n`;
44
+ contextEntries.forEach(([key, occs]) => {
45
+ const snippets = occs.map(o => o.snippet).join('\n---\n');
46
+ prompt += `Key "${key}": used in these code locations:\n${snippets}\n`;
47
+ });
48
+ }
49
+
50
+ try {
51
+ const result = await this._callAI(prompt);
52
+ const translationsMap = JSON.parse(result);
53
+
54
+ if (isArray) {
55
+ return textsToTranslate.map(t => translationsMap[t] || t);
56
+ }
57
+ return translationsMap[textsToTranslate[0]] || textsToTranslate[0];
58
+ } catch (error) {
59
+ throw new Error(`AI Translation Error (${this.provider}): ${error.message}`);
60
+ }
61
+ }
62
+
63
+ async _callAI(prompt) {
64
+ if (this.provider === 'openai') {
65
+ return await this._callOpenAI(prompt);
66
+ } else if (this.provider === 'gemini') {
67
+ return await this._callGemini(prompt);
68
+ }
69
+ throw new Error(`Unsupported AI provider: ${this.provider}`);
70
+ }
71
+
72
+ async _callOpenAI(prompt) {
73
+ // We'll use fetch to avoid adding heavy dependencies like 'openai' package
74
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ 'Authorization': `Bearer ${this.apiKey}`
79
+ },
80
+ body: JSON.stringify({
81
+ model: this.model,
82
+ messages: [
83
+ { role: 'system', content: 'You are a professional translator for software applications.' },
84
+ { role: 'user', content: prompt }
85
+ ],
86
+ response_format: { type: 'json_object' }
87
+ })
88
+ });
89
+
90
+ const data = await response.json();
91
+ if (data.error) throw new Error(data.error.message);
92
+ return data.choices[0].message.content;
93
+ }
94
+
95
+ async _callGemini(prompt) {
96
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`, {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify({
100
+ contents: [{ parts: [{ text: prompt }] }],
101
+ generationConfig: { response_mime_type: 'application/json' }
102
+ })
103
+ });
104
+
105
+ const data = await response.json();
106
+ if (data.error) throw new Error(data.error.message);
107
+ return data.candidates[0].content.parts[0].text;
108
+ }
109
+ }
110
+
111
+ module.exports = AITranslator;
package/src/server.js CHANGED
@@ -141,12 +141,11 @@ function startServer(targetDir, port = 3000, config = {}) {
141
141
 
142
142
  app.get('/api/export-missing', async (req, res) => {
143
143
  try {
144
- const { sourceLang = 'en' } = req.query;
145
- const exportResult = await manager.exportMissingKeys(sourceLang);
144
+ const exportResult = await manager.exportMissingFromFiles();
146
145
 
147
146
  // Set headers for file download
148
147
  res.setHeader('Content-Type', 'application/json');
149
- res.setHeader('Content-Disposition', `attachment; filename="missing-translations-${new Date().toISOString().split('T')[0]}.json"`);
148
+ res.setHeader('Content-Disposition', `attachment; filename="missing-keys-from-code-${new Date().toISOString().split('T')[0]}.json"`);
150
149
 
151
150
  res.json(exportResult.exportData);
152
151
  } catch (err) {
@@ -171,8 +170,7 @@ function startServer(targetDir, port = 3000, config = {}) {
171
170
 
172
171
  app.get('/api/export-missing/preview', async (req, res) => {
173
172
  try {
174
- const { sourceLang = 'en' } = req.query;
175
- const exportResult = await manager.exportMissingKeys(sourceLang);
173
+ const exportResult = await manager.exportMissingFromFiles();
176
174
  res.json(exportResult);
177
175
  } catch (err) {
178
176
  res.status(500).json({ error: err.message });
@@ -0,0 +1,40 @@
1
+ # Skill: Manage Translations
2
+
3
+ This skill enables an AI agent to efficiently manage i18n translations using the `@tuhama/translation-manager` library.
4
+
5
+ ## Context
6
+ This project uses a custom translation manager that supports AI-powered context extraction. Translation keys are stored in JSON files (usually in `src/locales` or `locales`).
7
+
8
+ ## Workflow
9
+
10
+ ### 1. Audit Translation Health
11
+ Before starting any work, always check the current status:
12
+ - **Command**: `npx @tuhama/translation-manager status`
13
+ - **Goal**: Identify missing translations, unused keys, and coverage percentage across all languages.
14
+
15
+ ### 2. Identify Missing Keys from Code
16
+ Find keys that are used in the source code (e.g., `t('key.name')`) but aren't defined in the JSON files:
17
+ - **Action**: Use the `Scanner` or the Web UI to find "Missing from files" keys.
18
+ - **AI Task**: If you find missing keys, suggest descriptive key names based on their usage.
19
+
20
+ ### 3. Generate Context-Aware Translations
21
+ When translating, leverage the extracted code context:
22
+ - **Rule**: Use the surrounding code snippets to determine the correct nuance (e.g., is "Save" a button or a noun?).
23
+ - **Style**: Maintain consistent tone across the app (default: professional and concise).
24
+ - **Placeholders**: Always preserve `{name}`, `{{count}}`, or other interpolation markers.
25
+
26
+ ### 4. Maintain Key Consistency
27
+ - **Naming**: Use dot-notation for nesting (e.g., `auth.login.button_label`).
28
+ - **Sorting**: Keys should be kept in alphabetical order. Use the "Normalize" feature or `npx @tuhama/translation-manager normalize` (if available).
29
+ - **Cleanup**: Periodically check for unused keys and remove them to keep bundle sizes small.
30
+
31
+ ## AI Guidelines for this Repo
32
+ - **Source Language**: Usually English (`en`).
33
+ - **Target Languages**: Check the `locales` directory for existing languages.
34
+ - **Naming Convention**: Prefer `snake_case` or `camelCase` based on existing patterns in the JSON files.
35
+ - **Key Discovery**: If you see hardcoded strings in `.js`, `.jsx`, `.ts`, or `.tsx` files, recommend wrapping them in `t()` and adding them to the translations.
36
+
37
+ ## Commands Reference
38
+ - `npx @tuhama/translation-manager`: Start the Web UI.
39
+ - `npx @tuhama/translation-manager status`: Get JSON health report.
40
+ - `pnpm test`: Run the test suite to ensure core logic remains robust.
@@ -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{grid-template-columns:350px 1fr;width:100%;height:100vh;display:grid}.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;display:flex;overflow:hidden}.sidebar-header{border-bottom:1px solid var(--border-color);padding:32px 24px 20px}.brand-row{align-items:center;gap:16px;margin-bottom:24px;display:flex}.logo{filter:drop-shadow(0 0 10px #7c3aed66);border-radius:10px;width:42px;height:42px}.sidebar-header h1{background:linear-gradient(135deg,#a78bfa 0%,#3b82f6 100%);-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text;margin-bottom:0;font-size:1.5rem;font-weight:800}.search-box{margin-bottom:16px;position:relative}.search-box input{border:1px solid var(--border-color);color:#fff;background:#0003;border-radius:10px;width:100%;padding:12px 16px;font-size:.95rem}.sidebar-actions{gap:8px;margin-top:16px;display:flex}.create-btn{border:1px solid var(--accent-color);color:var(--accent-color);cursor:pointer;transition:var(--transition);background:#7c3aed1a;border-radius:10px;flex:2;padding:11px;font-weight:600}.normalize-btn{border:1px solid var(--green);color:var(--green);cursor:pointer;transition:var(--transition);background:#10b9811a;border-radius:10px;flex:1;justify-content:center;align-items:center;padding:11px;font-size:.85rem;font-weight:600;display:flex}.normalize-btn:hover{background:var(--green);color:#fff}.clean-btn{color:#f59e0b;cursor:pointer;transition:var(--transition);background:#f59e0b1a;border:1px solid #f59e0b;border-radius:10px;flex:1;justify-content:center;align-items:center;padding:11px;font-size:.85rem;font-weight:600;display:flex}.clean-btn:hover{color:#fff;background:#f59e0b}.create-btn:hover{background:var(--accent-color);color:#fff}.tree-view{flex:1;padding:16px;overflow-y:auto}.key-list{list-style:none}.key-list li{cursor:pointer;transition:var(--transition);color:var(--text-muted);border-radius:8px;justify-content:space-between;align-items:center;margin-bottom:4px;padding:10px 14px;font-size:.9rem;display:flex}.key-list li.unused{opacity:.5;font-style:italic}.key-list li.unused span:after{content:" (unused)";opacity:.7;margin-left:4px;font-size:.7rem}.key-list li span{white-space:nowrap;text-overflow:ellipsis;flex:1;overflow:hidden}.key-list li:hover{color:#fff;background:#ffffff0d}.key-list li.active{color:#a78bfa;background:#7c3aed26;font-weight:600}.key-list li.incomplete span{color:var(--text-muted)}.warning-dot{filter:drop-shadow(0 0 5px #ef444480);margin-left:8px;font-size:.8rem}.delete-icon{opacity:0;color:var(--red);cursor:pointer;background:0 0;border:none;padding:0 4px;font-size:1.25rem}.key-list li:hover .delete-icon{opacity:1}.editor{justify-content:center;align-items:flex-start;padding:40px;display:flex;overflow-y:auto}.editor-form{width:100%;max-width:800px;-webkit-backdrop-filter:var(--glass-blur);backdrop-filter:var(--glass-blur);border:1px solid var(--border-color);background:#1f293766;border-radius:20px;padding:40px;box-shadow:0 25px 50px -12px #00000080}.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{gap:16px;margin-top:32px;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-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}.sidebar-tools{gap:8px;margin-top:12px;display:flex}.tool-btn{border:1px solid var(--border-color);color:var(--text-muted);cursor:pointer;transition:var(--transition);background:#ffffff08;border-radius:8px;flex:1;justify-content:center;align-items:center;gap:6px;padding:8px 12px;font-size:.8rem;font-weight:500;display:flex}.tool-btn:hover{color:#fff;background:#ffffff14;border-color:#fff3}.tool-btn.icon-text{flex:2}.editor-header-row{justify-content:space-between;align-items:center;margin-bottom:32px;display:flex}.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}.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}