mirror of
https://github.com/YuzuZensai/Crowdin-Localization-Tools.git
synced 2026-01-30 12:32:39 +00:00
✨ feat: Version 1.1.0
This commit is contained in:
220
script.user.js
220
script.user.js
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Crowdin Localization Tools
|
// @name Crowdin Localization Tools
|
||||||
// @namespace https://yuzu.kirameki.cafe/
|
// @namespace https://yuzu.kirameki.cafe/
|
||||||
// @version 1.0.7
|
// @version 1.1.0
|
||||||
// @description A tool for translating Crowdin projects using a CSV file
|
// @description A tool for translating Crowdin projects using a CSV file
|
||||||
// @author Yuzu (YuzuZensai)
|
// @author Yuzu (YuzuZensai)
|
||||||
// @match https://crowdin.com/editor/*
|
// @match https://crowdin.com/editor/*
|
||||||
@@ -27,6 +27,7 @@ const CONFIG = {
|
|||||||
// Update check
|
// Update check
|
||||||
updateCheckUrl:
|
updateCheckUrl:
|
||||||
"https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/main/data/version.json",
|
"https://raw.githubusercontent.com/YuzuZensai/Crowdin-Localization-Tools/main/data/version.json",
|
||||||
|
autoUpdateInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
|
||||||
// Remote CSV
|
// Remote CSV
|
||||||
remoteCSVUrl:
|
remoteCSVUrl:
|
||||||
@@ -44,7 +45,7 @@ const CONFIG = {
|
|||||||
fuzzyThreshold: 0.7,
|
fuzzyThreshold: 0.7,
|
||||||
|
|
||||||
metadata: {
|
metadata: {
|
||||||
version: "1.0.7",
|
version: "1.1.0",
|
||||||
repository: "https://github.com/YuzuZensai/Crowdin-Localization-Tools",
|
repository: "https://github.com/YuzuZensai/Crowdin-Localization-Tools",
|
||||||
authorGithub: "https://github.com/YuzuZensai",
|
authorGithub: "https://github.com/YuzuZensai",
|
||||||
},
|
},
|
||||||
@@ -174,12 +175,13 @@ function TranslatorTool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const predefinedColors = {
|
const predefinedColors = {
|
||||||
UI: "#2196F3",
|
UI: "#c6dbe1",
|
||||||
"Unity / 3D": "#9C27B0",
|
"Unity / 3D": "#3d3d3d",
|
||||||
"VRChat Specific": "#4CAF50",
|
"Trust Rank": "#e6cff2",
|
||||||
"Trust Rank": "#FF9800",
|
"Instance Type": "#d4edbc",
|
||||||
"Instance Type": "#795548",
|
"Avatar Performance Rank": "#ffc8aa",
|
||||||
"Avatar Performance Rank": "#F44336",
|
"VRChat Specific": "#bfe1f6",
|
||||||
|
"Common": "#e6e6e6",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (predefinedColors[category]) {
|
if (predefinedColors[category]) {
|
||||||
@@ -198,10 +200,75 @@ function TranslatorTool() {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isColorBright(color) {
|
||||||
|
// Convert hex to RGB
|
||||||
|
let r, g, b;
|
||||||
|
if (color.startsWith('#')) {
|
||||||
|
const hex = color.replace('#', '');
|
||||||
|
r = parseInt(hex.substr(0, 2), 16);
|
||||||
|
g = parseInt(hex.substr(2, 2), 16);
|
||||||
|
b = parseInt(hex.substr(4, 2), 16);
|
||||||
|
} else if (color.startsWith('hsl')) {
|
||||||
|
// 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;
|
||||||
|
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;
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
const p = 2 * l - q;
|
||||||
|
r = hue2rgb(p, q, h + 1/3) * 255;
|
||||||
|
g = hue2rgb(p, q, h) * 255;
|
||||||
|
b = hue2rgb(p, q, h - 1/3) * 255;
|
||||||
|
}
|
||||||
|
} 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)
|
||||||
|
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));
|
||||||
|
|
||||||
|
// Calculate YIQ
|
||||||
|
const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||||
|
|
||||||
|
// 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) {
|
function createCategoryChip(category) {
|
||||||
if (!category) return "";
|
if (!category) return "";
|
||||||
|
|
||||||
const color = generateColorForCategory(category);
|
const color = generateColorForCategory(category);
|
||||||
|
const textColor = isColorBright(color) ? "#000000" : "#ffffff";
|
||||||
return `<span style="
|
return `<span style="
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@@ -209,7 +276,7 @@ function TranslatorTool() {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
background-color: ${color};
|
background-color: ${color};
|
||||||
color: white;
|
color: ${textColor};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
">${category}</span>`;
|
">${category}</span>`;
|
||||||
}
|
}
|
||||||
@@ -227,8 +294,13 @@ function TranslatorTool() {
|
|||||||
fetchRemoteCSV(CONFIG.remoteCSVUrl);
|
fetchRemoteCSV(CONFIG.remoteCSVUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
log("info", "Running automatic update check");
|
||||||
|
checkForUpdates();
|
||||||
|
}, CONFIG.autoUpdateInterval);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
checkForEditorContent();
|
checkForEditorContent(true);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
log(
|
log(
|
||||||
@@ -813,7 +885,7 @@ function TranslatorTool() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForEditorContent() {
|
function checkForEditorContent(forceRefresh = false) {
|
||||||
if (!visible || translationData.length === 0) {
|
if (!visible || translationData.length === 0) {
|
||||||
log("debug", "Skipping editor content check", {
|
log("debug", "Skipping editor content check", {
|
||||||
visible: visible,
|
visible: visible,
|
||||||
@@ -825,7 +897,7 @@ function TranslatorTool() {
|
|||||||
try {
|
try {
|
||||||
var content = parseEditorContent();
|
var content = parseEditorContent();
|
||||||
if (content && content.fullText) {
|
if (content && content.fullText) {
|
||||||
if (content.fullText !== lastSearchedText) {
|
if (content.fullText !== lastSearchedText || forceRefresh) {
|
||||||
lastSearchedText = content.fullText;
|
lastSearchedText = content.fullText;
|
||||||
|
|
||||||
const currentStringLabel = document.getElementById(
|
const currentStringLabel = document.getElementById(
|
||||||
@@ -852,8 +924,6 @@ function TranslatorTool() {
|
|||||||
length: content.fullText.length,
|
length: content.fullText.length,
|
||||||
});
|
});
|
||||||
findMatches(content.fullText);
|
findMatches(content.fullText);
|
||||||
} else {
|
|
||||||
log("debug", "Editor content unchanged");
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log("debug", "No valid editor content found");
|
log("debug", "No valid editor content found");
|
||||||
@@ -939,14 +1009,14 @@ function TranslatorTool() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.fullText) {
|
// if (result.fullText) {
|
||||||
log("debug", "Successfully parsed editor content", {
|
// log("debug", "Successfully parsed editor content", {
|
||||||
length: result.fullText.length,
|
// length: result.fullText.length,
|
||||||
stringId: result.stringId || "none",
|
// stringId: result.stringId || "none",
|
||||||
});
|
// });
|
||||||
} else {
|
// } else {
|
||||||
log("debug", "No text content found in editor");
|
// log("debug", "No text content found in editor");
|
||||||
}
|
// }
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -988,16 +1058,31 @@ function TranslatorTool() {
|
|||||||
function fetchRemoteCSV(url) {
|
function fetchRemoteCSV(url) {
|
||||||
log("info", "Fetching remote CSV from", { url: url });
|
log("info", "Fetching remote CSV from", { url: url });
|
||||||
GM_xmlhttpRequest({
|
GM_xmlhttpRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: url,
|
url: url,
|
||||||
onload: function (response) {
|
onload: function (response) {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
parseCSV(response.responseText);
|
try {
|
||||||
currentCSVSource = url;
|
const newData = parseCSVToArray(response.responseText);
|
||||||
log("success", "Successfully loaded remote CSV");
|
translationData = newData;
|
||||||
|
currentCSVSource = url;
|
||||||
|
|
||||||
|
log("debug", "Translation data", {
|
||||||
|
translationData: JSON.stringify(translationData),
|
||||||
|
newData: JSON.stringify(newData)
|
||||||
|
});
|
||||||
|
|
||||||
|
log("success", "Successfully loaded remote CSV", {
|
||||||
|
entries: translationData.length
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (csvError) {
|
||||||
|
log("error", "Error parsing CSV data", csvError);
|
||||||
|
updateResults("Error parsing CSV data. Please check the file format and try again.");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log("error", "Failed to fetch remote CSV", {
|
log("error", "Failed to fetch remote CSV", {
|
||||||
status: response.status,
|
status: response.status
|
||||||
});
|
});
|
||||||
updateResults(
|
updateResults(
|
||||||
"Failed to fetch remote CSV. Please check the URL and try again."
|
"Failed to fetch remote CSV. Please check the URL and try again."
|
||||||
@@ -1009,7 +1094,7 @@ function TranslatorTool() {
|
|||||||
updateResults(
|
updateResults(
|
||||||
"Error fetching remote CSV. Please check your connection and try again."
|
"Error fetching remote CSV. Please check your connection and try again."
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,28 +1229,39 @@ function TranslatorTool() {
|
|||||||
|
|
||||||
var words = text.split(/\s+/);
|
var words = text.split(/\s+/);
|
||||||
var matches = [];
|
var matches = [];
|
||||||
var seenWords = new Set();
|
var seenCombinations = new Set();
|
||||||
|
|
||||||
words.forEach(function (word) {
|
words.forEach(function (word) {
|
||||||
// Clean the word from punctuation
|
if (!word) return;
|
||||||
var cleanWord = word.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
|
|
||||||
if (!cleanWord || cleanWord.length <= 1) return; // Skip single characters
|
|
||||||
|
|
||||||
if (seenWords.has(cleanWord.toLowerCase())) return;
|
// First try exact match with punctuation
|
||||||
seenWords.add(cleanWord.toLowerCase());
|
|
||||||
|
|
||||||
// Find matches
|
|
||||||
translationData.forEach(function (entry) {
|
translationData.forEach(function (entry) {
|
||||||
|
const uniqueKey = `${entry.source.toLowerCase()}_${entry.category || 'default'}`;
|
||||||
|
|
||||||
|
// Exact match (case-insensitive)
|
||||||
|
if (entry.source.toLowerCase() === word.toLowerCase() && !seenCombinations.has(uniqueKey)) {
|
||||||
|
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
|
||||||
|
|
||||||
// For short words (2-3 chars), use stricter matching
|
// For short words (2-3 chars), use stricter matching
|
||||||
if (cleanWord.length <= 3) {
|
if (cleanWord.length <= 3) {
|
||||||
// Only match if it's a complete word match or surrounded by word boundaries
|
// Only match if it's a complete word match or surrounded by word boundaries
|
||||||
const regex = new RegExp(`\\b${cleanWord}\\b`, "i");
|
const regex = new RegExp(`\\b${cleanWord}\\b`, "i");
|
||||||
if (
|
if (
|
||||||
regex.test(entry.source) &&
|
regex.test(entry.source) &&
|
||||||
!matches.some(function (m) {
|
!seenCombinations.has(uniqueKey)
|
||||||
return m.entry.source === entry.source;
|
|
||||||
})
|
|
||||||
) {
|
) {
|
||||||
|
seenCombinations.add(uniqueKey);
|
||||||
matches.push({
|
matches.push({
|
||||||
entry: entry,
|
entry: entry,
|
||||||
score: 1,
|
score: 1,
|
||||||
@@ -1180,10 +1276,9 @@ function TranslatorTool() {
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
score >= CONFIG.fuzzyThreshold &&
|
score >= CONFIG.fuzzyThreshold &&
|
||||||
!matches.some(function (m) {
|
!seenCombinations.has(uniqueKey)
|
||||||
return m.entry.source === entry.source;
|
|
||||||
})
|
|
||||||
) {
|
) {
|
||||||
|
seenCombinations.add(uniqueKey);
|
||||||
matches.push({
|
matches.push({
|
||||||
entry: entry,
|
entry: entry,
|
||||||
score: score,
|
score: score,
|
||||||
@@ -1194,8 +1289,14 @@ function TranslatorTool() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Sort matches by score first, then by category
|
||||||
matches.sort(function (a, b) {
|
matches.sort(function (a, b) {
|
||||||
if (b.score === a.score) {
|
if (b.score === a.score) {
|
||||||
|
// 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
|
||||||
return b.matchedWord.length - a.matchedWord.length;
|
return b.matchedWord.length - a.matchedWord.length;
|
||||||
}
|
}
|
||||||
return b.score - a.score;
|
return b.score - a.score;
|
||||||
@@ -1210,7 +1311,7 @@ function TranslatorTool() {
|
|||||||
if (!query || query.length <= 1) {
|
if (!query || query.length <= 1) {
|
||||||
updateResults("");
|
updateResults("");
|
||||||
lastSearchedText = "";
|
lastSearchedText = "";
|
||||||
checkForEditorContent();
|
checkForEditorContent(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1360,19 +1461,7 @@ function TranslatorTool() {
|
|||||||
|
|
||||||
// Add category chip first if in note column
|
// Add category chip first if in note column
|
||||||
if (!isSource && !isTarget && category) {
|
if (!isSource && !isTarget && category) {
|
||||||
var chip = document.createElement("div");
|
container.innerHTML += createCategoryChip(category);
|
||||||
const color = generateColorForCategory(category);
|
|
||||||
chip.style.backgroundColor = color;
|
|
||||||
chip.style.color = "white";
|
|
||||||
chip.style.padding = "1px 6px";
|
|
||||||
chip.style.borderRadius = "10px";
|
|
||||||
chip.style.fontSize = "10px";
|
|
||||||
chip.style.whiteSpace = "nowrap";
|
|
||||||
chip.style.fontWeight = "500";
|
|
||||||
chip.style.opacity = "0.9";
|
|
||||||
chip.style.width = "fit-content";
|
|
||||||
chip.textContent = category;
|
|
||||||
container.appendChild(chip);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add main text
|
// Add main text
|
||||||
@@ -1504,9 +1593,18 @@ function TranslatorTool() {
|
|||||||
if (csvResponse.status === 200) {
|
if (csvResponse.status === 200) {
|
||||||
try {
|
try {
|
||||||
const newData = parseCSVToArray(csvResponse.responseText);
|
const newData = parseCSVToArray(csvResponse.responseText);
|
||||||
const needsDataUpdate =
|
|
||||||
JSON.stringify(translationData) !==
|
function isEqual(obj1, obj2) {
|
||||||
JSON.stringify(newData);
|
return JSON.stringify(obj1) === JSON.stringify(obj2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsDataUpdate = !isEqual(translationData, newData);
|
||||||
|
|
||||||
|
log("debug", "Translation data", {
|
||||||
|
translationData: JSON.stringify(translationData),
|
||||||
|
newData: JSON.stringify(newData),
|
||||||
|
});
|
||||||
|
|
||||||
log("info", "CSV check complete", {
|
log("info", "CSV check complete", {
|
||||||
needsUpdate: needsDataUpdate,
|
needsUpdate: needsDataUpdate,
|
||||||
currentEntries: translationData.length,
|
currentEntries: translationData.length,
|
||||||
@@ -1635,7 +1733,7 @@ function TranslatorTool() {
|
|||||||
updateLink.style.color = "#F44336";
|
updateLink.style.color = "#F44336";
|
||||||
showUpdateNotification(true, false);
|
showUpdateNotification(true, false);
|
||||||
} else if (needsDataUpdate) {
|
} else if (needsDataUpdate) {
|
||||||
updateLink.textContent = "New translations available!";
|
updateLink.textContent = "New translations applied!";
|
||||||
updateLink.style.color = "#F44336";
|
updateLink.style.color = "#F44336";
|
||||||
showUpdateNotification(false, true);
|
showUpdateNotification(false, true);
|
||||||
|
|
||||||
@@ -1649,7 +1747,7 @@ function TranslatorTool() {
|
|||||||
updateLink.style.color = "#4CAF50";
|
updateLink.style.color = "#4CAF50";
|
||||||
// Trigger content check after updating data
|
// Trigger content check after updating data
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
checkForEditorContent();
|
checkForEditorContent(true);
|
||||||
}, 500);
|
}, 500);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateLink.textContent = "Check for updates";
|
updateLink.textContent = "Check for updates";
|
||||||
|
|||||||
Reference in New Issue
Block a user