Files
Crowdin-Localization-Tools/script.user.js

1948 lines
62 KiB
JavaScript
Raw Normal View History

2025-02-16 03:37:19 +07:00
// ==UserScript==
// @name Crowdin Localization Tools
// @namespace https://yuzu.kirameki.cafe/
2025-02-19 18:15:01 +07:00
// @version 1.1.2
2025-02-16 03:54:23 +07:00
// @description A tool for translating Crowdin projects using a CSV file
2025-02-16 03:37:19 +07:00
// @author Yuzu (YuzuZensai)
// @match https://crowdin.com/editor/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
2025-02-16 04:07:23 +07:00
// @updateURL https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/refs/heads/main/script.user.js
// @downloadURL https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/refs/heads/main/script.user.js
2025-02-16 03:59:19 +07:00
// @connect github.com
// @connect raw.githubusercontent.com
2025-02-16 03:37:19 +07:00
// ==/UserScript==
// Global configuration
const CONFIG = {
defaultVisible: true,
defaultPosition: { right: "20px", bottom: "20px" },
2025-02-16 03:37:19 +07:00
windowDimensions: {
width: "600px",
height: "600px",
2025-02-16 03:37:19 +07:00
},
debug: true,
// Update check
updateCheckUrl:
"https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/main/data/version.json",
2025-02-17 22:28:58 +07:00
autoUpdateInterval: 15 * 60 * 1000, // 15 minutes
2025-02-16 03:37:19 +07:00
// Remote CSV
remoteCSVUrl:
"https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/main/data/data.csv",
2025-02-16 03:54:23 +07:00
allowLocalOverride: true,
allowUrlOverride: true,
2025-02-16 03:37:19 +07:00
// 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",
2025-02-16 03:37:19 +07:00
autoSearchInterval: 1000,
2025-02-16 03:37:19 +07:00
fuzzyThreshold: 0.7,
metadata: {
2025-02-19 18:15:01 +07:00
version: "1.1.2",
repository: "https://github.com/YuzuZensai/Crowdin-Localization-Tools",
authorGithub: "https://github.com/YuzuZensai",
},
2025-02-16 03:37:19 +07:00
};
function log(type, message, data = null) {
if (!CONFIG.debug) return;
2025-02-16 03:54:23 +07:00
2025-02-16 03:37:19 +07:00
const timestamp = new Date().toLocaleTimeString();
const prefix = `[Crowdin Localization Tools][${timestamp}]`;
2025-02-16 03:54:23 +07:00
2025-02-16 03:37:19 +07:00
switch (type.toLowerCase()) {
case "info":
console.log(`${prefix} ${message}`, data || "");
2025-02-16 03:37:19 +07:00
break;
case "warn":
console.warn(`${prefix} ⚠️ ${message}`, data || "");
2025-02-16 03:37:19 +07:00
break;
case "error":
console.error(`${prefix}${message}`, data || "");
2025-02-16 03:37:19 +07:00
break;
case "success":
console.log(`${prefix}${message}`, data || "");
2025-02-16 03:37:19 +07:00
break;
case "debug":
console.debug(`${prefix} 🔍 ${message}`, data || "");
2025-02-16 03:37:19 +07:00
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(/&lt;\s*script[^&]*&gt;.*?&lt;\/\s*script\s*&gt;/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),
};
}
2025-02-16 03:37:19 +07:00
function levenshteinDistance(a, b) {
const matrix = [];
for (let i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
2025-02-16 03:54:23 +07:00
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
2025-02-16 03:37:19 +07:00
} else {
matrix[i][j] = Math.min(
2025-02-16 03:54:23 +07:00
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1 // deletion
2025-02-16 03:37:19 +07:00
);
}
}
}
return matrix[b.length][a.length];
}
function similarity(s1, s2) {
if (s1.length === 0 || s2.length === 0) return 0;
const longerLength = Math.max(s1.length, s2.length);
return (longerLength - levenshteinDistance(s1, s2)) / longerLength;
}
function TranslatorTool() {
var container;
var translationData = [];
var resultsDiv;
var searchInput;
var isDragging = false;
var dragOffsetX = 0;
var dragOffsetY = 0;
var toggleButton;
var visible = CONFIG.defaultVisible;
var lastSearchedText = "";
2025-02-16 03:37:19 +07:00
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 = {
2025-02-17 22:28:58 +07:00
UI: "#c6dbe1",
"Unity / 3D": "#3d3d3d",
"Trust Rank": "#e6cff2",
"Instance Type": "#d4edbc",
"Avatar Performance Rank": "#ffc8aa",
"VRChat Specific": "#bfe1f6",
2025-02-17 22:30:28 +07:00
Common: "#e6e6e6",
};
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;
}
2025-02-17 22:28:58 +07:00
function isColorBright(color) {
// Convert hex to RGB
let r, g, b;
2025-02-17 22:30:28 +07:00
if (color.startsWith("#")) {
const hex = color.replace("#", "");
2025-02-17 22:28:58 +07:00
r = parseInt(hex.substr(0, 2), 16);
g = parseInt(hex.substr(2, 2), 16);
b = parseInt(hex.substr(4, 2), 16);
2025-02-17 22:30:28 +07:00
} else if (color.startsWith("hsl")) {
2025-02-17 22:28:58 +07:00
// Convert HSL to RGB
const matches = color.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/);
if (matches) {
const h = parseInt(matches[1]) / 360;
const s = parseInt(matches[2]) / 100;
const l = parseInt(matches[3]) / 100;
if (s === 0) {
r = g = b = l * 255;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
2025-02-17 22:30:28 +07:00
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
2025-02-17 22:28:58 +07:00
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
2025-02-17 22:30:28 +07:00
r = hue2rgb(p, q, h + 1 / 3) * 255;
2025-02-17 22:28:58 +07:00
g = hue2rgb(p, q, h) * 255;
2025-02-17 22:30:28 +07:00
b = hue2rgb(p, q, h - 1 / 3) * 255;
2025-02-17 22:28:58 +07:00
}
} else {
r = g = b = 128; // Fallback to gray if parsing fails
}
} else {
r = g = b = 128; // Fallback to gray
}
// Convert RGB values to 0-1 range
const rr = r / 255;
const gg = g / 255;
const bb = b / 255;
// Calculate relative luminance (WCAG 2.0)
2025-02-17 22:30:28 +07:00
const luminance =
0.2126 *
(rr <= 0.03928 ? rr / 12.92 : Math.pow((rr + 0.055) / 1.055, 2.4)) +
0.7152 *
(gg <= 0.03928 ? gg / 12.92 : Math.pow((gg + 0.055) / 1.055, 2.4)) +
0.0722 *
(bb <= 0.03928 ? bb / 12.92 : Math.pow((bb + 0.055) / 1.055, 2.4));
2025-02-17 22:28:58 +07:00
// Calculate YIQ
2025-02-17 22:30:28 +07:00
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
2025-02-17 22:28:58 +07:00
// Combine both methods
// For pastel colors (high luminance but moderate YIQ)
if (luminance > 0.7) {
return true; // Definitely bright
} else if (luminance > 0.5 && yiq > 128) {
return true; // Moderately bright and good YIQ
}
return false;
}
function createCategoryChip(category) {
if (!category) return "";
const color = generateColorForCategory(category);
2025-02-17 22:28:58 +07:00
const textColor = isColorBright(color) ? "#000000" : "#ffffff";
return `<span style="
display: inline-block;
padding: 2px 8px;
margin: 2px;
border-radius: 12px;
font-size: 11px;
background-color: ${color};
2025-02-17 22:28:58 +07:00
color: ${textColor};
white-space: nowrap;
">${category}</span>`;
}
2025-02-16 03:37:19 +07:00
function init() {
log("info", "Initializing translator tool");
2025-02-16 03:37:19 +07:00
createUI();
createToggleButton();
setupEventListeners();
setupExternalTextboxListener();
setupCrowdinEditorListener();
const sourceToggle = document.querySelector("#source-toggle");
2025-02-16 03:37:19 +07:00
if (!sourceToggle || !sourceToggle.checked) {
fetchRemoteCSV(CONFIG.remoteCSVUrl);
}
2025-02-16 03:54:23 +07:00
2025-02-17 22:28:58 +07:00
setInterval(() => {
log("info", "Running automatic update check");
checkForUpdates();
}, CONFIG.autoUpdateInterval);
setTimeout(() => {
2025-02-17 22:28:58 +07:00
checkForEditorContent(true);
}, 2000);
log(
"success",
"Crowdin Localization Tools version " +
CONFIG.metadata.version +
" by " +
CONFIG.metadata.authorGithub +
" initialized successfully"
);
2025-02-16 03:37:19 +07:00
}
function createUI() {
log("info", "Creating UI elements");
2025-02-16 03:37:19 +07:00
// Container
container = document.createElement("div");
container.id = "translator-tool";
container.style.position = "fixed";
2025-02-16 03:37:19 +07:00
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';
2025-02-16 03:37:19 +07:00
// 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 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");
2025-02-16 03:37:19 +07:00
toggleVisibility();
});
header.appendChild(title);
header.appendChild(closeButton);
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";
2025-02-16 03:37:19 +07:00
var mainTab = createTab("Translator", true);
var settingsTab = createTab("Settings", false);
2025-02-16 03:37:19 +07:00
tabMenu.appendChild(mainTab);
tabMenu.appendChild(settingsTab);
container.appendChild(tabMenu);
// Content containers
var mainContent = createMainContent();
var settingsContent = createSettingsContent();
container.appendChild(mainContent);
container.appendChild(settingsContent);
// Footer
var footer = createFooter();
container.appendChild(footer);
// Inject
try {
document.body.appendChild(container);
log("success", "Main container added to document body");
2025-02-16 03:37:19 +07:00
} catch (error) {
log("error", "Error appending container to body:", error);
2025-02-16 03:37:19 +07:00
}
setupDraggable(header);
log("success", "UI elements created successfully");
2025-02-16 03:37:19 +07:00
}
function createTab(text, isActive) {
var tab = document.createElement("button");
2025-02-16 03:37:19 +07:00
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.addEventListener("click", function () {
2025-02-16 03:37:19 +07:00
switchTab(text.toLowerCase());
});
return tab;
}
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";
2025-02-16 03:37:19 +07:00
// Search container
var searchContainer = document.createElement("div");
searchContainer.style.position = "relative";
searchContainer.style.marginBottom = "16px";
searchContainer.style.flexShrink = "0";
2025-02-16 03:37:19 +07:00
2025-02-16 08:52:27 +07:00
// 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";
2025-02-16 08:52:27 +07:00
searchContainer.appendChild(currentStringLabel);
2025-02-16 03:37:19 +07:00
// 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.addEventListener("mouseover", function () {
2025-02-16 03:37:19 +07:00
if (document.activeElement !== this) {
this.style.borderColor = "#ccc";
2025-02-16 03:37:19 +07:00
}
});
searchInput.addEventListener("mouseout", function () {
2025-02-16 03:37:19 +07:00
if (document.activeElement !== this) {
this.style.borderColor = "#e0e0e0";
2025-02-16 03:37:19 +07:00
}
});
searchInput.addEventListener("focus", function () {
this.style.borderColor = "#1a73e8";
this.style.backgroundColor = "#fff";
2025-02-16 03:37:19 +07:00
});
searchInput.addEventListener("blur", function () {
this.style.borderColor = "#e0e0e0";
this.style.backgroundColor = "#f8f9fa";
2025-02-16 03:37:19 +07:00
});
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";
2025-02-16 03:37:19 +07:00
content.appendChild(resultsDiv);
return content;
}
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";
2025-02-16 03:37:19 +07:00
// CSV Source Settings
var csvSourceSection = document.createElement("div");
csvSourceSection.style.marginBottom = "20px";
2025-02-16 03:37:19 +07:00
var csvSourceTitle = document.createElement("h4");
csvSourceTitle.textContent = "CSV Source Settings";
csvSourceTitle.style.margin = "0 0 12px 0";
csvSourceTitle.style.color = "#333";
2025-02-16 03:37:19 +07:00
// 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 sourceToggle = document.createElement("input");
sourceToggle.type = "checkbox";
sourceToggle.id = "source-toggle";
sourceToggle.style.margin = "0";
2025-02-16 03:37:19 +07:00
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";
2025-02-16 03:37:19 +07:00
sourceToggleContainer.appendChild(sourceToggle);
sourceToggleContainer.appendChild(sourceToggleLabel);
// Remote URL Input Container
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 remoteUrlInput = document.createElement("input");
remoteUrlInput.type = "text";
2025-02-16 03:37:19 +07:00
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";
2025-02-16 03:37:19 +07:00
urlContainer.appendChild(urlLabel);
urlContainer.appendChild(remoteUrlInput);
// Local File Input Container
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 localFileInput = document.createElement("input");
localFileInput.type = "file";
localFileInput.accept = ".csv";
localFileInput.style.width = "100%";
localFileInput.style.fontSize = "14px";
localFileInput.addEventListener("change", function () {
2025-02-16 03:37:19 +07:00
if (this.files.length > 0) {
readCSVFile(this.files[0]);
}
});
fileContainer.appendChild(fileLabel);
fileContainer.appendChild(localFileInput);
sourceToggle.addEventListener("change", function () {
urlContainer.style.display = this.checked ? "none" : "block";
fileContainer.style.display = this.checked ? "block" : "none";
2025-02-16 03:37:19 +07:00
if (this.checked) {
remoteUrlInput.value = "";
2025-02-16 03:37:19 +07:00
} else {
localFileInput.value = "";
2025-02-16 03:37:19 +07:00
}
});
// Global style for refresh button
2025-02-16 03:37:19 +07:00
GM_addStyle(`
.csv-translator-refresh-btn {
padding: 8px 16px;
background-color: #1a73e8;
color: #ffffff !important;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
width: fit-content;
transition: all 0.2s ease;
}
.csv-translator-refresh-btn:hover {
background-color: #1557b0;
color: #ffffff !important;
}
`);
// Refresh Button
var refreshButton = document.createElement("button");
refreshButton.textContent = "Refresh Data";
refreshButton.className = "csv-translator-refresh-btn";
2025-02-16 03:37:19 +07:00
refreshButton.addEventListener("click", function () {
2025-02-16 03:37:19 +07:00
refreshTranslationData();
});
csvSourceSection.appendChild(csvSourceTitle);
csvSourceSection.appendChild(sourceToggleContainer);
csvSourceSection.appendChild(urlContainer);
csvSourceSection.appendChild(fileContainer);
csvSourceSection.appendChild(refreshButton);
content.appendChild(csvSourceSection);
return content;
}
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 credits = document.createElement("div");
var authorLink = document.createElement("a");
2025-02-16 03:37:19 +07:00
authorLink.href = CONFIG.metadata.authorGithub;
authorLink.textContent = "YuzuZensai";
authorLink.style.color = "#1a73e8";
authorLink.style.textDecoration = "none";
authorLink.style.cursor = "pointer";
authorLink.target = "_blank";
2025-02-16 03:54:23 +07:00
credits.appendChild(document.createTextNode("Made with 💖 by "));
2025-02-16 03:37:19 +07:00
credits.appendChild(authorLink);
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) {
2025-02-16 03:37:19 +07:00
e.preventDefault();
checkForUpdates();
});
footer.appendChild(credits);
footer.appendChild(updateLink);
return footer;
}
function switchTab(tabName) {
var mainContent = document.getElementById("translator-main-content");
var settingsContent = document.getElementById(
"translator-settings-content"
);
var tabs = container.querySelectorAll("button");
2025-02-16 03:37:19 +07:00
tabs.forEach((tab) => {
2025-02-16 03:37:19 +07:00
if (tab.textContent.toLowerCase() === tabName) {
tab.style.borderBottom = "2px solid #1a73e8";
tab.style.color = "#1a73e8";
2025-02-16 03:37:19 +07:00
} else {
tab.style.borderBottom = "2px solid transparent";
tab.style.color = "#666";
2025-02-16 03:37:19 +07:00
}
});
if (tabName === "translator") {
mainContent.style.display = "flex";
settingsContent.style.display = "none";
2025-02-16 03:37:19 +07:00
} else {
mainContent.style.display = "none";
settingsContent.style.display = "flex";
2025-02-16 03:37:19 +07:00
}
}
function setupDraggable(element) {
element.addEventListener("mousedown", function (e) {
2025-02-16 03:37:19 +07:00
isDragging = true;
var rect = container.getBoundingClientRect();
dragOffsetX = e.clientX - rect.left;
dragOffsetY = e.clientY - rect.top;
e.preventDefault();
log("info", "Started dragging window");
2025-02-16 03:37:19 +07:00
});
document.addEventListener("mousemove", function (e) {
2025-02-16 03:37:19 +07:00
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";
2025-02-16 03:37:19 +07:00
}
});
document.addEventListener("mouseup", function () {
2025-02-16 03:37:19 +07:00
if (isDragging) {
isDragging = false;
log("info", "Stopped dragging window");
2025-02-16 03:37:19 +07:00
}
});
}
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)";
toggleButton.addEventListener("click", function () {
log("info", "Toggle button clicked");
2025-02-16 03:37:19 +07:00
toggleVisibility();
});
try {
document.body.appendChild(toggleButton);
log("success", "Toggle button added to document body");
2025-02-16 03:37:19 +07:00
} catch (error) {
log("error", "Error appending toggle button to body:", error);
2025-02-16 03:37:19 +07:00
}
}
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",
});
2025-02-16 03:37:19 +07:00
}
function setupEventListeners() {
log("info", "Setting up event listeners");
searchInput.addEventListener("input", function () {
log("info", "Search input detected");
2025-02-16 03:37:19 +07:00
searchTranslations();
});
}
function setupExternalTextboxListener() {
log("info", "Setting up external textbox listener");
2025-02-16 03:37:19 +07:00
2025-02-16 03:54:23 +07:00
var observer = new MutationObserver(function (mutations) {
2025-02-16 03:37:19 +07:00
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");
2025-02-16 03:37:19 +07:00
findMatches(textbox.value);
});
textbox.addEventListener("mouseup", function () {
2025-02-16 03:37:19 +07:00
var selectedText = window.getSelection()?.toString();
if (selectedText) {
log("info", "Text selection detected", {
selectedText:
selectedText.substring(0, 20) +
(selectedText.length > 20 ? "..." : ""),
});
2025-02-16 03:37:19 +07:00
findMatches(selectedText);
}
});
}
});
try {
observer.observe(document.body, {
childList: true,
subtree: true,
2025-02-16 03:37:19 +07:00
});
log("success", "MutationObserver started");
2025-02-16 03:37:19 +07:00
} catch (error) {
log("error", "Error setting up MutationObserver:", error);
2025-02-16 03:37:19 +07:00
}
}
function setupCrowdinEditorListener() {
log("info", "Setting up Crowdin editor listener");
2025-02-16 03:37:19 +07:00
if (autoSearchIntervalId) {
clearInterval(autoSearchIntervalId);
}
setTimeout(() => {
checkForEditorContent();
}, 1000);
2025-02-16 03:54:23 +07:00
autoSearchIntervalId = setInterval(function () {
2025-02-16 03:37:19 +07:00
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);
}
2025-02-16 03:37:19 +07:00
}
2025-02-17 22:28:58 +07:00
function checkForEditorContent(forceRefresh = false) {
if (!visible || translationData.length === 0) {
log("debug", "Skipping editor content check", {
visible: visible,
hasTranslations: translationData.length > 0,
});
return;
}
2025-02-16 03:37:19 +07:00
try {
var content = parseEditorContent();
if (content && content.fullText) {
2025-02-17 22:28:58 +07:00
if (content.fullText !== lastSearchedText || forceRefresh) {
2025-02-16 03:37:19 +07:00
lastSearchedText = content.fullText;
2025-02-16 08:52:27 +07:00
const currentStringLabel = document.getElementById(
"current-string-label"
);
2025-02-16 08:52:27 +07:00
if (currentStringLabel) {
const stringIdText = content.stringId
? ` [ID: ${content.stringId}]`
: "";
currentStringLabel.textContent =
"Current string" +
stringIdText +
": " +
content.fullText.substring(0, 100) +
(content.fullText.length > 100 ? "..." : "");
2025-02-16 08:52:27 +07:00
}
log("debug", "Editor content changed", {
text:
content.fullText.substring(0, 50) +
(content.fullText.length > 50 ? "..." : ""),
2025-02-16 03:37:19 +07:00
terms: content.terms,
2025-02-16 08:52:27 +07:00
stringId: content.stringId,
length: content.fullText.length,
2025-02-16 03:37:19 +07:00
});
findMatches(content.fullText);
2025-02-16 03:37:19 +07:00
}
} else {
log("debug", "No valid editor content found");
2025-02-16 03:37:19 +07:00
}
} catch (error) {
log("error", "Error in checkForEditorContent", error);
2025-02-16 03:37:19 +07:00
}
}
function parseEditorContent() {
const editorContainer = document.querySelector(
CONFIG.editorSourceContainer
);
if (!editorContainer) {
log("debug", "Editor container not found", {
selector: CONFIG.editorSourceContainer,
});
return null;
}
2025-02-16 03:54:23 +07:00
const sourceContainer = document.querySelector(
CONFIG.sourceStringContainer
);
if (!sourceContainer) {
log("debug", "Source container not found", {
selector: CONFIG.sourceStringContainer,
});
return null;
}
2025-02-16 03:37:19 +07:00
const result = {
fullText: "",
2025-02-16 08:52:27 +07:00
terms: [],
stringId: "",
2025-02-16 03:37:19 +07:00
};
try {
// Try to get text content directly first
result.fullText = sourceContainer.textContent.trim();
// If no text found, try alternative selectors
if (!result.fullText) {
const alternativeSelectors = [
".source-string",
".source-string__content",
'[data-test="source-string"]',
".singular",
];
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;
}
}
2025-02-16 08:52:27 +07:00
}
}
// Get string ID from URL if possible
const urlMatch = window.location.href.match(
/\/translate\/([^\/]+)\/([^\/]+)\/([^-]+)-(\d+)/
);
if (urlMatch && urlMatch[4]) {
result.stringId = urlMatch[4];
}
2025-02-16 03:37:19 +07:00
// 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];
2025-02-16 03:37:19 +07:00
}
}
}
2025-02-17 22:28:58 +07:00
// 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");
// }
2025-02-16 03:54:23 +07:00
2025-02-16 03:37:19 +07:00
return result;
} catch (error) {
log("error", "Error parsing editor content:", error);
2025-02-16 03:37:19 +07:00
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"]'
);
2025-02-16 03:54:23 +07:00
2025-02-16 03:37:19 +07:00
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");
2025-02-16 03:37:19 +07:00
}
} else {
const url = remoteUrlInput
? remoteUrlInput.value.trim()
: CONFIG.remoteCSVUrl;
2025-02-16 03:37:19 +07:00
if (url) {
fetchRemoteCSV(url);
} else {
updateResults("Please enter a valid remote CSV URL.");
log("warn", "No remote URL provided");
2025-02-16 03:37:19 +07:00
}
}
}
function fetchRemoteCSV(url) {
log("info", "Fetching remote CSV from", { url: url });
2025-02-16 03:37:19 +07:00
GM_xmlhttpRequest({
2025-02-17 22:30:28 +07:00
method: "GET",
2025-02-16 03:37:19 +07:00
url: url,
2025-02-16 03:54:23 +07:00
onload: function (response) {
2025-02-16 03:37:19 +07:00
if (response.status === 200) {
2025-02-17 22:28:58 +07:00
try {
const newData = parseCSVToArray(response.responseText);
translationData = newData;
currentCSVSource = url;
2025-02-17 22:30:28 +07:00
2025-02-17 22:28:58 +07:00
log("debug", "Translation data", {
translationData: JSON.stringify(translationData),
2025-02-17 22:30:28 +07:00
newData: JSON.stringify(newData),
2025-02-17 22:28:58 +07:00
});
log("success", "Successfully loaded remote CSV", {
2025-02-17 22:30:28 +07:00
entries: translationData.length,
2025-02-17 22:28:58 +07:00
});
} catch (csvError) {
log("error", "Error parsing CSV data", csvError);
2025-02-17 22:30:28 +07:00
updateResults(
"Error parsing CSV data. Please check the file format and try again."
);
2025-02-17 22:28:58 +07:00
}
2025-02-16 03:37:19 +07:00
} else {
log("error", "Failed to fetch remote CSV", {
2025-02-17 22:30:28 +07:00
status: response.status,
});
updateResults(
"Failed to fetch remote CSV. Please check the URL and try again."
);
2025-02-16 03:37:19 +07:00
}
},
2025-02-16 03:54:23 +07:00
onerror: function (error) {
log("error", "Error fetching remote CSV", error);
updateResults(
"Error fetching remote CSV. Please check your connection and try again."
);
2025-02-17 22:30:28 +07:00
},
2025-02-16 03:37:19 +07:00
});
}
function readCSVFile(file) {
log("info", "Reading CSV file");
2025-02-16 03:37:19 +07:00
var reader = new FileReader();
2025-02-16 03:54:23 +07:00
reader.onload = function (e) {
var csvContent = e.target?.result;
log("info", "CSV file loaded, content length", {
length: csvContent.length,
});
parseCSV(csvContent);
2025-02-16 03:37:19 +07:00
currentCSVSource = file.name;
};
2025-02-16 03:54:23 +07:00
reader.onerror = function (error) {
log("error", "Error reading file", error);
updateResults("Error reading CSV file. Please try again.");
2025-02-16 03:37:19 +07:00
};
reader.readAsText(file);
}
function parseCSV(csvContent) {
log("info", "Parsing CSV content", {
lines: csvContent.split("\n").length,
});
var lines = csvContent.split("\n");
2025-02-16 03:37:19 +07:00
translationData = [];
categoryColors.clear();
if (!lines || lines.length < 2) {
log("error", "Invalid CSV structure: insufficient lines");
updateResults("Error: Invalid CSV file structure");
return;
}
2025-02-16 03:37:19 +07:00
// Skip header
for (var i = 1; i < lines.length; i++) {
var line = lines[i].trim();
if (line) {
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];
if (char === '"' && (j === 0 || line[j - 1] !== "\\")) {
inQuotes = !inQuotes;
} else if (char === "," && !inQuotes) {
values.push(currentValue);
currentValue = "";
} else {
currentValue += char;
}
2025-02-16 03:37:19 +07:00
}
values.push(currentValue);
2025-02-16 03:37:19 +07:00
// Remove quotes if present and validate
values = values.map(function (v) {
return validateCSVField(v.replace(/^"(.*)"$/, "$1"));
2025-02-16 03:37:19 +07:00
});
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;
2025-02-16 03:37:19 +07:00
}
}
}
log("success", "CSV parsing complete", {
2025-02-16 03:37:19 +07:00
entries: translationData.length,
source: currentCSVSource || "CSV",
2025-02-16 03:37:19 +07:00
});
updateResults(
`Loaded ${translationData.length} translations from ${sanitizeHTML(
currentCSVSource || "CSV"
)}`
);
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);
}
2025-02-16 03:37:19 +07:00
}
function findMatches(text) {
if (!text || !translationData.length) return;
log("debug", "Finding matches", {
text: text.substring(0, 50) + (text.length > 50 ? "..." : ""),
length: text.length,
2025-02-16 03:37:19 +07:00
});
var words = text.split(/\s+/);
var matches = [];
2025-02-17 22:28:58 +07:00
var seenCombinations = new Set();
2025-02-16 03:37:19 +07:00
2025-02-16 03:54:23 +07:00
words.forEach(function (word) {
2025-02-17 22:28:58 +07:00
if (!word) return;
2025-02-16 03:37:19 +07:00
2025-02-17 22:28:58 +07:00
// First try exact match with punctuation
2025-02-16 03:54:23 +07:00
translationData.forEach(function (entry) {
2025-02-17 22:30:28 +07:00
const uniqueKey = `${entry.source.toLowerCase()}_${
entry.category || "default"
}`;
2025-02-17 22:28:58 +07:00
// Exact match (case-insensitive)
2025-02-17 22:30:28 +07:00
if (
entry.source.toLowerCase() === word.toLowerCase() &&
!seenCombinations.has(uniqueKey)
) {
2025-02-17 22:28:58 +07:00
seenCombinations.add(uniqueKey);
matches.push({
entry: entry,
score: 1,
matchedWord: word,
});
return;
}
// Clean the word from punctuation for fuzzy matching
var cleanWord = word.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
if (!cleanWord || cleanWord.length <= 1) return; // Skip if cleaned word is too short
2025-02-16 03:37:19 +07:00
// 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");
2025-02-17 22:30:28 +07:00
if (regex.test(entry.source) && !seenCombinations.has(uniqueKey)) {
2025-02-17 22:28:58 +07:00
seenCombinations.add(uniqueKey);
2025-02-16 03:37:19 +07:00
matches.push({
entry: entry,
score: 1,
matchedWord: cleanWord,
2025-02-16 03:37:19 +07:00
});
}
} else {
// For longer words, use fuzzy match with higher threshold
const score = similarity(
entry.source.toLowerCase(),
cleanWord.toLowerCase()
);
if (
score >= CONFIG.fuzzyThreshold &&
2025-02-17 22:28:58 +07:00
!seenCombinations.has(uniqueKey)
) {
2025-02-17 22:28:58 +07:00
seenCombinations.add(uniqueKey);
2025-02-16 03:37:19 +07:00
matches.push({
entry: entry,
score: score,
matchedWord: cleanWord,
2025-02-16 03:37:19 +07:00
});
}
}
});
});
2025-02-17 22:28:58 +07:00
// Sort matches by score first, then by category
2025-02-16 03:54:23 +07:00
matches.sort(function (a, b) {
2025-02-16 03:37:19 +07:00
if (b.score === a.score) {
2025-02-17 22:28:58 +07:00
// If scores are equal, sort by category presence (entries with category come first)
if (!!a.entry.category !== !!b.entry.category) {
return a.entry.category ? -1 : 1;
}
// If both have or don't have categories, sort by matched word length
2025-02-16 03:37:19 +07:00
return b.matchedWord.length - a.matchedWord.length;
}
return b.score - a.score;
});
log("success", "Found matches", { count: matches.length });
2025-02-16 03:37:19 +07:00
displayFuzzyMatches(matches);
}
function searchTranslations() {
var query = searchInput.value.toLowerCase().trim();
if (!query || query.length <= 1) {
updateResults("");
lastSearchedText = "";
2025-02-17 22:28:58 +07:00
checkForEditorContent(true);
2025-02-16 03:37:19 +07:00
return;
}
log("info", "Searching translations for", { query: query });
2025-02-16 03:37:19 +07:00
var matches = [];
// Find matches
2025-02-16 03:54:23 +07:00
translationData.forEach(function (entry) {
2025-02-16 03:37:19 +07:00
let score = 0;
2025-02-16 03:54:23 +07:00
2025-02-16 03:37:19 +07:00
// 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))
) {
2025-02-16 03:37:19 +07:00
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;
2025-02-16 03:54:23 +07:00
2025-02-16 03:37:19 +07:00
// 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)
) {
2025-02-16 03:37:19 +07:00
matches.push({
entry: entry,
score: score,
2025-02-16 03:37:19 +07:00
});
}
});
// Sort matches by score (highest first) and text length (longer matches first)
2025-02-16 03:54:23 +07:00
matches.sort(function (a, b) {
2025-02-16 03:37:19 +07:00
if (b.score === a.score) {
return b.entry.source.length - a.entry.source.length;
}
return b.score - a.score;
});
// Limit results for performance
matches = matches.slice(0, 50);
log("success", "Search found matches", { count: matches.length });
2025-02-16 03:37:19 +07:00
displayFuzzyMatches(matches);
}
function displayFuzzyMatches(matches) {
if (matches.length === 0) {
updateResults(
'<div style="color: #666; text-align: center; padding: 16px;">No matches found</div>'
);
2025-02-16 03:37:19 +07:00
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";
2025-02-16 03:37:19 +07:00
// 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";
2025-02-16 03:37:19 +07:00
var table = document.createElement("table");
table.style.width = "100%";
table.style.borderCollapse = "collapse";
table.style.tableLayout = "fixed";
2025-02-19 18:15:01 +07:00
table.style.color = "#000";
2025-02-16 03:37:19 +07:00
// Header
var thead = document.createElement("thead");
thead.style.position = "sticky";
thead.style.top = "0";
thead.style.backgroundColor = "#f8f9fa";
thead.style.zIndex = "1";
2025-02-16 03:37:19 +07:00
var headerRow = document.createElement("tr");
2025-02-16 03:37:19 +07:00
var columns = [
{ name: "Source", width: "35%" },
{ name: "Target", width: "35%" },
{ name: "Note", width: "30%" },
2025-02-16 03:37:19 +07:00
];
if (matches[0].matchedWord) {
columns = [
{ name: "Source", width: "30%" },
{ name: "Target", width: "30%" },
{ name: "Note", width: "25%" },
{ name: "Match", width: "15%" },
];
2025-02-16 03:37:19 +07:00
}
columns.forEach((col) => {
var th = document.createElement("th");
2025-02-16 03:37:19 +07:00
th.textContent = col.name;
th.style.textAlign = "left";
th.style.padding = "8px";
th.style.border = "1px solid #e0e0e0";
2025-02-16 03:37:19 +07:00
th.style.width = col.width;
th.style.backgroundColor = "#f8f9fa";
2025-02-16 03:37:19 +07:00
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create table body
var tbody = document.createElement("tbody");
2025-02-16 03:54:23 +07:00
matches.forEach(function (match) {
var row = document.createElement("tr");
2025-02-16 03:37:19 +07:00
const scorePercentage = Math.round(match.score * 100);
const bgColor = `rgba(26, 115, 232, ${match.score * 0.1})`;
row.style.backgroundColor = bgColor;
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";
2025-02-19 18:15:01 +07:00
container.style.color = "#000";
// Add category chip first if in note column
if (!isSource && !isTarget && category) {
2025-02-17 22:28:58 +07:00
container.innerHTML += createCategoryChip(category);
}
// 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";
2025-02-16 11:25:21 +07:00
// Hover effect
cell.addEventListener("mouseover", function () {
this.style.backgroundColor = "rgba(26, 115, 232, 0.1)";
2025-02-16 11:25:21 +07:00
});
cell.addEventListener("mouseout", function () {
this.style.backgroundColor = "transparent";
2025-02-16 11:25:21 +07:00
});
cell.addEventListener("click", function (e) {
2025-02-16 11:25:21 +07:00
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%)";
2025-02-16 11:25:21 +07:00
cell.appendChild(tooltip);
setTimeout(() => {
tooltip.remove();
}, 1000);
});
});
return cell;
}
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)
);
2025-02-16 03:37:19 +07:00
if (match.matchedWord) {
row.appendChild(
createCopyableCell(`${match.matchedWord} (${scorePercentage}%)`)
);
2025-02-16 03:37:19 +07:00
}
tbody.appendChild(row);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
wrapper.appendChild(tableContainer);
2025-02-18 11:02:48 +07:00
// Copy label at bottom
var copyLabelContainer = document.createElement("div");
copyLabelContainer.style.padding = "4px 8px";
copyLabelContainer.style.backgroundColor = "#f8f9fa";
copyLabelContainer.style.borderTop = "1px solid #e0e0e0";
copyLabelContainer.style.display = "flex";
copyLabelContainer.style.alignItems = "center";
copyLabelContainer.style.gap = "4px";
copyLabelContainer.style.fontSize = "11px";
copyLabelContainer.style.color = "#666";
var matchCount = document.createElement("span");
matchCount.textContent = `${matches.length} matches`;
copyLabelContainer.appendChild(matchCount);
var separator = document.createElement("span");
separator.textContent = "•";
separator.style.color = "#ccc";
copyLabelContainer.appendChild(separator);
var copyButton = document.createElement("span");
copyButton.textContent = "Copy as CSV";
copyButton.style.color = "#1a73e8";
copyButton.style.cursor = "pointer";
copyButton.style.transition = "color 0.2s";
copyButton.addEventListener("mouseover", function () {
this.style.color = "#1557b0";
this.style.textDecoration = "underline";
});
copyButton.addEventListener("mouseout", function () {
this.style.color = "#1a73e8";
this.style.textDecoration = "none";
});
copyButton.addEventListener("click", function () {
let csvContent = "Source,Target,Note,Category\n";
matches.forEach(function (match) {
const escapeField = (field) => {
if (!field) return "";
const escaped = field.replace(/"/g, '""');
return `"${escaped}"`;
};
csvContent +=
[
escapeField(match.entry.source),
escapeField(match.entry.target),
escapeField(match.entry.note),
escapeField(match.entry.category),
].join(",") + "\n";
});
navigator.clipboard
.writeText(csvContent)
.then(() => {
const originalText = copyButton.textContent;
2025-02-18 11:32:50 +07:00
copyButton.textContent = "Copied!";
2025-02-18 11:02:48 +07:00
copyButton.style.color = "#4CAF50";
setTimeout(() => {
copyButton.textContent = originalText;
copyButton.style.color = "#1a73e8";
}, 2000);
})
.catch((err) => {
log("error", "Failed to copy CSV", err);
copyButton.textContent = "failed to copy";
copyButton.style.color = "#F44336";
setTimeout(() => {
copyButton.textContent = "copy as CSV";
copyButton.style.color = "#1a73e8";
}, 2000);
});
});
copyLabelContainer.appendChild(copyButton);
wrapper.appendChild(copyLabelContainer);
resultsDiv.innerHTML = "";
2025-02-16 03:37:19 +07:00
resultsDiv.appendChild(wrapper);
log("success", "Updated results panel with table layout");
2025-02-16 03:37:19 +07:00
}
function updateResults(content) {
resultsDiv.innerHTML = content;
log("success", "Updated results panel");
2025-02-16 03:37:19 +07:00
}
function checkForUpdates() {
log("info", "Checking for updates");
updateLink.textContent = "Checking for updates...";
updateLink.style.color = "#666";
2025-02-16 03:37:19 +07:00
// Check version first
GM_xmlhttpRequest({
method: "GET",
2025-02-16 03:37:19 +07:00
url: CONFIG.updateCheckUrl,
2025-02-16 03:54:23 +07:00
onload: function (response) {
2025-02-16 03:37:19 +07:00
if (response.status === 200) {
try {
const versionInfo = JSON.parse(response.responseText);
const latestVersion = versionInfo.latest;
const needsVersionUpdate =
latestVersion !== CONFIG.metadata.version;
2025-02-16 03:54:23 +07:00
log("info", "Retrieved version info", {
2025-02-16 08:11:11 +07:00
current: CONFIG.metadata.version,
latest: latestVersion,
2025-02-16 03:37:19 +07:00
});
// 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;
2025-02-16 03:37:19 +07:00
if (csvUrl) {
log("info", "Checking CSV updates from", { url: csvUrl });
2025-02-16 03:37:19 +07:00
GM_xmlhttpRequest({
method: "GET",
2025-02-16 03:37:19 +07:00
url: csvUrl,
2025-02-16 03:54:23 +07:00
onload: function (csvResponse) {
2025-02-16 03:37:19 +07:00
if (csvResponse.status === 200) {
try {
const newData = parseCSVToArray(csvResponse.responseText);
2025-02-17 22:28:58 +07:00
function isEqual(obj1, obj2) {
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
2025-02-17 22:30:28 +07:00
const needsDataUpdate = !isEqual(
translationData,
newData
);
2025-02-17 22:28:58 +07:00
log("debug", "Translation data", {
translationData: JSON.stringify(translationData),
newData: JSON.stringify(newData),
});
log("info", "CSV check complete", {
2025-02-16 03:37:19 +07:00
needsUpdate: needsDataUpdate,
currentEntries: translationData.length,
newEntries: newData.length,
2025-02-16 03:37:19 +07:00
});
updateUIAfterChecks(
needsVersionUpdate,
needsDataUpdate,
latestVersion,
newData
);
2025-02-16 03:37:19 +07:00
} catch (csvError) {
log("error", "Error parsing CSV data", csvError);
updateUIAfterChecks(
needsVersionUpdate,
false,
latestVersion,
null
);
2025-02-16 03:37:19 +07:00
}
} else {
log("error", "Failed to fetch CSV", {
status: csvResponse.status,
});
updateUIAfterChecks(
needsVersionUpdate,
false,
latestVersion,
null
);
2025-02-16 03:37:19 +07:00
}
},
2025-02-16 03:54:23 +07:00
onerror: function (csvError) {
log("error", "Error fetching CSV", csvError);
updateUIAfterChecks(
needsVersionUpdate,
false,
latestVersion,
null
);
},
2025-02-16 03:37:19 +07:00
});
} else {
log("info", "Skipping CSV check - using local file");
updateUIAfterChecks(
needsVersionUpdate,
false,
latestVersion,
null
);
2025-02-16 03:37:19 +07:00
}
} catch (e) {
log("error", "Error parsing version info", e);
updateLink.textContent = "Error checking for updates";
updateLink.style.color = "#F44336";
2025-02-16 03:37:19 +07:00
}
} else {
log("error", "Failed to check for updates", {
status: response.status,
});
2025-02-18 11:02:48 +07:00
updateLink.textContent = "Error checking for updates";
updateLink.style.color = "#F44336";
2025-02-16 03:37:19 +07:00
}
},
2025-02-16 03:54:23 +07:00
onerror: function (error) {
log("error", "Error checking for updates", error);
updateLink.textContent = "Error checking for updates";
updateLink.style.color = "#F44336";
},
2025-02-16 03:37:19 +07:00
});
}
function parseCSVToArray(csvContent) {
const lines = csvContent.split("\n");
2025-02-16 03:37:19 +07:00
const result = [];
2025-02-16 03:54:23 +07:00
2025-02-16 03:37:19 +07:00
// Skip header
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (line) {
let values = [];
let inQuotes = false;
let currentValue = "";
2025-02-16 03:37:19 +07:00
for (let j = 0; j < line.length; j++) {
const char = line[j];
if (char === '"' && (j === 0 || line[j - 1] !== "\\")) {
2025-02-16 03:37:19 +07:00
inQuotes = !inQuotes;
} else if (char === "," && !inQuotes) {
2025-02-16 03:37:19 +07:00
values.push(currentValue);
currentValue = "";
2025-02-16 03:37:19 +07:00
} else {
currentValue += char;
}
}
values.push(currentValue);
values = values.map((v) => v.replace(/^"(.*)"$/, "$1"));
2025-02-16 03:37:19 +07:00
if (values.length >= 2) {
result.push({
source: values[0],
target: values[1],
note: values[2] || "",
category: values[3] || "",
2025-02-16 03:37:19 +07:00
});
}
}
}
return result;
}
function updateUIAfterChecks(
needsVersionUpdate,
needsDataUpdate,
newVersion,
newData
) {
2025-02-16 03:37:19 +07:00
if (needsVersionUpdate && needsDataUpdate) {
updateLink.textContent = `Update available! v${newVersion} + new translations`;
updateLink.style.color = "#F44336";
2025-02-16 03:37:19 +07:00
showUpdateNotification(true, true);
} else if (needsVersionUpdate) {
updateLink.textContent = `Update available! v${newVersion}`;
updateLink.style.color = "#F44336";
2025-02-16 03:37:19 +07:00
showUpdateNotification(true, false);
} else if (needsDataUpdate) {
2025-02-17 22:28:58 +07:00
updateLink.textContent = "New translations applied!";
updateLink.style.color = "#F44336";
2025-02-16 03:37:19 +07:00
showUpdateNotification(false, true);
if (newData) {
translationData = newData;
log("success", "Updated translation data", { entries: newData.length });
2025-02-16 03:37:19 +07:00
updateResults(`Updated with ${newData.length} translations`);
2025-02-16 03:54:23 +07:00
2025-02-16 03:37:19 +07:00
setTimeout(() => {
updateLink.textContent = "Translations updated ✓";
updateLink.style.color = "#4CAF50";
// Trigger content check after updating data
setTimeout(() => {
2025-02-17 22:28:58 +07:00
checkForEditorContent(true);
}, 500);
2025-02-16 03:37:19 +07:00
setTimeout(() => {
updateLink.textContent = "Check for updates";
updateLink.style.color = "#1a73e8";
2025-02-16 03:37:19 +07:00
}, 2000);
}, 1000);
}
} else {
log("info", "No updates available");
updateLink.textContent = "No updates available ✓";
2025-02-16 03:37:19 +07:00
setTimeout(() => {
updateLink.textContent = "Check for updates";
updateLink.style.color = "#1a73e8";
2025-02-16 03:37:19 +07:00
}, 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";
let message = "";
2025-02-16 03:37:19 +07:00
if (hasVersionUpdate && hasDataUpdate) {
message = "New version and translations available!";
2025-02-16 03:37:19 +07:00
} else if (hasVersionUpdate) {
message = "New version available!";
2025-02-16 03:37:19 +07:00
} else if (hasDataUpdate) {
message = "New translations available!";
2025-02-16 03:37:19 +07:00
}
notification.innerHTML = `
<div style="margin-bottom:12px">
<b>${message}</b>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px">
<button id="csv-translator-dismiss" style="padding:8px 16px;border:none;background:#2E7D32;color:white;border-radius:4px;cursor:pointer">Dismiss</button>
${
hasVersionUpdate
? `<button id="csv-translator-update" style="padding:8px 16px;border:none;background:#1a73e8;color:white;border-radius:4px;cursor:pointer">Open Repository</button>`
: ""
}
2025-02-16 03:37:19 +07:00
</div>
`;
document.body.appendChild(notification);
document
.getElementById("csv-translator-dismiss")
.addEventListener("click", function () {
2025-02-16 03:37:19 +07:00
document.body.removeChild(notification);
});
if (hasVersionUpdate) {
document
.getElementById("csv-translator-update")
.addEventListener("click", function () {
window.open(CONFIG.metadata.repository, "_blank");
document.body.removeChild(notification);
});
2025-02-16 03:37:19 +07:00
}
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 10000);
}
init();
}
document.addEventListener("DOMContentLoaded", function () {
log("info", "DOMContentLoaded event fired");
2025-02-16 03:37:19 +07:00
try {
new TranslatorTool();
} catch (error) {
log("error", "Error initializing tool:", error);
2025-02-16 03:37:19 +07:00
}
});
// Fallback initialization
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
log("info", "Document already loaded, initializing immediately");
2025-02-16 03:54:23 +07:00
setTimeout(function () {
2025-02-16 03:37:19 +07:00
try {
new TranslatorTool();
} catch (error) {
log("error", "Error initializing tool (fallback):", error);
2025-02-16 03:37:19 +07:00
}
}, 1000);
}
log("info", "Script loaded. Current document.readyState:", document.readyState);