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})();