MediaWiki:Gadget-I18n-js.js: Difference between revisions
From Tardis Wiki, the free Doctor Who reference
0.6.7: Do not submit JS Review before fully tested
m (reverting to cqm's edit due to edits made after breaking script (fallbacks not working correctly)) |
(0.6.7: Do not submit JS Review before fully tested) |
||
Line 6: | Line 6: | ||
* @author OneTwoThreeFall <https://dev.fandom.com/User:OneTwoThreeFall> | * @author OneTwoThreeFall <https://dev.fandom.com/User:OneTwoThreeFall> | ||
* | * | ||
* @version 0.6. | * @version 0.6.7 | ||
* | * | ||
* @notes Also used by | * @notes Also used by SOAP Wiki for their reporting forms (with a non-dev i18n.json page) | ||
* @notes This is apparently a commonly used library for a number of scripts and also | * @notes This is apparently a commonly used library for a number of scripts and also | ||
* | * includes a check to prevent double loading. This can make it painful to test from your | ||
* | * JS console. To get around this, add ?usesitejs=0&useuserjs=0 to your URL. | ||
*/ | */ | ||
Line 30: | Line 30: | ||
window.dev.i18n = window.dev.i18n || {}; | window.dev.i18n = window.dev.i18n || {}; | ||
// | // Prevent double loading and loss of cache | ||
if (window.dev.i18n.loadMessages !== undefined) { | if (window.dev.i18n.loadMessages !== undefined) { | ||
return; | return; | ||
} | } | ||
/* | |||
* Cache of mw config variables. | |||
* | |||
* @var {object} conf Cache of mw config variables: | |||
* - {boolean} debug | |||
* - {string} wgContentLanguage Site language | |||
* Be careful to use this: | |||
* - In languages with variants, this will block the language conversion; | |||
* see <https://www.mediawiki.org/wiki/Writing_systems>. | |||
* - In multilingual wikis like "Feed The Beast", this will block both the | |||
* multilingual content providing and language conversion. | |||
* - {string} wgPageContentLanguage Page Language or Content Modal Language | |||
* or Site Language or 'en' | |||
* Be careful to use this: | |||
* - In Special: pages, this will be the user language. | |||
* - In Module: pages, this will be the content modal language 'en'. | |||
* - {string} wgUserLanguage | |||
* - {(string|null)} wgUserVariant The language variant user currently using, | |||
* 'null' when the page lannguage doesn't have language variants. | |||
*/ | |||
var conf = mw.config.get([ | var conf = mw.config.get([ | ||
'debug', | |||
'wgContentLanguage', | |||
'wgPageContentLanguage', | |||
]), | 'wgUserLanguage', | ||
'wgUserVariant' | |||
]), | |||
/* | /* | ||
* Current time in milliseconds, used to set and check cache age. | * @var {number} Current time in milliseconds, used to set and check cache age. | ||
*/ | */ | ||
now = Date.now(), | now = Date.now(), | ||
/* | /* | ||
* Length of one day in milliseconds, used in cache age calculations. | * @var {number} Length of one day in milliseconds, used in cache age calculations. | ||
*/ | */ | ||
oneDay = 1000 * 60 * 60 * 24, | oneDay = 1000 * 60 * 60 * 24, | ||
/* | /* | ||
* Prefix used for localStorage keys that contain i18n-js cache data. | * @var {string} Prefix used for localStorage keys that contain i18n-js cache data. | ||
*/ | */ | ||
cachePrefix = 'i18n-cache-', | cachePrefix = 'i18n-cache-', | ||
/* | /* | ||
* | * @var {boolean} Whether a fallback loop warning been shown | ||
*/ | */ | ||
warnedAboutFallbackLoop = false, | warnedAboutFallbackLoop = false, | ||
/* | /* | ||
* Cache of loaded I18n instances. | * @var {object} Cache of loaded I18n instances. | ||
*/ | */ | ||
cache = {}, | cache = {}, | ||
Line 71: | Line 90: | ||
/* | /* | ||
* Initial overrides object, initialised below with the i18n global variable. | * Initial overrides object, initialised below with the i18n global variable. | ||
* Allows end-users to override specific messages. See documentation for how to use. | * Allows end-users to override specific messages. | ||
* See documentation for how to use. | |||
* | |||
* @var {(null|object)} overrides | |||
*/ | */ | ||
overrides = null, | overrides = null, | ||
/* | /* | ||
* Language fallbacks for those that don't fallback to | * Mapping of deprecated language codes that were used in previous | ||
* Shouldn't need updating unless | * versions of MediaWiki to up-to-date, current language codes. | ||
* | |||
* These codes shouldn't be used to store translations unless there are | |||
* language changes to /includes/language/LanguageCode.php in mediawiki/core. | |||
* | |||
* These may or may not be valid BCP 47 codes; they are included here | |||
* because MediaWiki renamed these particular codes at some point. | |||
* | |||
* Note that 'als' is actually a valid ISO 639 code (Tosk Albanian), but it | |||
* was previously used in MediaWiki for Alsatian, which comes under 'gsw'. | |||
* | |||
* @var {object.<string, string>} Mapping from deprecated MediaWiki-internal | |||
* language code to replacement MediaWiki-internal language code. | |||
* | |||
* @see /includes/language/LanguageCode.php in MediaWiki core | |||
* @see https://meta.wikimedia.org/wiki/Special_language_codes | |||
*/ | |||
deprecatedCodes = { | |||
'als': 'gsw', // T25215 | |||
'bat-smg': 'sgs', // T27522 | |||
'be-x-old': 'be-tarask', // T11823 | |||
'fiu-vro': 'vro', // T31186 | |||
'roa-rup': 'rup', // T17988 | |||
'zh-classical': 'lzh', // T30443 | |||
'zh-min-nan': 'nan', // T30442 | |||
'zh-yue': 'yue' // T30441 | |||
}, | |||
/** | |||
* Mapping of non-standard language codes used in MediaWiki to | |||
* standardized BCP 47 codes. | |||
* | |||
* @var {object.<string, string>} Mapping from nonstandard | |||
* MediaWiki-internal codes to BCP 47 codes | |||
* | |||
* @see /includes/language/LanguageCode.php in MediaWiki core | |||
* @see https://meta.wikimedia.org/wiki/Special_language_codes | |||
* @see https://phabricator.wikimedia.org/T125073 | |||
*/ | |||
nonStandardCodes = { | |||
'cbk-zam': 'cbk', // T124657 | |||
'de-formal': 'de-x-formal', | |||
'eml': 'egl', // T36217 | |||
'en-rtl': 'en-x-rtl', | |||
'es-formal': 'es-x-formal', | |||
'hu-formal': 'hu-x-formal', | |||
'kk-cn': 'kk-Arab-CN', | |||
'kk-kz': 'kk-Cyrl-KZ', | |||
'kk-tr': 'kk-Latn-TR', | |||
'map-bms': 'jv-x-bms', // [[wikipedia:en:Banyumasan_dialect]] T125073 | |||
'mo': 'ro-Cyrl-MD', // T125073 | |||
'nrm': 'nrf', // [[wikipedia:en:Norman_language]] T25216 | |||
'nl-informal': 'nl-x-informal', | |||
'roa-tara': 'nap-x-tara', // [[wikipedia:en:Tarantino_dialect]] | |||
'simple': 'en-x-simple', | |||
'sr-ec': 'sr-Cyrl', // T117845 | |||
'sr-el': 'sr-Latn', // T117845 | |||
'zh-cn': 'zh-Hans-CN', | |||
'zh-sg': 'zh-Hans-SG', | |||
'zh-my': 'zh-Hans-MY', | |||
'zh-tw': 'zh-Hant-TW', | |||
'zh-hk': 'zh-Hant-HK', | |||
'zh-mo': 'zh-Hant-MO' | |||
}, | |||
/* | |||
* Language fallbacks for those that don't only fallback to 'en' or have no | |||
* fallbacks ('en'). | |||
* | |||
* Shouldn't need updating unless there're language fallback chain changes | |||
* to /languages/messages files in mediawiki/core. | |||
* | * | ||
* To generate this, use `$ grep -R "fallback =" /path/to/messages/`, | * To generate this, use `$ grep -R "fallback =" /path/to/messages/`, | ||
* pipe the result to a text file and format the result. | * pipe the result to a text file and format the result. | ||
* | |||
* Please note that there's bidirectional/multidirectional fallback in languages, | |||
* including 'cdo' <=> 'nan', 'pt' <=> 'pt-br', 'zh' <=> 'zh-hans' <=> 'zh-hant' | |||
* | |||
* @var {object.<string, string[]>} Mapping from language codes to fallback | |||
* language codes | |||
*/ | */ | ||
fallbacks = { | fallbacks = { | ||
'ab': 'ru', | 'ab': ['ru'], | ||
' | 'abs': ['id'], | ||
' | 'ace': ['id'], | ||
' | 'ady': ['ady-cyrl'], | ||
' | 'aeb': ['aeb-arab'], | ||
' | 'aeb-arab': ['ar'], | ||
' | 'aln': ['sq'], | ||
' | 'alt': ['ru'], | ||
' | 'ami': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'], | ||
'an': ['es'], | |||
' | 'anp': ['hi'], | ||
' | 'arn': ['es'], | ||
' | 'arq': ['ar'], | ||
' | 'ary': ['ar'], | ||
' | 'arz': ['ar'], | ||
' | 'ast': ['es'], | ||
' | 'atj': ['fr'], | ||
' | 'av': ['ru'], | ||
' | 'avk': ['fr', 'es', 'ru'], | ||
' | 'awa': ['hi'], | ||
' | 'ay': ['es'], | ||
' | 'azb': ['fa'], | ||
' | 'ba': ['ru'], | ||
' | 'ban': ['id'], | ||
' | 'ban-bali': ['ban'], | ||
' | 'bar': ['de'], | ||
' | 'bbc': ['bbc-latn'], | ||
' | 'bbc-latn': ['id'], | ||
' | 'bcc': ['fa'], | ||
'bci': ['fr'], | |||
' | 'be-tarask': ['be'], | ||
' | 'bgn': ['fa'], | ||
' | 'bh': ['bho'], | ||
' | 'bi': ['en'], | ||
' | 'bjn': ['id'], | ||
' | 'blk': ['my'], | ||
' | 'bm': ['fr'], | ||
' | 'bpy': ['bn'], | ||
' | 'bqi': ['fa'], | ||
' | 'br': ['fr'], | ||
' | 'btm': ['id'], | ||
' | 'bug': ['id'], | ||
' | 'bxr': ['ru'], | ||
' | 'ca': ['oc'], | ||
' | 'cbk-zam': ['es'], | ||
' | 'cdo': ['nan', 'zh-hant', 'zh', 'zh-hans'], | ||
' | 'ce': ['ru'], | ||
'co': ['it'], | |||
'crh': ['crh-latn'], | |||
' | 'crh-cyrl': ['ru'], | ||
' | 'cs': ['sk'], | ||
' | 'csb': ['pl'], | ||
' | 'cv': ['ru'], | ||
' | 'de-at': ['de'], | ||
' | 'de-ch': ['de'], | ||
' | 'de-formal': ['de'], | ||
' | 'dsb': ['hsb', 'de'], | ||
' | 'dtp': ['ms'], | ||
' | 'dty': ['ne'], | ||
' | 'egl': ['it'], | ||
' | 'eml': ['it'], | ||
' | 'es-formal': ['es'], | ||
' | 'ext': ['es'], | ||
' | 'ff': ['fr'], | ||
' | 'fit': ['fi'], | ||
' | 'fon': ['fr'], | ||
' | 'frc': ['fr'], | ||
' | 'frp': ['fr'], | ||
' | 'frr': ['de'], | ||
' | 'fur': ['it'], | ||
' | 'gag': ['tr'], | ||
' | 'gan': ['gan-hant', 'gan-hans', 'zh-hant', 'zh', 'zh-hans'], | ||
' | 'gan-hans': ['gan', 'gan-hant', 'zh-hans', 'zh', 'zh-hant'], | ||
' | 'gan-hant': ['gan', 'gan-hans', 'zh-hant', 'zh', 'zh-hans'], | ||
' | 'gcr': ['fr'], | ||
'gl': ['pt'], | |||
'gld': ['ru'], | |||
' | 'glk': ['fa'], | ||
'gn': ['es'], | |||
'gom': ['gom-deva'], | |||
' | 'gom-deva': ['hi'], | ||
'gor': ['id'], | |||
'gsw': ['de'], | |||
' | 'guc': ['es'], | ||
' | 'hak': ['zh-hant', 'zh', 'zh-hans'], | ||
' | 'hif': ['hif-latn'], | ||
' | 'hrx': ['de'], | ||
' | 'hsb': ['dsb', 'de'], | ||
' | 'hsn': ['zh-cn', 'zh-hans', 'zh', 'zh-hant'], | ||
' | 'ht': ['fr'], | ||
' | 'hu-formal': ['hu'], | ||
' | 'hyw': ['hy'], | ||
' | 'ii': ['zh-cn', 'zh-hans', 'zh', 'zh-hant'], | ||
' | 'inh': ['ru'], | ||
'io': ['eo'], | |||
' | 'iu': ['ike-cans'], | ||
' | 'jam': ['en'], | ||
' | 'jut': ['da'], | ||
' | 'jv': ['id'], | ||
'kaa': ['kk-latn', 'kk-cyrl'], | |||
' | 'kab': ['fr'], | ||
' | 'kbd': ['kbd-cyrl'], | ||
' | 'kbp': ['fr'], | ||
' | 'kea': ['pt'], | ||
'khw': ['ur'], | |||
' | 'kiu': ['tr'], | ||
' | 'kjp': ['my'], | ||
' | 'kk': ['kk-cyrl'], | ||
' | 'kk-arab': ['kk-cyrl'], | ||
'kk-cn': ['kk-arab', 'kk-cyrl'], | |||
' | 'kk-kz': ['kk-cyrl'], | ||
' | 'kk-latn': ['kk-cyrl'], | ||
' | 'kk-tr': ['kk-latn', 'kk-cyrl'], | ||
' | 'kl': ['da'], | ||
' | 'koi': ['ru'], | ||
' | 'ko-kp': ['ko'], | ||
' | 'krc': ['ru'], | ||
' | 'krl': ['fi'], | ||
' | 'ks': ['ks-arab'], | ||
' | 'ksh': ['de'], | ||
' | 'ksw': ['my'], | ||
' | 'ku': ['ku-latn'], | ||
' | 'kum': ['ru'], | ||
' | 'ku-arab': ['ckb'], | ||
' | 'kv': ['ru'], | ||
' | 'lad': ['es'], | ||
' | 'lb': ['de'], | ||
' | 'lbe': ['ru'], | ||
' | 'lez': ['ru', 'az'], | ||
' | 'li': ['nl'], | ||
' | 'lij': ['it'], | ||
' | 'liv': ['et'], | ||
' | 'lki': ['fa'], | ||
' | 'lld': ['it', 'rm', 'fur'], | ||
' | 'lmo': ['pms', 'eml', 'lij', 'vec', 'it'], | ||
' | 'ln': ['fr'], | ||
' | 'lrc': ['fa'], | ||
' | 'ltg': ['lv'], | ||
' | 'luz': ['fa'], | ||
' | 'lzh': ['zh-hant', 'zh', 'zh-hans'], | ||
' | 'lzz': ['tr'], | ||
'mad': ['id'], | |||
' | 'mai': ['hi'], | ||
' | 'map-bms': ['jv', 'id'], | ||
' | 'mdf': ['myv', 'ru'], | ||
' | 'mg': ['fr'], | ||
' | 'mhr': ['mrj', 'ru'], | ||
'min': ['id'], | |||
' | 'mnw': ['my'], | ||
'mo': ['ro'], | |||
' | 'mrj': ['mhr', 'ru'], | ||
' | 'ms-arab': ['ms'], | ||
' | 'mwl': ['pt'], | ||
' | 'myv': ['mdf', 'ru'], | ||
' | 'mzn': ['fa'], | ||
'nah': ['es'], | |||
' | 'nan': ['cdo', 'zh-hant', 'zh', 'zh-hans'], | ||
' | 'nap': ['it'], | ||
' | 'nb': ['nn'], | ||
' | 'nds': ['de'], | ||
' | 'nds-nl': ['nl'], | ||
' | 'nia': ['id'], | ||
' | 'nl-informal': ['nl'], | ||
' | 'nn': ['nb'], | ||
' | 'nrm': ['nrf', 'fr'], | ||
'oc': ['ca', 'fr'], | |||
'olo': ['fi'], | |||
'os': ['ru'], | |||
'pcd': ['fr'], | |||
'pdc': ['de'], | |||
'pdt': ['de'], | |||
'pfl': ['de'], | |||
'pih': ['en'], | |||
'pms': ['it'], | |||
'pnt': ['el'], | |||
'pt': ['pt-br'], | |||
'pt-br': ['pt'], | |||
'pwn': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'], | |||
'qu': ['qug', 'es'], | |||
'qug': ['qu', 'es'], | |||
'rgn': ['it'], | |||
'rmy': ['ro'], | |||
'roa-tara': ['it'], | |||
'rsk': ['sr-ec'], | |||
'rue': ['uk', 'ru'], | |||
'rup': ['ro'], | |||
'ruq': ['ruq-latn', 'ro'], | |||
'ruq-cyrl': ['mk'], | |||
'ruq-latn': ['ro'], | |||
'sa': ['hi'], | |||
'sah': ['ru'], | |||
'scn': ['it'], | |||
'sco': ['en'], | |||
'sdc': ['it'], | |||
'sdh': ['cbk', 'fa'], | |||
'se': ['nb', 'fi'], | |||
'ses': ['fr'], | |||
'se-fi': ['se', 'fi', 'sv'], | |||
'se-no': ['se', 'nb', 'nn'], | |||
'se-se': ['se', 'sv'], | |||
'sg': ['fr'], | |||
'sgs': ['lt'], | |||
'sh': ['bs', 'sr-el', 'hr'], | |||
'shi': ['fr'], | |||
'shy': ['shy-latn'], | |||
'shy-latn': ['fr'], | |||
'sjd': ['ru'], | |||
'sk': ['cs'], | |||
'skr': ['skr-arab'], | |||
'skr-arab': ['ur', 'pnb'], | |||
'sli': ['de'], | |||
'smn': ['fi'], | |||
'sr': ['sr-ec'], | |||
'srn': ['nl'], | |||
'stq': ['de'], | |||
'sty': ['ru'], | |||
'su': ['id'], | |||
'szl': ['pl'], | |||
'szy': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'], | |||
'tay': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'], | |||
'tcy': ['kn'], | |||
'tet': ['pt'], | |||
'tg': ['tg-cyrl'], | |||
'trv': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'], | |||
'tt': ['tt-cyrl', 'ru'], | |||
'tt-cyrl': ['ru'], | |||
'ty': ['fr'], | |||
'tyv': ['ru'], | |||
'udm': ['ru'], | |||
'ug': ['ug-arab'], | |||
'vec': ['it'], | |||
'vep': ['et'], | |||
'vls': ['nl'], | |||
'vmf': ['de'], | |||
'vmw': ['pt'], | |||
'vot': ['fi'], | |||
'vro': ['et'], | |||
'wa': ['fr'], | |||
'wls': ['fr'], | |||
'wo': ['fr'], | |||
'wuu': ['zh-hans', 'zh', 'zh-hant'], | |||
'xal': ['ru'], | |||
'xmf': ['ka'], | |||
'yi': ['he'], | |||
'yue': ['zh-hk', 'zh-hant', 'zh', 'zh-hans'], | |||
'za': ['zh-hans', 'zh', 'zh-hant'], | |||
'zea': ['nl'], | |||
'zgh': ['kab'], | |||
'zh': ['zh-hans', 'zh-hant', 'zh-cn', 'zh-tw', 'zh-hk'], | |||
'zh-cn': ['zh-hans', 'zh', 'zh-hant'], | |||
'zh-hans': ['zh-cn', 'zh', 'zh-hant'], | |||
'zh-hant': ['zh-tw', 'zh-hk', 'zh', 'zh-hans'], | |||
'zh-hk': ['zh-hant', 'zh-tw', 'zh', 'zh-hans'], | |||
'zh-mo': ['zh-hk', 'zh-hant', 'zh-tw', 'zh', 'zh-hans'], | |||
'zh-my': ['zh-sg', 'zh-hans', 'zh-cn', 'zh', 'zh-hant'], | |||
'zh-sg': ['zh-hans', 'zh-cn', 'zh', 'zh-hant'], | |||
'zh-tw': ['zh-hant', 'zh-hk', 'zh', 'zh-hans'] | |||
}; | }; | ||
/* | |||
* Get the normalised IETF/BCP 47 language tag. | |||
* | |||
* mediawiki.language.bcp47 doesn't handle deprecated language codes, and | |||
* some non-standard language codes are missed from LanguageCode.php, so | |||
* this function is added to override the behavior. | |||
* | |||
* @param {string} lang The language code to convert. | |||
* @return {string} The language code complying with BCP 47 standards. | |||
* | |||
* @see https://gerrit.wikimedia.org/r/c/mediawiki/core/+/376506/ | |||
* @see /resources/src/mediawiki.language/mediawiki.language.js in MediaWiki core | |||
* @see /includes/language/LanguageCode.php in MediaWiki core | |||
*/ | |||
function bcp47(lang) { | |||
if (nonStandardCodes[lang]) { | |||
return nonStandardCodes[lang]; | |||
} | |||
if (deprecatedCodes[lang]) { | |||
return bcp47(deprecatedCodes[lang]); | |||
} | |||
/* | |||
* @var {string[]} formatted | |||
* @var {boolean} isFirstSegment Whether is the first segment | |||
* @var {boolean} isPrivate Whether the code of the segment is private use | |||
* @var {string[]} segments The segments of language code | |||
*/ | |||
var formatted, | |||
isFirstSegment = true, | |||
isPrivate = false, | |||
segments = lang.split('-'); | |||
formatted = segments.map(function (segment) { | |||
/* | |||
* @var {string} newSegment The converted segment of language code | |||
*/ | |||
var newSegment; | |||
// when previous segment is x, it is a private segment and should be lc | |||
if (isPrivate) { | |||
newSegment = segment.toLowerCase(); | |||
// ISO 3166 country code | |||
} else if (segment.length === 2 && !isFirstSegment) { | |||
newSegment = segment.toUpperCase(); | |||
// ISO 15924 script code | |||
} else if (segment.length === 4 && !isFirstSegment) { | |||
newSegment = segment.charAt(0).toUpperCase() + segment.substring(1).toLowerCase(); | |||
// Use lowercase for other cases | |||
} else { | |||
newSegment = segment.toLowerCase(); | |||
} | |||
isPrivate = segment.toLowerCase() === 'x'; | |||
isFirstSegment = false; | |||
return newSegment; | |||
}); | |||
return formatted.join('-'); | |||
} | |||
/* | /* | ||
Line 254: | Line 508: | ||
* about to start a loop. Only logs once to prevent flooding the browser console. | * about to start a loop. Only logs once to prevent flooding the browser console. | ||
* | * | ||
* @param lang Language in use when loop was found. | * @param {string} lang Language in use when loop was found. | ||
* @param fallbackChain Array of languages involved in the loop. | * @param {string[]} fallbackChain Array of languages involved in the loop. | ||
*/ | */ | ||
function warnOnFallbackLoop(lang, fallbackChain) { | function warnOnFallbackLoop(lang, fallbackChain) { | ||
Line 264: | Line 518: | ||
fallbackChain.push(lang); | fallbackChain.push(lang); | ||
console.error('[I18n-js] | console.error('[I18n-js] Duplicated fallback language found. Please leave a message at <https://dev.fandom.com/wiki/Talk:I18n-js> and include the following line: \nLanguage fallback chain:', fallbackChain.join(', ')); | ||
} | } | ||
Line 271: | Line 525: | ||
* requested language. | * requested language. | ||
* | * | ||
* @param messages The message object to look translations up in. | * @param {object} messages The message object to look translations up in. | ||
* @param msgName The name of the message to get. | * @param {string} msgName The name of the message to get. | ||
* @param lang The language to get the message in. | * @param {string} lang The language to get the message in. | ||
* @param fallbackChain Array of languages that have already been checked. | * @param {string[]} fallbackChain Array of languages that have already been checked. | ||
* Used to detect if the fallback chain is looping. | * Used to detect if the fallback chain is looping. | ||
* @return {(string|boolean)} The requested translation or `false` if no message could be found. | |||
* @return The requested translation or `false` if no message could be found. | |||
*/ | */ | ||
function getMsg(messages, msgName, lang, fallbackChain) { | function getMsg(messages, msgName, lang, fallbackChain) { | ||
if (deprecatedCodes[lang]) { | |||
return getMsg(messages, msgName, deprecatedCodes[lang], fallbackChain); | |||
} | |||
if (messages[lang] && messages[lang][msgName]) { | if (messages[lang] && messages[lang][msgName]) { | ||
return messages[lang][msgName]; | return messages[lang][msgName]; | ||
} | } | ||
Line 291: | Line 544: | ||
fallbackChain = []; | fallbackChain = []; | ||
} | } | ||
lang = fallbacks[lang] | /* | ||
* Try to find fallback messages by using the fallback chain. | |||
* We need to check whether the lang is defined in the fallback list before | |||
* trying to go through them. | |||
* | |||
* @var {string} fallbackLang | |||
*/ | |||
for (var i = 0; (fallbacks[lang] && i < fallbacks[lang].length); i += 1) { | |||
var fallbackLang = fallbacks[lang][i]; | |||
if (messages[fallbackLang] && messages[fallbackLang][msgName]) { | |||
return messages[fallbackLang][msgName]; | |||
} | |||
if (fallbackChain.indexOf(fallbackLang) !== -1) { | |||
/* | |||
* Duplicated language code in fallback list | |||
* Try to find next fallback language from list | |||
*/ | |||
warnOnFallbackLoop(fallbackLang, fallbackChain); | |||
continue; | |||
} | |||
fallbackChain.push(fallbackLang); | |||
} | |||
// "No language" or "no more languages" in fallback list - switch to 'en' | |||
if (messages.en && messages.en[msgName]) { | |||
return messages.en[msgName]; | |||
} | } | ||
return | return false; | ||
} | } | ||
Line 308: | Line 581: | ||
* as $n where n > 0. | * as $n where n > 0. | ||
* | * | ||
* @param message The message to substitute arguments into | * @param {string} message The message to substitute arguments into | ||
* @param arguments The arguments to substitute in. | * @param {array} arguments The arguments to substitute in. | ||
* @return {string} The resulting message. | |||
* @return The resulting message. | |||
*/ | */ | ||
function handleArgs(message, args) { | function handleArgs(message, args) { | ||
args.forEach(function (elem, index) { | args.forEach(function (elem, index) { | ||
/* | |||
* @var {RegExp} rgx | |||
*/ | |||
var rgx = new RegExp('\\$' + (index + 1), 'g'); | var rgx = new RegExp('\\$' + (index + 1), 'g'); | ||
message = message.replace(rgx, elem); | message = message.replace(rgx, elem); | ||
Line 325: | Line 600: | ||
* Generate a HTML link using the supplied parameters. | * Generate a HTML link using the supplied parameters. | ||
* | * | ||
* @param href The href of the link which will be converted to | * @param {string} href The href of the link which will be converted to | ||
* '/wiki/href'. | * '/wiki/href'. | ||
* @param text The text and title of the link. If this is not supplied, it | * @param {string} text The text and title of the link. If this is not supplied, it | ||
* will default to href. | * will default to href. | ||
* @param hasProtocol True if the href parameter already includes the | * @param {boolean} hasProtocol True if the href parameter already includes the | ||
* protocol (i.e. it begins with 'http://', 'https://', or '//'). | * protocol (i.e. it begins with 'http://', 'https://', or '//'). | ||
* @return {string} The generated link. | |||
* @return The generated link. | |||
*/ | */ | ||
function makeLink(href, text, hasProtocol) { | function makeLink(href, text, hasProtocol) { | ||
Line 363: | Line 637: | ||
* | * | ||
* @param html | * @param html | ||
* @return The sanitised HTML code. | * @return The sanitised HTML code. | ||
*/ | */ | ||
function sanitiseHtml(html) { | function sanitiseHtml(html) { | ||
/* | |||
* @var context | |||
*/ | |||
var context = document.implementation.createHTMLDocument(''), | var context = document.implementation.createHTMLDocument(''), | ||
$html = $.parseHTML(html, /* document */ context, /* keepscripts */ false), | $html = $.parseHTML(html, /* document */ context, /* keepscripts */ false), | ||
Line 408: | Line 684: | ||
} | } | ||
// | // Make sure there's nothing nasty in style attributes | ||
if (attr.name === 'style') { | if (attr.name === 'style') { | ||
style = $this.attr('style'); | style = $this.attr('style'); | ||
Line 438: | Line 714: | ||
* - {{GENDER:gender|masculine|feminine|neutral}} | * - {{GENDER:gender|masculine|feminine|neutral}} | ||
* | * | ||
* @param message The message to process. | * @param {string} message The message to process. | ||
* @return {string} The resulting string. | |||
* @return The resulting string. | |||
*/ | */ | ||
function parse(message) { | function parse(message) { | ||
// [url text] -> [$1 $2] | |||
var urlRgx = /\[((?:https?:)?\/\/.+?) (.+?)\]/g, | var urlRgx = /\[((?:https?:)?\/\/.+?) (.+?)\]/g, | ||
// [[pagename]] -> [[$1]] | // [[pagename]] -> [[$1]] | ||
Line 479: | Line 754: | ||
* Create a new Message instance. | * Create a new Message instance. | ||
* | * | ||
* @param messages The message object to look translations up in. | * @param {object} messages The message object to look translations up in. | ||
* @param lang The language to get the message in. | * @param {string} lang The language to get the message in. | ||
* @param args Any arguments to substitute into the message, [0] is message name. | * @param {array} args Any arguments to substitute into the message, [0] is message name. | ||
* @param name The name of the script the messages are for. | * @param {string} name The name of the script the messages are for. | ||
* @return | |||
*/ | */ | ||
function message(messages, lang, args, name) { | function message(messages, lang, args, name) { | ||
Line 489: | Line 765: | ||
} | } | ||
/* | |||
* @var msgName | |||
* @var {string} descriptiveMsgName | |||
* @var {object} msg | |||
* @var {boolean} msgExists | |||
*/ | |||
var msgName = args.shift(), | var msgName = args.shift(), | ||
descriptiveMsgName = 'i18njs-' + name + '-' + msgName, | descriptiveMsgName = 'i18njs-' + name + '-' + msgName, | ||
Line 514: | Line 796: | ||
return { | return { | ||
/* | /* | ||
* | * @return {boolean} Representing whether the message exists. | ||
*/ | */ | ||
exists: msgExists, | exists: msgExists, | ||
Line 521: | Line 803: | ||
* Parse wikitext links in the message and return the result. | * Parse wikitext links in the message and return the result. | ||
* | * | ||
* @return The resulting string. | * @return {string} The resulting string. | ||
*/ | */ | ||
parse: function () { | parse: function () { | ||
/ | /* | ||
* Skip parsing if the message wasn't found; otherwise | |||
* the sanitisation will mess with it. | |||
*/ | |||
if (!this.exists) { | if (!this.exists) { | ||
return this.escape(); | return this.escape(); | ||
Line 536: | Line 820: | ||
* Escape any HTML in the message and return the result. | * Escape any HTML in the message and return the result. | ||
* | * | ||
* @return The resulting string. | * @return {string} The resulting string. | ||
*/ | */ | ||
escape: function () { | escape: function () { | ||
Line 545: | Line 829: | ||
* Return the message as is. | * Return the message as is. | ||
* | * | ||
* @return The resulting string. | * @return {string} The resulting string. | ||
*/ | */ | ||
plain: function () { | plain: function () { | ||
Line 556: | Line 840: | ||
* Create a new i18n object. | * Create a new i18n object. | ||
* | * | ||
* @param messages The message object to look translations up in. | * @param {object} messages The message object to look translations up in. | ||
* @param name The name of the script the messages are for. | * @param {string} name The name of the script the messages are for. | ||
* @param options Options set by the loading script. | * @param {object} options Options set by the loading script. | ||
* @return {object} | |||
*/ | */ | ||
function i18n(messages, name, options) { | function i18n(messages, name, options) { | ||
Line 578: | Line 863: | ||
* Set the language for the next msg call. | * Set the language for the next msg call. | ||
* | * | ||
* @param lang The language code to use for the next `msg` call. | * @param {string} lang The language code to use for the next `msg` call. | ||
* | * | ||
* @return The current object for use in chaining. | * @return {object} The current object for use in chaining. | ||
*/ | */ | ||
inLang: function (lang) { | inLang: function (lang) { | ||
Line 601: | Line 886: | ||
* Set the language for the next `msg` call to the content language. | * Set the language for the next `msg` call to the content language. | ||
* | * | ||
* @return The current object for use in chaining. | * @return {object} The current object for use in chaining. | ||
*/ | */ | ||
inContentLang: function () { | inContentLang: function () { | ||
Line 608: | Line 893: | ||
}, | }, | ||
/* | |||
* Set the default language to the page language. | |||
*/ | |||
usePageLang: function () { | |||
defaultLang = conf.wgPageContentLanguage; | |||
}, | |||
/* | |||
* Set the language for the next `msg` call to the page language. | |||
* | |||
* @return {object} The current object for use in chaining. | |||
*/ | |||
inPageLang: function () { | |||
tempLang = conf.wgPageContentLanguage; | |||
return this; | |||
}, | |||
/* | |||
* Set the default language to the page view language. | |||
* This is also known as the user language variant. | |||
*/ | |||
usePageViewLang: function () { | |||
defaultLang = conf.wgUserVariant || conf.wgContentLanguage; | |||
}, | |||
/* | |||
* Set the language for the next `msg` call to the page view language. | |||
* This is also known as the user language variant. | |||
* | |||
* @return {object} The current object for use in chaining. | |||
*/ | |||
inPageViewLang: function () { | |||
tempLang = conf.wgUserVariant || conf.wgContentLanguage; | |||
return this; | |||
}, | |||
/* | /* | ||
Line 619: | Line 939: | ||
* Set the language for the next msg call to the user's language. | * Set the language for the next msg call to the user's language. | ||
* | * | ||
* @return The current object for use in chaining. | * @return {object} The current object for use in chaining. | ||
*/ | */ | ||
inUserLang: function () { | inUserLang: function () { | ||
Line 628: | Line 948: | ||
/* | /* | ||
* Create a new instance of Message. | * Create a new instance of Message. | ||
* | |||
* @return {object} | |||
*/ | */ | ||
msg: function () { | msg: function () { | ||
Line 653: | Line 975: | ||
* This allows us to save only those messages needed to the cache. | * This allows us to save only those messages needed to the cache. | ||
* | * | ||
* @param name The name of the script the messages are for. | * @param {string} name The name of the script the messages are for. | ||
* @param messages The message object to look translations up in. | * @param {object} messages The message object to look translations up in. | ||
* @param options Options set by the loading script. | * @param {object} options Options set by the loading script. | ||
*/ | */ | ||
function optimiseMessages(name, messages, options) { | function optimiseMessages(name, messages, options) { | ||
Line 664: | Line 986: | ||
if (!msgKeys.length) { | if (!msgKeys.length) { | ||
// | // No English messages, don't bother optimising | ||
return messages; | return messages; | ||
} | } | ||
/* | |||
* @var addMsgsForLanguage | |||
*/ | |||
var addMsgsForLanguage = function (lang) { | var addMsgsForLanguage = function (lang) { | ||
if (optimised[lang]) { | if (optimised[lang]) { | ||
// | // Language already exists | ||
return; | return; | ||
} | } | ||
Line 677: | Line 1,002: | ||
msgKeys.forEach(function (msgName) { | msgKeys.forEach(function (msgName) { | ||
/* | |||
* @var msg | |||
*/ | |||
var msg = getMsg(messages, msgName, lang); | var msg = getMsg(messages, msgName, lang); | ||
Line 689: | Line 1,017: | ||
} | } | ||
/ | /* | ||
* If cache exists and is optimised, preserve existing languages. | |||
* This allows an optimised cache even when using different | |||
* language wikis on same domain (i.e. sharing same cache). | |||
*/ | |||
if (existingLangs) { | if (existingLangs) { | ||
existingLangs.forEach(function (lang) { | existingLangs.forEach(function (lang) { | ||
Line 702: | Line 1,032: | ||
langs.forEach(addMsgsForLanguage); | langs.forEach(addMsgsForLanguage); | ||
/ | /* | ||
* `cacheAll` is an array of message names for which translations | |||
* should not be optimised - save all translations of these messages | |||
*/ | |||
if (Array.isArray(options.cacheAll)) { | if (Array.isArray(options.cacheAll)) { | ||
msgKeys = options.cacheAll; | msgKeys = options.cacheAll; | ||
Line 715: | Line 1,047: | ||
/* | /* | ||
* Check that the cache for a script exists and, if optimised, contains the | * Check that the cache for a script exists and, if optimised, contains the | ||
* | * necessary languages. | ||
* | * | ||
* @return | * @param {string} name The name of the script to check for. | ||
* @param {object} options Options set by the loading script. | |||
* @return {boolean} Whether the cache should be used. | |||
*/ | */ | ||
function cacheIsSuitable(name, options) { | function cacheIsSuitable(name, options) { | ||
var messages = cache[name] && cache[name]._messages; | var messages = cache[name] && cache[name]._messages; | ||
// | // Nothing in cache | ||
if (!messages) { | if (!messages) { | ||
return false; | return false; | ||
} | } | ||
/ | /* | ||
* Optimised messages missing user or content language. | |||
* We'll need to load from server in this case. | |||
*/ | |||
if ( | if ( | ||
messages._isOptimised && | messages._isOptimised && | ||
Line 756: | Line 1,090: | ||
storageKeys.filter(function (key) { | storageKeys.filter(function (key) { | ||
return isCacheKey.test(key); | |||
}).forEach(function (key) { | }).forEach(function (key) { | ||
var keyPrefix = key.match(isCacheKey)[1], | var keyPrefix = key.match(isCacheKey)[1], | ||
cacheTimestamp; | cacheTimestamp; | ||
try { | try { | ||
cacheTimestamp = Number(localStorage.getItem(keyPrefix + '-timestamp')); | cacheTimestamp = Number(localStorage.getItem(keyPrefix + '-timestamp')); | ||
Line 766: | Line 1,100: | ||
if (now - cacheTimestamp < oneDay * 2) { | if (now - cacheTimestamp < oneDay * 2) { | ||
// | // Cached within last two days, keep it | ||
return; | return; | ||
} | } | ||
Line 782: | Line 1,116: | ||
* This is a bit basic, so will remove comments inside strings too. | * This is a bit basic, so will remove comments inside strings too. | ||
* | * | ||
* @param json The JSON string. | * @param {string} json The JSON string. | ||
* @return {string} The JSON string after any comments have been removed. | |||
* @return The JSON string after any comments have been removed. | |||
*/ | */ | ||
function stripComments(json) { | function stripComments(json) { | ||
Line 796: | Line 1,129: | ||
* Save messages string to local storage for caching. | * Save messages string to local storage for caching. | ||
* | * | ||
* @param name The name of the script the messages are for. | * @param {string} name The name of the script the messages are for. | ||
* @param json The JSON object. | * @param {object} json The JSON object. | ||
* @param cacheVersion Cache version requested by the loading script. | * @param {number} cacheVersion Cache version requested by the loading script. | ||
*/ | */ | ||
function saveToCache(name, json, cacheVersion) { | function saveToCache(name, json, cacheVersion) { | ||
/* | |||
* @var {string} keyPrefix | |||
*/ | |||
var keyPrefix = cachePrefix + name; | var keyPrefix = cachePrefix + name; | ||
// | // Don't cache empty JSON | ||
if (Object.keys(json).length === 0) { | if (Object.keys(json).length === 0) { | ||
return; | return; | ||
Line 818: | Line 1,154: | ||
* Parse JSON string loaded from page and create an i18n object. | * Parse JSON string loaded from page and create an i18n object. | ||
* | * | ||
* @param name The name of the script the messages are for. | * @param {string} name The name of the script the messages are for. | ||
* @param res The JSON string. | * @param {string} res The JSON string. | ||
* @param options Options set by the loading script. | * @param {object} options Options set by the loading script. | ||
* @return {object} The resulting i18n object. | |||
* @return The resulting i18n object. | |||
*/ | */ | ||
function parseMessagesToObject(name, res, options) { | function parseMessagesToObject(name, res, options) { | ||
Line 829: | Line 1,164: | ||
msg; | msg; | ||
// | // Handle parse errors gracefully | ||
try { | try { | ||
res = stripComments(res); | res = stripComments(res); | ||
Line 847: | Line 1,182: | ||
!options.loadedFromCache && | !options.loadedFromCache && | ||
options.cacheAll !== true | options.cacheAll !== true | ||
) { | |||
json = optimiseMessages(name, json, options); | json = optimiseMessages(name, json, options); | ||
} | } | ||
Line 853: | Line 1,188: | ||
obj = i18n(json, name, options); | obj = i18n(json, name, options); | ||
// | // Cache the result in case it's used multiple times | ||
cache[name] = obj; | cache[name] = obj; | ||
Line 866: | Line 1,201: | ||
* Load messages string from local storage cache and add to cache object. | * Load messages string from local storage cache and add to cache object. | ||
* | * | ||
* @param name The name of the script the messages are for. | * @param {string} name The name of the script the messages are for. | ||
* @param options Options set by the loading script. | * @param {object} options Options set by the loading script. | ||
*/ | */ | ||
function loadFromCache(name, options) { | function loadFromCache(name, options) { | ||
Line 879: | Line 1,214: | ||
} catch (e) {} | } catch (e) {} | ||
// | // Cache exists, and its version is greater than or equal to requested version | ||
if (cacheContent && cacheVersion >= options.cacheVersion) { | if (cacheContent && cacheVersion >= options.cacheVersion) { | ||
options.loadedFromCache = true; | options.loadedFromCache = true; | ||
Line 889: | Line 1,224: | ||
* Load messages stored as JSON on a page. | * Load messages stored as JSON on a page. | ||
* | * | ||
* @param name The name of the script the messages are for. This will be | * @param {string} name The name of the script the messages are for. This will be | ||
* used to get messages from | * used to get messages from | ||
* https://dev.fandom.com/wiki/MediaWiki:Custom-name/i18n.json. | * https://dev.fandom.com/wiki/MediaWiki:Custom-name/i18n.json. | ||
* @param options Options set by the loading script: | * Use `u:<subdomain>` or `u:<language-path>.<subdomain>` to set other Fandom | ||
* cacheAll: Either an array of message names for which translations should not be optimised, or `true` to disable the optimised cache. | * wikis as the source. | ||
* | * @param {object} options Options set by the loading script: | ||
* | * - {string} apiEndpoint: Use `u:<subdomain>` or `u:<language-path>.<subdomain>` | ||
* | * to set other sites as the API endpoint of the source. Currently only | ||
* support Fandom wikis. | |||
* - {string} page: Set other format of the full page name for the i18n JSON. | |||
* Use $1 for the placeholder of name. | |||
* - {(array|boolean)} cacheAll: Either an array of message names for which | |||
* translations should not be optimised, or `true` to disable the optimised cache. | |||
* - {number} cacheVersion: Minimum cache version requested by the loading script. | |||
* - {string} language: Set a default language for the script to use, instead of wgUserLanguage. | |||
* - noCache: Never load i18n from cache (not recommended for general use). | |||
* | * | ||
* @return A jQuery.Deferred instance. | * @return {object} A jQuery.Deferred instance. | ||
*/ | */ | ||
function loadMessages(name, options) { | function loadMessages(name, options) { | ||
/* | |||
* @var {object} deferred | |||
* @var {string} apiEndpoint | |||
* @var {RegExp} apiEndpointRgx | |||
* @var {string} page | |||
* @var {object} params | |||
*/ | |||
var deferred = $.Deferred(), | var deferred = $.Deferred(), | ||
customSource = name.match(/^u:(?:([a-z-]+)\.)?([a-z0-9-]+):/), | customSource = name.match(/^u:(?:([a-z-]+)\.)?([a-z0-9-]+):/), | ||
apiEndpoint = 'https://dev.fandom.com/api.php', | apiEndpoint = 'https://dev.fandom.com/api.php', | ||
apiEndpointRgx = new RegExp( | |||
// '^(https:\/\/(([a-z0-9-]+)\.fandom\.com(?:\/([a-z-]+))?|(([a-z-]+)\.wikipedia\.org\/w))\/api\.php)$' | |||
'^(https:\/\/(([a-z0-9-]+)\.fandom\.com(?:\/([a-z-]+))?)\/api\.php)$' | |||
), | |||
page = 'MediaWiki:Custom-' + name + '/i18n.json', | page = 'MediaWiki:Custom-' + name + '/i18n.json', | ||
params; | params; | ||
options = options || {}; | options = options || {}; | ||
if (options.apiEndpoint && apiEndpointRgx.test(options.apiEndpoint)) { | |||
options.apiEndpoint = options.apiEndpoint; | |||
} else { | |||
options.apiEndpoint = apiEndpoint; | |||
} | |||
options.page = (options.page && options.page.replace(/\$1/g, name)) || page; | |||
options.cacheVersion = Number(options.cacheVersion) || 0; | options.cacheVersion = Number(options.cacheVersion) || 0; | ||
options.language = options.language || conf.wgUserLanguage; | options.language = options.language || conf.wgUserLanguage; | ||
Line 920: | Line 1,280: | ||
} | } | ||
// | // Cache isn't suitable - loading from server | ||
options.loadedFromCache = false; | options.loadedFromCache = false; | ||
/ | /* | ||
* Allow custom i18n pages to be specified on other wikis. | |||
* Mainly for SOAP Wiki to keep their own JSON file. | |||
* Note this only supports loading from wikis on fandom.com. | |||
*/ | |||
if (customSource) { | if (customSource) { | ||
apiEndpoint = apiEndpoint.replace('dev', customSource[2]); | apiEndpoint = apiEndpoint.replace('dev', customSource[2]); | ||
Line 952: | Line 1,314: | ||
}; | }; | ||
/ | /* | ||
* 'site' and 'user' are dependencies so end-users can set overrides in their local JS | |||
* and have it take effect before we load the messages. | |||
* Generally, we will implicitly depend on those anyway due to where/when this is loaded. | |||
*/ | |||
mw.loader.using(['mediawiki.language', 'mediawiki.util'/*, 'site', 'user'*/], function () { | mw.loader.using(['mediawiki.language', 'mediawiki.util'/*, 'site', 'user'*/], function () { | ||
$.ajax(apiEndpoint, { | $.ajax(apiEndpoint, { | ||
Line 973: | Line 1,337: | ||
} | } | ||
// | // Expose under the dev global | ||
window.dev.i18n = $.extend(window.dev.i18n, { | window.dev.i18n = $.extend(window.dev.i18n, { | ||
loadMessages: loadMessages, | loadMessages: loadMessages, | ||
/ | /* | ||
* "Hidden" functions to allow testing and debugging | |||
* they may be changed or removed without warning. | |||
* Scripts should not rely on these existing or their output being in any particular format. | |||
*/ | |||
_bcp47: bcp47, | |||
_stripComments: stripComments, | _stripComments: stripComments, | ||
_saveToCache: saveToCache, | _saveToCache: saveToCache, | ||
Line 989: | Line 1,356: | ||
}); | }); | ||
// | // Initialise overrides object | ||
window.dev.i18n.overrides = window.dev.i18n.overrides || {}; | window.dev.i18n.overrides = window.dev.i18n.overrides || {}; | ||
overrides = window.dev.i18n.overrides; | overrides = window.dev.i18n.overrides; | ||
/ | /* | ||
* Fire an event on load. | |||
* Alternatively, use $.getScript (or mw.loader) and use the returned promise. | |||
*/ | |||
mw.hook('dev.i18n').fire(window.dev.i18n); | mw.hook('dev.i18n').fire(window.dev.i18n); | ||
// | // Tidy the localStorage cache of old entries | ||
removeOldCacheEntries(); | removeOldCacheEntries(); | ||
}(this, jQuery, mediaWiki)); | } (this, jQuery, mediaWiki)); |