MediaWiki:Gadget-TargetedTranslations.js

From Linguifex
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">
				&#32;
				[<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();