O'Reilly Tech Term Translator

Select technical terms to translate them to Korean

Size

11.5 KB

Version

1.1.2

Created

Oct 31, 2025

Updated

15 days ago

1// ==UserScript==
2// @name		O'Reilly Tech Term Translator
3// @description		Select technical terms to translate them to Korean
4// @version		1.1.2
5// @match		https://*.learning.oreilly.com/*
6// @icon		https://www.oreilly.com/favicon.ico
7// ==/UserScript==
8(function() {
9    'use strict';
10    
11    console.log('O\'Reilly Tech Term Translator initialized');
12    
13    let translationsVisible = true;
14    let translationCache = {};
15    let isTranslating = false;
16    
17    // Create toggle button
18    function createToggleButton() {
19        const button = document.createElement('button');
20        button.id = 'translation-toggle-btn';
21        button.innerHTML = '🌐 Hide Korean Translation';
22        button.style.cssText = `
23            position: fixed;
24            top: 80px;
25            right: 20px;
26            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
27            color: white;
28            padding: 12px 24px;
29            border: none;
30            border-radius: 8px;
31            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
32            z-index: 10000;
33            cursor: pointer;
34            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
35            font-size: 14px;
36            font-weight: 600;
37            transition: all 0.2s ease;
38        `;
39        
40        button.addEventListener('mouseenter', () => {
41            button.style.transform = 'scale(1.05)';
42        });
43        
44        button.addEventListener('mouseleave', () => {
45            button.style.transform = 'scale(1)';
46        });
47        
48        button.addEventListener('click', toggleTranslations);
49        
50        document.body.appendChild(button);
51        return button;
52    }
53    
54    // Toggle translations visibility
55    function toggleTranslations() {
56        translationsVisible = !translationsVisible;
57        const button = document.getElementById('translation-toggle-btn');
58        const translations = document.querySelectorAll('.korean-translation');
59        
60        if (translationsVisible) {
61            button.innerHTML = '🌐 Hide Korean Translation';
62            translations.forEach(t => t.style.display = 'block');
63        } else {
64            button.innerHTML = '🌐 Show Korean Translation';
65            translations.forEach(t => t.style.display = 'none');
66        }
67    }
68    
69    // Create translation element
70    function createTranslationElement(koreanText) {
71        const translation = document.createElement('span');
72        translation.className = 'korean-translation';
73        translation.style.cssText = `
74            display: ${translationsVisible ? 'inline' : 'none'};
75            color: #667eea;
76            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans KR', sans-serif;
77            font-size: 0.95em;
78            font-weight: 500;
79            margin-left: 8px;
80            padding: 2px 6px;
81            background: linear-gradient(to right, #e8f0fe 0%, #f0e8ff 100%);
82            border-radius: 3px;
83        `;
84        translation.textContent = `[${koreanText}]`;
85        return translation;
86    }
87    
88    // Split text into sentences
89    function splitIntoSentences(text) {
90        // Split by period, exclamation, question mark followed by space or end of string
91        // But preserve abbreviations like "e.g.", "i.e.", "Dr.", etc.
92        const sentences = text.match(/[^.!?]+[.!?]+(?:\s|$)|[^.!?]+$/g) || [text];
93        return sentences.map(s => s.trim()).filter(s => s.length > 0);
94    }
95    
96    // Translate text using AI with batching
97    async function translateTextBatch(texts) {
98        console.log('Translating batch of', texts.length, 'texts');
99        
100        try {
101            const textList = texts.map((t, i) => `[${i}] ${t}`).join('\n\n');
102            
103            const response = await RM.aiCall(
104                `You are a technical translator specializing in software engineering and IT terminology. 
105                Translate the following English texts to Korean. Each text is numbered with [0], [1], [2], etc.
106                Provide natural Korean translations that are commonly used in the Korean tech industry.
107                Maintain the technical accuracy while making it readable for Korean developers.
108                
109                Texts to translate:
110                ${textList}`,
111                {
112                    type: 'json_schema',
113                    json_schema: {
114                        name: 'batch_translation',
115                        schema: {
116                            type: 'object',
117                            properties: {
118                                translations: {
119                                    type: 'array',
120                                    items: {
121                                        type: 'object',
122                                        properties: {
123                                            index: { type: 'number' },
124                                            korean: { type: 'string' }
125                                        },
126                                        required: ['index', 'korean']
127                                    }
128                                }
129                            },
130                            required: ['translations']
131                        }
132                    }
133                }
134            );
135            
136            console.log('Translation response received');
137            return response.translations;
138        } catch (error) {
139            console.error('Translation error:', error);
140            return texts.map((_, i) => ({ index: i, korean: '번역 실패' }));
141        }
142    }
143    
144    // Process and translate content
145    async function translatePageContent() {
146        if (isTranslating) {
147            console.log('Translation already in progress');
148            return;
149        }
150        
151        isTranslating = true;
152        console.log('Starting page translation...');
153        
154        // Show loading indicator
155        const loadingIndicator = document.createElement('div');
156        loadingIndicator.id = 'translation-loading';
157        loadingIndicator.style.cssText = `
158            position: fixed;
159            top: 140px;
160            right: 20px;
161            background: white;
162            color: #667eea;
163            padding: 12px 20px;
164            border-radius: 8px;
165            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
166            z-index: 10000;
167            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
168            font-size: 14px;
169            font-weight: 600;
170        `;
171        loadingIndicator.innerHTML = '⏳ Translating page...';
172        document.body.appendChild(loadingIndicator);
173        
174        try {
175            // Find all content elements
176            const contentElements = document.querySelectorAll('#sbo-rt-content p, #sbo-rt-content h1:not(.chapter-number), #sbo-rt-content h2, #sbo-rt-content h3, #sbo-rt-content li');
177            console.log('Found', contentElements.length, 'elements to translate');
178            
179            // Collect all sentences from all elements
180            const sentencesToTranslate = [];
181            contentElements.forEach(element => {
182                // Skip if already has translation
183                if (element.querySelector('.korean-translation')) {
184                    return;
185                }
186                
187                const text = element.textContent.trim();
188                // Skip very short texts, images captions, or empty elements
189                if (text.length < 10 || text.startsWith('Figure ') || text.startsWith('Tip')) {
190                    return;
191                }
192                
193                const sentences = splitIntoSentences(text);
194                sentences.forEach(sentence => {
195                    if (sentence.length > 5) {
196                        sentencesToTranslate.push({ element, sentence, originalText: text });
197                    }
198                });
199            });
200            
201            console.log('Sentences to translate:', sentencesToTranslate.length);
202            
203            // Process in batches of 10 sentences
204            const batchSize = 10;
205            for (let i = 0; i < sentencesToTranslate.length; i += batchSize) {
206                const batch = sentencesToTranslate.slice(i, i + batchSize);
207                const texts = batch.map(item => item.sentence);
208                
209                loadingIndicator.innerHTML = `⏳ Translating... ${Math.min(i + batch.length, sentencesToTranslate.length)}/${sentencesToTranslate.length}`;
210                
211                const translations = await translateTextBatch(texts);
212                
213                // Replace element content with sentence-by-sentence translation
214                const processedElements = new Set();
215                batch.forEach((item, index) => {
216                    if (!processedElements.has(item.element)) {
217                        processedElements.add(item.element);
218                        
219                        // Get all sentences for this element
220                        const elementSentences = batch.filter(b => b.element === item.element);
221                        const elementTranslations = elementSentences.map(es => {
222                            const idx = batch.indexOf(es);
223                            return translations[idx];
224                        });
225                        
226                        // Rebuild element with inline translations
227                        const sentences = splitIntoSentences(item.originalText);
228                        const newContent = document.createElement('span');
229                        
230                        sentences.forEach((sentence, sIdx) => {
231                            const sentenceSpan = document.createElement('span');
232                            sentenceSpan.textContent = sentence + ' ';
233                            newContent.appendChild(sentenceSpan);
234                            
235                            if (elementTranslations[sIdx] && elementTranslations[sIdx].korean) {
236                                const translationElement = createTranslationElement(elementTranslations[sIdx].korean);
237                                newContent.appendChild(translationElement);
238                                newContent.appendChild(document.createTextNode(' '));
239                            }
240                        });
241                        
242                        item.element.innerHTML = '';
243                        item.element.appendChild(newContent);
244                    }
245                });
246                
247                // Small delay between batches to avoid overwhelming the API
248                if (i + batchSize < sentencesToTranslate.length) {
249                    await new Promise(resolve => setTimeout(resolve, 500));
250                }
251            }
252            
253            loadingIndicator.innerHTML = '✅ Translation complete!';
254            setTimeout(() => loadingIndicator.remove(), 2000);
255            
256        } catch (error) {
257            console.error('Error during translation:', error);
258            loadingIndicator.innerHTML = '❌ Translation failed';
259            setTimeout(() => loadingIndicator.remove(), 3000);
260        }
261        
262        isTranslating = false;
263    }
264    
265    // Initialize
266    async function init() {
267        console.log('Setting up page translator');
268        
269        // Create toggle button
270        createToggleButton();
271        
272        // Wait a bit for page to fully load
273        setTimeout(() => {
274            translatePageContent();
275        }, 1000);
276        
277        console.log('Page Translator ready!');
278    }
279    
280    // Wait for page to be ready
281    if (document.readyState === 'loading') {
282        document.addEventListener('DOMContentLoaded', init);
283    } else {
284        init();
285    }
286})();
O'Reilly Tech Term Translator | Robomonkey