2025-02-16 03:37:19 +07:00
|
|
|
|
// ==UserScript==
|
|
|
|
|
|
// @name Crowdin Localization Tools
|
|
|
|
|
|
// @namespace https://yuzu.kirameki.cafe/
|
2025-02-16 08:11:11 +07:00
|
|
|
|
// @version 1.0.4
|
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' },
|
|
|
|
|
|
windowDimensions: {
|
|
|
|
|
|
width: '600px',
|
|
|
|
|
|
height: '600px'
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
debug: true,
|
|
|
|
|
|
|
|
|
|
|
|
// Update check
|
|
|
|
|
|
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',
|
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',
|
|
|
|
|
|
editorSourceContainer: '.editor-current-translation-source',
|
|
|
|
|
|
sourceStringContainer: '#source_phrase_container',
|
|
|
|
|
|
|
|
|
|
|
|
autoSearchInterval: 1500,
|
|
|
|
|
|
fuzzyThreshold: 0.7,
|
|
|
|
|
|
|
|
|
|
|
|
metadata: {
|
2025-02-16 08:11:11 +07:00
|
|
|
|
version: '1.0.4',
|
2025-02-16 03:37:19 +07:00
|
|
|
|
repository: 'https://github.com/YuzuZensai/Crowdin-Localization-Tools',
|
|
|
|
|
|
authorGithub: 'https://github.com/YuzuZensai'
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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 || '');
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'warn':
|
|
|
|
|
|
console.warn(`${prefix} ⚠️ ${message}`, data || '');
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'error':
|
|
|
|
|
|
console.error(`${prefix} ❌ ${message}`, data || '');
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'success':
|
|
|
|
|
|
console.log(`${prefix} ✅ ${message}`, data || '');
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'debug':
|
|
|
|
|
|
console.debug(`${prefix} 🔍 ${message}`, data || '');
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 = '';
|
|
|
|
|
|
var autoSearchIntervalId = null;
|
|
|
|
|
|
var updateLink;
|
|
|
|
|
|
var currentCSVSource = null;
|
|
|
|
|
|
|
|
|
|
|
|
function init() {
|
|
|
|
|
|
log('info', 'Initializing translator tool');
|
|
|
|
|
|
createUI();
|
|
|
|
|
|
createToggleButton();
|
|
|
|
|
|
setupEventListeners();
|
|
|
|
|
|
setupExternalTextboxListener();
|
|
|
|
|
|
setupCrowdinEditorListener();
|
|
|
|
|
|
|
|
|
|
|
|
const sourceToggle = document.querySelector('#source-toggle');
|
|
|
|
|
|
if (!sourceToggle || !sourceToggle.checked) {
|
|
|
|
|
|
fetchRemoteCSV(CONFIG.remoteCSVUrl);
|
|
|
|
|
|
}
|
2025-02-16 03:54:23 +07:00
|
|
|
|
|
2025-02-16 03:37:19 +07:00
|
|
|
|
log('success', 'Crowdin Localization Tools version ' + CONFIG.metadata.version + ' by ' + CONFIG.metadata.authorGithub + ' initialized successfully');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createUI() {
|
|
|
|
|
|
log('info', 'Creating UI elements');
|
|
|
|
|
|
|
|
|
|
|
|
// Container
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
// 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';
|
2025-02-16 03:54:23 +07:00
|
|
|
|
closeButton.addEventListener('click', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
log('info', 'Close button clicked');
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
var mainTab = createTab('Translator', true);
|
|
|
|
|
|
var settingsTab = createTab('Settings', false);
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Error appending container to body:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setupDraggable(header);
|
|
|
|
|
|
|
|
|
|
|
|
log('success', 'UI elements created successfully');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function createTab(text, isActive) {
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
// Search container
|
|
|
|
|
|
var searchContainer = document.createElement('div');
|
|
|
|
|
|
searchContainer.style.position = 'relative';
|
|
|
|
|
|
searchContainer.style.marginBottom = '16px';
|
|
|
|
|
|
searchContainer.style.flexShrink = '0';
|
|
|
|
|
|
|
|
|
|
|
|
// 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';
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
searchInput.addEventListener('mouseover', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
if (document.activeElement !== this) {
|
|
|
|
|
|
this.style.borderColor = '#ccc';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-02-16 03:54:23 +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:54:23 +07:00
|
|
|
|
searchInput.addEventListener('focus', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
this.style.borderColor = '#1a73e8';
|
|
|
|
|
|
this.style.backgroundColor = '#fff';
|
|
|
|
|
|
});
|
2025-02-16 03:54:23 +07:00
|
|
|
|
searchInput.addEventListener('blur', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
// CSV Source Settings
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
// 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';
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
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 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';
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
sourceToggle.addEventListener('change', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
urlContainer.style.display = this.checked ? 'none' : 'block';
|
|
|
|
|
|
fileContainer.style.display = this.checked ? 'block' : 'none';
|
|
|
|
|
|
|
|
|
|
|
|
if (this.checked) {
|
|
|
|
|
|
remoteUrlInput.value = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
localFileInput.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Add global style for refresh button
|
|
|
|
|
|
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:54:23 +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');
|
|
|
|
|
|
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
|
|
|
|
|
2025-02-16 03:37:19 +07:00
|
|
|
|
credits.appendChild(document.createTextNode('Made with 💖 by '));
|
|
|
|
|
|
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';
|
2025-02-16 03:54:23 +07:00
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
|
|
tabs.forEach(tab => {
|
|
|
|
|
|
if (tab.textContent.toLowerCase() === tabName) {
|
|
|
|
|
|
tab.style.borderBottom = '2px solid #1a73e8';
|
|
|
|
|
|
tab.style.color = '#1a73e8';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tab.style.borderBottom = '2px solid transparent';
|
|
|
|
|
|
tab.style.color = '#666';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (tabName === 'translator') {
|
|
|
|
|
|
mainContent.style.display = 'flex';
|
|
|
|
|
|
settingsContent.style.display = 'none';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mainContent.style.display = 'none';
|
|
|
|
|
|
settingsContent.style.display = 'flex';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setupDraggable(element) {
|
2025-02-16 03:54:23 +07:00
|
|
|
|
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:54:23 +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:54:23 +07:00
|
|
|
|
document.addEventListener('mouseup', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
if (isDragging) {
|
|
|
|
|
|
isDragging = false;
|
|
|
|
|
|
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)';
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
toggleButton.addEventListener('click', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
log('info', 'Toggle button clicked');
|
|
|
|
|
|
toggleVisibility();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
document.body.appendChild(toggleButton);
|
|
|
|
|
|
log('success', 'Toggle button added to document body');
|
|
|
|
|
|
} catch (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' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setupEventListeners() {
|
|
|
|
|
|
log('info', 'Setting up event listeners');
|
2025-02-16 03:54:23 +07:00
|
|
|
|
searchInput.addEventListener('input', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
log('info', 'Search input detected');
|
|
|
|
|
|
searchTranslations();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setupExternalTextboxListener() {
|
|
|
|
|
|
log('info', 'Setting up external textbox listener');
|
|
|
|
|
|
|
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';
|
2025-02-16 03:54:23 +07:00
|
|
|
|
textbox.addEventListener('input', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
log('info', 'External textbox input detected');
|
|
|
|
|
|
findMatches(textbox.value);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
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 ? '...' : '') });
|
|
|
|
|
|
findMatches(selectedText);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
observer.observe(document.body, {
|
|
|
|
|
|
childList: true,
|
|
|
|
|
|
subtree: true
|
|
|
|
|
|
});
|
|
|
|
|
|
log('success', 'MutationObserver started');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Error setting up MutationObserver:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setupCrowdinEditorListener() {
|
|
|
|
|
|
log('info', 'Setting up Crowdin editor listener');
|
|
|
|
|
|
|
|
|
|
|
|
if (autoSearchIntervalId) {
|
|
|
|
|
|
clearInterval(autoSearchIntervalId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
autoSearchIntervalId = setInterval(function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
checkForEditorContent();
|
|
|
|
|
|
}, CONFIG.autoSearchInterval);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function checkForEditorContent() {
|
|
|
|
|
|
if (!visible || translationData.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
var content = parseEditorContent();
|
|
|
|
|
|
if (content && content.fullText) {
|
|
|
|
|
|
if (content.fullText !== lastSearchedText) {
|
|
|
|
|
|
lastSearchedText = content.fullText;
|
|
|
|
|
|
log('debug', 'Editor content changed', {
|
|
|
|
|
|
text: content.fullText,
|
|
|
|
|
|
terms: content.terms,
|
|
|
|
|
|
length: content.fullText.length
|
|
|
|
|
|
});
|
|
|
|
|
|
findMatches(lastSearchedText);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Error in auto-search', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseEditorContent() {
|
|
|
|
|
|
const editorContainer = document.querySelector(CONFIG.editorSourceContainer);
|
|
|
|
|
|
if (!editorContainer) return null;
|
2025-02-16 03:54:23 +07:00
|
|
|
|
|
2025-02-16 03:37:19 +07:00
|
|
|
|
const sourceContainer = document.querySelector(CONFIG.sourceStringContainer);
|
|
|
|
|
|
if (!sourceContainer) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
|
|
fullText: '',
|
|
|
|
|
|
terms: []
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const singularContainer = sourceContainer.querySelector('.singular');
|
|
|
|
|
|
if (singularContainer) {
|
|
|
|
|
|
let nodes = singularContainer.childNodes;
|
|
|
|
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
|
|
|
|
let node = nodes[i];
|
|
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Fallback, if something went wrong, just get the text content
|
|
|
|
|
|
result.fullText = sourceContainer.textContent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result.fullText = result.fullText.trim();
|
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);
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fetchRemoteCSV(url) {
|
|
|
|
|
|
log('info', 'Fetching remote CSV from', { url: url });
|
|
|
|
|
|
GM_xmlhttpRequest({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
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) {
|
|
|
|
|
|
parseCSV(response.responseText);
|
|
|
|
|
|
currentCSVSource = url;
|
|
|
|
|
|
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.');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-02-16 03:54:23 +07:00
|
|
|
|
onerror: function (error) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
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');
|
|
|
|
|
|
var reader = new FileReader();
|
2025-02-16 03:54:23 +07:00
|
|
|
|
reader.onload = function (e) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
var content = e.target?.result;
|
|
|
|
|
|
log('info', 'CSV file loaded, content length', { length: content.length });
|
|
|
|
|
|
parseCSV(content);
|
|
|
|
|
|
currentCSVSource = file.name;
|
|
|
|
|
|
};
|
2025-02-16 03:54:23 +07:00
|
|
|
|
reader.onerror = function (error) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
|
|
translationData = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 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 = '';
|
|
|
|
|
|
|
|
|
|
|
|
for (var j = 0; j < line.length; j++) {
|
|
|
|
|
|
var char = line[j];
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
if (char === '"' && (j === 0 || line[j - 1] !== '\\')) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
inQuotes = !inQuotes;
|
|
|
|
|
|
} else if (char === ',' && !inQuotes) {
|
|
|
|
|
|
values.push(currentValue);
|
|
|
|
|
|
currentValue = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
currentValue += char;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Add the last value
|
|
|
|
|
|
values.push(currentValue);
|
|
|
|
|
|
|
|
|
|
|
|
// Remove quotes if present
|
2025-02-16 03:54:23 +07:00
|
|
|
|
values = values.map(function (v) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
return v.replace(/^"(.*)"$/, '$1');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (values.length >= 2) {
|
|
|
|
|
|
translationData.push({
|
|
|
|
|
|
source: values[0],
|
|
|
|
|
|
target: values[1],
|
|
|
|
|
|
note: values[2] || ''
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
log('success', 'CSV parsing complete', {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
entries: translationData.length,
|
|
|
|
|
|
source: currentCSVSource || 'CSV'
|
|
|
|
|
|
});
|
|
|
|
|
|
updateResults(`Loaded ${translationData.length} translations from ${currentCSVSource || 'CSV'}`);
|
|
|
|
|
|
|
|
|
|
|
|
checkForEditorContent();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findMatches(text) {
|
|
|
|
|
|
if (!text || !translationData.length) return;
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
log('debug', 'Finding matches', {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
text: text.substring(0, 50) + (text.length > 50 ? '...' : ''),
|
|
|
|
|
|
length: text.length
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
var words = text.split(/\s+/);
|
|
|
|
|
|
var matches = [];
|
|
|
|
|
|
var seenWords = new Set();
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
words.forEach(function (word) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
// Clean the word from punctuation
|
|
|
|
|
|
var cleanWord = word.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
|
|
|
|
|
|
if (!cleanWord || cleanWord.length <= 1) return; // Skip single characters
|
|
|
|
|
|
|
|
|
|
|
|
if (seenWords.has(cleanWord.toLowerCase())) return;
|
|
|
|
|
|
seenWords.add(cleanWord.toLowerCase());
|
|
|
|
|
|
|
|
|
|
|
|
// Find matches
|
2025-02-16 03:54:23 +07:00
|
|
|
|
translationData.forEach(function (entry) {
|
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');
|
|
|
|
|
|
if (regex.test(entry.source) &&
|
2025-02-16 03:54:23 +07:00
|
|
|
|
!matches.some(function (m) { return m.entry.source === entry.source; })) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
matches.push({
|
|
|
|
|
|
entry: entry,
|
|
|
|
|
|
score: 1,
|
|
|
|
|
|
matchedWord: cleanWord
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// For longer words, use fuzzy match with higher threshold
|
|
|
|
|
|
const score = similarity(entry.source.toLowerCase(), cleanWord.toLowerCase());
|
|
|
|
|
|
if (score >= CONFIG.fuzzyThreshold &&
|
2025-02-16 03:54:23 +07:00
|
|
|
|
!matches.some(function (m) { return m.entry.source === entry.source; })) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
matches.push({
|
|
|
|
|
|
entry: entry,
|
|
|
|
|
|
score: score,
|
|
|
|
|
|
matchedWord: cleanWord
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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.matchedWord.length - a.matchedWord.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
return b.score - a.score;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
log('success', 'Found matches', { count: matches.length });
|
|
|
|
|
|
displayFuzzyMatches(matches);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function searchTranslations() {
|
|
|
|
|
|
var query = searchInput.value.toLowerCase().trim();
|
|
|
|
|
|
if (!query || query.length <= 1) {
|
|
|
|
|
|
updateResults('');
|
|
|
|
|
|
lastSearchedText = '';
|
|
|
|
|
|
checkForEditorContent();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log('info', 'Searching translations for', { query: query });
|
|
|
|
|
|
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))) {
|
|
|
|
|
|
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)) {
|
|
|
|
|
|
matches.push({
|
|
|
|
|
|
entry: entry,
|
|
|
|
|
|
score: score
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 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 });
|
|
|
|
|
|
displayFuzzyMatches(matches);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function displayFuzzyMatches(matches) {
|
|
|
|
|
|
if (matches.length === 0) {
|
|
|
|
|
|
updateResults('<div style="color: #666; text-align: center; padding: 16px;">No matches found</div>');
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
// 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 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 headerRow = document.createElement('tr');
|
|
|
|
|
|
var columns = [
|
|
|
|
|
|
{ name: 'Source', width: '30%' },
|
|
|
|
|
|
{ name: 'Target', width: '30%' },
|
|
|
|
|
|
{ name: 'Note', width: '20%' }
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (matches[0].matchedWord) {
|
|
|
|
|
|
columns.push({ name: 'Match', width: '20%' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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.width = col.width;
|
|
|
|
|
|
th.style.backgroundColor = '#f8f9fa';
|
|
|
|
|
|
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) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Add cells
|
|
|
|
|
|
var sourceCell = document.createElement('td');
|
|
|
|
|
|
sourceCell.textContent = match.entry.source;
|
|
|
|
|
|
sourceCell.style.padding = '8px';
|
|
|
|
|
|
sourceCell.style.border = '1px solid #e0e0e0';
|
2025-02-16 08:10:46 +07:00
|
|
|
|
sourceCell.style.wordBreak = 'break-word';
|
|
|
|
|
|
sourceCell.style.whiteSpace = 'normal';
|
|
|
|
|
|
sourceCell.style.verticalAlign = 'top';
|
2025-02-16 03:37:19 +07:00
|
|
|
|
row.appendChild(sourceCell);
|
|
|
|
|
|
|
|
|
|
|
|
var targetCell = document.createElement('td');
|
|
|
|
|
|
targetCell.textContent = match.entry.target;
|
|
|
|
|
|
targetCell.style.padding = '8px';
|
|
|
|
|
|
targetCell.style.border = '1px solid #e0e0e0';
|
2025-02-16 08:10:46 +07:00
|
|
|
|
targetCell.style.wordBreak = 'break-word';
|
|
|
|
|
|
targetCell.style.whiteSpace = 'normal';
|
|
|
|
|
|
targetCell.style.verticalAlign = 'top';
|
2025-02-16 03:37:19 +07:00
|
|
|
|
row.appendChild(targetCell);
|
|
|
|
|
|
|
|
|
|
|
|
var noteCell = document.createElement('td');
|
|
|
|
|
|
noteCell.textContent = match.entry.note;
|
|
|
|
|
|
noteCell.style.padding = '8px';
|
|
|
|
|
|
noteCell.style.border = '1px solid #e0e0e0';
|
2025-02-16 08:10:46 +07:00
|
|
|
|
noteCell.style.wordBreak = 'break-word';
|
|
|
|
|
|
noteCell.style.whiteSpace = 'normal';
|
|
|
|
|
|
noteCell.style.verticalAlign = 'top';
|
2025-02-16 03:37:19 +07:00
|
|
|
|
row.appendChild(noteCell);
|
|
|
|
|
|
|
|
|
|
|
|
if (match.matchedWord) {
|
|
|
|
|
|
var matchCell = document.createElement('td');
|
|
|
|
|
|
matchCell.textContent = `${match.matchedWord} (${scorePercentage}%)`;
|
|
|
|
|
|
matchCell.style.padding = '8px';
|
|
|
|
|
|
matchCell.style.border = '1px solid #e0e0e0';
|
2025-02-16 08:11:11 +07:00
|
|
|
|
matchCell.style.wordBreak = 'break-word';
|
|
|
|
|
|
matchCell.style.whiteSpace = 'normal';
|
|
|
|
|
|
matchCell.style.verticalAlign = 'top';
|
2025-02-16 03:37:19 +07:00
|
|
|
|
row.appendChild(matchCell);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tbody.appendChild(row);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
table.appendChild(tbody);
|
|
|
|
|
|
tableContainer.appendChild(table);
|
|
|
|
|
|
wrapper.appendChild(tableContainer);
|
|
|
|
|
|
|
|
|
|
|
|
resultsDiv.innerHTML = '';
|
|
|
|
|
|
resultsDiv.appendChild(wrapper);
|
|
|
|
|
|
log('success', 'Updated results panel with table layout');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateResults(content) {
|
|
|
|
|
|
resultsDiv.innerHTML = content;
|
|
|
|
|
|
log('success', 'Updated results panel');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function checkForUpdates() {
|
|
|
|
|
|
log('info', 'Checking for updates');
|
|
|
|
|
|
updateLink.textContent = 'Checking for updates...';
|
|
|
|
|
|
updateLink.style.color = '#666';
|
|
|
|
|
|
|
|
|
|
|
|
// Check version first
|
|
|
|
|
|
GM_xmlhttpRequest({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
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;
|
2025-02-16 08:11:11 +07:00
|
|
|
|
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,
|
2025-02-16 03:54:23 +07:00
|
|
|
|
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"]');
|
2025-02-16 03:54:23 +07:00
|
|
|
|
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 });
|
|
|
|
|
|
GM_xmlhttpRequest({
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
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);
|
|
|
|
|
|
const needsDataUpdate = JSON.stringify(translationData) !== JSON.stringify(newData);
|
2025-02-16 03:54:23 +07:00
|
|
|
|
log('info', 'CSV check complete', {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
needsUpdate: needsDataUpdate,
|
|
|
|
|
|
currentEntries: translationData.length,
|
|
|
|
|
|
newEntries: newData.length
|
|
|
|
|
|
});
|
|
|
|
|
|
updateUIAfterChecks(needsVersionUpdate, needsDataUpdate, latestVersion, newData);
|
|
|
|
|
|
} catch (csvError) {
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-02-16 03:54:23 +07:00
|
|
|
|
onerror: function (csvError) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
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';
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-02-16 03:54:23 +07:00
|
|
|
|
onerror: function (error) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
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 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 = '';
|
|
|
|
|
|
|
|
|
|
|
|
for (let j = 0; j < line.length; j++) {
|
|
|
|
|
|
const char = line[j];
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
if (char === '"' && (j === 0 || line[j - 1] !== '\\')) {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
inQuotes = !inQuotes;
|
|
|
|
|
|
} else if (char === ',' && !inQuotes) {
|
|
|
|
|
|
values.push(currentValue);
|
|
|
|
|
|
currentValue = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
currentValue += char;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
values.push(currentValue);
|
|
|
|
|
|
values = values.map(v => v.replace(/^"(.*)"$/, '$1'));
|
|
|
|
|
|
|
|
|
|
|
|
if (values.length >= 2) {
|
|
|
|
|
|
result.push({
|
|
|
|
|
|
source: values[0],
|
|
|
|
|
|
target: values[1],
|
|
|
|
|
|
note: values[2] || ''
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateUIAfterChecks(needsVersionUpdate, needsDataUpdate, newVersion, newData) {
|
|
|
|
|
|
if (needsVersionUpdate && needsDataUpdate) {
|
|
|
|
|
|
updateLink.textContent = `Update available! v${newVersion} + new translations`;
|
|
|
|
|
|
updateLink.style.color = '#F44336';
|
|
|
|
|
|
showUpdateNotification(true, true);
|
|
|
|
|
|
} else if (needsVersionUpdate) {
|
|
|
|
|
|
updateLink.textContent = `Update available! v${newVersion}`;
|
|
|
|
|
|
updateLink.style.color = '#F44336';
|
|
|
|
|
|
showUpdateNotification(true, false);
|
|
|
|
|
|
} else if (needsDataUpdate) {
|
|
|
|
|
|
updateLink.textContent = 'New translations available!';
|
|
|
|
|
|
updateLink.style.color = '#F44336';
|
|
|
|
|
|
showUpdateNotification(false, true);
|
|
|
|
|
|
|
|
|
|
|
|
if (newData) {
|
|
|
|
|
|
translationData = newData;
|
|
|
|
|
|
log('success', 'Updated translation data', { entries: newData.length });
|
|
|
|
|
|
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';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
updateLink.textContent = 'Check for updates';
|
|
|
|
|
|
updateLink.style.color = '#1a73e8';
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log('info', 'No updates available');
|
|
|
|
|
|
updateLink.textContent = 'No updates available ✓';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
|
|
|
|
let message = '';
|
|
|
|
|
|
if (hasVersionUpdate && hasDataUpdate) {
|
|
|
|
|
|
message = 'New version and translations available!';
|
|
|
|
|
|
} else if (hasVersionUpdate) {
|
|
|
|
|
|
message = 'New version available!';
|
|
|
|
|
|
} else if (hasDataUpdate) {
|
|
|
|
|
|
message = 'New translations available!';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
document.getElementById('csv-translator-dismiss').addEventListener('click', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
document.body.removeChild(notification);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (hasVersionUpdate) {
|
2025-02-16 03:54:23 +07:00
|
|
|
|
document.getElementById('csv-translator-update').addEventListener('click', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
window.open(CONFIG.metadata.repository, '_blank');
|
|
|
|
|
|
document.body.removeChild(notification);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (document.body.contains(notification)) {
|
|
|
|
|
|
document.body.removeChild(notification);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 10000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
init();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-16 03:54:23 +07:00
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
2025-02-16 03:37:19 +07:00
|
|
|
|
log('info', 'DOMContentLoaded event fired');
|
|
|
|
|
|
try {
|
|
|
|
|
|
new TranslatorTool();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
log('error', 'Error initializing tool:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log('info', 'Script loaded. Current document.readyState:', document.readyState);
|