User:Guycn2/UserInfoPopup.js
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
== User Info Popup ==
Adds an "i" (info) icon at the top of user-related pages
(e.g. user pages, user talk pages, "Contributions" pages, etc.)
The color of the "i" icon represents the amount of time passed since the user last edited:
* Green – user last edited less than 20 minutes ago
* Orange – user last edited more than 20 minutes ago, but less than 3 months ago
* Red – user last edited more than 3 months ago
Hover over the "i" icon to quickly view useful information about the relevant user:
* Registration date
* Number of edits
* Time elapsed since last edit
* User groups (rights), incl. global ones
* Latest block time (incl. range and global blocks, when applicable)
* Gender (if disclosed)
See full documentation at:
[[User:Guycn2/UserInfoPopup]]
See also:
* [[User:Guycn2/UserInfoPopup.css]] – for the corresponding style sheet
Skins supported:
Vector (both 2022 and 2010), Monobook, Timeless, and Minerva.
Also fully supported on the mobile interface.
Dependencies:
* mediawiki.api
* mediawiki.language.months
* mediawiki.user
* mediawiki.util
* user.options
* oojs-ui-core
Written by: [[User:Guycn2]]
*/
( async () => {
'use strict';
const username = mw.config.get( 'wgRelevantUserName' );
if ( !username || mw.config.get( 'userInfoPopupLoaded' ) ) {
return;
}
mw.config.set( 'userInfoPopupLoaded', true );
await mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] );
const isAnon = mw.util.isIPAddress( username );
const api = new mw.Api();
async function checkIfUserExists() {
if ( isAnon ) {
return true;
}
const data = await api.get( { list: 'users', ususers: username } );
if ( data.query.users[ 0 ].userid ) {
return true;
}
return false;
}
if ( !( await checkIfUserExists() ) ) {
return;
}
mw.loader.load(
'https://en.wikipedia.org/w/index.php?title=User:Guycn2/UserInfoPopup.css&action=raw&ctype=text/css',
'text/css'
);
const scriptData = {
lang: mw.config.get( 'wgUserLanguage' ),
skin: mw.config.get( 'skin' ),
secsFromLastEdit: await calcSecsFromLastEdit()
};
createInfoIcon();
await $.when( mw.loader.using( 'oojs-ui-core' ), $.ready );
addInfoIconToPage();
attachEventListeners();
function i18n( key ) {
const messages = {
en: {
infoIconAlt: 'Info icon',
femaleSymbolAlt: 'Female',
maleSymbolAlt: 'Male',
fetchingData: 'Fetching data…',
regUnknown: 'Unknown',
joined: 'Joined:',
editCount: 'Edits:',
lastEdited: 'Last edited:',
lastEditedNever: 'Never',
lastEditedUnknown: 'Unknown',
groups: 'Groups:',
noGroups: 'None',
lastBlocked: 'Last blocked:',
neverBlocked: 'Never',
partiallyBlocked: 'Currently blocked (partially)',
fullyBlocked: 'Currently blocked',
rangeBlockedPartially: 'Currently range-blocked (partially)',
rangeBlockedFully: 'Currently range-blocked',
globallyBlocked: 'Currently blocked globally',
globallyLocked: 'Currently locked globally',
ago: '$1 ago',
seconds: [ '1 second', '$1 seconds' ],
minutes: [ '1 minute', '$1 minutes' ],
hours: [ '1 hour', '$1 hours' ],
days: [ '1 day', '$1 days' ],
weeks: [ '1 week', '$1 weeks' ],
months: [ '1 month', '$1 months' ],
years: [ '1 year', '$1 years' ]
},
he: {
infoIconAlt: 'צלמית מידע',
femaleSymbolAlt: 'נקבה',
maleSymbolAlt: 'זכר',
fetchingData: 'המידע בטעינה…',
regUnknown: 'לא ידוע',
joined: 'הרשמה:',
editCount: 'עריכות:',
lastEdited: 'עריכה אחרונה:',
lastEditedNever: 'אין',
lastEditedUnknown: 'לא ידוע',
groups: 'קבוצות:',
noGroups: 'ללא',
lastBlocked: 'חסימה אחרונה:',
neverBlocked: 'אין',
partiallyBlocked: 'חסימה פעילה כעת (חלקית)',
fullyBlocked: 'חסימה פעילה כעת',
rangeBlockedPartially: 'חסימת טווח פעילה כעת (חלקית)',
rangeBlockedFully: 'חסימת טווח פעילה כעת',
globallyBlocked: 'חסימה גלובלית פעילה כעת',
globallyLocked: 'נעילה גלובלית פעילה כעת',
ago: 'לפני $1',
seconds: [ 'שנייה', '$1 שניות' ],
minutes: [ 'דקה', '$1 דקות' ],
hours: [ 'שעה', 'שעתיים', '$1 שעות' ],
days: [ 'יום', 'יומיים', '$1 ימים' ],
weeks: [ 'שבוע', 'שבועיים', '$1 שבועות' ],
months: [ 'חודש', 'חודשיים', '$1 חודשים' ],
years: [ 'שנה', 'שנתיים', '$1 שנים' ]
}
};
if (
messages[ scriptData.lang ] &&
messages[ scriptData.lang ][ key ]
) {
return messages[ scriptData.lang ][ key ];
} else {
return messages.en[ key ];
}
}
async function calcSecsFromLastEdit() {
const params = {
list: 'usercontribs',
ucuser: username,
ucprop: 'timestamp',
uclimit: 1
};
const data = await api.get( params );
if ( data.query.usercontribs.length === 0 ) {
return null;
}
const lastEditTime =
new Date( data.query.usercontribs[ 0 ].timestamp ).getTime();
return ( mw.now() - lastEditTime ) / 1000;
}
function createInfoIcon() {
const $img = $( '<img>' )
.addClass( 'user-info-popup-icon' )
.attr( {
alt: i18n( 'infoIconAlt' ),
width: '20.3',
height: '20.3'
} );
if ( scriptData.secsFromLastEdit === null ) {
$img
.addClass( 'user-info-popup-grey-icon' )
.attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/d/df/Information_grey.svg' );
} else if ( scriptData.secsFromLastEdit < 60 * 20 ) {
$img
.addClass( 'user-info-popup-green-icon' )
.attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/7/7d/Information_green.svg' );
} else if ( scriptData.secsFromLastEdit < 60 * 60 * 24 * 30 * 3 ) {
$img
.addClass( 'user-info-popup-orange-icon' )
.attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/f/f0/Information_orange.svg' );
} else {
$img
.addClass( 'user-info-popup-red-icon' )
.attr( 'src', 'https://upload.wikimedia.org/wikipedia/commons/5/55/Information_red.svg' );
}
scriptData.$indicator = $( '<div>' )
.addClass( 'mw-indicator' )
.attr( { id: 'mw-indicator-user-info-popup-indicator', tabindex: '0' } )
.append( $img );
}
function addInfoIconToPage() {
const $throbberImg = $( '<img>' ).attr( {
alt: i18n( 'fetchingData' ),
id: 'user-info-popup-throbber',
src: 'https://upload.wikimedia.org/wikipedia/commons/f/f8/Ajax-loader(2).gif'
} );
const $placeholderText = $( '<p>' )
.attr( 'id', 'user-info-popup-placeholder-text' )
.text( i18n( 'fetchingData' ) );
scriptData.$popupPlaceholder = $( '<div>' )
.attr( 'id', 'user-info-popup-placeholder' )
.append( $throbberImg, $placeholderText );
scriptData.popup = new OO.ui.PopupWidget( {
$content: scriptData.$popupPlaceholder,
align: 'backwards',
autoFlip: false,
id: 'user-info-popup-popup',
hideWhenOutOfView: false,
padded: true,
position: 'below',
width: 225
} );
scriptData.$indicator.append( scriptData.popup.$element );
if (
scriptData.skin === 'vector-2022' &&
$( '.vector-page-toolbar-container:has( #ca-nstab-user )' ).length
) {
scriptData.$indicator
.insertBefore( '.vector-page-tools-landmark:has( #vector-page-tools-dropdown )' );
} else {
const $indicatorsContainer = $( '.mw-indicators' );
if (
!window.matchMedia( '( orientation: portrait )' ).matches ||
scriptData.skin === 'vector-2022' ||
scriptData.skin === 'vector' ||
( scriptData.skin === 'monobook' && !$( '#sidebar-toggle:visible' ).length )
) {
scriptData.popup.setAlignment( 'forwards' );
scriptData.popup.setPosition( 'before' );
if ( $indicatorsContainer.children( '.mw-indicator' ).length >= 6 ) {
scriptData.popup.setAutoFlip( true );
}
}
if ( scriptData.skin === 'minerva' ) {
scriptData.$indicator
.css( 'float', $( 'body.rtl' ).length ? 'left' : 'right' )
.appendTo( '.header-container' );
} else {
$indicatorsContainer.prepend( scriptData.$indicator );
}
}
}
function attachEventListeners() {
scriptData.popup.on( 'ready', () => {
// Prevent mobile browsers from occasionally jumping
// to the top of the page when tapping the "i" icon.
window.scrollTo( scriptData.posX, scriptData.posY );
if (
document.documentElement.clientWidth < 600 &&
scriptData.skin === 'vector-2022' &&
scriptData.popup.$element.hasClass( 'oo-ui-popupWidget-anchored-top' )
) {
adaptPopupPosition();
}
scriptData.popup.$element.hide().fadeIn();
} );
scriptData.$indicator.on( 'mouseenter focusin keydown', e => {
if ( e.type === 'keydown' ) {
if ( ![ 'Enter', ' ' ].includes( e.key ) ) {
return;
}
if ( e.key === ' ' ) {
e.preventDefault();
}
}
clearTimeout( scriptData.mouseLeaveTimeout );
scriptData.mouseEnterTimeout = setTimeout( openPopup, 200 );
} );
scriptData.$indicator.on( 'mouseleave focusout', () => {
if (
document.activeElement.id === 'mw-indicator-user-info-popup-indicator' ||
document.activeElement.parentElement.classList.contains(
'user-info-popup-value'
)
) {
return;
}
clearTimeout( scriptData.mouseEnterTimeout );
scriptData.mouseLeaveTimeout = setTimeout( closePopup, 2500 );
} );
$( document ).on( 'keydown', e => {
if ( e.key === 'Escape' ) {
closePopup();
}
} );
$( document ).on( 'click', closePopup );
$( '.oo-ui-fieldsetLayout-header, .ext-discussiontools-init-section-bar' )
.on( 'click', closePopup );
scriptData.$indicator.on( 'click', e => e.stopPropagation() );
}
function adaptPopupPosition() {
const innerBody = document.querySelector( '.mw-page-container' );
const innerBodyRect = innerBody.getBoundingClientRect();
const indicator = scriptData.$indicator[ 0 ];
const indicatorRect = indicator.getBoundingClientRect();
const dir = $( 'body.rtl' ).length ? 'left' : 'right';
const pos =
Math.abs( indicatorRect[ dir ] - innerBodyRect[ dir ] ) -
indicator.offsetWidth / 2;
scriptData.popupCss = mw.util.addCSS(
`#user-info-popup-popup { ${ dir }: ${ pos }px !important; }`
);
}
function openPopup() {
if ( !scriptData.popup.isVisible() ) {
// posX and posY are used to prevent mobile browsers from
// occasionally jumping to the top of the page when tapping
// the "i" icon. See the popup's "ready" event listener above.
scriptData.posX = window.scrollX;
scriptData.posY = window.scrollY;
scriptData.popup.toggle( true );
if ( !scriptData.dataFetched ) {
getUserData().then( fillPopupContent );
scriptData.dataFetched = true;
}
}
}
function closePopup() {
clearTimeout( scriptData.mouseLeaveTimeout );
if ( scriptData.popup.isVisible() ) {
scriptData.popup.$element.fadeOut( () => {
scriptData.popup.toggle( false );
scriptData.popup.$element.show();
if ( scriptData.popupCss ) {
scriptData.popupCss.disabled = true;
}
} );
}
}
async function getUserData() {
let params;
if ( isAnon ) {
params = {
list: 'blocks|globalblocks|logevents|usercontribs',
bkip: username,
bkprop: 'flags|user',
bklimit: 2,
bgip: username,
bgprop: 'address',
bglimit: 1,
leaction: 'block/block',
letitle: `User:${ username }`,
leprop: 'timestamp',
lelimit: 1,
ucuser: username,
ucprop: '',
uclimit: 'max'
};
} else {
params = {
list: 'blocks|logevents|usercontribs|users',
meta: 'globaluserinfo',
bkusers: username,
bkprop: 'flags',
bklimit: 1,
leaction: 'block/block',
letitle: `User:${ username }`,
leprop: 'timestamp',
lelimit: 1,
ucuser: username,
ucdir: 'newer',
ucprop: 'timestamp',
uclimit: 1,
ususers: username,
usprop: 'editcount|gender|groupmemberships|registration',
guiuser: username,
guiprop: 'groups'
};
}
const data = await api.get( params );
if ( isAnon ) {
const editCount = data.query.usercontribs.length;
scriptData.editCount = await renderAnonEditCount( editCount );
scriptData.isGloballyBlocked = data.query.globalblocks.length;
if ( scriptData.isGloballyBlocked ) {
scriptData.globalBlockTarget = data.query.globalblocks[ 0 ].address;
}
} else {
scriptData.gender = data.query.users[ 0 ].gender;
if ( data.query.users[ 0 ].registration ) {
scriptData.regDate =
await formatDate( data.query.users[ 0 ].registration, true );
} else if ( data.query.usercontribs[ 0 ] ) {
scriptData.regDate =
await formatDate( data.query.usercontribs[ 0 ].timestamp, true );
} else {
scriptData.regDate = i18n( 'regUnknown' );
}
scriptData.editCount = data.query.users[ 0 ].editcount.toLocaleString();
const localGroups =
data.query.users[ 0 ].groupmemberships.map( item => item.group );
scriptData.localGroups = await renderGroups( localGroups );
if ( data.query.globaluserinfo.groups ) {
const globalGroups = data.query.globaluserinfo.groups.filter(
item => !localGroups.includes( item )
);
scriptData.globalGroups = await renderGroups( globalGroups );
scriptData.isLocked = data.query.globaluserinfo.locked === '';
}
}
const blocks = data.query.blocks;
scriptData.isBlocked = blocks.length;
if ( scriptData.isBlocked ) {
if ( isAnon && blocks[ 0 ].user !== username && blocks[ 1 ] ) {
blocks.shift();
}
scriptData.isPartiallyBlocked = blocks[ 0 ].partial === '';
scriptData.isRangeBlocked = isAnon && blocks[ 0 ].user !== username;
if ( scriptData.isRangeBlocked ) {
scriptData.rangeBlockTarget = blocks[ 0 ].user;
}
} else if ( data.query.logevents.length ) {
scriptData.lastBlockDate =
await formatDate( data.query.logevents[ 0 ].timestamp, false );
}
}
async function renderAnonEditCount( editCount ) {
if ( editCount < 500 ) {
return editCount.toLocaleString();
}
await mw.loader.using( 'mediawiki.user' );
const rights = await mw.user.getRights();
const maxAnonEditCount = rights.includes( 'apihighlimits' ) ? 5000 : 500;
if ( editCount === maxAnonEditCount ) {
return `${ editCount.toLocaleString() }+`;
} else {
return editCount.toLocaleString();
}
}
async function renderGroups( groups ) {
if ( groups.length === 0 ) {
return '';
}
let sysMsgGroups = '';
groups.forEach( ( group, index ) => {
sysMsgGroups += `{${ '{' }int:group-${ group }}}`;
if ( index < groups.length - 1 ) {
sysMsgGroups += ', ';
}
} );
const params = {
action: 'parse',
uselang: scriptData.lang,
text: sysMsgGroups,
prop: 'text',
contentmodel: 'wikitext',
disablelimitreport: true
};
const data = await api.get( params );
return $( data.parse.text[ '*' ] ).find( 'p' ).text().trim();
}
async function formatDate( timestamp, includeDay ) {
await mw.loader.using( 'mediawiki.language.months' );
const date = new Date( timestamp );
const monthName = mw.language.months.names[ date.getMonth() ];
const monthNameGen = mw.language.months.genitive[ date.getMonth() ];
const year = date.getFullYear();
if ( includeDay ) {
const day = date.getDate();
await mw.loader.using( 'user.options' );
if ( mw.user.options.get( 'date' ) === 'mdy' ) {
return `${ monthName } ${ day }, ${ year }`;
} else {
return `${ day } ${ monthNameGen } ${ year }`;
}
} else {
return `${ monthName } ${ year }`;
}
}
function fillPopupContent() {
const $container = $( '<aside>' ).attr( 'id', 'user-info-popup-content' );
const $header = $( '<header>' ).attr( 'id', 'user-info-popup-header' );
$header.append(
$( '<bdi>' )
.attr( 'id', 'user-info-popup-username' )
.text( mw.util.prettifyIP( username ) )
);
const $ul = $( '<ul>' ).attr( 'id', 'user-info-popup-list' );
$container.append( $header, $ul );
if ( !isAnon ) {
addListItem( $ul, i18n( 'joined' ), scriptData.regDate );
}
const editCounterUrl =
`https://xtools.wmcloud.org/ec/${ mw.config.get( 'wgServerName' ) }/${ encodeURIComponent( username ) }`;
addListItem(
$ul,
i18n( 'editCount' ),
`<a target="_blank" href="${ editCounterUrl }">${ scriptData.editCount }</a>`
);
const contribsUrl = mw.util.getUrl( `Special:Contributions/${ username }` );
let lastEditedText;
if ( scriptData.editCount === ( 0 ).toLocaleString() ) {
lastEditedText = i18n( 'lastEditedNever' );
} else if ( scriptData.secsFromLastEdit === null ) {
lastEditedText = i18n( 'lastEditedUnknown' );
} else {
lastEditedText = i18n( 'ago' ).replace( '$1', calcTimeFromLastEdit() );
}
addListItem(
$ul,
i18n( 'lastEdited' ),
`<a href="${ contribsUrl }">${ lastEditedText }</a>`
);
if ( !isAnon ) {
const localGroupsUrl = mw.util.getUrl( `Special:UserRights/${ username }` );
const globalGroupsUrl =
mw.util.getUrl( `m:Special:GlobalUserRights/${ username }` );
let groupsHtml;
if ( !scriptData.localGroups && !scriptData.globalGroups ) {
groupsHtml =
`<a href="${ localGroupsUrl }">${ i18n( 'noGroups' ) }</a>`;
}
if ( scriptData.localGroups && !scriptData.globalGroups ) {
groupsHtml =
`<a href="${ localGroupsUrl }">${ scriptData.localGroups }</a>`;
}
if ( !scriptData.localGroups && scriptData.globalGroups ) {
groupsHtml =
`<a href="${ globalGroupsUrl }">${ scriptData.globalGroups }</a>`;
}
if ( scriptData.localGroups && scriptData.globalGroups ) {
groupsHtml =
`<a href="${ localGroupsUrl }">${ scriptData.localGroups }</a>,
<a href="${ globalGroupsUrl }">${ scriptData.globalGroups }</a>`;
}
addListItem( $ul, i18n( 'groups' ), groupsHtml );
}
let lastBlockText;
let blockLogUrl = mw.util.getUrl( 'Special:Log', {
type: 'block',
page: `User:${ username }`
} );
if ( scriptData.isGloballyBlocked ) {
lastBlockText = i18n( 'globallyBlocked' );
blockLogUrl = mw.util.getUrl( 'm:Special:Log', {
type: 'gblblock',
page: `User:${ scriptData.globalBlockTarget }`
} );
} else if ( scriptData.isLocked ) {
lastBlockText = i18n( 'globallyLocked' );
blockLogUrl = mw.util.getUrl( 'm:Special:Log', {
type: 'globalauth',
page: `User:${ username }@global`
} );
} else if ( scriptData.isBlocked ) {
if ( scriptData.isRangeBlocked ) {
if ( scriptData.isPartiallyBlocked ) {
lastBlockText = i18n( 'rangeBlockedPartially' );
} else {
lastBlockText = i18n( 'rangeBlockedFully' );
}
blockLogUrl = mw.util.getUrl( 'Special:Log', {
type: 'block',
page: `User:${ scriptData.rangeBlockTarget }`
} );
} else {
if ( scriptData.isPartiallyBlocked ) {
lastBlockText = i18n( 'partiallyBlocked' );
} else {
lastBlockText = i18n( 'fullyBlocked' );
}
}
} else {
lastBlockText = scriptData.lastBlockDate || i18n( 'neverBlocked' );
}
addListItem(
$ul,
i18n( 'lastBlocked' ),
`<a href="${ blockLogUrl }">${ lastBlockText }</a>`
);
if ( !isAnon && scriptData.gender !== 'unknown' ) {
const images = {
female: {
alt: i18n( 'femaleSymbolAlt' ),
path: 'https://upload.wikimedia.org/wikipedia/commons/archive/1/1d/20240712201036!Venus_symbol_(light_pink).svg'
},
male: {
alt: i18n( 'maleSymbolAlt' ),
path: 'https://upload.wikimedia.org/wikipedia/commons/archive/b/b4/20240624040032!Mars_symbol_(bold_light_blue).svg'
}
};
$( '<img>' ).attr( {
alt: images[ scriptData.gender ].alt,
id: 'user-info-popup-gender-symbol',
src: images[ scriptData.gender ].path,
width: '16.6',
height: '16.6'
} ).appendTo( $header );
}
scriptData.$popupPlaceholder.replaceWith( $container );
}
function addListItem( $ul, property, value ) {
const $li = $( '<li>' );
const $property = $( '<span>' )
.addClass( 'user-info-popup-property' )
.text( property );
const $value = $( '<span>' )
.addClass( 'user-info-popup-value' )
.html( value );
$li.append( $property, ' ', $value ).appendTo( $ul );
}
function calcTimeFromLastEdit() {
const secs = scriptData.secsFromLastEdit;
const days = secs / 60 / 60 / 24;
if ( secs < 60 ) {
let fullSecs = Math.floor( secs );
if ( fullSecs < 1 ) {
fullSecs = 1;
}
const secsArrLength = i18n( 'seconds' ).length;
if ( fullSecs < secsArrLength ) {
return i18n( 'seconds' )[ fullSecs - 1 ];
} else {
return i18n( 'seconds' )[ secsArrLength - 1 ].replace( '$1', fullSecs );
}
} else if ( secs < 60 * 60 ) {
const fullMins = Math.floor( secs / 60 );
const minsArrLength = i18n( 'minutes' ).length;
if ( fullMins < minsArrLength ) {
return i18n( 'minutes' )[ fullMins - 1 ];
} else {
return i18n( 'minutes' )[ minsArrLength - 1 ].replace( '$1', fullMins );
}
} else if ( secs < 60 * 60 * 24 ) {
const fullHours = Math.floor( secs / 60 / 60 );
const hoursArrLength = i18n( 'hours' ).length;
if ( fullHours < hoursArrLength ) {
return i18n( 'hours' )[ fullHours - 1 ];
} else {
return i18n( 'hours' )[ hoursArrLength - 1 ].replace( '$1', fullHours );
}
} else if ( days < 7 ) {
const fullDays = Math.floor( days );
const daysArrLength = i18n( 'days' ).length;
if ( fullDays < daysArrLength ) {
return i18n( 'days' )[ fullDays - 1 ];
} else {
return i18n( 'days' )[ daysArrLength - 1 ].replace( '$1', fullDays );
}
} else if ( days < 30 ) {
const fullWeeks = Math.floor( days / 7 );
const weeksArrLength = i18n( 'weeks' ).length;
if ( fullWeeks < weeksArrLength ) {
return i18n( 'weeks' )[ fullWeeks - 1 ];
} else {
return i18n( 'weeks' )[ weeksArrLength - 1 ].replace( '$1', fullWeeks );
}
} else if ( days < 365 ) {
let fullMonths = Math.floor( days / 30 );
if ( fullMonths === 12 ) {
fullMonths = 11;
}
const monthsArrLength = i18n( 'months' ).length;
if ( fullMonths < monthsArrLength ) {
return i18n( 'months' )[ fullMonths - 1 ];
} else {
return i18n( 'months' )[ monthsArrLength - 1 ].replace( '$1', fullMonths );
}
} else {
const fullYears = Math.floor( days / 365 );
const yearsArrLength = i18n( 'years' ).length;
if ( fullYears < yearsArrLength ) {
return i18n( 'years' )[ fullYears - 1 ];
} else {
return i18n( 'years' )[ yearsArrLength - 1 ].replace( '$1', fullYears );
}
}
}
} )();