src/mobile.languages.structured/util.js
const
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
* @ignore
*/
/**
* 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'.
*
* @ignore
* @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 ) {
let parentLanguage;
const
hasOwn = Object.prototype.hasOwnProperty,
deviceLanguagesWithVariants = {};
if ( !deviceLanguage ) {
return;
}
// Are we dealing with a variant?
const index = deviceLanguage.indexOf( '-' );
if ( index !== -1 ) {
parentLanguage = deviceLanguage.slice( 0, index );
}
languages.forEach( function ( language ) {
if ( language.lang === parentLanguage || language.lang === deviceLanguage ) {
deviceLanguagesWithVariants[ language.lang ] = true;
}
} );
if ( 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
* @private
*/
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 ) {
const dir = rtlLanguages.indexOf( language.lang ) > -1 ? 'rtl' : 'ltr';
return mfUtils.extend( {}, language, { 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
* @ignore
* @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
) {
const hasOwn = Object.prototype.hasOwnProperty,
self = this;
let maxFrequency = 0,
minFrequency = 0,
suggestedLanguages = [],
allLanguages = [];
// Is the article available in the user's device language?
deviceLanguage = getDeviceLanguageOrParent( languages, deviceLanguage );
if ( deviceLanguage ) {
Object.keys( frequentlyUsedLanguages ).forEach( function ( language ) {
const 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.
if ( 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 ) {
if ( 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 () {
const 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 ) {
let 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 );
}
};