MediaWiki:Gadget-TargetedTranslations.js
Jump to navigation
Jump to search
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
// Initialize preferred languages from local storage.
const storedLangs = localStorage.getItem("targetedTranslationsLangs");
let preferredLanguages = new Set(storedLangs ? JSON.parse(storedLangs) : []);
// SVG icons for bookmark states.
const ICONS = {
unselected: `
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="17" viewBox="0 0 20 20"
fill="currentColor" style="vertical-align: text-top; cursor: pointer">
<g>
<path d="M5 1a2 2 0 00-2 2v16l7-5 7 5V3a2 2 0 00-2-2zm10 14.25-5-3.5-5 3.5V3h10z"/>
</g>
</svg>`,
selected: `
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="17" viewBox="0 0 20 20"
fill="currentColor" style="vertical-align: text-top; cursor: pointer">
<g>
<path d="M5 1a2 2 0 00-2 2v16l7-5 7 5V3a2 2 0 00-2-2z"/>
</g>
</svg>`,
};
// Cache all translation tables up front.
const translationTables = Array.from(
document.querySelectorAll("table.translations")
);
/**
* Iterate over each non-empty translation line (<li> or <dd>) and compute its language key.
* Calls callback(lineElem, langKey) for each valid line.
*/
function forEachTranslationLine(table, callback) {
const lines = table.querySelectorAll(
".translations-cell li, .translations-cell dd"
);
for (const line of lines) {
const text = line.textContent.trim();
// Must have “Lang: …” format.
if (!text.includes(":")) continue;
const [base, ...rest] = text.split(":");
const baseLang = base.trim();
if (!baseLang || rest.join(":").trim().length === 0) continue;
let fullKey = baseLang;
// If this <li> is actually a sub‐entry, its grandparent is an <li> whose text is the macrolanguage.
const containerLi =
line.parentElement.parentElement.tagName === "LI"
? line.parentElement.parentElement
: null;
if (containerLi) {
const parentText = containerLi.textContent.split(":")[0].trim();
fullKey = `${parentText}>${baseLang}`;
}
callback(line, fullKey);
}
}
/**
* Render the “— Lang: translation; …” snippet under each NavHead,
* based on preferredLanguages.
*/
function renderTargetedTranslations() {
// Remove any existing markers.
document
.querySelectorAll(".targeted-translations")
.forEach((el) => el.remove());
for (const table of translationTables) {
// *** FIXED: NavHead is the previous sibling of table.parentElement, not table itself.
const navHead = table.parentElement.previousElementSibling;
if (!navHead || !navHead.matches("div.NavHead")) {
continue;
}
const frag = document.createDocumentFragment();
let foundAny = false;
forEachTranslationLine(table, (line, langKey) => {
if (!preferredLanguages.has(langKey)) return;
if (foundAny) {
frag.append(document.createTextNode("; "));
}
foundAny = true;
const span = document.createElement("span");
const [macro, sub] = langKey.split(">");
if (sub) {
span.append(`${macro} (${sub}): `);
} else {
span.append(`${macro}: `);
}
// Clone all child nodes except the first text node (which holds the language name).
for (let i = 1; i < line.childNodes.length; i++) {
if (line.childNodes[i].tagName !== "DL") {
span.append(line.childNodes[i].cloneNode(true));
}
}
frag.append(span);
});
if (foundAny) {
const wrapper = document.createElement("span");
wrapper.className = "targeted-translations";
wrapper.append(" — ", frag);
navHead.append(wrapper);
}
}
}
/**
* Toggle a single icon’s class and update preferredLanguages.
*/
function toggleIcon(iconEl, langKey) {
if (preferredLanguages.has(langKey)) {
preferredLanguages.delete(langKey);
iconEl.classList.replace("tt-icon-selected", "tt-icon-unselected");
iconEl.innerHTML = ICONS.unselected;
} else {
preferredLanguages.add(langKey);
iconEl.classList.replace("tt-icon-unselected", "tt-icon-selected");
iconEl.innerHTML = ICONS.selected;
}
}
/**
* Enter “select mode” on a given table:
* - Convert each translation line into a clickable icon.
* - Change the button to “Save preferred languages.”
* - Show the “Clear all” link.
*/
function enterSelectMode(table, selectBtn, clearBtn) {
// If any other table is already in “save” mode, force it to save first.
document.querySelectorAll(".tt-button-select").forEach((btn) => {
if (btn !== selectBtn && btn.textContent === "Save preferred languages") {
btn.click();
}
});
selectBtn.textContent = "Save preferred languages";
clearBtn.parentElement.style.display = "";
forEachTranslationLine(table, (line, langKey) => {
const icon = document.createElement("span");
icon.setAttribute("role", "button");
icon.classList.add(
preferredLanguages.has(langKey)
? "tt-icon-selected"
: "tt-icon-unselected"
);
icon.innerHTML = preferredLanguages.has(langKey)
? ICONS.selected
: ICONS.unselected;
line.insertAdjacentElement("afterbegin", icon);
icon.addEventListener("click", () => toggleIcon(icon, langKey));
});
}
/**
* Exit “select mode” on a given table:
* - Save preferredLanguages → localStorage.
* - Remove all icons.
* - Re-render the targeted translations.
*/
function exitSelectMode(table, selectBtn, clearBtn) {
localStorage.setItem(
"targetedTranslationsLangs",
JSON.stringify([...preferredLanguages])
);
table
.querySelectorAll(".tt-icon-selected, .tt-icon-unselected")
.forEach((el) => el.remove());
selectBtn.textContent = "Select preferred languages";
clearBtn.parentElement.style.display = "none";
renderTargetedTranslations();
}
// ----------------------------------------------------
// INITIAL SETUP: insert the control row & wire up events
// ----------------------------------------------------
translationTables.forEach((table) => {
// Build the <tr> for “Select preferred languages” / “Clear all”.
const headerRow = document.createElement("tr");
headerRow.innerHTML = `
<td colspan="100%" style="font-size: 85%">
[<a role="button" class="tt-button-select">Select preferred languages</a>]
<span style="display: none">
 
[<a role="button" class="tt-button-clear">Clear all</a>]
</span>
</td>
`;
// Insert at the very top of <tbody>.
const tbody = table.firstElementChild;
tbody.insertBefore(headerRow, tbody.firstElementChild);
const selectBtn = table.querySelector(".tt-button-select");
const clearBtn = table.querySelector(".tt-button-clear");
selectBtn.addEventListener("click", () => {
if (selectBtn.textContent === "Select preferred languages") {
enterSelectMode(table, selectBtn, clearBtn);
} else {
exitSelectMode(table, selectBtn, clearBtn);
}
});
clearBtn.addEventListener("click", () => {
preferredLanguages.clear();
table.querySelectorAll(".tt-icon-selected").forEach((icon) => {
icon.classList.replace("tt-icon-selected", "tt-icon-unselected");
icon.innerHTML = ICONS.unselected;
});
});
});
// Initial display of preferred translations.
renderTargetedTranslations();