MediaWiki:Gadget-I18n-js.js: Difference between revisions

From Tardis Wiki, the free Doctor Who reference
No edit summary
(Remove Wikimedia-Gerrit-style whitespaces)
Line 12: Line 12:
  *  includes a check to prevent double loading. This can make it painful to test from your
  *  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.
  *  JS console. To get around this, add ?usesitejs=0&useuserjs=0 to your URL.
* @notes Coding convensions:
*  https://www.mediawiki.org/wiki/Manual:Coding_conventions/JavaScript
  */
  */


Line 26: Line 24:
  */
  */


( function ( window, $, mw, undefined ) {
(function (window, $, mw, undefined) {
     'use strict';
     'use strict';


Line 33: Line 31:


     /* Prevent double loading and loss of cache */
     /* Prevent double loading and loss of cache */
     if ( window.dev.i18n.loadMessages !== undefined ) {
     if (window.dev.i18n.loadMessages !== undefined) {
         return;
         return;
     }
     }
Line 57: Line 55:
     *    'null' when the page lannguage doesn't have language variants.
     *    'null' when the page lannguage doesn't have language variants.
     */
     */
     var conf = mw.config.get( [
     var conf = mw.config.get([
         'debug',
         'debug',
         'wgContentLanguage',
         'wgContentLanguage',
Line 63: Line 61:
         'wgUserLanguage',
         'wgUserLanguage',
         'wgUserVariant'
         'wgUserVariant'
    ] ),
  ]),


         /*
         /*
Line 92: 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
         * @var {(null|object)} overrides
Line 102: Line 101:
         * versions of MediaWiki to up-to-date, current language codes.
         * versions of MediaWiki to up-to-date, current language codes.
         *
         *
         * These codes shouldn't be used to store translations unless there are language
         * These codes shouldn't be used to store translations unless there are
         * changes to /includes/language/LanguageCode.php in mediawiki/core.
         * language changes to /includes/language/LanguageCode.php in mediawiki/core.
         *
         *
         * These may or may not be valid BCP 47 codes; they are included here
         * These may or may not be valid BCP 47 codes; they are included here
Line 180: Line 179:
         * including cdo <=> nan, pt <=> pt-br, zh <=> zh-hans <=> zh-hant
         * including cdo <=> nan, pt <=> pt-br, zh <=> zh-hans <=> zh-hant
         *
         *
         * @var {object.<string, string[]>} Mapping from language codes to fallback language codes
         * @var {object.<string, string[]>} Mapping from language codes to fallback
        * language codes
         */
         */
         fallbacks = {
         fallbacks = {
             'ab': [ 'ru' ],
             'ab': ['ru'],
             'abs': [ 'id' ],
             'abs': ['id'],
             'ace': [ 'id' ],
             'ace': ['id'],
             'ady': [ 'ady-cyrl' ],
             'ady': ['ady-cyrl'],
             'aeb': [ 'aeb-arab' ],
             'aeb': ['aeb-arab'],
             'aeb-arab': [ 'ar' ],
             'aeb-arab': ['ar'],
             'aln': [ 'sq' ],
             'aln': ['sq'],
             'alt': [ 'ru' ],
             'alt': ['ru'],
             'ami': [ 'zh-tw', 'zh-hant', 'zh', 'zh-hans' ],
             'ami': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'],
             'an': [ 'es' ],
             'an': ['es'],
             'anp': [ 'hi' ],
             'anp': ['hi'],
             'arn': [ 'es' ],
             'arn': ['es'],
             'arq': [ 'ar' ],
             'arq': ['ar'],
             'ary': [ 'ar' ],
             'ary': ['ar'],
             'arz': [ 'ar' ],
             'arz': ['ar'],
             'ast': [ 'es' ],
             'ast': ['es'],
             'atj': [ 'fr' ],
             'atj': ['fr'],
             'av': [ 'ru' ],
             'av': ['ru'],
             'avk': [ 'fr', 'es', 'ru' ],
             'avk': ['fr', 'es', 'ru'],
             'awa': [ 'hi' ],
             'awa': ['hi'],
             'ay': [ 'es' ],
             'ay': ['es'],
             'azb': [ 'fa' ],
             'azb': ['fa'],
             'ba': [ 'ru' ],
             'ba': ['ru'],
             'ban': [ 'id' ],
             'ban': ['id'],
             'ban-bali': [ 'ban' ],
             'ban-bali': ['ban'],
             'bar': [ 'de' ],
             'bar': ['de'],
             'bbc': [ 'bbc-latn' ],
             'bbc': ['bbc-latn'],
             'bbc-latn': [ 'id' ],
             'bbc-latn': ['id'],
             'bcc': [ 'fa' ],
             'bcc': ['fa'],
             'bci': [ 'fr' ],
             'bci': ['fr'],
             'be-tarask': [ 'be' ],
             'be-tarask': ['be'],
             'bgn': [ 'fa' ],
             'bgn': ['fa'],
             'bh': [ 'bho' ],
             'bh': ['bho'],
             'bi': [ 'en' ],
             'bi': ['en'],
             'bjn': [ 'id' ],
             'bjn': ['id'],
             'blk': [ 'my' ],
             'blk': ['my'],
             'bm': [ 'fr' ],
             'bm': ['fr'],
             'bpy': [ 'bn' ],
             'bpy': ['bn'],
             'bqi': [ 'fa' ],
             'bqi': ['fa'],
             'br': [ 'fr' ],
             'br': ['fr'],
             'btm': [ 'id' ],
             'btm': ['id'],
             'bug': [ 'id' ],
             'bug': ['id'],
             'bxr': [ 'ru' ],
             'bxr': ['ru'],
             'ca': [ 'oc' ],
             'ca': ['oc'],
             'cbk-zam': [ 'es' ],
             'cbk-zam': ['es'],
             'cdo': [ 'nan', 'zh-hant', 'zh', 'zh-hans' ],
             'cdo': ['nan', 'zh-hant', 'zh', 'zh-hans'],
             'ce': [ 'ru' ],
             'ce': ['ru'],
             'co': [ 'it' ],
             'co': ['it'],
             'crh': [ 'crh-latn' ],
             'crh': ['crh-latn'],
             'crh-cyrl': [ 'ru' ],
             'crh-cyrl': ['ru'],
             'cs': [ 'sk' ],
             'cs': ['sk'],
             'csb': [ 'pl' ],
             'csb': ['pl'],
             'cv': [ 'ru' ],
             'cv': ['ru'],
             'de-at': [ 'de' ],
             'de-at': ['de'],
             'de-ch': [ 'de' ],
             'de-ch': ['de'],
             'de-formal': [ 'de' ],
             'de-formal': ['de'],
             'dsb': [ 'hsb', 'de' ],
             'dsb': ['hsb', 'de'],
             'dtp': [ 'ms' ],
             'dtp': ['ms'],
             'dty': [ 'ne' ],
             'dty': ['ne'],
             'egl': [ 'it' ],
             'egl': ['it'],
             'eml': [ 'it' ],
             'eml': ['it'],
             'es-formal': [ 'es' ],
             'es-formal': ['es'],
             'ext': [ 'es' ],
             'ext': ['es'],
             'ff': [ 'fr' ],
             'ff': ['fr'],
             'fit': [ 'fi' ],
             'fit': ['fi'],
             'fon': [ 'fr' ],
             'fon': ['fr'],
             'frc': [ 'fr' ],
             'frc': ['fr'],
             'frp': [ 'fr' ],
             'frp': ['fr'],
             'frr': [ 'de' ],
             'frr': ['de'],
             'fur': [ 'it' ],
             'fur': ['it'],
             'gag': [ 'tr' ],
             'gag': ['tr'],
             'gan': [ 'gan-hant', 'gan-hans', 'zh-hant', 'zh', 'zh-hans' ],
             'gan': ['gan-hant', 'gan-hans', 'zh-hant', 'zh', 'zh-hans'],
             'gan-hans': [ 'gan', 'gan-hant', 'zh-hans', 'zh', 'zh-hant' ],
             'gan-hans': ['gan', 'gan-hant', 'zh-hans', 'zh', 'zh-hant'],
             'gan-hant': [ 'gan', 'gan-hans', 'zh-hant', 'zh', 'zh-hans' ],
             'gan-hant': ['gan', 'gan-hans', 'zh-hant', 'zh', 'zh-hans'],
             'gcr': [ 'fr' ],
             'gcr': ['fr'],
             'gl': [ 'pt' ],
             'gl': ['pt'],
             'gld': [ 'ru' ],
             'gld': ['ru'],
             'glk': [ 'fa' ],
             'glk': ['fa'],
             'gn': [ 'es' ],
             'gn': ['es'],
             'gom': [ 'gom-deva' ],
             'gom': ['gom-deva'],
             'gom-deva': [ 'hi' ],
             'gom-deva': ['hi'],
             'gor': [ 'id' ],
             'gor': ['id'],
             'gsw': [ 'de' ],
             'gsw': ['de'],
             'guc': [ 'es' ],
             'guc': ['es'],
             'hak': [ 'zh-hant', 'zh', 'zh-hans' ],
             'hak': ['zh-hant', 'zh', 'zh-hans'],
             'hif': [ 'hif-latn' ],
             'hif': ['hif-latn'],
             'hrx': [ 'de' ],
             'hrx': ['de'],
             'hsb': [ 'dsb', 'de' ],
             'hsb': ['dsb', 'de'],
             'hsn': [ 'zh-cn', 'zh-hans', 'zh', 'zh-hant' ],
             'hsn': ['zh-cn', 'zh-hans', 'zh', 'zh-hant'],
             'ht': [ 'fr' ],
             'ht': ['fr'],
             'hu-formal': [ 'hu' ],
             'hu-formal': ['hu'],
             'hyw': [ 'hy' ],
             'hyw': ['hy'],
             'ii': [ 'zh-cn', 'zh-hans', 'zh', 'zh-hant' ],
             'ii': ['zh-cn', 'zh-hans', 'zh', 'zh-hant'],
             'inh': [ 'ru' ],
             'inh': ['ru'],
             'io': [ 'eo' ],
             'io': ['eo'],
             'iu': [ 'ike-cans' ],
             'iu': ['ike-cans'],
             'jam': [ 'en' ],
             'jam': ['en'],
             'jut': [ 'da' ],
             'jut': ['da'],
             'jv': [ 'id' ],
             'jv': ['id'],
             'kaa': [ 'kk-latn', 'kk-cyrl' ],
             'kaa': ['kk-latn', 'kk-cyrl'],
             'kab': [ 'fr' ],
             'kab': ['fr'],
             'kbd': [ 'kbd-cyrl' ],
             'kbd': ['kbd-cyrl'],
             'kbp': [ 'fr' ],
             'kbp': ['fr'],
             'kea': [ 'pt' ],
             'kea': ['pt'],
             'khw': [ 'ur' ],
             'khw': ['ur'],
             'kiu': [ 'tr' ],
             'kiu': ['tr'],
             'kjp': [ 'my' ],
             'kjp': ['my'],
             'kk': [ 'kk-cyrl' ],
             'kk': ['kk-cyrl'],
             'kk-arab': [ 'kk-cyrl' ],
             'kk-arab': ['kk-cyrl'],
             'kk-cn': [ 'kk-arab', 'kk-cyrl' ],
             'kk-cn': ['kk-arab', 'kk-cyrl'],
             'kk-kz': [ 'kk-cyrl' ],
             'kk-kz': ['kk-cyrl'],
             'kk-latn': [ 'kk-cyrl' ],
             'kk-latn': ['kk-cyrl'],
             'kk-tr': [ 'kk-latn', 'kk-cyrl' ],
             'kk-tr': ['kk-latn', 'kk-cyrl'],
             'kl': [ 'da' ],
             'kl': ['da'],
             'koi': [ 'ru' ],
             'koi': ['ru'],
             'ko-kp': [ 'ko' ],
             'ko-kp': ['ko'],
             'krc': [ 'ru' ],
             'krc': ['ru'],
             'krl': [ 'fi' ],
             'krl': ['fi'],
             'ks': [ 'ks-arab' ],
             'ks': ['ks-arab'],
             'ksh': [ 'de' ],
             'ksh': ['de'],
             'ksw': [ 'my' ],
             'ksw': ['my'],
             'ku': [ 'ku-latn' ],
             'ku': ['ku-latn'],
             'kum': [ 'ru' ],
             'kum': ['ru'],
             'ku-arab': [ 'ckb' ],
             'ku-arab': ['ckb'],
             'kv': [ 'ru' ],
             'kv': ['ru'],
             'lad': [ 'es' ],
             'lad': ['es'],
             'lb': [ 'de' ],
             'lb': ['de'],
             'lbe': [ 'ru' ],
             'lbe': ['ru'],
             'lez': [ 'ru', 'az' ],
             'lez': ['ru', 'az'],
             'li': [ 'nl' ],
             'li': ['nl'],
             'lij': [ 'it' ],
             'lij': ['it'],
             'liv': [ 'et' ],
             'liv': ['et'],
             'lki': [ 'fa' ],
             'lki': ['fa'],
             'lld': [ 'it', 'rm', 'fur' ],
             'lld': ['it', 'rm', 'fur'],
             'lmo': [ 'pms', 'eml', 'lij', 'vec', 'it' ],
             'lmo': ['pms', 'eml', 'lij', 'vec', 'it'],
             'ln': [ 'fr' ],
             'ln': ['fr'],
             'lrc': [ 'fa' ],
             'lrc': ['fa'],
             'ltg': [ 'lv' ],
             'ltg': ['lv'],
             'luz': [ 'fa' ],
             'luz': ['fa'],
             'lzh': [ 'zh-hant', 'zh', 'zh-hans' ],
             'lzh': ['zh-hant', 'zh', 'zh-hans'],
             'lzz': [ 'tr' ],
             'lzz': ['tr'],
             'mad': [ 'id' ],
             'mad': ['id'],
             'mai': [ 'hi' ],
             'mai': ['hi'],
             'map-bms': [ 'jv', 'id' ],
             'map-bms': ['jv', 'id'],
             'mdf': [ 'myv', 'ru' ],
             'mdf': ['myv', 'ru'],
             'mg': [ 'fr' ],
             'mg': ['fr'],
             'mhr': [ 'mrj', 'ru' ],
             'mhr': ['mrj', 'ru'],
             'min': [ 'id' ],
             'min': ['id'],
             'mnw': [ 'my' ],
             'mnw': ['my'],
             'mo': [ 'ro' ],
             'mo': ['ro'],
             'mrj': [ 'mhr', 'ru' ],
             'mrj': ['mhr', 'ru'],
             'ms-arab': [ 'ms' ],
             'ms-arab': ['ms'],
             'mwl': [ 'pt' ],
             'mwl': ['pt'],
             'myv': [ 'mdf', 'ru' ],
             'myv': ['mdf', 'ru'],
             'mzn': [ 'fa' ],
             'mzn': ['fa'],
             'nah': [ 'es' ],
             'nah': ['es'],
             'nan': [ 'cdo', 'zh-hant', 'zh', 'zh-hans' ],
             'nan': ['cdo', 'zh-hant', 'zh', 'zh-hans'],
             'nap': [ 'it' ],
             'nap': ['it'],
             'nb': [ 'nn' ],
             'nb': ['nn'],
             'nds': [ 'de' ],
             'nds': ['de'],
             'nds-nl': [ 'nl' ],
             'nds-nl': ['nl'],
             'nia': [ 'id' ],
             'nia': ['id'],
             'nl-informal': [ 'nl' ],
             'nl-informal': ['nl'],
             'nn': [ 'nb' ],
             'nn': ['nb'],
             'nrm': [ 'nrf', 'fr' ],
             'nrm': ['nrf', 'fr'],
             'oc': [ 'ca', 'fr' ],
             'oc': ['ca', 'fr'],
             'olo': [ 'fi' ],
             'olo': ['fi'],
             'os': [ 'ru' ],
             'os': ['ru'],
             'pcd': [ 'fr' ],
             'pcd': ['fr'],
             'pdc': [ 'de' ],
             'pdc': ['de'],
             'pdt': [ 'de' ],
             'pdt': ['de'],
             'pfl': [ 'de' ],
             'pfl': ['de'],
             'pih': [ 'en' ],
             'pih': ['en'],
             'pms': [ 'it' ],
             'pms': ['it'],
             'pnt': [ 'el' ],
             'pnt': ['el'],
             'pt': [ 'pt-br' ],
             'pt': ['pt-br'],
             'pt-br': [ 'pt' ],
             'pt-br': ['pt'],
             'pwn': [ 'zh-tw', 'zh-hant', 'zh', 'zh-hans' ],
             'pwn': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'],
             'qu': [ 'qug', 'es' ],
             'qu': ['qug', 'es'],
             'qug': [ 'qu', 'es' ],
             'qug': ['qu', 'es'],
             'rgn': [ 'it' ],
             'rgn': ['it'],
             'rmy': [ 'ro' ],
             'rmy': ['ro'],
             'roa-tara': [ 'it' ],
             'roa-tara': ['it'],
             'rsk': [ 'sr-ec' ],
             'rsk': ['sr-ec'],
             'rue': [ 'uk', 'ru' ],
             'rue': ['uk', 'ru'],
             'rup': [ 'ro' ],
             'rup': ['ro'],
             'ruq': [ 'ruq-latn', 'ro' ],
             'ruq': ['ruq-latn', 'ro'],
             'ruq-cyrl': [ 'mk' ],
             'ruq-cyrl': ['mk'],
             'ruq-latn': [ 'ro' ],
             'ruq-latn': ['ro'],
             'sa': [ 'hi' ],
             'sa': ['hi'],
             'sah': [ 'ru' ],
             'sah': ['ru'],
             'scn': [ 'it' ],
             'scn': ['it'],
             'sco': [ 'en' ],
             'sco': ['en'],
             'sdc': [ 'it' ],
             'sdc': ['it'],
             'sdh': [ 'cbk', 'fa' ],
             'sdh': ['cbk', 'fa'],
             'se': [ 'nb', 'fi' ],
             'se': ['nb', 'fi'],
             'ses': [ 'fr' ],
             'ses': ['fr'],
             'se-fi': [ 'se', 'fi', 'sv' ],
             'se-fi': ['se', 'fi', 'sv'],
             'se-no': [ 'se', 'nb', 'nn' ],
             'se-no': ['se', 'nb', 'nn'],
             'se-se': [ 'se', 'sv' ],
             'se-se': ['se', 'sv'],
             'sg': [ 'fr' ],
             'sg': ['fr'],
             'sgs': [ 'lt' ],
             'sgs': ['lt'],
             'sh': [ 'bs', 'sr-el', 'hr' ],
             'sh': ['bs', 'sr-el', 'hr'],
             'shi': [ 'fr' ],
             'shi': ['fr'],
             'shy': [ 'shy-latn' ],
             'shy': ['shy-latn'],
             'shy-latn': [ 'fr' ],
             'shy-latn': ['fr'],
             'sjd': [ 'ru' ],
             'sjd': ['ru'],
             'sk': [ 'cs' ],
             'sk': ['cs'],
             'skr': [ 'skr-arab' ],
             'skr': ['skr-arab'],
             'skr-arab': [ 'ur', 'pnb' ],
             'skr-arab': ['ur', 'pnb'],
             'sli': [ 'de' ],
             'sli': ['de'],
             'smn': [ 'fi' ],
             'smn': ['fi'],
             'sr': [ 'sr-ec' ],
             'sr': ['sr-ec'],
             'srn': [ 'nl' ],
             'srn': ['nl'],
             'stq': [ 'de' ],
             'stq': ['de'],
             'sty': [ 'ru' ],
             'sty': ['ru'],
             'su': [ 'id' ],
             'su': ['id'],
             'szl': [ 'pl' ],
             'szl': ['pl'],
             'szy': [ 'zh-tw', 'zh-hant', 'zh', 'zh-hans' ],
             'szy': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'],
             'tay': [ 'zh-tw', 'zh-hant', 'zh', 'zh-hans' ],
             'tay': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'],
             'tcy': [ 'kn' ],
             'tcy': ['kn'],
             'tet': [ 'pt' ],
             'tet': ['pt'],
             'tg': [ 'tg-cyrl' ],
             'tg': ['tg-cyrl'],
             'trv': [ 'zh-tw', 'zh-hant', 'zh', 'zh-hans' ],
             'trv': ['zh-tw', 'zh-hant', 'zh', 'zh-hans'],
             'tt': [ 'tt-cyrl', 'ru' ],
             'tt': ['tt-cyrl', 'ru'],
             'tt-cyrl': [ 'ru' ],
             'tt-cyrl': ['ru'],
             'ty': [ 'fr' ],
             'ty': ['fr'],
             'tyv': [ 'ru' ],
             'tyv': ['ru'],
             'udm': [ 'ru' ],
             'udm': ['ru'],
             'ug': [ 'ug-arab' ],
             'ug': ['ug-arab'],
             'vec': [ 'it' ],
             'vec': ['it'],
             'vep': [ 'et' ],
             'vep': ['et'],
             'vls': [ 'nl' ],
             'vls': ['nl'],
             'vmf': [ 'de' ],
             'vmf': ['de'],
             'vmw': [ 'pt' ],
             'vmw': ['pt'],
             'vot': [ 'fi' ],
             'vot': ['fi'],
             'vro': [ 'et' ],
             'vro': ['et'],
             'wa': [ 'fr' ],
             'wa': ['fr'],
             'wls': [ 'fr' ],
             'wls': ['fr'],
             'wo': [ 'fr' ],
             'wo': ['fr'],
             'wuu': [ 'zh-hans', 'zh', 'zh-hant' ],
             'wuu': ['zh-hans', 'zh', 'zh-hant'],
             'xal': [ 'ru' ],
             'xal': ['ru'],
             'xmf': [ 'ka' ],
             'xmf': ['ka'],
             'yi': [ 'he' ],
             'yi': ['he'],
             'yue': [ 'zh-hk', 'zh-hant', 'zh', 'zh-hans' ],
             'yue': ['zh-hk', 'zh-hant', 'zh', 'zh-hans'],
             'za': [ 'zh-hans', 'zh', 'zh-hant' ],
             'za': ['zh-hans', 'zh', 'zh-hant'],
             'zea': [ 'nl' ],
             'zea': ['nl'],
             'zgh': [ 'kab' ],
             'zgh': ['kab'],
             'zh': [ 'zh-hans', 'zh-hant', 'zh-cn', 'zh-tw', 'zh-hk' ],
             'zh': ['zh-hans', 'zh-hant', 'zh-cn', 'zh-tw', 'zh-hk'],
             'zh-cn': [ 'zh-hans', 'zh', 'zh-hant' ],
             'zh-cn': ['zh-hans', 'zh', 'zh-hant'],
             'zh-hans': [ 'zh-cn', 'zh', 'zh-hant' ],
             'zh-hans': ['zh-cn', 'zh', 'zh-hant'],
             'zh-hant': [ 'zh-tw', 'zh-hk', 'zh', 'zh-hans' ],
             'zh-hant': ['zh-tw', 'zh-hk', 'zh', 'zh-hans'],
             'zh-hk': [ 'zh-hant', 'zh-tw', 'zh', 'zh-hans' ],
             'zh-hk': ['zh-hant', 'zh-tw', 'zh', 'zh-hans'],
             'zh-mo': [ '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-my': ['zh-sg', 'zh-hans', 'zh-cn', 'zh', 'zh-hant'],
             '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' ]
             'zh-tw': ['zh-hant', 'zh-hk', 'zh', 'zh-hans']
         };
         };


Line 456: Line 456:
     * @see /includes/language/LanguageCode.php in MediaWiki core
     * @see /includes/language/LanguageCode.php in MediaWiki core
     */
     */
     function bcp47( lang ) {
     function bcp47(lang) {
         if ( nonStandardCodes[ lang ] ) {
         if (nonStandardCodes[lang]) {
             return nonStandardCodes[ lang ];
             return nonStandardCodes[lang];
         }
         }


         if ( deprecatedCodes[ lang ] ) {
         if (deprecatedCodes[lang]) {
             return bcp47( deprecatedCodes[ lang ] );
             return bcp47(deprecatedCodes[lang]);
         }
         }


Line 474: Line 474:
             isFirstSegment = true,
             isFirstSegment = true,
             isPrivate = false,
             isPrivate = false,
             segments = lang.split( '-' );
             segments = lang.split('-');


         formatted = segments.map( function ( segment ) {
         formatted = segments.map(function (segment) {
             /*
             /*
             * @var {string} newSegment The converted segment of language code
             * @var {string} newSegment The converted segment of language code
Line 483: Line 483:


             /* when previous segment is x, it is a private segment and should be lc */
             /* when previous segment is x, it is a private segment and should be lc */
             if ( isPrivate ) {
             if (isPrivate) {
                 newSegment = segment.toLowerCase();
                 newSegment = segment.toLowerCase();
             /* ISO 3166 country code */
             /* ISO 3166 country code */
             } else if ( segment.length === 2 && !isFirstSegment ) {
             } else if (segment.length === 2 && !isFirstSegment) {
                 newSegment = segment.toUpperCase();
                 newSegment = segment.toUpperCase();
             /* ISO 15924 script code */
             /* ISO 15924 script code */
             } else if ( segment.length === 4 && !isFirstSegment ) {
             } else if (segment.length === 4 && !isFirstSegment) {
                 newSegment = segment.charAt( 0 ).toUpperCase() + segment.substring( 1 ).toLowerCase();
                 newSegment = segment.charAt(0).toUpperCase() + segment.substring(1).toLowerCase();
             /* Use lowercase for other cases */
             /* Use lowercase for other cases */
             } else {
             } else {
Line 500: Line 500:


             return newSegment;
             return newSegment;
         } );
         });


         return formatted.join( '-' );
         return formatted.join('-');
     }
     }


Line 512: Line 512:
     * @param {string[]} 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) {
         if ( warnedAboutFallbackLoop ) {
         if (warnedAboutFallbackLoop) {
             return;
             return;
         }
         }
         warnedAboutFallbackLoop = true;
         warnedAboutFallbackLoop = true;


         fallbackChain.push( lang );
         fallbackChain.push(lang);
         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( ', ' ) );
         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 533: Line 533:
     * @return {(string|boolean)} The requested translation or `false` if no message could be found.
     * @return {(string|boolean)} 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 ] ) {
         if (deprecatedCodes[lang]) {
             return getMsg( messages, msgName, deprecatedCodes[ lang ], fallbackChain );
             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];
         }
         }


         if ( !fallbackChain ) {
         if (!fallbackChain) {
             fallbackChain = [];
             fallbackChain = [];
         }
         }
Line 549: Line 549:
         * @var {string} fallbackLang
         * @var {string} fallbackLang
         */
         */
         for ( var fallbackLang of fallbacks[ lang ] ) {
         for (var fallbackLang of fallbacks[lang]) {
             if ( messages[ fallbackLang ] && messages[ fallbackLang ][ msgName ] ) {
             if (messages[fallbackLang] && messages[fallbackLang][msgName]) {
                 return messages[ fallbackLang ][ msgName ];
                 return messages[fallbackLang][msgName];
             }
             }


             if ( fallbackChain.indexOf( fallbackLang ) !== -1 ) {
             if (fallbackChain.indexOf(fallbackLang) !== -1) {
                 /*
                 /*
                 * Duplicated language code in fallback list
                 * Duplicated language code in fallback list
                 * Try to find next fallback language from list
                 * Try to find next fallback language from list
                 */
                 */
                 warnOnFallbackLoop( fallbackLang, fallbackChain );
                 warnOnFallbackLoop(fallbackLang, fallbackChain);
                 continue;
                 continue;
             }
             }
             fallbackChain.push( fallbackLang );
             fallbackChain.push(fallbackLang);
         }
         }


         /* No more languages in fallback list - switch to English */
         /* No more languages in fallback list - switch to English */
         if ( messages.en && messages.en[ msgName ] ) {
         if (messages.en && messages.en[msgName]) {
             return messages.en[ msgName ];
             return messages.en[msgName];
         }
         }


Line 581: Line 581:
     * @return {string} The resulting message.
     * @return {string} 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 {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);
         } );
         });


         return message;
         return message;
Line 604: Line 604:
     * @return {string} The generated link.
     * @return {string} The generated link.
     */
     */
     function makeLink( href, text, hasProtocol ) {
     function makeLink(href, text, hasProtocol) {
         text = text || href;
         text = text || href;
         href = hasProtocol ? href : mw.util.getUrl( href );
         href = hasProtocol ? href : mw.util.getUrl(href);


         text = mw.html.escape( text );
         text = mw.html.escape(text);
         href = mw.html.escape( href );
         href = mw.html.escape(href);


         return '<a href="' + href + '" title="' + text + '">' + text + '</a>';
         return '<a href="' + href + '" title="' + text + '">' + text + '</a>';
Line 635: Line 635:
     * @return The sanitised HTML code.
     * @return The sanitised HTML code.
     */
     */
     function sanitiseHtml( html ) {
     function sanitiseHtml(html) {
         /*
         /*
         * @var context
         * @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),
             $div = $( '<div>', context ).append( $html ),
             $div = $('<div>', context).append($html),
             allowedAttrs = [
             allowedAttrs = [
                 'title',
                 'title',
                 'style',
                 'style',
                 'class'
                 'class'
            ],
          ],
             allowedTags = [
             allowedTags = [
                 'i',
                 'i',
Line 655: Line 655:
                 'strong',
                 'strong',
                 'span',
                 'span',
            ];
          ];


         $div.find( '*' ).each( function () {
         $div.find('*').each(function () {
             var $this = $( this ),
             var $this = $(this),
                 tagname = $this.prop( 'tagName' ).toLowerCase(),
                 tagname = $this.prop('tagName').toLowerCase(),
                 attrs,
                 attrs,
                 array,
                 array,
                 style;
                 style;


             if ( allowedTags.indexOf( tagname ) === -1 ) {
             if (allowedTags.indexOf(tagname) === -1) {
                 mw.log( '[I18n-js] Disallowed tag in message: ' + tagname );
                 mw.log('[I18n-js] Disallowed tag in message: ' + tagname);
                 $this.remove();
                 $this.remove();
                 return;
                 return;
             }
             }


             attrs = $this.prop( 'attributes' );
             attrs = $this.prop('attributes');
             array = Array.prototype.slice.call( attrs );
             array = Array.prototype.slice.call(attrs);


             array.forEach( function ( attr ) {
             array.forEach(function (attr) {
                 if ( allowedAttrs.indexOf( attr.name ) === -1 ) {
                 if (allowedAttrs.indexOf(attr.name) === -1) {
                     mw.log( '[I18n-js] Disallowed attribute in message: ' + attr.name + ', tag: ' + tagname );
                     mw.log('[I18n-js] Disallowed attribute in message: ' + attr.name + ', tag: ' + tagname);
                     $this.removeAttr( attr.name );
                     $this.removeAttr(attr.name);
                     return;
                     return;
                 }
                 }


                 /* Make sure there's nothing nasty in style attributes */
                 /* 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');


                     if ( style.indexOf( 'url(' ) > -1 ) {
                     if (style.indexOf('url(') > -1) {
                         mw.log( '[I18n-js] Disallowed url() in style attribute' );
                         mw.log('[I18n-js] Disallowed url() in style attribute');
                         $this.removeAttr( 'style' );
                         $this.removeAttr('style');


                     /* https://phabricator.wikimedia.org/T208881 */
                     /* https://phabricator.wikimedia.org/T208881 */
                     } else if ( style.indexOf( 'var(' ) > -1 ) {
                     } else if (style.indexOf('var(') > -1) {
                         mw.log( '[I18n-js] Disallowed var() in style attribute' );
                         mw.log('[I18n-js] Disallowed var() in style attribute');
                         $this.removeAttr( 'style' );
                         $this.removeAttr('style');
                     }
                     }
                 }
                 }
             } );
             });
         } );
         });


         return $div.prop( 'innerHTML' );
         return $div.prop('innerHTML');
     }
     }


Line 713: Line 713:
     * @return {string} The resulting string.
     * @return {string} The resulting string.
     */
     */
     function parse( message ) {
     function parse(message) {
         /* [url text] -> [$1 $2] */
         /* [url text] -> [$1 $2] */
         var urlRgx = /\[((?:https?:)?\/\/.+?) (.+?)\]/g,
         var urlRgx = /\[((?:https?:)?\/\/.+?) (.+?)\]/g,
Line 725: Line 725:
             genderRgx = /\{\{GENDER:([^|]+)\|(.+?)\}\}/gi;
             genderRgx = /\{\{GENDER:([^|]+)\|(.+?)\}\}/gi;


         if ( message.indexOf( '<' ) > -1 ) {
         if (message.indexOf('<') > -1) {
             message = sanitiseHtml( message );
             message = sanitiseHtml(message);
         }
         }


         return message
         return message
             .replace( urlRgx, function ( _match, href, text ) {
             .replace(urlRgx, function (_match, href, text) {
                 return makeLink( href, text, true );
                 return makeLink(href, text, true);
             } )
             })
             .replace( simplePageRgx, function ( _match, href ) {
             .replace(simplePageRgx, function (_match, href) {
                 return makeLink( href );
                 return makeLink(href);
             } )
             })
             .replace( pageWithTextRgx, function ( _match, href, text ) {
             .replace(pageWithTextRgx, function (_match, href, text) {
                 return makeLink( href, text );
                 return makeLink(href, text);
             } )
             })
             .replace( pluralRgx, function ( _match, count, forms ) {
             .replace(pluralRgx, function (_match, count, forms) {
                 return mw.language.convertPlural( Number( count ), forms.split( '|' ) );
                 return mw.language.convertPlural(Number(count), forms.split('|'));
             } )
             })
             .replace(genderRgx, function ( _match, gender, forms ) {
             .replace(genderRgx, function (_match, gender, forms) {
                 return mw.language.gender( gender, forms.split( '|' ) );
                 return mw.language.gender(gender, forms.split('|'));
             } );
             });
     }
     }


Line 756: Line 756:
     * @return
     * @return
     */
     */
     function message( messages, lang, args, name ) {
     function message(messages, lang, args, name) {
         if ( !args.length ) {
         if (!args.length) {
             return;
             return;
         }
         }
Line 769: Line 769:
         var msgName = args.shift(),
         var msgName = args.shift(),
             descriptiveMsgName = 'i18njs-' + name + '-' + msgName,
             descriptiveMsgName = 'i18njs-' + name + '-' + msgName,
             msg = getMsg( messages, msgName, lang ),
             msg = getMsg(messages, msgName, lang),
             msgExists = msg !== false;
             msgExists = msg !== false;


         if ( !msgExists ) {
         if (!msgExists) {
             /* use name wrapped in < > for missing message, per MediaWiki convention */
             /* use name wrapped in < > for missing message, per MediaWiki convention */
             msg = '<' + descriptiveMsgName + '>';
             msg = '<' + descriptiveMsgName + '>';
         }
         }


         if ( conf.wgUserLanguage === 'qqx' && msgExists ) {
         if (conf.wgUserLanguage === 'qqx' && msgExists) {
             /* https://www.mediawiki.org/wiki/Help:System_message#Finding_messages_and_documentation */
             /* https://www.mediawiki.org/wiki/Help:System_message#Finding_messages_and_documentation */
             msg = '(' + descriptiveMsgName + ')';
             msg = '(' + descriptiveMsgName + ')';
         } else if ( overrides[ name ] && overrides[ name ][ msgName ] ) {
         } else if (overrides[name] && overrides[name][msgName]) {
             /* if the message has been overridden, use that without checking the language */
             /* if the message has been overridden, use that without checking the language */
             msg = overrides[ name ][ msgName ];
             msg = overrides[name][msgName];
             msgExists = true;
             msgExists = true;
         }
         }


         if ( args.length ) {
         if (args.length) {
             msg = handleArgs( msg, args );
             msg = handleArgs(msg, args);
         }
         }


Line 806: Line 806:
                 * the sanitisation will mess with it.
                 * the sanitisation will mess with it.
                 */
                 */
                 if ( !this.exists ) {
                 if (!this.exists) {
                     return this.escape();
                     return this.escape();
                 }
                 }


                 return parse( msg );
                 return parse(msg);
             },
             },


Line 819: Line 819:
             */
             */
             escape: function () {
             escape: function () {
                 return mw.html.escape( msg );
                 return mw.html.escape(msg);
             },
             },


Line 841: Line 841:
     * @return {object}
     * @return {object}
     */
     */
     function i18n( messages, name, options ) {
     function i18n(messages, name, options) {
         var defaultLang = options.language,
         var defaultLang = options.language,
             tempLang = null;
             tempLang = null;
Line 852: Line 852:
             */
             */
             useLang: function () {
             useLang: function () {
                 console.warn( '[I18n-js] “useLang()” is no longer supported by I18n-js (used in “' + name + '”) - using user language.' );
                 console.warn('[I18n-js] “useLang()” is no longer supported by I18n-js (used in “' + name + '”) - using user language.');
                 this.useUserLang();
                 this.useUserLang();
             },
             },
Line 864: Line 864:
             */
             */
             inLang: function (lang) {
             inLang: function (lang) {
                 if ( !options.cacheAll ) {
                 if (!options.cacheAll) {
                     console.warn( '[I18n-js] “inLang()” is not supported without configuring `options.cacheAll` (used in “' + name + '”) - using user language.' );
                     console.warn('[I18n-js] “inLang()” is not supported without configuring `options.cacheAll` (used in “' + name + '”) - using user language.');
                     lang = options.language;
                     lang = options.language;
                 }
                 }
Line 948: Line 948:
             */
             */
             msg: function () {
             msg: function () {
                 var args = Array.prototype.slice.call( arguments ),
                 var args = Array.prototype.slice.call(arguments),
                     lang = defaultLang;
                     lang = defaultLang;


                 if ( tempLang !== null ) {
                 if (tempLang !== null) {
                     lang = tempLang;
                     lang = tempLang;
                     tempLang = null;
                     tempLang = null;
                 }
                 }


                 return message( messages, lang, args, name );
                 return message(messages, lang, args, name);
             },
             },


Line 975: Line 975:
     * @param {object} 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) {
         var existingLangs = cache[ name ] && cache[ name ]._messages._isOptimised,
         var existingLangs = cache[name] && cache[name]._messages._isOptimised,
             langs = [ options.language ],
             langs = [options.language],
             msgKeys = Object.keys( messages.en || {} ),
             msgKeys = Object.keys(messages.en || {}),
             optimised = {};
             optimised = {};


         if ( !msgKeys.length ) {
         if (!msgKeys.length) {
             /* No English messages, don't bother optimising */
             /* No English messages, don't bother optimising */
             return messages;
             return messages;
Line 990: Line 990:
         */
         */
         var addMsgsForLanguage = function (lang) {
         var addMsgsForLanguage = function (lang) {
             if ( optimised[ lang ] ) {
             if (optimised[lang]) {
                 /* Language already exists */
                 /* Language already exists */
                 return;
                 return;
             }
             }


             optimised[ lang ] = {};
             optimised[lang] = {};


             msgKeys.forEach( function ( msgName ) {
             msgKeys.forEach(function (msgName) {
                 /*
                 /*
                 * @var msg
                 * @var msg
                 */
                 */
                 var msg = getMsg( messages, msgName, lang );
                 var msg = getMsg(messages, msgName, lang);


                 if ( msg !== false ) {
                 if (msg !== false) {
                     optimised[ lang ][ msgName ] = msg;
                     optimised[lang][msgName] = msg;
                 }
                 }
             } );
             });
         };
         };


         if ( langs.indexOf( conf.wgContentLanguage ) === -1 ) {
         if (langs.indexOf(conf.wgContentLanguage) === -1) {
             langs.push( conf.wgContentLanguage );
             langs.push(conf.wgContentLanguage);
         }
         }


Line 1,018: Line 1,018:
         * language wikis on same domain (i.e. sharing same cache).
         * language wikis on same domain (i.e. sharing same cache).
         */
         */
         if ( existingLangs ) {
         if (existingLangs) {
             existingLangs.forEach( function ( lang ) {
             existingLangs.forEach(function (lang) {
                 if ( langs.indexOf( lang ) === -1 ) {
                 if (langs.indexOf(lang) === -1) {
                     langs.push( lang );
                     langs.push(lang);
                 }
                 }
             } );
             });
         }
         }


         langs.forEach( addMsgsForLanguage );
         langs.forEach(addMsgsForLanguage);


         /*
         /*
Line 1,032: Line 1,032:
         * should not be optimised - save all translations of these messages
         * 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;
             Object.keys( messages ).forEach( addMsgsForLanguage );
             Object.keys(messages).forEach(addMsgsForLanguage);
         }
         }


Line 1,043: Line 1,043:


     /*
     /*
     * Check that the cache for a script exists and, if optimised, contains the necessary languages.
     * Check that the cache for a script exists and, if optimised, contains the
    * necessary languages.
     *
     *
     * @param {string} name The name of the script to check for.
     * @param {string} name The name of the script to check for.
Line 1,049: Line 1,050:
     * @return {boolean} Whether the cache should be used.
     * @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 */
         /* Nothing in cache */
         if ( !messages ) {
         if (!messages) {
             return false;
             return false;
         }
         }
Line 1,063: Line 1,064:
         if (
         if (
             messages._isOptimised &&
             messages._isOptimised &&
             !( messages[ options.language ] && messages[ conf.wgContentLanguage ] )
             !(messages[options.language] && messages[conf.wgContentLanguage])
        ) {
      ) {
             return false;
             return false;
         }
         }
Line 1,077: Line 1,078:
     */
     */
     function removeOldCacheEntries() {
     function removeOldCacheEntries() {
         var isCacheKey = new RegExp( '^(' + cachePrefix + '.+)-content$' ),
         var isCacheKey = new RegExp('^(' + cachePrefix + '.+)-content$'),
             storageKeys = [];
             storageKeys = [];


         try {
         try {
             storageKeys = Object.keys( localStorage );
             storageKeys = Object.keys(localStorage);
         } catch ( e ) {}
         } catch (e) {}


         storageKeys.filter( function ( key ) {
         storageKeys.filter(function (key) {
             return isCacheKey.test( 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'));
             } catch ( e ) {}
             } catch (e) {}


             if ( now - cacheTimestamp < oneDay * 2 ) {
             if (now - cacheTimestamp < oneDay * 2) {
                 /* Cached within last two days, keep it */
                 /* Cached within last two days, keep it */
                 return;
                 return;
Line 1,100: Line 1,101:


             try {
             try {
                 localStorage.removeItem( keyPrefix + '-content' );
                 localStorage.removeItem(keyPrefix + '-content');
                 localStorage.removeItem( keyPrefix + '-timestamp' );
                 localStorage.removeItem(keyPrefix + '-timestamp');
                 localStorage.removeItem( keyPrefix + '-version' );
                 localStorage.removeItem(keyPrefix + '-version');
             } catch ( e ) {}
             } catch (e) {}
         } );
         });
     }
     }


Line 1,114: Line 1,115:
     * @return {string} The JSON string after any comments have been removed.
     * @return {string} The JSON string after any comments have been removed.
     */
     */
     function stripComments( json ) {
     function stripComments(json) {
         json = json
         json = json
             .trim()
             .trim()
             .replace( /\/\*[\s\S]*?\*\//g, '' );
             .replace(/\/\*[\s\S]*?\*\//g, '');
         return json;
         return json;
     }
     }
Line 1,128: Line 1,129:
     * @param {number} 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 {string} keyPrefix
Line 1,135: Line 1,136:


         /* Don't cache empty JSON */
         /* Don't cache empty JSON */
         if ( Object.keys( json ).length === 0 ) {
         if (Object.keys(json).length === 0) {
             return;
             return;
         }
         }


         try {
         try {
             localStorage.setItem( keyPrefix + '-content', JSON.stringify( json ) );
             localStorage.setItem(keyPrefix + '-content', JSON.stringify(json));
             localStorage.setItem( keyPrefix + '-timestamp', now );
             localStorage.setItem(keyPrefix + '-timestamp', now);
             localStorage.setItem( keyPrefix + '-version', cacheVersion || 0 );
             localStorage.setItem(keyPrefix + '-version', cacheVersion || 0);
         } catch ( e ) {}
         } catch (e) {}
     }
     }


Line 1,154: Line 1,155:
     * @return {object} The resulting i18n object.
     * @return {object} The resulting i18n object.
     */
     */
     function parseMessagesToObject( name, res, options ) {
     function parseMessagesToObject(name, res, options) {
         var json = {},
         var json = {},
             obj,
             obj,
Line 1,161: Line 1,162:
         /* Handle parse errors gracefully */
         /* Handle parse errors gracefully */
         try {
         try {
             res = stripComments( res );
             res = stripComments(res);
             json = JSON.parse( res );
             json = JSON.parse(res);
         } catch ( e ) {
         } catch (e) {
             msg = e.message;
             msg = e.message;


             if ( msg === 'Unexpected end of JSON input' ) {
             if (msg === 'Unexpected end of JSON input') {
                 msg += '. This may be caused by a non-existent i18n.json page.';
                 msg += '. This may be caused by a non-existent i18n.json page.';
             }
             }


             console.warn( '[I18n-js] SyntaxError in messages: ' + msg );
             console.warn('[I18n-js] SyntaxError in messages: ' + msg);
         }
         }


Line 1,177: Line 1,178:
             !options.loadedFromCache &&
             !options.loadedFromCache &&
             options.cacheAll !== true
             options.cacheAll !== true
        ) {
      ) {
             json = optimiseMessages( name, json, options );
             json = optimiseMessages(name, json, options);
         }
         }


         obj = i18n( json, name, options );
         obj = i18n(json, name, options);


         /* Cache the result in case it's used multiple times */
         /* Cache the result in case it's used multiple times */
         cache[ name ] = obj;
         cache[name] = obj;


         if ( !options.loadedFromCache ) {
         if (!options.loadedFromCache) {
             saveToCache( name, json, options.cacheVersion );
             saveToCache(name, json, options.cacheVersion);
         }
         }


Line 1,199: Line 1,200:
     * @param {object} options Options set by the loading script.
     * @param {object} options Options set by the loading script.
     */
     */
     function loadFromCache( name, options ) {
     function loadFromCache(name, options) {
         var keyPrefix = cachePrefix + name,
         var keyPrefix = cachePrefix + name,
             cacheContent,
             cacheContent,
Line 1,205: Line 1,206:


         try {
         try {
             cacheContent = localStorage.getItem( keyPrefix + '-content' );
             cacheContent = localStorage.getItem(keyPrefix + '-content');
             cacheVersion = Number( localStorage.getItem( keyPrefix + '-version' ) );
             cacheVersion = Number(localStorage.getItem(keyPrefix + '-version'));
         } catch ( e ) {}
         } catch (e) {}


         /* Cache exists, and its version is greater than or equal to requested version */
         /* 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;
             parseMessagesToObject( name, cacheContent, options );
             parseMessagesToObject(name, cacheContent, options);
         }
         }
     }
     }
Line 1,230: Line 1,231:
     * - {string} page: Set other format of the full page name for the i18n JSON.
     * - {string} page: Set other format of the full page name for the i18n JSON.
     *    Use $1 for the placeholder of name.
     *    Use $1 for the placeholder of name.
     * - {(array|boolean)} cacheAll: Either an array of message names for which translations should not be optimised,
     * - {(array|boolean)} cacheAll: Either an array of message names for which
    *    or `true` to disable the optimised cache.
    *    translations should not be optimised, or `true` to disable the optimised cache.
     * - {number} cacheVersion: Minimum cache version requested by the loading script.
     * - {number} cacheVersion: Minimum cache version requested by the loading script.
     * - {string} language: Set a default language for the script to use, instead of wgUserLanguage.
     * - {string} language: Set a default language for the script to use, instead of wgUserLanguage.
Line 1,238: Line 1,239:
     * @return {object} A jQuery.Deferred instance.
     * @return {object} A jQuery.Deferred instance.
     */
     */
     function loadMessages( name, options ) {
     function loadMessages(name, options) {
         /*
         /*
         * @var {object} deferred
         * @var {object} deferred
Line 1,247: Line 1,248:
         */
         */
         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(
             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-]+))?|(([a-z-]+)\.wikipedia\.org\/w))\/api\.php)$' */
                 '^(https:\/\/(([a-z0-9-]+)\.fandom\.com(?:\/([a-z-]+))?)\/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 ) ) {
         if (options.apiEndpoint && apiEndpointRgx.test(options.apiEndpoint)) {
             options.apiEndpoint = options.apiEndpoint;
             options.apiEndpoint = options.apiEndpoint;
         } else {
         } else {
             options.apiEndpoint = apiEndpoint;
             options.apiEndpoint = apiEndpoint;
         }
         }
         options.page = ( options.page && options.page.replace( /\$1/g, name ) ) || page;
         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;
         options.useCache = ( options.noCache || conf.debug ) !== true;
         options.useCache = (options.noCache || conf.debug) !== true;


         if ( options.useCache ) {
         if (options.useCache) {
             loadFromCache( name, options );
             loadFromCache(name, options);


             if ( cacheIsSuitable( name, options ) ) {
             if (cacheIsSuitable(name, options)) {
                 return deferred.resolve( cache[ name ] );
                 return deferred.resolve(cache[name]);
             }
             }
         }
         }
Line 1,283: Line 1,284:
         * Note this only supports loading from wikis on fandom.com.
         * 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]);
             page = name.slice( customSource[ 0 ].length );
             page = name.slice(customSource[0].length);


             // adjust endpoint when loading from interlanguage wiki
             // adjust endpoint when loading from interlanguage wiki
             if ( customSource[ 1 ] ) {
             if (customSource[1]) {
                 apiEndpoint = apiEndpoint.replace(
                 apiEndpoint = apiEndpoint.replace(
                     /api\.php$/,
                     /api\.php$/,
                     customSource[ 1 ] + '/$&'
                     customSource[1] + '/$&'
                );
              );
             }
             }
         }
         }
Line 1,314: Line 1,315:
         * Generally, we will implicitly depend on those anyway due to where/when this is loaded.
         * 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, {
                 data: params,
                 data: params,
             } ).always( function ( data ) {
             }).always(function (data) {
                 var res = '',
                 var res = '',
                     revisionData = data.query && data.query.pages[ data.query.pageids[ 0 ] ].revisions;
                     revisionData = data.query && data.query.pages[data.query.pageids[0]].revisions;


                 if ( revisionData ) {
                 if (revisionData) {
                     res = revisionData[ 0 ][ '*' ];
                     res = revisionData[0]['*'];
                 }
                 }


                 deferred.resolve( parseMessagesToObject( name, res, options ) );
                 deferred.resolve(parseMessagesToObject(name, res, options));
             } );
             });
         } );
         });


         return deferred;
         return deferred;
Line 1,333: Line 1,334:


     /* Expose under the dev global */
     /* Expose under the dev global */
     window.dev.i18n = $.extend( window.dev.i18n, {
     window.dev.i18n = $.extend(window.dev.i18n, {
         loadMessages: loadMessages,
         loadMessages: loadMessages,


Line 1,349: Line 1,350:
         _fallbacks: fallbacks,
         _fallbacks: fallbacks,
         _cache: cache
         _cache: cache
     } );
     });


     /* Initialise overrides object */
     /* Initialise overrides object */
Line 1,360: Line 1,361:
     * Alternatively, use $.getScript (or mw.loader) and use the returned promise
     * 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 */
     /* Tidy the localStorage cache of old entries */
     removeOldCacheEntries();
     removeOldCacheEntries();


} ( this, jQuery, mediaWiki ) );
} (this, jQuery, mediaWiki));

Revision as of 10:59, 23 May 2022

/* <nowiki>
 * Library for accessing i18n messages for use in Dev Wiki scripts.
 * See [[I18n-js]] for documentation.
 *
 * @author Cqm <https://dev.fandom.com/User:Cqm>
 * @author OneTwoThreeFall <https://dev.fandom.com/User:OneTwoThreeFall>
 *
 * @version 0.6.7
 *
 * @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
 *   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.
 */

/* global mediaWiki */

/* jshint bitwise:true, camelcase:true, curly:true, eqeqeq:true, es3:false,
     forin:true, immed:true, indent:4, latedef:true, newcap:true,
     noarg:true, noempty:true, nonew:true, plusplus:true, quotmark:single,
     undef:true, unused:true, strict:true, trailing:true,
     browser:true, devel:false, jquery:true,
     onevar:true
 */

(function (window, $, mw, undefined) {
    'use strict';

    window.dev = window.dev || {};
    window.dev.i18n = window.dev.i18n || {};

    /* Prevent double loading and loss of cache */
    if (window.dev.i18n.loadMessages !== undefined) {
        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([
        'debug',
        'wgContentLanguage',
        'wgPageContentLanguage',
        'wgUserLanguage',
        'wgUserVariant'
   ]),

        /*
         * @var {number} Current time in milliseconds, used to set and check cache age.
         */
        now = Date.now(),

        /*
         * @var {number} Length of one day in milliseconds, used in cache age calculations.
         */
        oneDay = 1000 * 60 * 60 * 24,

        /*
         * @var {string} Prefix used for localStorage keys that contain i18n-js cache data
         */
        cachePrefix = 'i18n-cache-',

        /*
         * @var {boolean} Whether a fallback loop warning been shown
         */
        warnedAboutFallbackLoop = false,

        /*
         * @var {object} Cache of loaded I18n instances.
         */
        cache = {},

        /*
         * Initial overrides object, initialised below with the i18n global variable.
         * Allows end-users to override specific messages.
         * See documentation for how to use.
         *
         * @var {(null|object)} overrides
         */
        overrides = null,

        /*
         * Mapping of deprecated language codes that were used in previous
         * 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',
            'zh-min-n': 'nan',
            '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 fallback to English.
         *
         * 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/`,
         * 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 = {
            '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('-');
    }

    /*
     * Log a warning message to the browser console if the language fallback chain is
     * about to start a loop. Only logs once to prevent flooding the browser console.
     *
     * @param {string} lang Language in use when loop was found.
     * @param {string[]} fallbackChain Array of languages involved in the loop.
     */
    function warnOnFallbackLoop(lang, fallbackChain) {
        if (warnedAboutFallbackLoop) {
            return;
        }
        warnedAboutFallbackLoop = true;

        fallbackChain.push(lang);
        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(', '));
    }

    /*
     * Get a translation of a message from the messages object in the
     * requested language.
     *
     * @param {object} messages The message object to look translations up in.
     * @param {string} msgName The name of the message to get.
     * @param {string} lang The language to get the message in.
     * @param {string[]} fallbackChain Array of languages that have already been checked.
     *     Used to detect if the fallback chain is looping.
     * @return {(string|boolean)} The requested translation or `false` if no message could be found.
     */
    function getMsg(messages, msgName, lang, fallbackChain) {
        if (deprecatedCodes[lang]) {
            return getMsg(messages, msgName, deprecatedCodes[lang], fallbackChain);
        }

        if (messages[lang] && messages[lang][msgName]) {
            return messages[lang][msgName];
        }

        if (!fallbackChain) {
            fallbackChain = [];
        }

        /*
         * @var {string} fallbackLang
         */
        for (var fallbackLang of fallbacks[lang]) {
            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 more languages in fallback list - switch to English */
        if (messages.en && messages.en[msgName]) {
            return messages.en[msgName];
        }

        return false;
    }

    /*
     * Substitute arguments into the string, where arguments are represented
     * as $n where n > 0.
     *
     * @param {string} message The message to substitute arguments into
     * @param {array} arguments The arguments to substitute in.
     * @return {string} The resulting message.
     */
    function handleArgs(message, args) {
        args.forEach(function (elem, index) {
            /*
             * @var {RegExp} rgx
             */
            var rgx = new RegExp('\\$' + (index + 1), 'g');
            message = message.replace(rgx, elem);
        });

        return message;
    }

    /*
     * Generate a HTML link using the supplied parameters.
     *
     * @param {string} href The href of the link which will be converted to
     *     '/wiki/href'.
     * @param {string} text The text and title of the link. If this is not supplied, it
     *     will default to href.
     * @param {boolean} hasProtocol True if the href parameter already includes the
     *     protocol (i.e. it begins with 'http://', 'https://', or '//').
     * @return {string} The generated link.
     */
    function makeLink(href, text, hasProtocol) {
        text = text || href;
        href = hasProtocol ? href : mw.util.getUrl(href);

        text = mw.html.escape(text);
        href = mw.html.escape(href);

        return '<a href="' + href + '" title="' + text + '">' + text + '</a>';
    }

    /*
     * Allow basic inline HTML tags in wikitext.does not support <a> as that's handled by the
     * wikitext links instead.
     *
     * Supports the following tags:
     * - <i>
     * - <b>
     * - <s>
     * - <br>
     * - <em>
     * - <strong>
     * - <span>
     *
     * Supports the following tag attributes:
     * - title
     * - style
     * - class
     *
     * @param html
     * @return The sanitised HTML code.
     */
    function sanitiseHtml(html) {
        /*
         * @var context
         */
        var context = document.implementation.createHTMLDocument(''),
            $html = $.parseHTML(html, /* document */ context, /* keepscripts */ false),
            $div = $('<div>', context).append($html),
            allowedAttrs = [
                'title',
                'style',
                'class'
           ],
            allowedTags = [
                'i',
                'b',
                's',
                'br',
                'em',
                'strong',
                'span',
           ];

        $div.find('*').each(function () {
            var $this = $(this),
                tagname = $this.prop('tagName').toLowerCase(),
                attrs,
                array,
                style;

            if (allowedTags.indexOf(tagname) === -1) {
                mw.log('[I18n-js] Disallowed tag in message: ' + tagname);
                $this.remove();
                return;
            }

            attrs = $this.prop('attributes');
            array = Array.prototype.slice.call(attrs);

            array.forEach(function (attr) {
                if (allowedAttrs.indexOf(attr.name) === -1) {
                    mw.log('[I18n-js] Disallowed attribute in message: ' + attr.name + ', tag: ' + tagname);
                    $this.removeAttr(attr.name);
                    return;
                }

                /* Make sure there's nothing nasty in style attributes */
                if (attr.name === 'style') {
                    style = $this.attr('style');

                    if (style.indexOf('url(') > -1) {
                        mw.log('[I18n-js] Disallowed url() in style attribute');
                        $this.removeAttr('style');

                    /* https://phabricator.wikimedia.org/T208881 */
                    } else if (style.indexOf('var(') > -1) {
                        mw.log('[I18n-js] Disallowed var() in style attribute');
                        $this.removeAttr('style');
                    }
                }
            });
        });

        return $div.prop('innerHTML');
    }

    /*
     * Parse some basic wikitext into HTML. Also supports basic inline HTML tags.
     *
     * Will process:
     * - [url text]
     * - [[pagename]]
     * - [[pagename|text]]
     * - {{PLURAL:count|singular|plural}}
     * - {{GENDER:gender|masculine|feminine|neutral}}
     *
     * @param {string} message The message to process.
     * @return {string} The resulting string.
     */
    function parse(message) {
        /* [url text] -> [$1 $2] */
        var urlRgx = /\[((?:https?:)?\/\/.+?) (.+?)\]/g,
            /* [[pagename]] -> [[$1]] */
            simplePageRgx = /\[\[([^|]*?)\]\]/g,
            /* [[pagename|text]] -> [[$1|$2]] */
            pageWithTextRgx = /\[\[(.+?)\|(.+?)\]\]/g,
            /* {{PLURAL:count|singular|plural}} -> {{PLURAL:$1|$2}} */
            pluralRgx = /\{\{PLURAL:(\d+)\|(.+?)\}\}/gi,
            /* {{GENDER:gender|masculine|feminine|neutral}} -> {{GENDER:$1|$2}} */
            genderRgx = /\{\{GENDER:([^|]+)\|(.+?)\}\}/gi;

        if (message.indexOf('<') > -1) {
            message = sanitiseHtml(message);
        }

        return message
            .replace(urlRgx, function (_match, href, text) {
                return makeLink(href, text, true);
            })
            .replace(simplePageRgx, function (_match, href) {
                return makeLink(href);
            })
            .replace(pageWithTextRgx, function (_match, href, text) {
                return makeLink(href, text);
            })
            .replace(pluralRgx, function (_match, count, forms) {
                return mw.language.convertPlural(Number(count), forms.split('|'));
            })
            .replace(genderRgx, function (_match, gender, forms) {
                return mw.language.gender(gender, forms.split('|'));
            });
    }

    /*
     * Create a new Message instance.
     *
     * @param {object} messages The message object to look translations up in.
     * @param {string} lang The language to get the message in.
     * @param {array} args Any arguments to substitute into the message, [0] is message name.
     * @param {string} name The name of the script the messages are for.
     * @return
     */
    function message(messages, lang, args, name) {
        if (!args.length) {
            return;
        }

        /*
         * @var msgName
         * @var {string} descriptiveMsgName
         * @var {object} msg
         * @var {boolean} msgExists
         */
        var msgName = args.shift(),
            descriptiveMsgName = 'i18njs-' + name + '-' + msgName,
            msg = getMsg(messages, msgName, lang),
            msgExists = msg !== false;

        if (!msgExists) {
            /* use name wrapped in < > for missing message, per MediaWiki convention */
            msg = '<' + descriptiveMsgName + '>';
        }

        if (conf.wgUserLanguage === 'qqx' && msgExists) {
            /* https://www.mediawiki.org/wiki/Help:System_message#Finding_messages_and_documentation */
            msg = '(' + descriptiveMsgName + ')';
        } else if (overrides[name] && overrides[name][msgName]) {
            /* if the message has been overridden, use that without checking the language */
            msg = overrides[name][msgName];
            msgExists = true;
        }

        if (args.length) {
            msg = handleArgs(msg, args);
        }

        return {
            /*
             * @return {boolean} Representing whether the message exists.
             */
            exists: msgExists,

            /*
             * Parse wikitext links in the message and return the result.
             *
             * @return {string} The resulting string.
             */
            parse: function () {
                /*
                 * Skip parsing if the message wasn't found; otherwise
                 * the sanitisation will mess with it.
                 */
                if (!this.exists) {
                    return this.escape();
                }

                return parse(msg);
            },

            /*
             * Escape any HTML in the message and return the result.
             *
             * @return {string} The resulting string.
             */
            escape: function () {
                return mw.html.escape(msg);
            },

            /*
             * Return the message as is.
             *
             * @return {string} The resulting string.
             */
            plain: function () {
                return msg;
            }
        };
    }

    /*
     * Create a new i18n object.
     *
     * @param {object} messages The message object to look translations up in.
     * @param {string} name The name of the script the messages are for.
     * @param {object} options Options set by the loading script.
     * @return {object}
     */
    function i18n(messages, name, options) {
        var defaultLang = options.language,
            tempLang = null;

        return {
            /*
             * Set the default language.
             *
             * @deprecated since v0.6 (2020-08-25), no longer supported.
             */
            useLang: function () {
                console.warn('[I18n-js] “useLang()” is no longer supported by I18n-js (used in “' + name + '”) - using user language.');
                this.useUserLang();
            },

            /*
             * Set the language for the next msg call.
             *
             * @param {string} lang The language code to use for the next `msg` call.
             *
             * @return {object} The current object for use in chaining.
             */
            inLang: function (lang) {
                if (!options.cacheAll) {
                    console.warn('[I18n-js] “inLang()” is not supported without configuring `options.cacheAll` (used in “' + name + '”) - using user language.');
                    lang = options.language;
                }
                tempLang = lang;
                return this;
            },

            /*
             * Set the default language to the content language.
             */
            useContentLang: function () {
                defaultLang = conf.wgContentLanguage;
            },

            /*
             * Set the language for the next `msg` call to the content language.
             *
             * @return {object} The current object for use in chaining.
             */
            inContentLang: function () {
                tempLang = conf.wgContentLanguage;
                return this;
            },

            /*
             * 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;
            },

            /*
             * Set the default language to the user's language.
             */
            useUserLang: function () {
                defaultLang = options.language;
            },

            /*
             * Set the language for the next msg call to the user's language.
             *
             * @return {object} The current object for use in chaining.
             */
            inUserLang: function () {
                tempLang = options.language;
                return this;
            },

            /*
             * Create a new instance of Message.
             *
             * @return {object}
             */
            msg: function () {
                var args = Array.prototype.slice.call(arguments),
                    lang = defaultLang;

                if (tempLang !== null) {
                    lang = tempLang;
                    tempLang = null;
                }

                return message(messages, lang, args, name);
            },

            /*
             * For accessing the raw messages.
             * Scripts should not rely on it or any of its properties existing.
             */
            _messages: messages
        };
    }

    /*
     * Preprocess each message's fallback chain for the user and content languages.
     * This allows us to save only those messages needed to the cache.
     *
     * @param {string} name The name of the script the messages are for.
     * @param {object} messages The message object to look translations up in.
     * @param {object} options Options set by the loading script.
     */
    function optimiseMessages(name, messages, options) {
        var existingLangs = cache[name] && cache[name]._messages._isOptimised,
            langs = [options.language],
            msgKeys = Object.keys(messages.en || {}),
            optimised = {};

        if (!msgKeys.length) {
            /* No English messages, don't bother optimising */
            return messages;
        }

        /*
         * @var addMsgsForLanguage
         */
        var addMsgsForLanguage = function (lang) {
            if (optimised[lang]) {
                /* Language already exists */
                return;
            }

            optimised[lang] = {};

            msgKeys.forEach(function (msgName) {
                /*
                 * @var msg
                 */
                var msg = getMsg(messages, msgName, lang);

                if (msg !== false) {
                    optimised[lang][msgName] = msg;
                }
            });
        };

        if (langs.indexOf(conf.wgContentLanguage) === -1) {
            langs.push(conf.wgContentLanguage);
        }

        /*
         * 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) {
            existingLangs.forEach(function (lang) {
                if (langs.indexOf(lang) === -1) {
                    langs.push(lang);
                }
            });
        }

        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)) {
            msgKeys = options.cacheAll;
            Object.keys(messages).forEach(addMsgsForLanguage);
        }

        optimised._isOptimised = langs;

        return optimised;
    }

    /*
     * Check that the cache for a script exists and, if optimised, contains the
     * necessary languages.
     *
     * @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) {
        var messages = cache[name] && cache[name]._messages;

        /* Nothing in cache */
        if (!messages) {
            return false;
        }

        /*
         * Optimised messages missing user or content language.
         * We'll need to load from server in this case.
         */
        if (
            messages._isOptimised &&
            !(messages[options.language] && messages[conf.wgContentLanguage])
       ) {
            return false;
        }

        return true;
    }

    /*
     * Remove out-of-date entries in the i18n cache (those older than two days).
     *
     * This can never be perfect: it will only work on wikis that are visited.
     */
    function removeOldCacheEntries() {
        var isCacheKey = new RegExp('^(' + cachePrefix + '.+)-content$'),
            storageKeys = [];

        try {
            storageKeys = Object.keys(localStorage);
        } catch (e) {}

        storageKeys.filter(function (key) {
            return isCacheKey.test(key);
        }).forEach(function (key) {
            var keyPrefix = key.match(isCacheKey)[1],
                cacheTimestamp;

            try {
                cacheTimestamp = Number(localStorage.getItem(keyPrefix + '-timestamp'));
            } catch (e) {}

            if (now - cacheTimestamp < oneDay * 2) {
                /* Cached within last two days, keep it */
                return;
            }

            try {
                localStorage.removeItem(keyPrefix + '-content');
                localStorage.removeItem(keyPrefix + '-timestamp');
                localStorage.removeItem(keyPrefix + '-version');
            } catch (e) {}
        });
    }

    /*
     * Strip block comments from a JSON string which are illegal under the JSON spec.
     * This is a bit basic, so will remove comments inside strings too.
     *
     * @param {string} json The JSON string.
     * @return {string} The JSON string after any comments have been removed.
     */
    function stripComments(json) {
        json = json
            .trim()
            .replace(/\/\*[\s\S]*?\*\//g, '');
        return json;
    }

    /*
     * Save messages string to local storage for caching.
     *
     * @param {string} name The name of the script the messages are for.
     * @param {object} json The JSON object.
     * @param {number} cacheVersion Cache version requested by the loading script.
     */
    function saveToCache(name, json, cacheVersion) {
        /*
         * @var {string} keyPrefix
         */
        var keyPrefix = cachePrefix + name;

        /* Don't cache empty JSON */
        if (Object.keys(json).length === 0) {
            return;
        }

        try {
            localStorage.setItem(keyPrefix + '-content', JSON.stringify(json));
            localStorage.setItem(keyPrefix + '-timestamp', now);
            localStorage.setItem(keyPrefix + '-version', cacheVersion || 0);
        } catch (e) {}
    }

    /*
     * Parse JSON string loaded from page and create an i18n object.
     *
     * @param {string} name The name of the script the messages are for.
     * @param {string} res The JSON string.
     * @param {object} options Options set by the loading script.
     * @return {object} The resulting i18n object.
     */
    function parseMessagesToObject(name, res, options) {
        var json = {},
            obj,
            msg;

        /* Handle parse errors gracefully */
        try {
            res = stripComments(res);
            json = JSON.parse(res);
        } catch (e) {
            msg = e.message;

            if (msg === 'Unexpected end of JSON input') {
                msg += '. This may be caused by a non-existent i18n.json page.';
            }

            console.warn('[I18n-js] SyntaxError in messages: ' + msg);
        }

        if (
            options.useCache &&
            !options.loadedFromCache &&
            options.cacheAll !== true
       ) {
            json = optimiseMessages(name, json, options);
        }

        obj = i18n(json, name, options);

        /* Cache the result in case it's used multiple times */
        cache[name] = obj;

        if (!options.loadedFromCache) {
            saveToCache(name, json, options.cacheVersion);
        }

        return obj;
    }

    /*
     * Load messages string from local storage cache and add to cache object.
     *
     * @param {string} name The name of the script the messages are for.
     * @param {object} options Options set by the loading script.
     */
    function loadFromCache(name, options) {
        var keyPrefix = cachePrefix + name,
            cacheContent,
            cacheVersion;

        try {
            cacheContent = localStorage.getItem(keyPrefix + '-content');
            cacheVersion = Number(localStorage.getItem(keyPrefix + '-version'));
        } catch (e) {}

        /* Cache exists, and its version is greater than or equal to requested version */
        if (cacheContent && cacheVersion >= options.cacheVersion) {
            options.loadedFromCache = true;
            parseMessagesToObject(name, cacheContent, options);
        }
    }

    /*
     * Load messages stored as JSON on a page.
     *
     * @param {string} name The name of the script the messages are for. This will be
     *     used to get messages from
     *     https://dev.fandom.com/wiki/MediaWiki:Custom-name/i18n.json.
     *   Use `u:<subdomain>` or `u:<language-path>.<subdomain>` to set other Fandom
     *   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 {object} A jQuery.Deferred instance.
     */
    function loadMessages(name, options) {
        /*
         * @var {object} deferred
         * @var {string} apiEndpoint
         * @var {RegExp} apiEndpointRgx
         * @var {string} page
         * @var {object} params
         */
        var deferred = $.Deferred(),
            customSource = name.match(/^u:(?:([a-z-]+)\.)?([a-z0-9-]+):/),
            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',
            params;

        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.language = options.language || conf.wgUserLanguage;
        options.useCache = (options.noCache || conf.debug) !== true;

        if (options.useCache) {
            loadFromCache(name, options);

            if (cacheIsSuitable(name, options)) {
                return deferred.resolve(cache[name]);
            }
        }

        /* Cache isn't suitable - loading from server */
        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) {
            apiEndpoint = apiEndpoint.replace('dev', customSource[2]);
            page = name.slice(customSource[0].length);

            // adjust endpoint when loading from interlanguage wiki
            if (customSource[1]) {
                apiEndpoint = apiEndpoint.replace(
                    /api\.php$/,
                    customSource[1] + '/$&'
               );
            }
        }

        params = {
            action: 'query',
            format: 'json',
            prop: 'revisions',
            rvprop: 'content',
            titles: page,
            indexpageids: 1,
            origin: '*',
            // Cache results for 5 minutes in CDN and browser
            maxage: 300,
            smaxage: 300
        };

        /*
         * '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 () {
            $.ajax(apiEndpoint, {
                data: params,
            }).always(function (data) {
                var res = '',
                    revisionData = data.query && data.query.pages[data.query.pageids[0]].revisions;

                if (revisionData) {
                    res = revisionData[0]['*'];
                }

                deferred.resolve(parseMessagesToObject(name, res, options));
            });
        });

        return deferred;
    }

    /* Expose under the dev global */
    window.dev.i18n = $.extend(window.dev.i18n, {
        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,
        _saveToCache: saveToCache,
        _getMsg: getMsg,
        _handleArgs: handleArgs,
        _parse: parse,
        _fallbacks: fallbacks,
        _cache: cache
    });

    /* Initialise overrides object */
    window.dev.i18n.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);

    /* Tidy the localStorage cache of old entries */
    removeOldCacheEntries();

} (this, jQuery, mediaWiki));