All files / mobile.languages.structured util.js

88.33% Statements 53/60
75% Branches 30/40
92.3% Functions 12/13
88.33% Lines 53/60

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247  1x 1x                                                                         10x 10x   10x         10x 10x 10x     10x 83x 1x       10x     10x   1x                   1x                           63x 63x                                                             10x 10x 10x 10x 10x 10x     10x 10x 1x               1x               93x 30x   63x         10x 10x 83x 18x 18x   65x                     10x 7x 10x     10x   10x         10x 33x                     143x     10x 10x                           10x   10x                     1x                         2x   2x   2x 2x      
var
	mfUtils = require( '../mobile.startup/util' ),
	rtlLanguages = require( './rtlLanguages' );
 
/**
 * @typedef {Object} Language
 * @prop {string} autonym of language e.g. français
 * @prop {string} langname in the user's current language e.g French
 * @prop {string} title of the page in the language e.g. Espagne
 * @prop {string} dir (rtl or ltr)
 * @prop {string} url of the page
 *
 * @typedef {Object} SuggestedLanguage
 * @prop {string} autonym of language e.g. français
 * @prop {string} langname in the user's current language e.g French
 * @prop {string} title of the page in the language e.g. Espagne
 * @prop {string} dir (rtl or ltr)
 * @prop {string} url of the page
 * @prop {number} frequency of times the language has been used by the given user
 *
 * @typedef {Object} StructuredLanguages
 * @prop {Language[]} all languages that are available
 * @prop {SuggestedLanguage[]} suggested languages based on users browsing history
 */
 
/**
 * Return the device language if it's in the list of article languages.
 * If the language is a variant of a general language, and if the article
 * is not available in that language, then return the general language
 * if article is available in it. For example, if the device language is
 * 'en-gb', and the article is only available in 'en', then return 'en'.
 *
 * @param {Object[]} languages list of language objects as returned by the API
 * @param {string|undefined} deviceLanguage the device's primary language
 * @return {string|undefined} Return undefined if the article is not available in
 *  the (general or variant) device language
 */
function getDeviceLanguageOrParent( languages, deviceLanguage ) {
	var parentLanguage, index,
		hasOwn = Object.prototype.hasOwnProperty,
		deviceLanguagesWithVariants = {};
 
	Iif ( !deviceLanguage ) {
		return;
	}
 
	// Are we dealing with a variant?
	index = deviceLanguage.indexOf( '-' );
	Eif ( index !== -1 ) {
		parentLanguage = deviceLanguage.slice( 0, index );
	}
 
	languages.forEach( function ( language ) {
		if ( language.lang === parentLanguage || language.lang === deviceLanguage ) {
			deviceLanguagesWithVariants[ language.lang ] = true;
		}
	} );
 
	Iif ( hasOwn.call( deviceLanguagesWithVariants, deviceLanguage ) ) {
		// the device language is one of the available languages
		return deviceLanguage;
	} else if ( hasOwn.call( deviceLanguagesWithVariants, parentLanguage ) ) {
		// no device language, but the parent language is one of the available languages
		return parentLanguage;
	}
}
 
/**
 * Utility function for the structured language overlay
 *
 * @class util
 * @singleton
 */
module.exports = {
	/**
	 * Determine whether a language is LTR or RTL
	 * This works around T74153 and T189036
	 * and the fact that adding dir attribute to HTML in core
	 * at time of writing is memory-intensive
	 * (I7cd8a3117f49467e3ff26f35371459a667c71470)
	 *
	 * @memberof util
	 * @instance
	 * @param {Object} language with 'lang' key.
	 * @return {Object} language with 'lang' key and new 'dir' key.
	 */
	getDir: function ( language ) {
		var dir = rtlLanguages.indexOf( language.lang ) > -1 ? 'rtl' : 'ltr';
		return mfUtils.extend( {}, language, { dir: dir } );
	},
 
	/**
	 * Return two sets of languages: suggested and all (everything else)
	 *
	 * Suggested languages are the ones that the user has used before. This also
	 * includes the user device's primary language. Suggested languages are ordered
	 * by frequency in descending order. The device's language is always at the top.
	 * This group also includes the variants.
	 *
	 * All languages are the languages that are not suggested.
	 * Languages in this list are ordered in the lexicographical order of
	 * their language names.
	 *
	 * @memberof util
	 * @instance
	 * @param {Object[]} languages list of language objects as returned by the API
	 * @param {Array|boolean} variants language variant objects or false if no variants exist
	 * @param {Object} frequentlyUsedLanguages list of the frequently used languages
	 * @param {boolean} showSuggestedLanguages
	 * @param {string} [deviceLanguage] the device's primary language
	 * @return {StructuredLanguages}
	 */
	getStructuredLanguages: function (
		languages,
		variants,
		frequentlyUsedLanguages,
		showSuggestedLanguages,
		deviceLanguage
	) {
		var hasOwn = Object.prototype.hasOwnProperty,
			maxFrequency = 0,
			minFrequency = 0,
			suggestedLanguages = [],
			allLanguages = [],
			self = this;
 
		// Is the article available in the user's device language?
		deviceLanguage = getDeviceLanguageOrParent( languages, deviceLanguage );
		if ( deviceLanguage ) {
			Object.keys( frequentlyUsedLanguages ).forEach( function ( language ) {
				var frequency = frequentlyUsedLanguages[ language ];
				maxFrequency = maxFrequency < frequency ? frequency : maxFrequency;
				minFrequency = minFrequency > frequency ? frequency : minFrequency;
			} );
 
			// Make the device language the most frequently used one so that
			// it appears at the top of the list when sorted by frequency.
			frequentlyUsedLanguages[ deviceLanguage ] = maxFrequency + 1;
		}
 
		/**
		 * @param {Object} language
		 * @return {Object} which has 'dir' key.
		 */
		function addLangDir( language ) {
			if ( language.dir ) {
				return language;
			} else {
				return self.getDir( language );
			}
		}
 
		// Separate languages into suggested and all languages.
		Eif ( showSuggestedLanguages ) {
			languages.map( addLangDir ).forEach( function ( language ) {
				if ( hasOwn.call( frequentlyUsedLanguages, language.lang ) ) {
					language.frequency = frequentlyUsedLanguages[language.lang];
					suggestedLanguages.push( language );
				} else {
					allLanguages.push( language );
				}
			} );
		} else {
			allLanguages = languages.map( addLangDir );
		}
 
		// Add variants to the suggested languages list and assign the lowest
		// frequency because the variant hasn't been clicked on yet.
		// Note that the variants data doesn't contain the article title, thus
		// we cannot show it for the variants.
		if ( variants && showSuggestedLanguages ) {
			variants.map( addLangDir ).forEach( function ( variant ) {
				Iif ( hasOwn.call( frequentlyUsedLanguages, variant.lang ) ) {
					variant.frequency = frequentlyUsedLanguages[variant.lang];
				} else {
					variant.frequency = minFrequency - 1;
				}
				suggestedLanguages.push( variant );
			} );
		}
 
		// sort suggested languages in descending order by frequency
		suggestedLanguages = suggestedLanguages.sort( function ( a, b ) {
			return b.frequency - a.frequency;
		} );
 
		/**
		 * Compare language names lexicographically
		 *
		 * @param {Object} a first language
		 * @param {Object} b second language
		 * @return {number} Comparison value, 1 or -1
		 */
		function compareLanguagesByLanguageName( a, b ) {
			return a.autonym.toLocaleLowerCase() < b.autonym.toLocaleLowerCase() ? -1 : 1;
		}
 
		allLanguages = allLanguages.sort( compareLanguagesByLanguageName );
		return {
			suggested: suggestedLanguages,
			all: allLanguages
		};
	},
 
	/**
	 * Return a map of frequently used languages on the current device.
	 *
	 * @memberof util
	 * @instance
	 * @return {Object}
	 */
	getFrequentlyUsedLanguages: function () {
		var languageMap = mw.storage.get( 'langMap' );
 
		return languageMap ? JSON.parse( languageMap ) : {};
	},
 
	/**
	 * Save the frequently used languages to the user's device
	 *
	 * @memberof util
	 * @instance
	 * @param {Object} languageMap
	 */
	saveFrequentlyUsedLanguages: function ( languageMap ) {
		mw.storage.set( 'langMap', JSON.stringify( languageMap ) );
	},
 
	/**
	 * Increment the current language usage by one and save it to the device.
	 * Cap the result at 100.
	 *
	 * @memberof util
	 * @instance
	 * @param {string} languageCode
	 * @param {Object} frequentlyUsedLanguages list of the frequently used languages
	 */
	saveLanguageUsageCount: function ( languageCode, frequentlyUsedLanguages ) {
		var count = frequentlyUsedLanguages[ languageCode ] || 0;
 
		count += 1;
		// cap at 100 as this is enough data to work on
		frequentlyUsedLanguages[ languageCode ] = count > 100 ? 100 : count;
		this.saveFrequentlyUsedLanguages( frequentlyUsedLanguages );
	}
};