MediaWiki:Gadget-AnonymousI18N.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * AnonymousI18N
 *
 * This script is responsible for the following:
 *
 * - Create a "Language select" menu in the sidebar which can add '&uselang=' to addresses
 *   and saves the user's choice in a temporary cookie.
 * - Make suggestions to users from urls without '&uselang' through a blue bar
 *   suggesting "View Wikimedia Commons in [language]" on top of the page. Its suggestions are
 *   based on the referral URL, and the browser's language setting.
 * - Regardless of suggestions and cookies, if your current url includes a
 *   'uselang' query string, and click on a link, it will append the same 'uselang'
 *   value to clicked link, unless that link already has a 'uselang' parameter.
 *
 * Canonical source code at <https://commons.wikimedia.org/wiki/MediaWiki:AnonymousI18N.js>.
 * @license MIT <https://opensource.org/licenses/MIT>
 * @tracking [[File:Krinkle_AnonymousI18N.js]]
 *
 * -------
 *
 * Copyright 2010-2019 Timo Tijhof
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
/*jshint curly:false, smarttabs:true, undef:true, browser:true */
/*global mw, $, wpAvailableLanguages, wgAnonUserLanguage, AnonymousI18N */
(function () {
var conf = mw.config.get([
	'skin',
	'wgUserLanguage',
	'wgContentLanguage',
	'wgSiteName'
]);

// Don't load in skins other than Vector
// Don't load twice
if ((conf.skin !== 'vector' && conf.skin !== 'vector-2022') || window.AnonymousI18N) {
	return;
}

// Hook for tools to suggest a language based on another system
// Commented out, other scripts may set it at any point in time
// (before this script loads).
// This commented-out declaration is here as a reminder.
//window.AnonymousI18N_suggestlang = null;

window.AnonymousI18N = {

	// Current revision, used for development, cache clearing and tracking
	rev: 'r16',

	// Name of the cookie that stores the language of choice
	cookie_lang: 'AnonymousI18N_lang',

	// Name of the cookie that stores wether the user has declined the notice
	cookie_decline: 'AnonymousI18N_decline',

	// Number of days new cookie's get as expiration
	cookie_expiration: 10,

	// Page with more documentation
	documentation: 'https://meta.wikimedia.org/wiki/User:Krinkle/Scripts/AnonymousI18N',

	/**
	 * Dear editors that add/remove/modify translations below,
	 *
	 * README:
	 * Please take great care to the commas.
	 * There should be a comma *after each definition* except for the *last entry*.
	 * This means if you remove the last line you have to remove the comma from the, now last, definition.
	 * This means if you add a line to the bottom that this line should NOT end in a comma
	 *    and the former last line should get a comma to the end !
	 */
	msgHelp: {
		'bn': 'ভাষা নির্বাচন',
		'ca': 'Llengua',
		'cs': 'Výběr jazyka',
		'da': 'Vælg sprog',
		'de': 'Sprachauswahl',
		'en': 'Language select',
		'eo': 'Lingvoelekto',
		'es': 'Seleccionar idioma',
		'et': 'Keele valimine',
		'fa': 'انتخاب زبان',
		'fi': 'Valitse kieli',
		'fr': 'Sélecteur de langue',
		'gl': 'Seleccionar idioma',
		'hu': 'Nyelvválasztás',
		'hr': 'Odaberi jezik',
		'id': 'Pilih bahasa',
		'ja': '言語選択',
		'ko': '언어 선택',
		'min': 'Piliah bahaso',
		'mk': 'Јазик',
		'ml': 'ഭാഷ തിരഞ്ഞെടുക്കുക',
		'nds': 'Spraakutwahl',
		'nl': 'Taal',
		'pl': 'Wybierz język',
		'pt': 'Seleção do idioma',
		'ru': 'Выбор языка',
		'sl': 'Izbira jezika',
		'sv': 'Välj språk',
		'tr': 'Dil seçimi',
		'zh-hans': '语言选择',
		'zh-hant': '語言選擇'
	},
	msgReset: {
		'bn': 'ভাষা পুনঃনির্ধারণ করুন',
		'ca': 'Anul·la',
		'cs': 'Vymazat paměť',
		'en': 'Reset language',
		'de': 'Auswahl zurücksetzen',
		'es': 'Reiniciar el idioma',
		'et': 'Puhasta mälu',
		'fa': 'پاک\u200cکردن حافظه',
		'fi': 'Unohda valinta',
		'fr': 'Réinitialiser',
		'gl': 'Reiniciar o idioma',
		'hu': 'Nyelv visszaállítása',
		'hr': 'Vrati na početne postavke',
		'id': 'Setel ulang bahasa',
		'ja': '言語をリセット',
		'ko': '언어 초기화',
		'min': 'Setel ulang bahaso',
		'mk': 'Врати',
		'nl': 'Geheugen wissen',
		'pt': 'Limpar a memória',
		'sl': 'Ponastavitev jezika',
		'sv': 'Rensa minnet',
		'tr': 'Dili sıfırla',
		'zh-hans': '重置语言',
		'zh-hant': '重置語言'
	},
	msgSuggest: { // $1 = site name, $2 = link containing language name (see below)
		'bn': '$1 $2 ভাষায় দেখুন',
		'ca': 'Vegeu $1 en $2',
		'cs': '$1 je dostupné $2',
		'da': 'Se $1 på $2',
		'de': '$1 auf $2',
		'en': 'View $1 in $2',
		'es': '$1 está disponible en $2',
		'et': 'Kuva $1 $2.',
		'fa': '$1 در $2 ببینید',
		'fi': '$1 on käytettävissä $2.',
		'fr': '$1 est disponible $2.',
		'gl': '$1 está dispoñible en $2',
		'hu': 'A $1 megtekintése $2 nyelven',
		'hr': 'Vidi $1 na $2',
		'id': 'Lihat $1 dalam $2',
		'ja': '$1を$2で表示',
		'ko': '$1를 $2로 보기',
		'min': 'Caliak $1 dalam $2',
		'mk': '$1 на $2',
		'nl': '$1 in het $2',
		'pl': '$1 można przeglądać $2',
		'pt': '$1 está disponível em $2.',
		'ru': '$1 доступен $2',
		'sl': 'Projekt $1 v $2',
		'sv': 'Visa $1 på $2',
		'tr': '$1 lehçesi $2',
		'zh-hans': '用$2浏览$1',
		'zh-hant': '用$2瀏覽$1'
	},
	// If the plain language name as shown in the select box doesn't work for the suggestion box link,
	// ("$2" above), you may specify a custom link text below.  XXX: The link text does _not_ fall back
	// from "aa-bb" to "aa", but instead to the language/dialect name from wpSupportedLanguages.  This
	// means that if you add custom link texts for any languages that have dialects supported by
	// MediaWiki, you'll probably need to add them for the dialects too.
	msgSuggestLink: {
		'ca': 'català',
		'cs': 'v češtině',
		'et': 'eesti keeles',
		'fi': 'suomeksi',
		'fr': 'en français',
		'mk': 'македонски',
		'pl': 'w języku polskim',
		'ru': 'на русском языке',
		'sl': 'slovenščini',
		'sv': 'svenska'
	},

	/**
	 * Get user language
	 *
	 * Priority:
	 *
	 * 1. Cookie
	 * 2. Hook (optional)
	 * 3. Browser language
	 * 4. Default user language (fallback)
	 */
	getUserLanguage: function() {
		/*global AnonymousI18N_suggestlang */
		var language;

		// Get user language (uselang=)
		if (conf.wgUserLanguage !== conf.wgContentLanguage) {
			language = conf.wgUserLanguage;
		}

		// Get cookie value
		if (!language) {
			language = mw.cookie.get(this.cookie_lang);
		}

		// If a hook or user/site override provided a suggestion, then use it
		if (!language && typeof AnonymousI18N_suggestlang === 'string') {
			language = AnonymousI18N_suggestlang;
		}

		// Get browser language
		if (!language) {
			language = navigator.userLanguage || navigator.language || navigator.browserLanguage || '';
		}

		// Convert to lower case
		language = language.toLowerCase();

		// Check if the (possibly dashed) code is an available language code
		if (typeof wpAvailableLanguages[language] === 'string') {
			return language;
		} else {
			// convert 'aa-BB' to 'aa' etc.
			language = language.split('-')[0];
			if (typeof wpAvailableLanguages[language] === 'string') {
				return language;
			} else {
				// The extracted code isn't available, fallback to default
				return conf.wgUserLanguage;
			}
		}

	},

	getLangDropdown: function() {
		// Begin dropdown
		var dropdownHTML = '<select name="mw-AnonymousI18N-picker" id="mw-AnonymousI18N-picker">';

		// Add option for each language
		$.each(wpAvailableLanguages, function(key, val) {
			if (key === conf.wgUserLanguage) {
				dropdownHTML += mw.html.element('option', { selected: 'selected', value: key }, val);
			} else {
				dropdownHTML += mw.html.element('option', { value: key }, val);
			}
		});

		// End dropdown
		var isCookieSet = !!mw.cookie.get(this.cookie_lang);
		dropdownHTML += '</select>' + (isCookieSet ?
			'<br /><a id="mw-AnonymousI18N-reset" href="#">' + (this.msgReset[wgAnonUserLanguage] || this.msgReset[wgAnonUserLanguage.split('-')[0]] || this.msgReset
			.en) + '</a>' : '');

		return dropdownHTML;
	},

	// Redirect location to another language of the current page
	redirectCurrentPageToLanguage: function(lang) {

		// If there is no lang passed, redirect to uselang-less page
		if (!lang) {
			location.href = location.protocol + '//' + location.host + location.pathname;
			return true;
		}

		// If there is no query string yet, create one
		if (location.search === '') {
			location.href = location.protocol + '//' + location.host + location.pathname + '?uselang=' + encodeURIComponent(lang) + location.hash;

			// There is a query string already
		} else {

			// If there's a uselang= already, replace it
			if (location.search.indexOf('uselang=') !== -1) {
				var search = location.search.split('uselang=' + encodeURIComponent(conf.wgUserLanguage)).join('uselang=' + encodeURIComponent(lang));
				location.href = location.protocol + '//' + location.host + location.pathname + search + location.hash;

				// Otherwise, add it
			} else {
				location.href = location.protocol + '//' + location.host + location.pathname + location.search + '&uselang=' + encodeURIComponent(lang) +
					location.hash;
			}
		}
	},

	// Redirect location to another language of a given url
	getUrlForLanguage: function(url, lang) {

		// Cut out hash if present
		if (url.indexOf('#') !== -1) {
			url = url.substr(0, url.indexOf('#'));
		}

		// Check if URL contains query string
		var m = url.match(/\?[^#]*/);
		if (m !== null && m[0] !== null) {

			// There is a query string already
			if (url.indexOf('uselang=') !== -1) {
				// If there's a uselang= already, replace it
				return url.split('uselang=' + encodeURIComponent(conf.wgUserLanguage)).join('uselang=' + encodeURIComponent(lang));
			} else {
				// Otherwise, add it
				return url + '&uselang=' + encodeURIComponent(lang);
			}
		} else {
			// If there is no query string yet, create one
			return url + '?uselang=' + encodeURIComponent(lang);
		}
	},

	init: function() {

		window.wgAnonUserLanguage = this.getUserLanguage();

		// If this language is suggested by special means (e.g. AnonymousI18N_suggestlang), then
		// redirect to that language now.
		// However since the language suggestion/override came from AnonymousI18N_suggestlang, we
		// should not set a cookie, because the user may have already made a (different) language choice and
		// is simply following an interwiki link to Commons. In this case AnonymousI18N preserves the
		// 'uselang'-parameter without the need for a cookie.
		// Another advantage of not storing the cookie is one can have multiple browser windows open, navigating
		// their own path and preserving different languages.

		// Insert Language-portal
		var portalHTML = ' <div class="portal persistent vector-menu-portal" id="p-AnonymousI18N"><h3 class="vector-menu-heading">' + (this.msgHelp[wgAnonUserLanguage] || this.msgHelp[
			wgAnonUserLanguage.split('-')[0]] || this.msgHelp.en) + '</h3><div class="body">' + this.getLangDropdown() + '</div></div>';
		$('#p-navigation').after(portalHTML);
		mw.util.addCSS(
			' #p-AnonymousI18N { overflow:hidden /* other browsers */; } ' +
			' #p-AnonymousI18N select { width:86% /* modern browsers */; margin-top: 2px; } ' +
			' #mw-AnonymousI18N-reset { font-size: 0.6em }');

		// Show suggestion if:
		// * the gotten language is an available language on the wiki
		// * the gotten is not the current language already
		// * there is no cookie present that represents the choice to decline suggestions
		if (typeof wpAvailableLanguages[wgAnonUserLanguage] === 'string' &&
			// Only render the blue bar when our suggestion is significantly different from the current language
			// E.g. don't suggest 'en-gb' when browsing on 'en', same for 'de-' etc. The difference is not as big,
			// and might even be intentional.
			conf.wgUserLanguage.split('-')[0] !== wgAnonUserLanguage.split('-')[0] &&
			mw.cookie.get(AnonymousI18N.cookie_decline) !== wgAnonUserLanguage + '-true')
		{
			// For the suggestion text, fall back first to undashed language code, then to English;
			// for the link, use name from wpAvailableLanguages if no custom link text is defined.
			// This may produce odd combinations like "View Wikimedia Commons in Suomi", but that's
			// still better than using the wrong language name entirely.
			var msgText = this.msgSuggest[wgAnonUserLanguage] || this.msgSuggest[wgAnonUserLanguage.split('-')[0]] || this.msgSuggest.en;
			var msgLink = this.msgSuggestLink[wgAnonUserLanguage] || wpAvailableLanguages[wgAnonUserLanguage];

			var link = mw.html.element('a', { href: '#', id: 'mw-AnonymousI18N-gosuggest' }, msgLink);
			var html = mw.html.escape(msgText).replace('$1', conf.wgSiteName).replace('$2', link);

			$('#bodyContent').before(
				'<div id="mw-AnonymousI18N-suggest" style="padding:0.3em 1em; font-size:0.8em; background:#F8F8FF; margin:0px 0px 0.5em 0px; border:1px solid #CCCCFF; text-align:center; position:relative;"><small style="position:absolute; top:0.3em; right:1em"><a href="#" id="mw-AnonymousI18N-decline"><img src="//upload.wikimedia.org/wikipedia/commons/thumb/3/36/CloseWindow.svg/17px-CloseWindow.svg.png" data-file-width="16" data-file-height="16" height="16" width="16"></a></small> ' +
				html + '</div>');

			$('#mw-AnonymousI18N-gosuggest').on('click', function() {
				mw.cookie.set(AnonymousI18N.cookie_lang, wgAnonUserLanguage, {
					expires: AnonymousI18N.cookie_expiration,
					path: '/'
				});
				if (wgAnonUserLanguage !== conf.wgUserLanguage) {
					AnonymousI18N.redirectCurrentPageToLanguage(wgAnonUserLanguage);
				}
				return false;
			});

			$('#mw-AnonymousI18N-decline').on('click', function() {
				// Remove the suggestion notive
				$('#mw-AnonymousI18N-suggest').remove();
				// Set a cookie to no longer suggest this language
				mw.cookie.set(AnonymousI18N.cookie_decline, wgAnonUserLanguage + '-true', {
					expires: AnonymousI18N.cookie_expiration,
					path: '/'
				});
				return false;
			});
		}

		$('#mw-AnonymousI18N-picker').on('change', function() {

			var lang = this.value;

			if (lang !== '') {
				mw.cookie.set(AnonymousI18N.cookie_lang, lang, {
					expires: AnonymousI18N.cookie_expiration,
					path: '/'
				});
				if (lang !== conf.wgUserLanguage) {
					AnonymousI18N.redirectCurrentPageToLanguage(lang);
				} else {
					$('#mw-AnonymousI18N-suggest').remove();
				}
			} // else { /* alert( 'No valid language selected.' ); */ }

			return false;
		});

		$('#mw-AnonymousI18N-reset').on('click', function() {

			mw.cookie.set(AnonymousI18N.cookie_lang, null, {
				expires: AnonymousI18N.cookie_expiration,
				path: '/'
			});
			mw.cookie.set(AnonymousI18N.cookie_decline, null, {
				expires: AnonymousI18N.cookie_expiration,
				path: '/'
			});
			AnonymousI18N.redirectCurrentPageToLanguage(false);
			return false;

		});

		// When `uselang=` is used (i.e. user have chosen a language other than English)
		// we alter the links and form submissions to keep the value of `uselang=`
		if (conf.wgUserLanguage !== conf.wgContentLanguage) {
			// Don't manipulate all anchor-elements
			// Instead listen for any anchor-clicks and maintain uselang when and where needed
			// This way also dynamically added links (such as tabs and toolbox-links) are accounted for
			$(document).on('click', 'a[href]', function(e) {
				var href_attr = $(this).attr('href'),
					// by going to element directlty instead of attribute the urls become complete
					// this makes '/wiki/Article' into 'http://domain.org/wiki/Article'
					// but also '#' into 'http://domain.org/wiki/Article#' !
					href_full = this.href;

				// If
				// * the link destination is a true url (ie. don't interrupt #toc links or 'javascript:call()' stuff)
				// * links to a domain within wikimedia such as http://nl.wiktionary, http://en.wikipedia
				// * The choosen language is not the default (which makes uselang redundant)
				if (href_attr.substr(0, 1) !== '#' &&
					href_attr !== '' &&
					href_full.substr(0, 4) === 'http' &&
					href_full.indexOf('.wik') !== -1 &&
					href_full.indexOf('.org') !== -1 &&
					href_full.indexOf('Special:Set') === -1 &&  // Fix for Wikidata UI
					e.ctrlKey !== true && e.metaKey !== true  // Don't abort attempts to open links in a new window/tab
				) {
					// Prevent the default linking functionalty and instead use getUrlForLanguage()
					e.preventDefault();
					location.href = AnonymousI18N.getUrlForLanguage(href_full, wgAnonUserLanguage);
				} // Else: Don't interrupt and follow link regularly
			});

			// Don't manipulate all form-elements
			// Listen for submit()'s and work the uselang in there
			// Works on edit pages (Save, Preview, Diff) - not for <inputbox>'es and Search (yet) though
			$(document).on('submit', 'form', function() {
				var $el = $(this);
				$el.attr('action', function() {
					var action = $el.attr('action');
					// Browsers ignore any query parameter that is appended to the action attribute when using GET
					if (action) action = AnonymousI18N.getUrlForLanguage($el.attr('action'), wgAnonUserLanguage);
					// so also inject an input. See http://stackoverflow.com/a/1116026/2683737
					if (!$el.find('input[name="uselang"]').length) $('<input type="hidden" name="uselang" />').val(wgAnonUserLanguage).appendTo($el);
					return action;
				});
			});
		}
	}
};

/**
 * referrerWikiUselang
 * @author Mdale
 * @created December 2, 2010
 *
 * Make a best guess at the user language based on the referring wiki.
 * To get the language call: referrerWikiUselang.getReferrerLang();
 * Returns a string with the language code or null if nothing was found in the referral.
 */
var referrerWikiUselang = {
	// The supported list of projects that include language codes at the start of their url
	projectList: ['wikipedia', 'wiktionary', 'wikinews', 'wikibooks', 'wikisource', 'wikiversity', 'wikiquote', 'wikivoyage'],

	// Some other common sites for which we happen to know its language
	alternateSites: {
		'http://www.wikiskripta.eu': 'cs'
	},

	/**
	 * Check if the language is supported
	 * @param {string} uselang The language code to check
	 * @return {boolean} true if language is supported, false if the language is not supported
	 */
	isSupportedRewriteLanguage: function(useLang) {
		// Check that the language is supported
		if (!(useLang in wpAvailableLanguages)) {
			return false;
		}
		// Just in case someone did not see documentation above ( don't rewrite default language )
		if (useLang === conf.wgContentLanguage) {
			return false;
		}
		return true;
	},
	/**
	 * Get the language code from wikimedia site the user came from
	 * @return {string} the referrer language code
	 */
	getReferrerLang: function() {
		var referrerLang;
		if (document.referrer) {
			// We should use mw.parseUri ( for now regEx )
			var matches = document.referrer.match(/^https?\:\/\/([^\.]*)\.([^\.]*)([^\/:]*)/);

			if (matches && $.inArray(matches[2], this.projectList) !== -1) {
				referrerLang = matches[1];
				if (this.isSupportedRewriteLanguage(referrerLang)) {
					return referrerLang;
				} else {
					return null;
				}
			} else if (matches && this.alternateSites[matches[0]] !== undefined) {
				return this.alternateSites[matches[0]];
			} else {
				return null;
			}
		}
		return null;
	}
};


$.when(mw.loader.using(['mediawiki.cookie', 'mediawiki.util']), $.ready).then(function() {
	if (!window.AnonymousI18N_suggestlang) {
		// Get the langcode of the referring wiki if there is one.
		// This will be used as extra suggestion by AnonymousI18N.getUserLanguage()
		window.AnonymousI18N_suggestlang = referrerWikiUselang.getReferrerLang();
	}
	// Verify the wpAvailableLanguages is set properly, then init stuff
	if (wpAvailableLanguages.en === 'English') {
		AnonymousI18N.init();
	}
});
})();