Sekai Viewer Story Translator (KR) - Tampermonkey

Translates Sekai Viewer story assets from Japanese to Korean using Gemini API.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Sekai Viewer Story Translator (KR) - Tampermonkey
// @namespace    http://tampermonkey.net/
// @version      0.5.5
// @description  Translates Sekai Viewer story assets from Japanese to Korean using Gemini API.
// @author       You
// @match        https://sekai.best/storyreader-live2d/*
// @match        https://sekai.best/storyreader/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=sekai.best
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @run-at       document-start
// @license MIT
// ==/UserScript==

(async function() {
    'use strict';

    console.log("Sekai Translator Script: Initializing...");

    // --- 설정값 관리 (비동기 로드) ---
    const config = {
        apiKey: await GM_getValue('geminiApiKey', ''),
        isEnabled: await GM_getValue('isTranslationEnabled', true)
    };

    // --- UI 생성 및 관리 함수 ---
    function createSettingsUI() {
        if (document.getElementById('sekai-translator-settings-ui')) {
            const ui = document.getElementById('sekai-translator-settings-ui');
            if(ui) ui.style.display = 'flex';
            return;
        }
        console.log("Sekai Translator Script: Creating settings UI elements...");

        GM_addStyle(`
            /* --- 기존 스타일 시작 (변경 없음) --- */
            #sekai-translator-settings-ui {
                position: fixed; top: 0; left: 0; width: 100%;
                background-color: rgba(40, 40, 40, 0.95); color: white;
                padding: 10px 20px; z-index: 99999; font-family: sans-serif;
                font-size: 14px; display: flex; align-items: center;
                justify-content: space-between; /* 기본은 양쪽 정렬 */
                box-shadow: 0 2px 5px rgba(0,0,0,0.3);
                box-sizing: border-box; border-bottom: 1px solid #555;
                flex-wrap: wrap; /* !!!! 추가: 기본적으로 줄바꿈 허용 !!!! */
                gap: 10px; /* 요소들 사이 기본 간격 */
            }
            #sekai-translator-settings-ui .panel {
                 display: flex; align-items: center; gap: 10px; /* 간격 약간 줄임 */
                 flex-wrap: wrap; /* 패널 내부 요소도 줄바꿈 허용 */
             }
            /* API 키 입력 필드의 최소 너비 제거 또는 조정 */
            #sekai-translator-settings-ui input[type="text"] {
                padding: 6px 10px; border: 1px solid #555; background-color: #333;
                color: white;
                /* min-width: 350px; */ /* !!!! 제거 또는 줄임 !!!! */
                flex-grow: 1; /* 가능한 공간 차지하도록 */
                min-width: 200px; /* 최소 너비는 유지 (선택 사항) */
                border-radius: 4px; box-sizing: border-box;
            }
            #sekai-translator-settings-ui button {
                padding: 6px 12px; cursor: pointer; background-color: #4CAF50;
                color: white; border: none; border-radius: 4px; white-space: nowrap;
                 flex-shrink: 0; /* 버튼 크기 줄어들지 않도록 */
            }
            #sekai-translator-settings-ui button:hover { opacity: 0.9; }
            #sekai-translator-settings-ui .status-message { font-size: 0.9em; margin-left: 5px; min-width: 100px; text-align: left; flex-basis: 100%; order: 3; /* 상태 메시지는 줄바꿈 시 아래로 */ }
            #sekai-translator-settings-ui a { color: #87CEEB; text-decoration: none; white-space: nowrap; }
            #sekai-translator-settings-ui a:hover { text-decoration: underline; }
            #sekai-translator-settings-ui .switch-container { display: flex; align-items: center; margin-left: auto; /* 스위치는 오른쪽으로 밀기 (기본) */ }
            #sekai-translator-settings-ui .switch { position: relative; display: inline-block; width: 40px; height: 20px; margin-left: 8px; }
            #sekai-translator-settings-ui .switch input { opacity: 0; width: 0; height: 0; }
            #sekai-translator-settings-ui .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 20px; transition: .4s; }
            #sekai-translator-settings-ui .slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; border-radius: 50%; transition: .4s; }
            #sekai-translator-settings-ui input:checked + .slider { background-color: #2196F3; }
            #sekai-translator-settings-ui input:checked + .slider:before { transform: translateX(20px); }
            #sekai-translator-settings-ui .close-button { background: none; border: none; color: #aaa; font-size: 20px; cursor: pointer; padding: 0 5px; line-height: 1; margin-left: 10px; }
            #sekai-translator-settings-ui .close-button:hover { color: white; }
             /* 키 발급 링크 스타일 */
            #sekai-translator-settings-ui span > a { flex-shrink: 0; margin-left: 5px;}
             /* --- 기존 스타일 끝 --- */


            /* !!!! --- 미디어 쿼리 시작 --- !!!! */
            /* 화면 너비가 768px 이하일 때 적용될 스타일 */
            @media (max-width: 768px) {
                #sekai-translator-settings-ui {
                    flex-direction: column; /* 세로 쌓기 */
                    align-items: stretch; /* 항목들 너비 채우기 */
                    padding: 10px; /* 패딩 줄이기 */
                    gap: 8px; /* 세로 간격 */
                }
                #sekai-translator-settings-ui .panel {
                    width: 100%; /* 패널 너비 100% */
                    gap: 8px; /* 내부 요소 간격 줄이기 */
                    justify-content: flex-start; /* 왼쪽 정렬 */
                }
                #sekai-translator-settings-ui input[type="text"] {
                    min-width: 150px; /* 모바일 최소 너비 */
                    flex-basis: auto; /* 너비 자동 조절 */
                }
                 #sekai-translator-settings-ui .status-message {
                     margin-left: 0; /* 왼쪽 여백 제거 */
                     margin-top: 5px; /* 위쪽 여백 추가 */
                     order: 0; /* 순서 원래대로 (필요시 조절) */
                     flex-basis: auto; /* 너비 자동 */
                 }
                 #sekai-translator-settings-ui .switch-container {
                     margin-left: 0; /* 자동 마진 제거 */
                     /* 필요하다면 오른쪽 패널 내 다른 요소들과의 정렬 조정 */
                     justify-content: flex-end; /* 오른쪽 정렬 시도 */
                     width: 100%;
                 }
                 #sekai-translator-settings-ui .close-button {
                     order: 1; /* 닫기 버튼을 스위치보다 앞으로 (선택적) */
                 }

            }
            /* !!!! --- 미디어 쿼리 끝 --- !!!! */
        `);

        const settingsDiv = document.createElement('div');
        settingsDiv.id = 'sekai-translator-settings-ui';

        const leftPanel = document.createElement('div');
        leftPanel.className = 'panel';
        leftPanel.innerHTML = `
            <label for="gmApiKeyInput">Gemini API Key:</label>
            <input type="text" id="gmApiKeyInput" placeholder="API 키 입력 후 Enter 또는 저장 버튼 클릭">
            <button id="gmSaveApiKeyBtn">저장</button>
            <span id="gmApiStatus" class="status-message"></span>
            <span><a href="https://aistudio.google.com/app/apikey?hl=ko" target="_blank" title="Get API Key">키 발급</a></span>
        `;
        settingsDiv.appendChild(leftPanel);

        const rightPanel = document.createElement('div');
        rightPanel.className = 'panel';
        rightPanel.innerHTML = `
            <div class="switch-container">
                <label for="gmEnableSwitch">번역 활성화</label>
                <label class="switch">
                  <input type="checkbox" id="gmEnableSwitch">
                  <span class="slider"></span>
                </label>
            </div>
            <button id="gmCloseSettingsBtn" class="close-button" title="설정 창 닫기">×</button>
        `;
        settingsDiv.appendChild(rightPanel);

        if (document.body) {
             document.body.appendChild(settingsDiv);
        } else {
             document.addEventListener('DOMContentLoaded', () => {
                 if (!document.getElementById('sekai-translator-settings-ui')) {
                      document.body.appendChild(settingsDiv);
                 }
             });
        }

        const apiKeyInput = document.getElementById('gmApiKeyInput');
        const saveButton = document.getElementById('gmSaveApiKeyBtn');
        const statusSpan = document.getElementById('gmApiStatus');
        const enableSwitch = document.getElementById('gmEnableSwitch');
        const closeButton = document.getElementById('gmCloseSettingsBtn');

        apiKeyInput.value = config.apiKey;
        enableSwitch.checked = config.isEnabled;
        if (!config.apiKey) {
            statusSpan.textContent = 'API 키를 입력하세요.';
            statusSpan.style.color = 'orange';
        }

        const saveApiKey = async () => { /* ... 이전과 동일 ... */
            const newApiKey = apiKeyInput.value.trim();
            if (!newApiKey) { statusSpan.textContent = 'API 키를 입력하세요.'; statusSpan.style.color = 'orange'; return; }
            await GM_setValue('geminiApiKey', newApiKey);
            config.apiKey = newApiKey;
            console.log('Sekai Translator Script: API Key saved.');
            statusSpan.textContent = 'API 키 저장됨!'; statusSpan.style.color = 'lightgreen';
            setTimeout(() => { statusSpan.textContent = ''; }, 2000);
        };
        const handleEnableSwitchChange = async () => { /* ... 이전과 동일 ... */
             const isEnabled = enableSwitch.checked;
             await GM_setValue('isTranslationEnabled', isEnabled);
             config.isEnabled = isEnabled;
             console.log(`Sekai Translator Script: Translation ${isEnabled ? 'ENABLED' : 'DISABLED'}. Refresh page to apply changes.`);
             statusSpan.textContent = `번역 ${isEnabled ? '활성화' : '비활성화'}됨 (페이지 새로고침 필요)`; statusSpan.style.color = 'lightblue';
             setTimeout(() => { statusSpan.textContent = ''; }, 3000);
        };

        saveButton.onclick = saveApiKey;
        apiKeyInput.onkeydown = (e) => { if (e.key === 'Enter') { saveApiKey(); } };
        enableSwitch.onchange = handleEnableSwitchChange;
        closeButton.onclick = () => {
             const ui = document.getElementById('sekai-translator-settings-ui');
             if(ui) ui.style.display = 'none';
        };
    }

     // --- 설정 UI 토글 버튼 생성 함수 ---
    function createSettingsToggleButton() {
        if (document.getElementById('sekai-translator-toggle-button')) return;
        console.log("Sekai Translator Script: Creating settings toggle button...");

        GM_addStyle(`
            #sekai-translator-toggle-button {
                position: fixed; bottom: 20px; right: 20px; z-index: 99998;
                padding: 8px 12px; background-color: rgba(0, 0, 0, 0.7); color: white;
                border: 1px solid #666; border-radius: 5px; cursor: pointer;
                font-size: 13px; opacity: 0.8; transition: opacity 0.3s, background-color 0.3s;
            }
            #sekai-translator-toggle-button:hover { opacity: 1.0; background-color: rgba(0, 0, 0, 0.9); }
        `);

        const toggleButton = document.createElement('button');
        toggleButton.id = 'sekai-translator-toggle-button';
        toggleButton.textContent = '⚙️ 번역 설정';

        toggleButton.onclick = () => {
            console.log("Sekai Translator Script: Toggle button clicked.");
            let settingsUI = document.getElementById('sekai-translator-settings-ui');
            if (!settingsUI) {
                console.log("Sekai Translator Script: Settings UI not found, creating...");
                createSettingsUI();
                settingsUI = document.getElementById('sekai-translator-settings-ui');
                if (settingsUI) {
                    console.log("Sekai Translator Script: Settings UI created and showing.");
                    settingsUI.style.display = 'flex';
                } else { console.error("Sekai Translator Script: Failed to create settings UI!"); }
            } else {
                if (settingsUI.style.display === 'none') {
                    console.log("Sekai Translator Script: Showing existing settings UI.");
                    settingsUI.style.display = 'flex';
                } else {
                    console.log("Sekai Translator Script: Hiding existing settings UI.");
                    settingsUI.style.display = 'none';
                }
            }
        };

        if (document.body) { document.body.appendChild(toggleButton); }
        else { document.addEventListener('DOMContentLoaded', () => { if (!document.getElementById('sekai-translator-toggle-button')) { document.body.appendChild(toggleButton); } }); }
    }

    // --- DOM 로드 완료 후 UI 초기화 ---
    function initializeUIAndButton() {
        if (window.self === window.top) {
             createSettingsToggleButton(); // 버튼은 항상 생성
             if (!config.apiKey) { // API 키 없으면 UI도 바로 생성 및 표시
                createSettingsUI();
                 const settingsUI = document.getElementById('sekai-translator-settings-ui');
                 if(settingsUI) settingsUI.style.display = 'flex';
             }
        }
    }

    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeUIAndButton); }
    else { initializeUIAndButton(); }


    // --- 번역 기능 활성화 및 API 키 확인 ---
    if (!config.isEnabled) { console.log("Sekai Translator Script: Translation is DISABLED."); return; }
    if (!config.apiKey) { console.warn("Sekai Translator Script: API Key is missing. Translation inactive."); return; }

    console.log("Sekai Translator Script: Translation is ENABLED and API Key found. Hooking XHR...");

    // --- XHR 가로채기 로직 ---
    const originalXhrOpen = XMLHttpRequest.prototype.open;
    const originalXhrSend = XMLHttpRequest.prototype.send;
    const xhrStates = new WeakMap();
    const targetAssetUrlPattern = /storage\.sekai\.best\/sekai-jp-assets\/(scenario\/|character\/(member\/res\d+_no\d+\/|card_story\/scenario\/)|event_story\/.*\/scenario\/|virtual_live\/.*\/scenario\/).*\.asset$/i;

    XMLHttpRequest.prototype.open = function(method, url, ...rest) { /* ... 이전과 동일 ... */
      let isTarget = false;
      if (typeof url === 'string' && targetAssetUrlPattern.test(url)) { isTarget = true; }
      xhrStates.set(this, { url: url, method: method, isTarget: isTarget, async: rest.length > 0 ? rest[0] !== false : true });
      return originalXhrOpen.apply(this, [method, url, ...rest]);
    };
    XMLHttpRequest.prototype.send = function(...args) { /* ... 이전과 동일 ... */
      const state = xhrStates.get(this);
      if (state && state.isTarget && config.isEnabled && config.apiKey) {
        console.log('Sekai Translator [XHR]: Intercepted send() for target asset pattern:', state.url);
        console.log('Sekai Translator [XHR]: Blocking original request and re-fetching manually...');
        processManually(this, state.url, state.method);
        return;
      } else { return originalXhrSend.apply(this, args); }
    };

    // --- 수동 처리 함수 ---
    async function processManually(xhrInstance, url, method) { /* ... 시작 부분 확인 로직 동일 ... */
      if (!config.isEnabled || !config.apiKey) { /* ... 비활성화 또는 API 키 없음 처리 ... */ return; }
      try {
        console.log(`Sekai Translator [Manual Fetch]: Fetching ${url} via GM_xmlhttpRequest...`);
        const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ /* ... GM_xmlhttpRequest 설정 ... */
            method: method.toUpperCase(), url: url, responseType: 'text',
            onload: res => (res.status >= 200 && res.status < 300) ? resolve(res) : reject(new Error(`GM_xmlhttpRequest failed: ${res.status}`)),
            onerror: res => reject(new Error(`GM_xmlhttpRequest error: ${res.error}`)),
            ontimeout: () => reject(new Error('GM_xmlhttpRequest timed out.'))
        }); });
        const originalTextData = response.responseText;
        console.log("Sekai Translator [Manual Fetch]: Original data fetched.");

        const textsToTranslate = extractJapaneseTextsFromAsset(originalTextData, url); // !!!! 수정 필요 !!!!
        let modifiedTextData = originalTextData;

        if (textsToTranslate && textsToTranslate.length > 0) {
          console.log(`Sekai Translator [Manual Fetch]: Found ${textsToTranslate.length} texts to translate.`);
          try {
            const translatedTexts = await requestTranslationViaGmXhr(textsToTranslate); // API 호출 함수 사용
            modifiedTextData = replaceJapaneseWithKorean(originalTextData, textsToTranslate, translatedTexts, url); // !!!! 수정 필요 !!!!
            console.log("Sekai Translator [Manual Fetch]: Text replaced with translation.");
          } catch (translationError) { console.error('Sekai Translator [Manual Fetch]: Translation/replacement failed:', translationError); }
        } else { console.log('Sekai Translator [Manual Fetch]: No Japanese text found to translate.'); }

        // XHR 상태 업데이트 및 이벤트 디스패치
        console.log("Sekai Translator [Manual Fetch]: Manually setting XHR properties and dispatching events...");
        Object.defineProperties(xhrInstance, { /* ... 상태 설정 ... */
          readyState: { value: 4, writable: true, configurable: true }, status: { value: response.status, writable: true, configurable: true },
          statusText: { value: response.statusText, writable: true, configurable: true }, response: { value: modifiedTextData, writable: true, configurable: true },
          responseText: { value: modifiedTextData, writable: true, configurable: true }, responseURL: { value: response.finalUrl || url, writable: true, configurable: true }
        });
        // 이벤트 디스패치
        if (typeof xhrInstance.onreadystatechange === 'function') { try { xhrInstance.onreadystatechange(); } catch (e) {}}
        if (typeof xhrInstance.onload === 'function') { try { xhrInstance.onload(); } catch (e) {}}
        if (typeof xhrInstance.onloadend === 'function') { try { xhrInstance.onloadend(); } catch (e) {}}
        console.log("Sekai Translator [Manual Fetch]: Manual processing complete.");
      } catch (error) { /* ... 오류 처리 ... */
        console.error('Sekai Translator [Manual Fetch]: Error during manual processing:', error);
        Object.defineProperties(xhrInstance, { /* ... 오류 상태 설정 ... */ });
        if (typeof xhrInstance.onerror === 'function') { try { xhrInstance.onerror(); } catch (e) {}}
        if (typeof xhrInstance.onloadend === 'function') { try { xhrInstance.onloadend(); } catch (e) {}}
      }
    }

    // --- Gemini API 호출 함수 (GM_xmlhttpRequest 사용) ---
    async function requestTranslationViaGmXhr(texts) {
        if (!config.apiKey) throw new Error("API Key is missing.");
        if (!texts || texts.length === 0) return [];

        const modelName = "gemini-2.0-flash";
        const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${config.apiKey}`;
        const delimiter = "<--SEP-->";
        const combinedTextPrompt = texts.join(delimiter);
        const prompt = `This is Project Sekai's script. Please translate each of the following Japanese texts (separated by "${delimiter}") into Korean. Respond with only the translated Korean texts, also separated by "${delimiter}. Please remain all ${delimiter}". Include no other explanatory text or markdown formatting.\n\n--- Japanese Texts Start ---\n${combinedTextPrompt}\n--- Japanese Texts End ---`;

        // !!!! 요청 본문 형식 "그대로" 사용 !!!!
        const requestBody = {
          contents: [{ parts: [{ text: prompt }] }]
          // generationConfig, safetySettings 등은 API 기본값 사용 (필요시 추가)
        };

        console.log(`Sekai Translator [GM_XHR]: Sending request to Gemini API (${modelName})...`);
        // console.log("Sekai Translator [GM_XHR]: Request Body:", JSON.stringify(requestBody, null, 2)); // 디버깅 필요시 주석 해제

        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST", url: apiUrl,
                headers: { "Content-Type": "application/json" },
                data: JSON.stringify(requestBody),
                responseType: 'json', timeout: 60000,
                onload: function(response) {
                    // ... (응답 처리 및 개수 검증 로직은 이전과 동일) ...
                     if (response.status === 200 && response.response) {
                        console.log("Sekai Translator [GM_XHR]: Received response from Gemini API.");
                        const resultData = response.response;
                        let combinedTranslatedText = "";
                        try {
                            combinedTranslatedText = resultData?.candidates?.[0]?.content?.parts?.[0]?.text?.trim();
                            if (!combinedTranslatedText) { throw new Error('Translation text not found in API response.'); }
                            const translatedTexts = combinedTranslatedText.split(delimiter).map(t => t.trim().replace(/^["|]*(.*?)["]|]*$/g, '$1'));
                            if (translatedTexts.length !== texts.length) {
                                console.warn(`Count mismatch: ${translatedTexts.length}/${texts.length}.`);
                                reject(new Error(`Translation count mismatch: ${translatedTexts.length}/${texts.length}`));
                            } else {
                                console.log("Sekai Translator [GM_XHR]: Translation successful.");
                                resolve(translatedTexts);
                            }
                        } catch (parseError) { reject(new Error('Failed to parse translation result.')); }
                    } else { reject(new Error(`Gemini API error: ${response.status} - ${response.responseText}`)); }
                },
                onerror: res => reject(new Error(`Network error: ${res.error}`)),
                ontimeout: () => reject(new Error('Gemini API request timed out.'))
            });
        });
    }

    function extractJapaneseTextsFromAsset(textData, url) { /* ... 이전 로직 또는 수정된 로직 ... */
         console.log(`Sekai Translator: Extracting Japanese text from ${url} (Robust approach needed!)`);
        if (!textData) return []; let japaneseTexts = [];
        try {
            const regex = /^\s*"(WindowDisplayName|Body|Name|Thought|serif|Message)"\s*:\s*"(.*?)",?$/gm;
            let match;
            while ((match = regex.exec(textData)) !== null) {
                let extracted = match[2];
                if (extracted && extracted.length > 0 && /\p{Script=Hiragana}|\p{Script=Katakana}|\p{Script=Han}/u.test(extracted)) {
                    extracted = extracted.replace(/\\n/g, '\n');
                    japaneseTexts.push(extracted);
                }
            }
             // console.log(`Sekai Translator: Extracted ${japaneseTexts.length} texts using regex.`);
        } catch (e) { console.error("Sekai Translator: Error during text extraction:", e); }
        console.log(`Sekai Translator: Finished extraction. Found ${japaneseTexts.length} texts.`);
        return japaneseTexts;
    }
    function replaceJapaneseWithKorean(originalTextData, originalJapanese, translatedKorean, url) { /* ... 이전 로직 또는 수정된 로직 ... */
         console.log(`Sekai Translator: Replacing text in ${url} (Robust approach needed!)`);
        let modifiedText = originalTextData;
        if (!originalJapanese || !translatedKorean || originalJapanese.length !== translatedKorean.length) {
            console.warn(`Sekai Translator: Mismatch text count (${originalJapanese?.length}/${translatedKorean?.length}). Returning original data.`);
            return originalTextData;
        }
        try {
            let k_idx = 0;
            const regex = /^(\s*"(?:WindowDisplayName|Body|Name|Thought|serif|Message)"\s*:\s*")(.*?)("?,?)$/gm;
            modifiedText = originalTextData.replace(regex, (match, prefix, originalValue, suffix) => {
                 if (k_idx < originalJapanese.length && originalValue === originalJapanese[k_idx].replace(/\n/g, '\\n')) {
                     if (k_idx < translatedKorean.length) {
                         const translatedEscaped = translatedKorean[k_idx].replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
                         k_idx++;
                         return prefix + translatedEscaped + suffix;
                     } else { return match; }
                 }
                 return match;
            });
             if (k_idx < originalJapanese.length) { console.warn(`Replacement warning: ${originalJapanese.length - k_idx} items were not replaced.`); }
        } catch (e) { console.error("Sekai Translator: Error during text replacement:", e); return originalTextData; }
        console.log("Sekai Translator: Text replacement finished.");
        return modifiedText;
    }

})(); // 즉시 실행 함수 끝