diff --git a/data/version.json b/data/version.json index d9d3032..f056e5d 100644 --- a/data/version.json +++ b/data/version.json @@ -1,3 +1,3 @@ { - "latest": "1.0.6" + "latest": "1.0.7" } diff --git a/script.user.js b/script.user.js index ee49b7c..b9a03b7 100644 --- a/script.user.js +++ b/script.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Crowdin Localization Tools // @namespace https://yuzu.kirameki.cafe/ -// @version 1.0.6 +// @version 1.0.7 // @description A tool for translating Crowdin projects using a CSV file // @author Yuzu (YuzuZensai) // @match https://crowdin.com/editor/* @@ -16,36 +16,38 @@ // Global configuration const CONFIG = { defaultVisible: true, - defaultPosition: { right: '20px', bottom: '20px' }, + defaultPosition: { right: "20px", bottom: "20px" }, windowDimensions: { - width: '600px', - height: '600px' + width: "600px", + height: "600px", }, debug: true, // Update check - updateCheckUrl: 'https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/main/data/version.json', + updateCheckUrl: + "https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/main/data/version.json", // Remote CSV - remoteCSVUrl: 'https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/main/data/data.csv', + remoteCSVUrl: + "https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/main/data/data.csv", allowLocalOverride: true, allowUrlOverride: true, // Crowdin editor - textboxSelector: '.editor-panel__editor-container textarea', - stringNumberSelector: '#file_options > li:nth-child(4) > a:nth-child(1)', - editorSourceContainer: '.editor-current-translation-source', - sourceStringContainer: '#source_phrase_container', + textboxSelector: ".editor-panel__editor-container textarea", + stringNumberSelector: "#file_options > li:nth-child(4) > a:nth-child(1)", + editorSourceContainer: ".editor-current-translation-source", + sourceStringContainer: "#source_phrase_container", - autoSearchInterval: 1500, + autoSearchInterval: 1000, fuzzyThreshold: 0.7, metadata: { - version: '1.0.6', - repository: 'https://github.com/YuzuZensai/Crowdin-Localization-Tools', - authorGithub: 'https://github.com/YuzuZensai' - } + version: "1.0.7", + repository: "https://github.com/YuzuZensai/Crowdin-Localization-Tools", + authorGithub: "https://github.com/YuzuZensai", + }, }; function log(type, message, data = null) { @@ -55,24 +57,66 @@ function log(type, message, data = null) { const prefix = `[Crowdin Localization Tools][${timestamp}]`; switch (type.toLowerCase()) { - case 'info': - console.log(`${prefix} ℹ️ ${message}`, data || ''); + case "info": + console.log(`${prefix} ℹ️ ${message}`, data || ""); break; - case 'warn': - console.warn(`${prefix} ⚠️ ${message}`, data || ''); + case "warn": + console.warn(`${prefix} ⚠️ ${message}`, data || ""); break; - case 'error': - console.error(`${prefix} ❌ ${message}`, data || ''); + case "error": + console.error(`${prefix} ❌ ${message}`, data || ""); break; - case 'success': - console.log(`${prefix} ✅ ${message}`, data || ''); + case "success": + console.log(`${prefix} ✅ ${message}`, data || ""); break; - case 'debug': - console.debug(`${prefix} 🔍 ${message}`, data || ''); + case "debug": + console.debug(`${prefix} 🔍 ${message}`, data || ""); break; } } +function sanitizeHTML(str) { + if (typeof str !== "string") return ""; + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; +} + +// Just for good measure, remove any potential script tags, even if they're encoded +function validateCSVField(field) { + if (typeof field !== "string") { + return ""; + } + + field = field + .replace(/<\s*script[^>]*>.*?<\s*\/\s*script\s*>/gi, "") + .replace(/<\s*script[^&]*>.*?<\/\s*script\s*>/gi, ""); + + // Remove potential event handlers + field = field.replace(/\bon\w+\s*=\s*["']?[^"']*["']?/gi, ""); + + // Remove data URLs + field = field.replace(/data:[^,]*,/gi, ""); + + // Remove any HTML tags + field = field.replace(/<[^>]*>/g, ""); + + return field.trim(); +} + +function validateCSVEntry(entry) { + if (!entry || typeof entry !== "object") { + return null; + } + + return { + source: validateCSVField(entry.source), + target: validateCSVField(entry.target), + note: validateCSVField(entry.note), + category: validateCSVField(entry.category), + }; +} + function levenshteinDistance(a, b) { const matrix = []; @@ -91,8 +135,8 @@ function levenshteinDistance(a, b) { } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, // substitution - matrix[i][j - 1] + 1, // insertion - matrix[i - 1][j] + 1 // deletion + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1 // deletion ); } } @@ -117,75 +161,135 @@ function TranslatorTool() { var dragOffsetY = 0; var toggleButton; var visible = CONFIG.defaultVisible; - var lastSearchedText = ''; + var lastSearchedText = ""; var autoSearchIntervalId = null; var updateLink; var currentCSVSource = null; + var categoryColors = new Map(); + + function generateColorForCategory(category) { + if (!category) return null; + if (categoryColors.has(category)) { + return categoryColors.get(category); + } + + const predefinedColors = { + UI: "#2196F3", + "Unity / 3D": "#9C27B0", + "VRChat Specific": "#4CAF50", + "Trust Rank": "#FF9800", + "Instance Type": "#795548", + "Avatar Performance Rank": "#F44336", + }; + + if (predefinedColors[category]) { + categoryColors.set(category, predefinedColors[category]); + return predefinedColors[category]; + } + + let hash = 0; + for (let i = 0; i < category.length; i++) { + hash = category.charCodeAt(i) + ((hash << 5) - hash); + } + + const hue = Math.abs(hash % 360); + const color = `hsl(${hue}, 65%, 55%)`; + categoryColors.set(category, color); + return color; + } + + function createCategoryChip(category) { + if (!category) return ""; + + const color = generateColorForCategory(category); + return `${category}`; + } function init() { - log('info', 'Initializing translator tool'); + log("info", "Initializing translator tool"); createUI(); createToggleButton(); setupEventListeners(); setupExternalTextboxListener(); setupCrowdinEditorListener(); - const sourceToggle = document.querySelector('#source-toggle'); + const sourceToggle = document.querySelector("#source-toggle"); if (!sourceToggle || !sourceToggle.checked) { fetchRemoteCSV(CONFIG.remoteCSVUrl); } - log('success', 'Crowdin Localization Tools version ' + CONFIG.metadata.version + ' by ' + CONFIG.metadata.authorGithub + ' initialized successfully'); + setTimeout(() => { + checkForEditorContent(); + }, 2000); + + log( + "success", + "Crowdin Localization Tools version " + + CONFIG.metadata.version + + " by " + + CONFIG.metadata.authorGithub + + " initialized successfully" + ); } function createUI() { - log('info', 'Creating UI elements'); + log("info", "Creating UI elements"); // Container - container = document.createElement('div'); - container.id = 'translator-tool'; - container.style.position = 'fixed'; + container = document.createElement("div"); + container.id = "translator-tool"; + container.style.position = "fixed"; container.style.bottom = CONFIG.defaultPosition.bottom; container.style.right = CONFIG.defaultPosition.right; container.style.width = CONFIG.windowDimensions.width; container.style.height = CONFIG.windowDimensions.height; - container.style.backgroundColor = '#fff'; - container.style.border = '1px solid #e0e0e0'; - container.style.borderRadius = '8px'; - container.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; - container.style.zIndex = '9999'; - container.style.display = CONFIG.defaultVisible ? 'flex' : 'none'; - container.style.flexDirection = 'column'; - container.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; + container.style.backgroundColor = "#fff"; + container.style.border = "1px solid #e0e0e0"; + container.style.borderRadius = "8px"; + container.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; + container.style.zIndex = "9999"; + container.style.display = CONFIG.defaultVisible ? "flex" : "none"; + container.style.flexDirection = "column"; + container.style.fontFamily = + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; // Header - var header = document.createElement('div'); - header.style.padding = '12px 16px'; - header.style.backgroundColor = '#f8f9fa'; - header.style.borderBottom = '1px solid #e0e0e0'; - header.style.display = 'flex'; - header.style.justifyContent = 'space-between'; - header.style.alignItems = 'center'; - header.style.cursor = 'move'; + var header = document.createElement("div"); + header.style.padding = "12px 16px"; + header.style.backgroundColor = "#f8f9fa"; + header.style.borderBottom = "1px solid #e0e0e0"; + header.style.display = "flex"; + header.style.justifyContent = "space-between"; + header.style.alignItems = "center"; + header.style.cursor = "move"; - var title = document.createElement('h3'); - title.textContent = 'Crowdin Localization Tools'; - title.style.margin = '0'; - title.style.fontSize = '16px'; - title.style.fontWeight = '600'; - title.style.color = '#1a73e8'; + var title = document.createElement("h3"); + title.textContent = "Crowdin Localization Tools"; + title.style.margin = "0"; + title.style.fontSize = "16px"; + title.style.fontWeight = "600"; + title.style.color = "#1a73e8"; - var closeButton = document.createElement('button'); - closeButton.textContent = '×'; - closeButton.style.border = 'none'; - closeButton.style.background = 'none'; - closeButton.style.cursor = 'pointer'; - closeButton.style.fontSize = '24px'; - closeButton.style.color = '#666'; - closeButton.style.padding = '0 4px'; - closeButton.style.lineHeight = '1'; - closeButton.addEventListener('click', function () { - log('info', 'Close button clicked'); + var closeButton = document.createElement("button"); + closeButton.textContent = "×"; + closeButton.style.border = "none"; + closeButton.style.background = "none"; + closeButton.style.cursor = "pointer"; + closeButton.style.fontSize = "24px"; + closeButton.style.color = "#666"; + closeButton.style.padding = "0 4px"; + closeButton.style.lineHeight = "1"; + closeButton.addEventListener("click", function () { + log("info", "Close button clicked"); toggleVisibility(); }); @@ -194,15 +298,15 @@ function TranslatorTool() { container.appendChild(header); // Tab menu - var tabMenu = document.createElement('div'); - tabMenu.style.padding = '0 16px'; - tabMenu.style.backgroundColor = '#f8f9fa'; - tabMenu.style.borderBottom = '1px solid #e0e0e0'; - tabMenu.style.display = 'flex'; - tabMenu.style.gap = '16px'; + var tabMenu = document.createElement("div"); + tabMenu.style.padding = "0 16px"; + tabMenu.style.backgroundColor = "#f8f9fa"; + tabMenu.style.borderBottom = "1px solid #e0e0e0"; + tabMenu.style.display = "flex"; + tabMenu.style.gap = "16px"; - var mainTab = createTab('Translator', true); - var settingsTab = createTab('Settings', false); + var mainTab = createTab("Translator", true); + var settingsTab = createTab("Settings", false); tabMenu.appendChild(mainTab); tabMenu.appendChild(settingsTab); @@ -222,30 +326,32 @@ function TranslatorTool() { // Inject try { document.body.appendChild(container); - log('success', 'Main container added to document body'); + log("success", "Main container added to document body"); } catch (error) { - log('error', 'Error appending container to body:', error); + log("error", "Error appending container to body:", error); } setupDraggable(header); - log('success', 'UI elements created successfully'); + log("success", "UI elements created successfully"); } function createTab(text, isActive) { - var tab = document.createElement('button'); + var tab = document.createElement("button"); tab.textContent = text; - tab.style.padding = '12px 16px'; - tab.style.border = 'none'; - tab.style.background = 'none'; - tab.style.borderBottom = isActive ? '2px solid #1a73e8' : '2px solid transparent'; - tab.style.color = isActive ? '#1a73e8' : '#666'; - tab.style.cursor = 'pointer'; - tab.style.fontSize = '14px'; - tab.style.fontWeight = '500'; - tab.style.transition = 'all 0.2s ease'; + tab.style.padding = "12px 16px"; + tab.style.border = "none"; + tab.style.background = "none"; + tab.style.borderBottom = isActive + ? "2px solid #1a73e8" + : "2px solid transparent"; + tab.style.color = isActive ? "#1a73e8" : "#666"; + tab.style.cursor = "pointer"; + tab.style.fontSize = "14px"; + tab.style.fontWeight = "500"; + tab.style.transition = "all 0.2s ease"; - tab.addEventListener('click', function () { + tab.addEventListener("click", function () { switchTab(text.toLowerCase()); }); @@ -253,82 +359,82 @@ function TranslatorTool() { } function createMainContent() { - var content = document.createElement('div'); - content.id = 'translator-main-content'; - content.style.display = 'flex'; - content.style.flexDirection = 'column'; - content.style.flexGrow = '1'; - content.style.padding = '16px'; - content.style.height = '100%'; - content.style.boxSizing = 'border-box'; - content.style.minHeight = '0'; + var content = document.createElement("div"); + content.id = "translator-main-content"; + content.style.display = "flex"; + content.style.flexDirection = "column"; + content.style.flexGrow = "1"; + content.style.padding = "16px"; + content.style.height = "100%"; + content.style.boxSizing = "border-box"; + content.style.minHeight = "0"; // Search container - var searchContainer = document.createElement('div'); - searchContainer.style.position = 'relative'; - searchContainer.style.marginBottom = '16px'; - searchContainer.style.flexShrink = '0'; + var searchContainer = document.createElement("div"); + searchContainer.style.position = "relative"; + searchContainer.style.marginBottom = "16px"; + searchContainer.style.flexShrink = "0"; // Current string label - var currentStringLabel = document.createElement('div'); - currentStringLabel.style.fontSize = '12px'; - currentStringLabel.style.color = '#666'; - currentStringLabel.style.marginBottom = '4px'; - currentStringLabel.style.padding = '4px'; - currentStringLabel.style.backgroundColor = '#f8f9fa'; - currentStringLabel.style.borderRadius = '4px'; - currentStringLabel.style.whiteSpace = 'nowrap'; - currentStringLabel.style.overflow = 'hidden'; - currentStringLabel.style.textOverflow = 'ellipsis'; - currentStringLabel.textContent = 'Current string: '; - currentStringLabel.id = 'current-string-label'; + var currentStringLabel = document.createElement("div"); + currentStringLabel.style.fontSize = "12px"; + currentStringLabel.style.color = "#666"; + currentStringLabel.style.marginBottom = "4px"; + currentStringLabel.style.padding = "4px"; + currentStringLabel.style.backgroundColor = "#f8f9fa"; + currentStringLabel.style.borderRadius = "4px"; + currentStringLabel.style.whiteSpace = "nowrap"; + currentStringLabel.style.overflow = "hidden"; + currentStringLabel.style.textOverflow = "ellipsis"; + currentStringLabel.textContent = "Current string: "; + currentStringLabel.id = "current-string-label"; searchContainer.appendChild(currentStringLabel); // Search input - searchInput = document.createElement('input'); - searchInput.type = 'text'; - searchInput.placeholder = 'Search translations...'; - searchInput.style.width = '100%'; - searchInput.style.padding = '10px 12px'; - searchInput.style.border = '2px solid #e0e0e0'; - searchInput.style.borderRadius = '6px'; - searchInput.style.fontSize = '14px'; - searchInput.style.boxSizing = 'border-box'; - searchInput.style.transition = 'all 0.2s ease'; - searchInput.style.backgroundColor = '#f8f9fa'; - searchInput.style.outline = 'none'; + searchInput = document.createElement("input"); + searchInput.type = "text"; + searchInput.placeholder = "Search translations..."; + searchInput.style.width = "100%"; + searchInput.style.padding = "10px 12px"; + searchInput.style.border = "2px solid #e0e0e0"; + searchInput.style.borderRadius = "6px"; + searchInput.style.fontSize = "14px"; + searchInput.style.boxSizing = "border-box"; + searchInput.style.transition = "all 0.2s ease"; + searchInput.style.backgroundColor = "#f8f9fa"; + searchInput.style.outline = "none"; - searchInput.addEventListener('mouseover', function () { + searchInput.addEventListener("mouseover", function () { if (document.activeElement !== this) { - this.style.borderColor = '#ccc'; + this.style.borderColor = "#ccc"; } }); - searchInput.addEventListener('mouseout', function () { + searchInput.addEventListener("mouseout", function () { if (document.activeElement !== this) { - this.style.borderColor = '#e0e0e0'; + this.style.borderColor = "#e0e0e0"; } }); - searchInput.addEventListener('focus', function () { - this.style.borderColor = '#1a73e8'; - this.style.backgroundColor = '#fff'; + searchInput.addEventListener("focus", function () { + this.style.borderColor = "#1a73e8"; + this.style.backgroundColor = "#fff"; }); - searchInput.addEventListener('blur', function () { - this.style.borderColor = '#e0e0e0'; - this.style.backgroundColor = '#f8f9fa'; + searchInput.addEventListener("blur", function () { + this.style.borderColor = "#e0e0e0"; + this.style.backgroundColor = "#f8f9fa"; }); searchContainer.appendChild(searchInput); content.appendChild(searchContainer); - resultsDiv = document.createElement('div'); - resultsDiv.style.display = 'flex'; - resultsDiv.style.flexDirection = 'column'; - resultsDiv.style.flexGrow = '1'; - resultsDiv.style.minHeight = '0'; - resultsDiv.style.padding = '8px'; - resultsDiv.style.backgroundColor = '#fff'; - resultsDiv.style.borderRadius = '4px'; - resultsDiv.style.overflow = 'hidden'; + resultsDiv = document.createElement("div"); + resultsDiv.style.display = "flex"; + resultsDiv.style.flexDirection = "column"; + resultsDiv.style.flexGrow = "1"; + resultsDiv.style.minHeight = "0"; + resultsDiv.style.padding = "8px"; + resultsDiv.style.backgroundColor = "#fff"; + resultsDiv.style.borderRadius = "4px"; + resultsDiv.style.overflow = "hidden"; content.appendChild(resultsDiv); @@ -336,91 +442,91 @@ function TranslatorTool() { } function createSettingsContent() { - var content = document.createElement('div'); - content.id = 'translator-settings-content'; - content.style.display = 'none'; - content.style.flexDirection = 'column'; - content.style.flexGrow = '1'; - content.style.padding = '16px'; - content.style.gap = '16px'; + var content = document.createElement("div"); + content.id = "translator-settings-content"; + content.style.display = "none"; + content.style.flexDirection = "column"; + content.style.flexGrow = "1"; + content.style.padding = "16px"; + content.style.gap = "16px"; // CSV Source Settings - var csvSourceSection = document.createElement('div'); - csvSourceSection.style.marginBottom = '20px'; + var csvSourceSection = document.createElement("div"); + csvSourceSection.style.marginBottom = "20px"; - var csvSourceTitle = document.createElement('h4'); - csvSourceTitle.textContent = 'CSV Source Settings'; - csvSourceTitle.style.margin = '0 0 12px 0'; - csvSourceTitle.style.color = '#333'; + var csvSourceTitle = document.createElement("h4"); + csvSourceTitle.textContent = "CSV Source Settings"; + csvSourceTitle.style.margin = "0 0 12px 0"; + csvSourceTitle.style.color = "#333"; // Source Type Toggle - var sourceToggleContainer = document.createElement('div'); - sourceToggleContainer.style.display = 'flex'; - sourceToggleContainer.style.alignItems = 'center'; - sourceToggleContainer.style.marginBottom = '16px'; - sourceToggleContainer.style.gap = '8px'; + var sourceToggleContainer = document.createElement("div"); + sourceToggleContainer.style.display = "flex"; + sourceToggleContainer.style.alignItems = "center"; + sourceToggleContainer.style.marginBottom = "16px"; + sourceToggleContainer.style.gap = "8px"; - var sourceToggle = document.createElement('input'); - sourceToggle.type = 'checkbox'; - sourceToggle.id = 'source-toggle'; - sourceToggle.style.margin = '0'; + var sourceToggle = document.createElement("input"); + sourceToggle.type = "checkbox"; + sourceToggle.id = "source-toggle"; + sourceToggle.style.margin = "0"; sourceToggle.checked = false; - var sourceToggleLabel = document.createElement('label'); - sourceToggleLabel.htmlFor = 'source-toggle'; - sourceToggleLabel.textContent = 'Use Local File'; - sourceToggleLabel.style.fontSize = '14px'; - sourceToggleLabel.style.color = '#666'; - sourceToggleLabel.style.userSelect = 'none'; - sourceToggleLabel.style.cursor = 'pointer'; + var sourceToggleLabel = document.createElement("label"); + sourceToggleLabel.htmlFor = "source-toggle"; + sourceToggleLabel.textContent = "Use Local File"; + sourceToggleLabel.style.fontSize = "14px"; + sourceToggleLabel.style.color = "#666"; + sourceToggleLabel.style.userSelect = "none"; + sourceToggleLabel.style.cursor = "pointer"; sourceToggleContainer.appendChild(sourceToggle); sourceToggleContainer.appendChild(sourceToggleLabel); // Remote URL Input Container - var urlContainer = document.createElement('div'); - urlContainer.style.marginBottom = '16px'; + var urlContainer = document.createElement("div"); + urlContainer.style.marginBottom = "16px"; - var urlLabel = document.createElement('label'); - urlLabel.textContent = 'Remote CSV URL'; - urlLabel.style.display = 'block'; - urlLabel.style.marginBottom = '8px'; - urlLabel.style.fontSize = '14px'; - urlLabel.style.color = '#666'; + var urlLabel = document.createElement("label"); + urlLabel.textContent = "Remote CSV URL"; + urlLabel.style.display = "block"; + urlLabel.style.marginBottom = "8px"; + urlLabel.style.fontSize = "14px"; + urlLabel.style.color = "#666"; - var remoteUrlInput = document.createElement('input'); - remoteUrlInput.type = 'text'; + var remoteUrlInput = document.createElement("input"); + remoteUrlInput.type = "text"; remoteUrlInput.value = CONFIG.remoteCSVUrl; - remoteUrlInput.placeholder = 'Enter remote CSV URL'; - remoteUrlInput.style.width = '100%'; - remoteUrlInput.style.padding = '8px 12px'; - remoteUrlInput.style.border = '1px solid #e0e0e0'; - remoteUrlInput.style.borderRadius = '4px'; - remoteUrlInput.style.boxSizing = 'border-box'; - remoteUrlInput.style.fontSize = '14px'; + remoteUrlInput.placeholder = "Enter remote CSV URL"; + remoteUrlInput.style.width = "100%"; + remoteUrlInput.style.padding = "8px 12px"; + remoteUrlInput.style.border = "1px solid #e0e0e0"; + remoteUrlInput.style.borderRadius = "4px"; + remoteUrlInput.style.boxSizing = "border-box"; + remoteUrlInput.style.fontSize = "14px"; urlContainer.appendChild(urlLabel); urlContainer.appendChild(remoteUrlInput); // Local File Input Container - var fileContainer = document.createElement('div'); - fileContainer.style.marginBottom = '16px'; - fileContainer.style.display = 'none'; // Hidden by default + var fileContainer = document.createElement("div"); + fileContainer.style.marginBottom = "16px"; + fileContainer.style.display = "none"; - var fileLabel = document.createElement('label'); - fileLabel.textContent = 'Local CSV File'; - fileLabel.style.display = 'block'; - fileLabel.style.marginBottom = '8px'; - fileLabel.style.fontSize = '14px'; - fileLabel.style.color = '#666'; + var fileLabel = document.createElement("label"); + fileLabel.textContent = "Local CSV File"; + fileLabel.style.display = "block"; + fileLabel.style.marginBottom = "8px"; + fileLabel.style.fontSize = "14px"; + fileLabel.style.color = "#666"; - var localFileInput = document.createElement('input'); - localFileInput.type = 'file'; - localFileInput.accept = '.csv'; - localFileInput.style.width = '100%'; - localFileInput.style.fontSize = '14px'; + var localFileInput = document.createElement("input"); + localFileInput.type = "file"; + localFileInput.accept = ".csv"; + localFileInput.style.width = "100%"; + localFileInput.style.fontSize = "14px"; - localFileInput.addEventListener('change', function () { + localFileInput.addEventListener("change", function () { if (this.files.length > 0) { readCSVFile(this.files[0]); } @@ -429,18 +535,18 @@ function TranslatorTool() { fileContainer.appendChild(fileLabel); fileContainer.appendChild(localFileInput); - sourceToggle.addEventListener('change', function () { - urlContainer.style.display = this.checked ? 'none' : 'block'; - fileContainer.style.display = this.checked ? 'block' : 'none'; + sourceToggle.addEventListener("change", function () { + urlContainer.style.display = this.checked ? "none" : "block"; + fileContainer.style.display = this.checked ? "block" : "none"; if (this.checked) { - remoteUrlInput.value = ''; + remoteUrlInput.value = ""; } else { - localFileInput.value = ''; + localFileInput.value = ""; } }); - // Add global style for refresh button + // Global style for refresh button GM_addStyle(` .csv-translator-refresh-btn { padding: 8px 16px; @@ -461,11 +567,11 @@ function TranslatorTool() { `); // Refresh Button - var refreshButton = document.createElement('button'); - refreshButton.textContent = 'Refresh Data'; - refreshButton.className = 'csv-translator-refresh-btn'; + var refreshButton = document.createElement("button"); + refreshButton.textContent = "Refresh Data"; + refreshButton.className = "csv-translator-refresh-btn"; - refreshButton.addEventListener('click', function () { + refreshButton.addEventListener("click", function () { refreshTranslationData(); }); @@ -481,36 +587,38 @@ function TranslatorTool() { } function createFooter() { - var footer = document.createElement('div'); - footer.style.padding = '12px 16px'; - footer.style.borderTop = '1px solid #e0e0e0'; - footer.style.backgroundColor = '#f8f9fa'; - footer.style.fontSize = '12px'; - footer.style.color = '#666'; - footer.style.display = 'flex'; - footer.style.justifyContent = 'space-between'; - footer.style.alignItems = 'center'; + var footer = document.createElement("div"); + footer.style.padding = "12px 16px"; + footer.style.borderTop = "1px solid #e0e0e0"; + footer.style.backgroundColor = "#f8f9fa"; + footer.style.fontSize = "12px"; + footer.style.color = "#666"; + footer.style.display = "flex"; + footer.style.justifyContent = "space-between"; + footer.style.alignItems = "center"; - var credits = document.createElement('div'); - var authorLink = document.createElement('a'); + var credits = document.createElement("div"); + var authorLink = document.createElement("a"); authorLink.href = CONFIG.metadata.authorGithub; - authorLink.textContent = 'YuzuZensai'; - authorLink.style.color = '#1a73e8'; - authorLink.style.textDecoration = 'none'; - authorLink.style.cursor = 'pointer'; - authorLink.target = '_blank'; + authorLink.textContent = "YuzuZensai"; + authorLink.style.color = "#1a73e8"; + authorLink.style.textDecoration = "none"; + authorLink.style.cursor = "pointer"; + authorLink.target = "_blank"; - credits.appendChild(document.createTextNode('Made with 💖 by ')); + credits.appendChild(document.createTextNode("Made with 💖 by ")); credits.appendChild(authorLink); - credits.appendChild(document.createTextNode(` • v${CONFIG.metadata.version}`)); + credits.appendChild( + document.createTextNode(` • v${CONFIG.metadata.version}`) + ); - updateLink = document.createElement('a'); - updateLink.href = 'javascript:void(0)'; - updateLink.textContent = 'Check for updates'; - updateLink.style.color = '#1a73e8'; - updateLink.style.textDecoration = 'none'; - updateLink.style.cursor = 'pointer'; - updateLink.addEventListener('click', function (e) { + updateLink = document.createElement("a"); + updateLink.href = "javascript:void(0)"; + updateLink.textContent = "Check for updates"; + updateLink.style.color = "#1a73e8"; + updateLink.style.textDecoration = "none"; + updateLink.style.cursor = "pointer"; + updateLink.addEventListener("click", function (e) { e.preventDefault(); checkForUpdates(); }); @@ -522,126 +630,136 @@ function TranslatorTool() { } function switchTab(tabName) { - var mainContent = document.getElementById('translator-main-content'); - var settingsContent = document.getElementById('translator-settings-content'); - var tabs = container.querySelectorAll('button'); + var mainContent = document.getElementById("translator-main-content"); + var settingsContent = document.getElementById( + "translator-settings-content" + ); + var tabs = container.querySelectorAll("button"); - tabs.forEach(tab => { + tabs.forEach((tab) => { if (tab.textContent.toLowerCase() === tabName) { - tab.style.borderBottom = '2px solid #1a73e8'; - tab.style.color = '#1a73e8'; + tab.style.borderBottom = "2px solid #1a73e8"; + tab.style.color = "#1a73e8"; } else { - tab.style.borderBottom = '2px solid transparent'; - tab.style.color = '#666'; + tab.style.borderBottom = "2px solid transparent"; + tab.style.color = "#666"; } }); - if (tabName === 'translator') { - mainContent.style.display = 'flex'; - settingsContent.style.display = 'none'; + if (tabName === "translator") { + mainContent.style.display = "flex"; + settingsContent.style.display = "none"; } else { - mainContent.style.display = 'none'; - settingsContent.style.display = 'flex'; + mainContent.style.display = "none"; + settingsContent.style.display = "flex"; } } function setupDraggable(element) { - element.addEventListener('mousedown', function (e) { + element.addEventListener("mousedown", function (e) { isDragging = true; var rect = container.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; e.preventDefault(); - log('info', 'Started dragging window'); + log("info", "Started dragging window"); }); - document.addEventListener('mousemove', function (e) { + document.addEventListener("mousemove", function (e) { if (isDragging) { var x = e.clientX - dragOffsetX; var y = e.clientY - dragOffsetY; - container.style.left = x + 'px'; - container.style.top = y + 'px'; - container.style.right = 'auto'; - container.style.bottom = 'auto'; + container.style.left = x + "px"; + container.style.top = y + "px"; + container.style.right = "auto"; + container.style.bottom = "auto"; } }); - document.addEventListener('mouseup', function () { + document.addEventListener("mouseup", function () { if (isDragging) { isDragging = false; - log('info', 'Stopped dragging window'); + log("info", "Stopped dragging window"); } }); } function createToggleButton() { - log('info', 'Creating toggle button'); - toggleButton = document.createElement('div'); - toggleButton.id = 'translator-toggle'; - toggleButton.textContent = 'T'; - toggleButton.style.position = 'fixed'; - toggleButton.style.bottom = '10px'; - toggleButton.style.right = '10px'; - toggleButton.style.width = '30px'; - toggleButton.style.height = '30px'; - toggleButton.style.backgroundColor = visible ? '#F44336' : '#4CAF50'; - toggleButton.style.color = 'white'; - toggleButton.style.borderRadius = '50%'; - toggleButton.style.display = 'flex'; - toggleButton.style.justifyContent = 'center'; - toggleButton.style.alignItems = 'center'; - toggleButton.style.cursor = 'pointer'; - toggleButton.style.fontSize = '16px'; - toggleButton.style.fontWeight = 'bold'; - toggleButton.style.zIndex = '10000'; - toggleButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; + log("info", "Creating toggle button"); + toggleButton = document.createElement("div"); + toggleButton.id = "translator-toggle"; + toggleButton.textContent = "T"; + toggleButton.style.position = "fixed"; + toggleButton.style.bottom = "10px"; + toggleButton.style.right = "10px"; + toggleButton.style.width = "30px"; + toggleButton.style.height = "30px"; + toggleButton.style.backgroundColor = visible ? "#F44336" : "#4CAF50"; + toggleButton.style.color = "white"; + toggleButton.style.borderRadius = "50%"; + toggleButton.style.display = "flex"; + toggleButton.style.justifyContent = "center"; + toggleButton.style.alignItems = "center"; + toggleButton.style.cursor = "pointer"; + toggleButton.style.fontSize = "16px"; + toggleButton.style.fontWeight = "bold"; + toggleButton.style.zIndex = "10000"; + toggleButton.style.boxShadow = "0 2px 5px rgba(0,0,0,0.2)"; - toggleButton.addEventListener('click', function () { - log('info', 'Toggle button clicked'); + toggleButton.addEventListener("click", function () { + log("info", "Toggle button clicked"); toggleVisibility(); }); try { document.body.appendChild(toggleButton); - log('success', 'Toggle button added to document body'); + log("success", "Toggle button added to document body"); } catch (error) { - log('error', 'Error appending toggle button to body:', error); + log("error", "Error appending toggle button to body:", error); } } function toggleVisibility() { visible = !visible; - container.style.display = visible ? 'flex' : 'none'; - toggleButton.style.backgroundColor = visible ? '#F44336' : '#4CAF50'; - toggleButton.textContent = visible ? 'X' : 'T'; - log('info', 'Toggled visibility', { visible: visible ? 'shown' : 'hidden' }); + container.style.display = visible ? "flex" : "none"; + toggleButton.style.backgroundColor = visible ? "#F44336" : "#4CAF50"; + toggleButton.textContent = visible ? "X" : "T"; + log("info", "Toggled visibility", { + visible: visible ? "shown" : "hidden", + }); } function setupEventListeners() { - log('info', 'Setting up event listeners'); - searchInput.addEventListener('input', function () { - log('info', 'Search input detected'); + log("info", "Setting up event listeners"); + searchInput.addEventListener("input", function () { + log("info", "Search input detected"); searchTranslations(); }); } function setupExternalTextboxListener() { - log('info', 'Setting up external textbox listener'); + log("info", "Setting up external textbox listener"); var observer = new MutationObserver(function (mutations) { var textbox = document.querySelector(CONFIG.textboxSelector); if (textbox && !textbox.dataset.translatorInitialized) { - log('info', 'Found target textbox', { selector: CONFIG.textboxSelector }); - textbox.dataset.translatorInitialized = 'true'; - textbox.addEventListener('input', function () { - log('info', 'External textbox input detected'); + log("info", "Found target textbox", { + selector: CONFIG.textboxSelector, + }); + textbox.dataset.translatorInitialized = "true"; + textbox.addEventListener("input", function () { + log("info", "External textbox input detected"); findMatches(textbox.value); }); - textbox.addEventListener('mouseup', function () { + textbox.addEventListener("mouseup", function () { var selectedText = window.getSelection()?.toString(); if (selectedText) { - log('info', 'Text selection detected', { selectedText: selectedText.substring(0, 20) + (selectedText.length > 20 ? '...' : '') }); + log("info", "Text selection detected", { + selectedText: + selectedText.substring(0, 20) + + (selectedText.length > 20 ? "..." : ""), + }); findMatches(selectedText); } }); @@ -651,28 +769,58 @@ function TranslatorTool() { try { observer.observe(document.body, { childList: true, - subtree: true + subtree: true, }); - log('success', 'MutationObserver started'); + log("success", "MutationObserver started"); } catch (error) { - log('error', 'Error setting up MutationObserver:', error); + log("error", "Error setting up MutationObserver:", error); } } function setupCrowdinEditorListener() { - log('info', 'Setting up Crowdin editor listener'); + log("info", "Setting up Crowdin editor listener"); if (autoSearchIntervalId) { clearInterval(autoSearchIntervalId); } + setTimeout(() => { + checkForEditorContent(); + }, 1000); + autoSearchIntervalId = setInterval(function () { checkForEditorContent(); }, CONFIG.autoSearchInterval); + + const editorObserver = new MutationObserver(function (mutations) { + checkForEditorContent(); + }); + + try { + const editorContainer = document.querySelector( + CONFIG.editorSourceContainer + ); + if (editorContainer) { + editorObserver.observe(editorContainer, { + childList: true, + subtree: true, + characterData: true, + }); + log("success", "Editor observer started"); + } + } catch (error) { + log("error", "Error setting up editor observer:", error); + } } function checkForEditorContent() { - if (!visible || translationData.length === 0) return; + if (!visible || translationData.length === 0) { + log("debug", "Skipping editor content check", { + visible: visible, + hasTranslations: translationData.length > 0, + }); + return; + } try { var content = parseEditorContent(); @@ -680,212 +828,318 @@ function TranslatorTool() { if (content.fullText !== lastSearchedText) { lastSearchedText = content.fullText; - const currentStringLabel = document.getElementById('current-string-label'); + const currentStringLabel = document.getElementById( + "current-string-label" + ); if (currentStringLabel) { - const stringIdText = content.stringId ? ` [ID: ${content.stringId}]` : ''; - currentStringLabel.textContent = 'Current string' + stringIdText + ': ' + content.fullText.substring(0, 100) + (content.fullText.length > 100 ? '...' : ''); + const stringIdText = content.stringId + ? ` [ID: ${content.stringId}]` + : ""; + currentStringLabel.textContent = + "Current string" + + stringIdText + + ": " + + content.fullText.substring(0, 100) + + (content.fullText.length > 100 ? "..." : ""); } - log('debug', 'Editor content changed', { - text: content.fullText, + log("debug", "Editor content changed", { + text: + content.fullText.substring(0, 50) + + (content.fullText.length > 50 ? "..." : ""), terms: content.terms, stringId: content.stringId, - length: content.fullText.length + length: content.fullText.length, }); - findMatches(lastSearchedText); + findMatches(content.fullText); + } else { + log("debug", "Editor content unchanged"); } + } else { + log("debug", "No valid editor content found"); } } catch (error) { - log('error', 'Error in auto-search', error); + log("error", "Error in checkForEditorContent", error); } } function parseEditorContent() { - const editorContainer = document.querySelector(CONFIG.editorSourceContainer); - if (!editorContainer) return null; + const editorContainer = document.querySelector( + CONFIG.editorSourceContainer + ); + if (!editorContainer) { + log("debug", "Editor container not found", { + selector: CONFIG.editorSourceContainer, + }); + return null; + } - const sourceContainer = document.querySelector(CONFIG.sourceStringContainer); - if (!sourceContainer) return null; + const sourceContainer = document.querySelector( + CONFIG.sourceStringContainer + ); + if (!sourceContainer) { + log("debug", "Source container not found", { + selector: CONFIG.sourceStringContainer, + }); + return null; + } const result = { - fullText: '', + fullText: "", terms: [], - stringId: '' + stringId: "", }; try { - const contextLink = document.querySelector('a[href*="view_in_context"]'); - if (contextLink) { - const href = contextLink.getAttribute('href'); - const match = href.match(/#(\d+)/); - if (match && match[1]) { - result.stringId = match[1]; - } - } + // Try to get text content directly first + result.fullText = sourceContainer.textContent.trim(); - const singularContainer = sourceContainer.querySelector('.singular'); - if (singularContainer) { - let nodes = singularContainer.childNodes; - for (let i = 0; i < nodes.length; i++) { - let node = nodes[i]; + // If no text found, try alternative selectors + if (!result.fullText) { + const alternativeSelectors = [ + ".source-string", + ".source-string__content", + '[data-test="source-string"]', + ".singular", + ]; - if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('term_item')) { - const termId = node.getAttribute('data-source-term-id'); - const termText = node.textContent; - const termTitle = node.getAttribute('title'); - - result.terms.push({ - id: termId, - text: termText, - title: termTitle || '' - }); - - result.fullText += termText; - } else if (node.nodeType === Node.TEXT_NODE) { - result.fullText += node.textContent; - } else if (node.nodeType === Node.ELEMENT_NODE) { - result.fullText += node.textContent; - } else { - log('warn', 'Unknown node type', { nodeType: node.nodeType }); + for (const selector of alternativeSelectors) { + const element = sourceContainer.querySelector(selector); + if (element) { + result.fullText = element.textContent.trim(); + if (result.fullText) { + log("debug", "Found text using alternative selector", { + selector, + }); + break; + } } } - } else { - // Fallback, if something went wrong, just get the text content - result.fullText = sourceContainer.textContent; } - result.fullText = result.fullText.trim(); + // Get string ID from URL if possible + const urlMatch = window.location.href.match( + /\/translate\/([^\/]+)\/([^\/]+)\/([^-]+)-(\d+)/ + ); + if (urlMatch && urlMatch[4]) { + result.stringId = urlMatch[4]; + } + + // Fallback to context link if URL parsing fails + if (!result.stringId) { + const contextLink = document.querySelector( + 'a[href*="view_in_context"]' + ); + if (contextLink) { + const href = contextLink.getAttribute("href"); + const match = href.match(/#(\d+)/); + if (match && match[1]) { + result.stringId = match[1]; + } + } + } + + if (result.fullText) { + log("debug", "Successfully parsed editor content", { + length: result.fullText.length, + stringId: result.stringId || "none", + }); + } else { + log("debug", "No text content found in editor"); + } return result; } catch (error) { - log('error', 'Error parsing editor content:', error); + log("error", "Error parsing editor content:", error); return null; } } function refreshTranslationData() { - log('info', 'Refreshing translation data'); - const sourceToggle = document.querySelector('#source-toggle'); - const remoteUrlInput = document.querySelector('#translator-settings-content input[type="text"]'); - const localFileInput = document.querySelector('#translator-settings-content input[type="file"]'); + log("info", "Refreshing translation data"); + const sourceToggle = document.querySelector("#source-toggle"); + const remoteUrlInput = document.querySelector( + '#translator-settings-content input[type="text"]' + ); + const localFileInput = document.querySelector( + '#translator-settings-content input[type="file"]' + ); if (sourceToggle.checked) { if (localFileInput && localFileInput.files.length > 0) { readCSVFile(localFileInput.files[0]); } else { - updateResults('Please select a local CSV file first.'); - log('warn', 'No local file selected'); + updateResults("Please select a local CSV file first."); + log("warn", "No local file selected"); } } else { - const url = remoteUrlInput ? remoteUrlInput.value.trim() : CONFIG.remoteCSVUrl; + const url = remoteUrlInput + ? remoteUrlInput.value.trim() + : CONFIG.remoteCSVUrl; if (url) { fetchRemoteCSV(url); } else { - updateResults('Please enter a valid remote CSV URL.'); - log('warn', 'No remote URL provided'); + updateResults("Please enter a valid remote CSV URL."); + log("warn", "No remote URL provided"); } } } function fetchRemoteCSV(url) { - log('info', 'Fetching remote CSV from', { url: url }); + log("info", "Fetching remote CSV from", { url: url }); GM_xmlhttpRequest({ - method: 'GET', + method: "GET", url: url, onload: function (response) { if (response.status === 200) { parseCSV(response.responseText); currentCSVSource = url; - log('success', 'Successfully loaded remote CSV'); + log("success", "Successfully loaded remote CSV"); } else { - log('error', 'Failed to fetch remote CSV', { status: response.status }); - updateResults('Failed to fetch remote CSV. Please check the URL and try again.'); + log("error", "Failed to fetch remote CSV", { + status: response.status, + }); + updateResults( + "Failed to fetch remote CSV. Please check the URL and try again." + ); } }, onerror: function (error) { - log('error', 'Error fetching remote CSV', error); - updateResults('Error fetching remote CSV. Please check your connection and try again.'); - } + log("error", "Error fetching remote CSV", error); + updateResults( + "Error fetching remote CSV. Please check your connection and try again." + ); + }, }); } function readCSVFile(file) { - log('info', 'Reading CSV file'); + log("info", "Reading CSV file"); var reader = new FileReader(); reader.onload = function (e) { - var content = e.target?.result; - log('info', 'CSV file loaded, content length', { length: content.length }); - parseCSV(content); + var csvContent = e.target?.result; + log("info", "CSV file loaded, content length", { + length: csvContent.length, + }); + parseCSV(csvContent); currentCSVSource = file.name; }; reader.onerror = function (error) { - log('error', 'Error reading file', error); - updateResults('Error reading CSV file. Please try again.'); + log("error", "Error reading file", error); + updateResults("Error reading CSV file. Please try again."); }; reader.readAsText(file); } - function parseCSV(content) { - log('info', 'Parsing CSV content', { lines: content.split('\n').length }); - var lines = content.split('\n'); + function parseCSV(csvContent) { + log("info", "Parsing CSV content", { + lines: csvContent.split("\n").length, + }); + var lines = csvContent.split("\n"); translationData = []; + categoryColors.clear(); + + if (!lines || lines.length < 2) { + log("error", "Invalid CSV structure: insufficient lines"); + updateResults("Error: Invalid CSV file structure"); + return; + } // Skip header for (var i = 1; i < lines.length; i++) { var line = lines[i].trim(); if (line) { - // Handle quoted values that might contain commas - var values = []; - var inQuotes = false; - var currentValue = ''; + try { + // Handle quoted values that might contain commas + var values = []; + var inQuotes = false; + var currentValue = ""; - for (var j = 0; j < line.length; j++) { - var char = line[j]; + for (var j = 0; j < line.length; j++) { + var char = line[j]; - if (char === '"' && (j === 0 || line[j - 1] !== '\\')) { - inQuotes = !inQuotes; - } else if (char === ',' && !inQuotes) { - values.push(currentValue); - currentValue = ''; - } else { - currentValue += char; + if (char === '"' && (j === 0 || line[j - 1] !== "\\")) { + inQuotes = !inQuotes; + } else if (char === "," && !inQuotes) { + values.push(currentValue); + currentValue = ""; + } else { + currentValue += char; + } } - } - // Add the last value - values.push(currentValue); + values.push(currentValue); - // Remove quotes if present - values = values.map(function (v) { - return v.replace(/^"(.*)"$/, '$1'); - }); - - if (values.length >= 2) { - translationData.push({ - source: values[0], - target: values[1], - note: values[2] || '' + // Remove quotes if present and validate + values = values.map(function (v) { + return validateCSVField(v.replace(/^"(.*)"$/, "$1")); }); + + if (values.length >= 2) { + const entry = validateCSVEntry({ + source: values[0], + target: values[1], + note: values[2] || "", + category: values[3] || "", + }); + + if (entry) { + translationData.push(entry); + } + } + } catch (error) { + log("error", "Error parsing CSV line", { line: i, error: error }); + continue; } } } - log('success', 'CSV parsing complete', { + log("success", "CSV parsing complete", { entries: translationData.length, - source: currentCSVSource || 'CSV' + source: currentCSVSource || "CSV", }); - updateResults(`Loaded ${translationData.length} translations from ${currentCSVSource || 'CSV'}`); + updateResults( + `Loaded ${translationData.length} translations from ${sanitizeHTML( + currentCSVSource || "CSV" + )}` + ); - checkForEditorContent(); + const editorContent = parseEditorContent(); + if (editorContent && editorContent.fullText) { + log("debug", "Found editor content after loading CSV", { + text: + editorContent.fullText.substring(0, 50) + + (editorContent.fullText.length > 50 ? "..." : ""), + length: editorContent.fullText.length, + }); + findMatches(editorContent.fullText); + } else { + log("debug", "No editor content found after loading CSV"); + // Try again after a short delay + setTimeout(() => { + const delayedContent = parseEditorContent(); + if (delayedContent && delayedContent.fullText) { + log("debug", "Found editor content after delay", { + text: + delayedContent.fullText.substring(0, 50) + + (delayedContent.fullText.length > 50 ? "..." : ""), + length: delayedContent.fullText.length, + }); + findMatches(delayedContent.fullText); + } else { + log("warn", "Still no editor content found after delay"); + } + }, 1000); + } } function findMatches(text) { if (!text || !translationData.length) return; - log('debug', 'Finding matches', { - text: text.substring(0, 50) + (text.length > 50 ? '...' : ''), - length: text.length + log("debug", "Finding matches", { + text: text.substring(0, 50) + (text.length > 50 ? "..." : ""), + length: text.length, }); var words = text.split(/\s+/); @@ -905,24 +1159,35 @@ function TranslatorTool() { // For short words (2-3 chars), use stricter matching if (cleanWord.length <= 3) { // Only match if it's a complete word match or surrounded by word boundaries - const regex = new RegExp(`\\b${cleanWord}\\b`, 'i'); - if (regex.test(entry.source) && - !matches.some(function (m) { return m.entry.source === entry.source; })) { + const regex = new RegExp(`\\b${cleanWord}\\b`, "i"); + if ( + regex.test(entry.source) && + !matches.some(function (m) { + return m.entry.source === entry.source; + }) + ) { matches.push({ entry: entry, score: 1, - matchedWord: cleanWord + matchedWord: cleanWord, }); } } else { // For longer words, use fuzzy match with higher threshold - const score = similarity(entry.source.toLowerCase(), cleanWord.toLowerCase()); - if (score >= CONFIG.fuzzyThreshold && - !matches.some(function (m) { return m.entry.source === entry.source; })) { + const score = similarity( + entry.source.toLowerCase(), + cleanWord.toLowerCase() + ); + if ( + score >= CONFIG.fuzzyThreshold && + !matches.some(function (m) { + return m.entry.source === entry.source; + }) + ) { matches.push({ entry: entry, score: score, - matchedWord: cleanWord + matchedWord: cleanWord, }); } } @@ -936,20 +1201,20 @@ function TranslatorTool() { return b.score - a.score; }); - log('success', 'Found matches', { count: matches.length }); + log("success", "Found matches", { count: matches.length }); displayFuzzyMatches(matches); } function searchTranslations() { var query = searchInput.value.toLowerCase().trim(); if (!query || query.length <= 1) { - updateResults(''); - lastSearchedText = ''; + updateResults(""); + lastSearchedText = ""; checkForEditorContent(); return; } - log('info', 'Searching translations for', { query: query }); + log("info", "Searching translations for", { query: query }); var matches = []; // Find matches @@ -959,25 +1224,34 @@ function TranslatorTool() { // For short queries (2-3 chars), use stricter matching if (query.length <= 3) { // Only match if it's a complete word match or surrounded by word boundaries - const regex = new RegExp(`\\b${query}\\b`, 'i'); - if (regex.test(entry.source) || regex.test(entry.target) || (entry.note && regex.test(entry.note))) { + const regex = new RegExp(`\\b${query}\\b`, "i"); + if ( + regex.test(entry.source) || + regex.test(entry.target) || + (entry.note && regex.test(entry.note)) + ) { score = 1; } } else { // For longer queries, use fuzzy match with context const sourceScore = similarity(entry.source.toLowerCase(), query); const targetScore = similarity(entry.target.toLowerCase(), query); - const noteScore = entry.note ? similarity(entry.note.toLowerCase(), query) : 0; + const noteScore = entry.note + ? similarity(entry.note.toLowerCase(), query) + : 0; // Use the highest score score = Math.max(sourceScore, targetScore, noteScore); } // Score is good enough - if ((query.length <= 3 && score > 0) || (query.length > 3 && score >= CONFIG.fuzzyThreshold)) { + if ( + (query.length <= 3 && score > 0) || + (query.length > 3 && score >= CONFIG.fuzzyThreshold) + ) { matches.push({ entry: entry, - score: score + score: score, }); } }); @@ -993,62 +1267,69 @@ function TranslatorTool() { // Limit results for performance matches = matches.slice(0, 50); - log('success', 'Search found matches', { count: matches.length }); + log("success", "Search found matches", { count: matches.length }); displayFuzzyMatches(matches); } function displayFuzzyMatches(matches) { if (matches.length === 0) { - updateResults('
No matches found
'); + updateResults( + '
No matches found
' + ); return; } // Wrapper for table with flex layout - var wrapper = document.createElement('div'); - wrapper.style.display = 'flex'; - wrapper.style.flexDirection = 'column'; - wrapper.style.height = '100%'; - wrapper.style.overflow = 'hidden'; - wrapper.style.position = 'relative'; + var wrapper = document.createElement("div"); + wrapper.style.display = "flex"; + wrapper.style.flexDirection = "column"; + wrapper.style.height = "100%"; + wrapper.style.overflow = "hidden"; + wrapper.style.position = "relative"; // Table container with scrolling - var tableContainer = document.createElement('div'); - tableContainer.style.flexGrow = '1'; - tableContainer.style.overflow = 'auto'; - tableContainer.style.position = 'relative'; - tableContainer.style.minHeight = '0'; + var tableContainer = document.createElement("div"); + tableContainer.style.flexGrow = "1"; + tableContainer.style.overflow = "auto"; + tableContainer.style.position = "relative"; + tableContainer.style.minHeight = "0"; - var table = document.createElement('table'); - table.style.width = '100%'; - table.style.borderCollapse = 'collapse'; - table.style.tableLayout = 'fixed'; + var table = document.createElement("table"); + table.style.width = "100%"; + table.style.borderCollapse = "collapse"; + table.style.tableLayout = "fixed"; // Header - var thead = document.createElement('thead'); - thead.style.position = 'sticky'; - thead.style.top = '0'; - thead.style.backgroundColor = '#f8f9fa'; - thead.style.zIndex = '1'; + var thead = document.createElement("thead"); + thead.style.position = "sticky"; + thead.style.top = "0"; + thead.style.backgroundColor = "#f8f9fa"; + thead.style.zIndex = "1"; - var headerRow = document.createElement('tr'); + var headerRow = document.createElement("tr"); var columns = [ - { name: 'Source', width: '30%' }, - { name: 'Target', width: '30%' }, - { name: 'Note', width: '20%' } + { name: "Source", width: "35%" }, + { name: "Target", width: "35%" }, + { name: "Note", width: "30%" }, ]; if (matches[0].matchedWord) { - columns.push({ name: 'Match', width: '20%' }); + columns = [ + { name: "Source", width: "30%" }, + { name: "Target", width: "30%" }, + { name: "Note", width: "25%" }, + { name: "Match", width: "15%" }, + ]; } - columns.forEach(col => { - var th = document.createElement('th'); + columns.forEach((col) => { + var th = document.createElement("th"); th.textContent = col.name; - th.style.textAlign = 'left'; - th.style.padding = '8px'; - th.style.border = '1px solid #e0e0e0'; + th.style.textAlign = "left"; + th.style.padding = "8px"; + th.style.border = "1px solid #e0e0e0"; th.style.width = col.width; - th.style.backgroundColor = '#f8f9fa'; + th.style.backgroundColor = "#f8f9fa"; headerRow.appendChild(th); }); @@ -1056,54 +1337,90 @@ function TranslatorTool() { table.appendChild(thead); // Create table body - var tbody = document.createElement('tbody'); + var tbody = document.createElement("tbody"); matches.forEach(function (match) { - var row = document.createElement('tr'); + var row = document.createElement("tr"); const scorePercentage = Math.round(match.score * 100); const bgColor = `rgba(26, 115, 232, ${match.score * 0.1})`; row.style.backgroundColor = bgColor; - function createCopyableCell(text, isTarget = false) { - var cell = document.createElement('td'); - cell.textContent = text; - cell.style.padding = '8px'; - cell.style.border = '1px solid #e0e0e0'; - cell.style.wordBreak = 'break-word'; - cell.style.whiteSpace = 'normal'; - cell.style.verticalAlign = 'top'; - cell.style.cursor = 'pointer'; - cell.style.userSelect = 'text'; - cell.style.position = 'relative'; - cell.title = 'Click to copy'; + function createCopyableCell( + text, + isSource = false, + isTarget = false, + category = "" + ) { + var cell = document.createElement("td"); + + // Create container for content + var container = document.createElement("div"); + container.style.display = "flex"; + container.style.flexDirection = "column"; + container.style.gap = "4px"; + + // Add category chip first if in note column + if (!isSource && !isTarget && category) { + var chip = document.createElement("div"); + const color = generateColorForCategory(category); + chip.style.backgroundColor = color; + chip.style.color = "white"; + chip.style.padding = "1px 6px"; + chip.style.borderRadius = "10px"; + chip.style.fontSize = "10px"; + chip.style.whiteSpace = "nowrap"; + chip.style.fontWeight = "500"; + chip.style.opacity = "0.9"; + chip.style.width = "fit-content"; + chip.textContent = category; + container.appendChild(chip); + } + + // Add main text + var mainText = document.createElement("div"); + mainText.textContent = text || ""; + mainText.style.flex = "1"; + container.appendChild(mainText); + + cell.appendChild(container); + cell.style.padding = "8px"; + cell.style.border = "1px solid #e0e0e0"; + cell.style.wordBreak = "break-word"; + cell.style.whiteSpace = "normal"; + cell.style.verticalAlign = "top"; + cell.style.cursor = "pointer"; + cell.style.userSelect = "text"; + cell.style.position = "relative"; + + cell.title = "Click to copy"; // Hover effect - cell.addEventListener('mouseover', function() { - this.style.backgroundColor = 'rgba(26, 115, 232, 0.1)'; + cell.addEventListener("mouseover", function () { + this.style.backgroundColor = "rgba(26, 115, 232, 0.1)"; }); - cell.addEventListener('mouseout', function() { - this.style.backgroundColor = 'transparent'; + cell.addEventListener("mouseout", function () { + this.style.backgroundColor = "transparent"; }); - cell.addEventListener('click', function(e) { + cell.addEventListener("click", function (e) { if (window.getSelection().toString()) { return; } navigator.clipboard.writeText(text).then(() => { - var tooltip = document.createElement('div'); - tooltip.textContent = 'Copied!'; - tooltip.style.position = 'absolute'; - tooltip.style.backgroundColor = '#333'; - tooltip.style.color = 'white'; - tooltip.style.padding = '4px 8px'; - tooltip.style.borderRadius = '4px'; - tooltip.style.fontSize = '12px'; - tooltip.style.zIndex = '1000'; - tooltip.style.top = '0'; - tooltip.style.left = '50%'; - tooltip.style.transform = 'translate(-50%, -100%)'; - + var tooltip = document.createElement("div"); + tooltip.textContent = "Copied!"; + tooltip.style.position = "absolute"; + tooltip.style.backgroundColor = "#333"; + tooltip.style.color = "white"; + tooltip.style.padding = "4px 8px"; + tooltip.style.borderRadius = "4px"; + tooltip.style.fontSize = "12px"; + tooltip.style.zIndex = "1000"; + tooltip.style.top = "0"; + tooltip.style.left = "50%"; + tooltip.style.transform = "translate(-50%, -100%)"; + cell.appendChild(tooltip); setTimeout(() => { @@ -1115,12 +1432,16 @@ function TranslatorTool() { return cell; } - row.appendChild(createCopyableCell(match.entry.source)); - row.appendChild(createCopyableCell(match.entry.target, true)); - row.appendChild(createCopyableCell(match.entry.note)); + row.appendChild(createCopyableCell(match.entry.source, true, false)); + row.appendChild(createCopyableCell(match.entry.target, false, true)); + row.appendChild( + createCopyableCell(match.entry.note, false, false, match.entry.category) + ); if (match.matchedWord) { - row.appendChild(createCopyableCell(`${match.matchedWord} (${scorePercentage}%)`)); + row.appendChild( + createCopyableCell(`${match.matchedWord} (${scorePercentage}%)`) + ); } tbody.appendChild(row); @@ -1130,99 +1451,136 @@ function TranslatorTool() { tableContainer.appendChild(table); wrapper.appendChild(tableContainer); - resultsDiv.innerHTML = ''; + resultsDiv.innerHTML = ""; resultsDiv.appendChild(wrapper); - log('success', 'Updated results panel with table layout'); + log("success", "Updated results panel with table layout"); } function updateResults(content) { resultsDiv.innerHTML = content; - log('success', 'Updated results panel'); + log("success", "Updated results panel"); } function checkForUpdates() { - log('info', 'Checking for updates'); - updateLink.textContent = 'Checking for updates...'; - updateLink.style.color = '#666'; + log("info", "Checking for updates"); + updateLink.textContent = "Checking for updates..."; + updateLink.style.color = "#666"; // Check version first GM_xmlhttpRequest({ - method: 'GET', + method: "GET", url: CONFIG.updateCheckUrl, onload: function (response) { if (response.status === 200) { try { const versionInfo = JSON.parse(response.responseText); const latestVersion = versionInfo.latest; - const needsVersionUpdate = latestVersion !== CONFIG.metadata.version; + const needsVersionUpdate = + latestVersion !== CONFIG.metadata.version; - log('info', 'Retrieved version info', { + log("info", "Retrieved version info", { current: CONFIG.metadata.version, - latest: latestVersion + latest: latestVersion, }); // Check CSV data - const sourceToggle = document.querySelector('#source-toggle'); - const remoteUrlInput = document.querySelector('#translator-settings-content input[type="text"]'); - const csvUrl = (!sourceToggle || !sourceToggle.checked) ? - (remoteUrlInput && remoteUrlInput.value.trim() ? remoteUrlInput.value.trim() : CONFIG.remoteCSVUrl) : - null; + const sourceToggle = document.querySelector("#source-toggle"); + const remoteUrlInput = document.querySelector( + '#translator-settings-content input[type="text"]' + ); + const csvUrl = + !sourceToggle || !sourceToggle.checked + ? remoteUrlInput && remoteUrlInput.value.trim() + ? remoteUrlInput.value.trim() + : CONFIG.remoteCSVUrl + : null; if (csvUrl) { - log('info', 'Checking CSV updates from', { url: csvUrl }); + log("info", "Checking CSV updates from", { url: csvUrl }); GM_xmlhttpRequest({ - method: 'GET', + method: "GET", url: csvUrl, onload: function (csvResponse) { if (csvResponse.status === 200) { try { const newData = parseCSVToArray(csvResponse.responseText); - const needsDataUpdate = JSON.stringify(translationData) !== JSON.stringify(newData); - log('info', 'CSV check complete', { + const needsDataUpdate = + JSON.stringify(translationData) !== + JSON.stringify(newData); + log("info", "CSV check complete", { needsUpdate: needsDataUpdate, currentEntries: translationData.length, - newEntries: newData.length + newEntries: newData.length, }); - updateUIAfterChecks(needsVersionUpdate, needsDataUpdate, latestVersion, newData); + updateUIAfterChecks( + needsVersionUpdate, + needsDataUpdate, + latestVersion, + newData + ); } catch (csvError) { - log('error', 'Error parsing CSV data', csvError); - updateUIAfterChecks(needsVersionUpdate, false, latestVersion, null); + log("error", "Error parsing CSV data", csvError); + updateUIAfterChecks( + needsVersionUpdate, + false, + latestVersion, + null + ); } } else { - log('error', 'Failed to fetch CSV', { status: csvResponse.status }); - updateUIAfterChecks(needsVersionUpdate, false, latestVersion, null); + log("error", "Failed to fetch CSV", { + status: csvResponse.status, + }); + updateUIAfterChecks( + needsVersionUpdate, + false, + latestVersion, + null + ); } }, onerror: function (csvError) { - log('error', 'Error fetching CSV', csvError); - updateUIAfterChecks(needsVersionUpdate, false, latestVersion, null); - } + log("error", "Error fetching CSV", csvError); + updateUIAfterChecks( + needsVersionUpdate, + false, + latestVersion, + null + ); + }, }); } else { - log('info', 'Skipping CSV check - using local file'); - updateUIAfterChecks(needsVersionUpdate, false, latestVersion, null); + log("info", "Skipping CSV check - using local file"); + updateUIAfterChecks( + needsVersionUpdate, + false, + latestVersion, + null + ); } } catch (e) { - log('error', 'Error parsing version info', e); - updateLink.textContent = 'Error checking for updates'; - updateLink.style.color = '#F44336'; + log("error", "Error parsing version info", e); + updateLink.textContent = "Error checking for updates"; + updateLink.style.color = "#F44336"; } } else { - log('error', 'Failed to check for updates', { status: response.status }); - updateLink.textContent = 'Failed to check updates'; - updateLink.style.color = '#F44336'; + log("error", "Failed to check for updates", { + status: response.status, + }); + updateLink.textContent = "Failed to check updates"; + updateLink.style.color = "#F44336"; } }, onerror: function (error) { - log('error', 'Error checking for updates', error); - updateLink.textContent = 'Error checking for updates'; - updateLink.style.color = '#F44336'; - } + log("error", "Error checking for updates", error); + updateLink.textContent = "Error checking for updates"; + updateLink.style.color = "#F44336"; + }, }); } function parseCSVToArray(csvContent) { - const lines = csvContent.split('\n'); + const lines = csvContent.split("\n"); const result = []; // Skip header @@ -1231,29 +1589,30 @@ function TranslatorTool() { if (line) { let values = []; let inQuotes = false; - let currentValue = ''; + let currentValue = ""; for (let j = 0; j < line.length; j++) { const char = line[j]; - if (char === '"' && (j === 0 || line[j - 1] !== '\\')) { + if (char === '"' && (j === 0 || line[j - 1] !== "\\")) { inQuotes = !inQuotes; - } else if (char === ',' && !inQuotes) { + } else if (char === "," && !inQuotes) { values.push(currentValue); - currentValue = ''; + currentValue = ""; } else { currentValue += char; } } values.push(currentValue); - values = values.map(v => v.replace(/^"(.*)"$/, '$1')); + values = values.map((v) => v.replace(/^"(.*)"$/, "$1")); if (values.length >= 2) { result.push({ source: values[0], target: values[1], - note: values[2] || '' + note: values[2] || "", + category: values[3] || "", }); } } @@ -1261,65 +1620,74 @@ function TranslatorTool() { return result; } - function updateUIAfterChecks(needsVersionUpdate, needsDataUpdate, newVersion, newData) { + function updateUIAfterChecks( + needsVersionUpdate, + needsDataUpdate, + newVersion, + newData + ) { if (needsVersionUpdate && needsDataUpdate) { updateLink.textContent = `Update available! v${newVersion} + new translations`; - updateLink.style.color = '#F44336'; + updateLink.style.color = "#F44336"; showUpdateNotification(true, true); } else if (needsVersionUpdate) { updateLink.textContent = `Update available! v${newVersion}`; - updateLink.style.color = '#F44336'; + updateLink.style.color = "#F44336"; showUpdateNotification(true, false); } else if (needsDataUpdate) { - updateLink.textContent = 'New translations available!'; - updateLink.style.color = '#F44336'; + updateLink.textContent = "New translations available!"; + updateLink.style.color = "#F44336"; showUpdateNotification(false, true); if (newData) { translationData = newData; - log('success', 'Updated translation data', { entries: newData.length }); + log("success", "Updated translation data", { entries: newData.length }); updateResults(`Updated with ${newData.length} translations`); setTimeout(() => { - updateLink.textContent = 'Translations updated ✓'; - updateLink.style.color = '#4CAF50'; + updateLink.textContent = "Translations updated ✓"; + updateLink.style.color = "#4CAF50"; + // Trigger content check after updating data setTimeout(() => { - updateLink.textContent = 'Check for updates'; - updateLink.style.color = '#1a73e8'; + checkForEditorContent(); + }, 500); + setTimeout(() => { + updateLink.textContent = "Check for updates"; + updateLink.style.color = "#1a73e8"; }, 2000); }, 1000); } } else { - log('info', 'No updates available'); - updateLink.textContent = 'No updates available ✓'; + log("info", "No updates available"); + updateLink.textContent = "No updates available ✓"; setTimeout(() => { - updateLink.textContent = 'Check for updates'; - updateLink.style.color = '#1a73e8'; + updateLink.textContent = "Check for updates"; + updateLink.style.color = "#1a73e8"; }, 3000); } } function showUpdateNotification(hasVersionUpdate, hasDataUpdate) { - log('info', 'Showing update notification'); - const notification = document.createElement('div'); - notification.style.position = 'fixed'; - notification.style.top = '10px'; - notification.style.right = '10px'; - notification.style.background = '#4CAF50'; - notification.style.color = 'white'; - notification.style.padding = '16px'; - notification.style.borderRadius = '8px'; - notification.style.zIndex = '10001'; - notification.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; - notification.style.maxWidth = '300px'; + log("info", "Showing update notification"); + const notification = document.createElement("div"); + notification.style.position = "fixed"; + notification.style.top = "10px"; + notification.style.right = "10px"; + notification.style.background = "#4CAF50"; + notification.style.color = "white"; + notification.style.padding = "16px"; + notification.style.borderRadius = "8px"; + notification.style.zIndex = "10001"; + notification.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)"; + notification.style.maxWidth = "300px"; - let message = ''; + let message = ""; if (hasVersionUpdate && hasDataUpdate) { - message = 'New version and translations available!'; + message = "New version and translations available!"; } else if (hasVersionUpdate) { - message = 'New version available!'; + message = "New version available!"; } else if (hasDataUpdate) { - message = 'New translations available!'; + message = "New translations available!"; } notification.innerHTML = ` @@ -1328,21 +1696,29 @@ function TranslatorTool() {
- ${hasVersionUpdate ? `` : ''} + ${ + hasVersionUpdate + ? `` + : "" + }
`; document.body.appendChild(notification); - document.getElementById('csv-translator-dismiss').addEventListener('click', function () { - document.body.removeChild(notification); - }); - - if (hasVersionUpdate) { - document.getElementById('csv-translator-update').addEventListener('click', function () { - window.open(CONFIG.metadata.repository, '_blank'); + document + .getElementById("csv-translator-dismiss") + .addEventListener("click", function () { document.body.removeChild(notification); }); + + if (hasVersionUpdate) { + document + .getElementById("csv-translator-update") + .addEventListener("click", function () { + window.open(CONFIG.metadata.repository, "_blank"); + document.body.removeChild(notification); + }); } setTimeout(() => { @@ -1355,25 +1731,28 @@ function TranslatorTool() { init(); } -document.addEventListener('DOMContentLoaded', function () { - log('info', 'DOMContentLoaded event fired'); +document.addEventListener("DOMContentLoaded", function () { + log("info", "DOMContentLoaded event fired"); try { new TranslatorTool(); } catch (error) { - log('error', 'Error initializing tool:', error); + log("error", "Error initializing tool:", error); } }); // Fallback initialization -if (document.readyState === 'complete' || document.readyState === 'interactive') { - log('info', 'Document already loaded, initializing immediately'); +if ( + document.readyState === "complete" || + document.readyState === "interactive" +) { + log("info", "Document already loaded, initializing immediately"); setTimeout(function () { try { new TranslatorTool(); } catch (error) { - log('error', 'Error initializing tool (fallback):', error); + log("error", "Error initializing tool (fallback):", error); } }, 1000); } -log('info', 'Script loaded. Current document.readyState:', document.readyState); +log("info", "Script loaded. Current document.readyState:", document.readyState);