Module:RosterChangeData

local util_args = require('Module:ArgsUtil') local util_cargo = require('Module:CargoUtil') local util_esports = require('Module:EsportsUtil') local util_html = require('Module:HtmlUtil') local util_math = require('Module:MathUtil') local util_map = require('Module:MapUtil') local util_news = require("Module:NewsUtil") local util_sentence = require("Module:SentenceUtil") local util_sort = require("Module:SortUtil") local util_source = require("Module:SourceUtil") local util_table = require('Module:TableUtil') local util_text = require('Module:TextUtil') local util_time = require("Module:TimeUtil") local util_title = require('Module:TitleUtil') local util_vars = require("Module:VarsUtil") local i18n = require('Module:I18nUtil') local WeeklyDataPages = require('Module:WeeklyDataPages') local OtherNewsDataSources = require('Module:OtherNewsDataSources').main local SENTENCES = require('Module:RosterChangeData/Sentences') local lang = mw.getLanguage('en')

local m_team = require('Module:Team')

local COLUMNS = util_news.COLUMNS

local JOIN_MODIFIERS = { 'sub', 'trainee' }

local h = {}

local p = {} function p.start(frame) return WeeklyDataPages.start('Roster Change Data') end

function p.date(frame) i18n.init('RosterChangeData') local args = util_args.merge util_vars.resetGlobalIndex('N_LineInDate') tbl, tr = WeeklyDataPages.date(args, COLUMNS, 'roster-change-data') return OtherNewsDataSources, tbl, tr end

function p.endTable(frame) return ' ' end

function p.line(frame) local args = util_args.merge util_cargo.setStoreNamespace('Data') h.validateArgs(args) util_news.setId i18n.init('RosterChangeData') local data = h.getDataFromArgs(args) h.storeRosterChangeData(data) local newsData = h.getNewsData(args, data) util_cargo.store(newsData) return util_news.makeSentenceOutput(args, newsData), h.makeRowOutput(data) end

function h.validateArgs(args) if not args.team then error(i18n.print('error_missingTeam')) end end

function h.getDataFromArgs(args) local pre = util_news.getPlayersFromArg(args.pre) local post = util_news.getPlayersFromArg(args.post) local data = h.joinPlayerData(pre, post, args) return data end

function h.joinPlayerData(pre, post, args) local ret = {} for _, player, data in ipairs(pre) do		ret[#ret+1] = h.joinOnePlayerData(player, data, post:get(player) or {}, args) post:removeKey(player) end for _, player, data in ipairs(post) do		ret[#ret+1] = h.joinOnePlayerData(player, {}, data, args) end return ret end

function h.joinOnePlayerData(player, pre, post, args) local ret = { Player = player, Leave = pre, Join = post, TeamStart = pre.Player and m_team.teamlinkname(args.team), RoleStart = pre.Role, IsSubStart = pre.IsSub, IsTraineeStart = pre.IsTrainee, TeamEnd = post.Player and m_team.teamlinkname(args.team), RoleEnd = post.Role, IsSubEnd = post.IsSub, IsTraineeEnd = post.IsTrainee, LoanedTo = post.LoanedTo or pre.LoanedTo, LoanedFrom = post.LoanedFrom or pre.LoanedFrom, EventLink = util_title.target(post.Event or pre.Event or args.event), Event = post.Event or pre.Event or args.event, Reason = post.Reason or pre.Reason, Replacing = post.Replacing or pre.Replacing, Phase = post.Phase or pre.Phase, contract_until = post.ContractUntil or pre.ContractUntil, leave_date = post.LeaveDate, assistance = util_args.castAsBool(post.Assistance), StatusStart = pre.Status and pre.Status:lower, StatusEnd = post.Status and post.Status:lower, pre_preload = pre.MoveType and pre.MoveType:lower, post_preload = post.MoveType and post.MoveType:lower, custom_text = post.Custom or pre.Custom, rejoin = util_args.castAsBool(post.Rejoin), remain_for = post.RemainFor or pre.RemainFor, remain_for_link = post.RemainForLink or pre.RemainForLink, sub = h.getNetRoleModifierStatus(pre, post, 'Sub'), player = player, -- for util_esports.playerWithRole trainee = h.getNetRoleModifierStatus(pre, post, 'Trainee'), unlinked = pre.Unlinked or post.Unlinked, AlreadyJoined = pre.AlreadyJoined or post.AlreadyJoined, IsGCD = util_source.isGCD(args.source), SisterTeam = pre.SisterTeam or post.SisterTeam, ChangedOnTeamRename = util_args.castAsBool(pre.ChangedOnTeamRename or post.ChangedOnTeamRename) }	ret.isLoan = ret.LoanedFrom or ret.LoanedTo ret.Team = ret.TeamStart or ret.TeamEnd ret.role = ret.Join.RoleSet or ret.Leave.RoleSet ret.Roles = post.Roles or pre.Roles ret.RolesStaff = post.RolesStaff or pre.RolesStaff ret.RoleDisplayStart = pre.RoleDisplay ret.RoleDisplayEnd = post.RoleDisplay ret.RolesIngameStart = pre.RolesIngame ret.RolesIngameEnd = post.RolesIngame ret.RolesStaffStart = pre.RolesStaff ret.RolesStaffEnd = post.RolesStaff ret.RolesStart = pre.Roles ret.RolesEnd = post.Roles ret.RoleDisplay = post.RoleDisplay or pre.RoleDisplay ret.RoleModifierStart = h.getRoleModifier(ret, 'Start') ret.RoleModifierEnd = h.getRoleModifier(ret, 'End') ret.move_type = ret.post_preload or ret.pre_preload h.addConstantsToRow(ret, args, post) ret.Preload = h.guessNewsPreload(ret) ret.PreloadSortNumber = SENTENCES.priority[ret.Preload] ret.sort_key_role = h.getRoleSortKey(ret, pre, post) ret.sort_key_player = mw.ustring.lower(ret.Player) ret.sort_key_sentence = h.getSentenceSortKey(ret, pre, post) ret.sentence_group = post.SentenceGroup or pre.SentenceGroup return ret end

function h.getNetRoleModifierStatus(pre, post, statustype) -- only allow sub/trainee status pulled from pre if he left the team if post.Player then return util_args.castAsBool(post[statustype]) end return util_args.castAsBool(pre[statustype]) end

function h.getRoleModifier(ret, when) -- to editors we want only trainee/sub available, but in the database its much more -- long-term convenient to treat this as its own freeform column that can take -- any arbitrary value if ret['IsTrainee' .. when] then return 'Trainee' elseif ret['IsSub' .. when] then return 'Sub' end return nil end

function h.addConstantsToRow(row, args, post) util_table.mergeDontOverwrite(row, util_news.getRCFieldsFromPlayerAndArgs(post, args)) row.source_display = util_source.makePopupRef(args.source) end

function h.getRoleSortKey(playerVariables, pre, post) -- put all maually specified order things first -- then fall back to the role's sort number if post.Order then return tonumber(post.Order) end if pre.Order then return tonumber(pre.Order) end return (playerVariables.role:sortnumber or 1000) * 1000 end

function h.getSentenceSortKey(ret, pre, post) if post.Order then return tonumber(post.Order) end if pre.Order then return tonumber(pre.Order) end return SENTENCES.priority[ret.Preload] end

function h.guessNewsPreload(info) local gcd_prefix = '' if info.move_type == 'confirm' then gcd_prefix = 'confirm_' end if info.IsGCD then gcd_prefix = 'gcd_' end if info.move_type == 'expire' or info.move_type == 'expire_will_leave' then return gcd_prefix .. h.determineExpirationMoveType(info) elseif info.move_type == 'to_academy' then return gcd_prefix .. h.determineToAcademyMoveType(info) elseif info.move_type == 'to_main' then return gcd_prefix .. h.determineToMainMoveType(info) elseif h.isAllowedMoveType(info, info.move_type, gcd_prefix) then return gcd_prefix .. info.move_type elseif info.contract_until and info.TeamStart and info.TeamEnd then return gcd_prefix .. 'extended' end local preload = h.guessNewsPreloadWithoutPrefix(info) if preload then return gcd_prefix .. preload end return 'unknown' end

function h.determineExpirationMoveType(info) if info.StatusStart == 'draft_pick' then return 'draft_pick_expire' elseif info.move_type == 'expire_will_leave' then return 'expire_notleave_yet' elseif info.TeamEnd then return 'expire_notleave' end return 'expire' end

function h.determineToAcademyMoveType(info) if info.StatusStart == 'draft_pick' then return 'draft_pick_to_academy' elseif info.TeamEnd then return 'to_academy_also_stay' end return 'to_academy' end

function h.determineToMainMoveType(info) if info.TeamEnd then return 'to_main_also_stay' end return 'to_main' end

function h.isAllowedMoveType(info, move_type, gcd_prefix) -- move_type, if legally specified, will usually be a literal preload to use -- we'll overload "confirm" though for editor convenience if not move_type then return false end if move_type == 'confirm' then return false end local move_type_lookup = gcd_prefix .. move_type if not SENTENCES.lookup[move_type_lookup] then error(i18n.print('error_unknownPreload', move_type_lookup)) end if SENTENCES.lookup[move_type_lookup].auto then error(i18n.print('error_forbiddenPreload', move_type_lookup, 'RCInfo')) end h.validateMoveType(info, move_type, gcd_prefix) return true end

function h.validateMoveType(info, move_type, gcd_prefix) -- can throw errors here if needed end

function h.guessNewsPreloadWithoutPrefix(info) if info.isLoan then return h.guessLoanMoveType(info) elseif h.isEverythingTheSame(info) then return 'remain' elseif info.TeamStart and info.TeamEnd and info.StatusEnd and info.StatusEnd:gsub('_', ' ') == 'set to leave' then return h.guessSetToLeaveStatus(info) elseif info.TeamStart and not info.TeamEnd and info.StatusStart == 'draft_pick' then return 'draft_pick_leave' elseif info.TeamEnd and not info.TeamStart and info.StatusEnd == 'draft_pick' then return 'draft_pick_start' elseif info.TeamStart and info.TeamEnd and info.StatusStart == 'draft_pick' then return h.guessDraftPickStayWithTeamMoveType(info) elseif not info.TeamStart and info.StatusEnd == 'temp_sub' then return 'join_as_temp_sub' elseif not info.TeamEnd and info.StatusStart == 'temp_sub' then return 'leave_as_temp_sub' elseif not info.TeamEnd and info.StatusStart == 'trial' then return 'leave_after_trial' elseif info.TeamStart and not info.TeamEnd then return 'leave' elseif info.TeamEnd and info.StatusStart == 'trial' then return 'join_after_trial' elseif info.TeamEnd and info.StatusStart == 'main_or_acad' then return 'join_main_or_acad' elseif info.TeamEnd and not info.TeamStart then return 'join' elseif info.StatusEnd == 'official_sub' and info.StatusStart ~= 'official_sub' then return 'to_official_sub' elseif info.StatusStart == 'official_sub' and info.StatusEnd ~= 'official_sub' then return 'end_official_sub' elseif info.RoleStart ~= info.RoleEnd then return h.guessRoleSwapType(info) elseif info.IsSubStart and not info.IsTraineeStart and info.IsTraineeEnd and not info.IsSubEnd then return 'sub_to_trainee' elseif not info.IsTraineeStart and info.IsTraineeEnd then return 'to_trainee' elseif not info.IsSubStart and info.IsSubEnd then return 'to_sub' elseif (info.IsTraineeStart and not info.IsTraineeEnd ) or (info.IsSubStart and not info.IsSubEnd) then return 'to_starting' elseif info.StatusStart == 'inactive' then return 'to_active' elseif info.StatusEnd == 'inactive' then return 'to_inactive' elseif info.StatusEnd == 'opportunities' then return 'opportunities' end end

function h.guessLoanMoveType(info) if info.LoanedTo and info.StatusStart == "loaned_out" and info.StatusEnd ~= 'loaned_out' then -- a loan ends, this is the team they were loaned FROM if info.TeamEnd then return 'loan_return' end return 'loan_return_and_leave' elseif info.LoanedTo and info.StatusEnd == 'loaned_out' and info.StatusStart ~= 'loaned_out' then -- a loan starts, this is the team they were loaned FROM if info.TeamStart ~= info.TeamEnd then error(i18n.print('error_missingLoanTagging')) end return 'loaned_to' elseif info.LoanedFrom and info.StatusStart == 'on_loan' and info.StatusEnd ~= 'on_loan' then -- a loan ends, this is the team they were loaned TO		if info.TeamEnd then return 'loan_end_and_join' end return 'loan_end' elseif info.LoanedFrom and info.StatusEnd == 'on_loan' and info.StatusStart ~= 'on_loan' then -- a loan starts, this is the team they were loaned TO		return 'loaned_from' end end

function h.isEverythingTheSame(info) for _, v in ipairs(util_news.ALL_POSSIBLE_CHANGES) do if info[v .. 'Start'] ~= info[v .. 'End'] then return false end end return true end

function h.guessSetToLeaveStatus(info) if info.AlreadyJoined then return 'set_to_leave_already_joined' end return 'set_to_leave' end

function h.guessDraftPickStayWithTeamMoveType(info) -- this only handles cases where the player stays with the main team -- leaving, expiring is handled elsewhere -- moves to academy are handled elsewhere if info.IsTraineeEnd then return 'draft_pick_to_trainee' elseif info.IsSubEnd then return 'draft_pick_to_sub' end return 'draft_pick_signed' end

function h.guessRoleSwapType(info) -- we might need to further break this up into 4 types: -- ingame only -> ingame + staff -- staff only -> ingame + staff -- ingame + staff -> ingame only -- ingame + staff -> staff only -- that setup would allow complete discrimination in what we accept if we wanna -- be able to show a brief period that someone was also on staff but then stopped being -- while they were, that entire time, also a player. -- currently since we aren't really doing anything of an organization query of Tenures table -- this is not needed; and we might NEVER do that, so we shouldn't prematurely complicate -- but we might need it later so just keep that in mind. -- these extra preloads are NOT ignored by TenuresUnbroken, whereas role_swap IS	-- so we'll properly create our current & former stuff -- see Brolia's tenures on 5 Ronin for an example requiring these extra preloads if info.Leave.RoleSet:hasIngame and not info.Join.RoleSet:hasIngame then return 'role_swap_from_ingame' end if not info.Leave.RoleSet:hasIngame and info.Join.RoleSet:hasIngame then return 'role_swap_to_ingame' end return 'role_swap' end

-- done guessing, output time! function h.makeRowOutput(data) local output = mw.html.create for _, row in ipairs(data) do		local tr = output:tag('tr') util_html.printRowByList(tr, row, COLUMNS) end return output end

-- store roster change cargo function h.storeRosterChangeData(data) local N_LineInNews = 1 for _, row in ipairs(data) do		row.N_LineInNews = N_LineInNews if not row.TeamStart or not row.TeamEnd then h.storeEntireRow(row) N_LineInNews = N_LineInNews + 1 else h.storeTwoRosterChangeRowsFromOneLine(row) N_LineInNews = N_LineInNews + 2 end end end

function h.storeEntireRow(row) local rowCopy = mw.clone(row) h.copyWhenArgsToRowCopy(rowCopy, row.TeamStart and 'Start' or 'End') util_news.storeRosterChangesRow(rowCopy) end

function h.copyWhenArgsToRowCopy(rowCopy, when) rowCopy.Direction = h.getRosterChangeDirection(when) for _, v in ipairs({ 'RoleDisplay', 'RoleModifier', 'Status', 'Role', 'RolesIngame', 'RolesStaff', 'Roles' }) do rowCopy[v] = rowCopy[v .. when] end end

function h.getRosterChangeDirection(when) return when == 'Start' and 'Leave' or 'Join' end

function h.storeTwoRosterChangeRowsFromOneLine(row) -- Start is a leave (on the team at the start) -- End is a join (on the team at the end) -- if the player is changing position/status/etc immediately on a rename -- then actually we don't want to store anything here for leaving -- because they never actually joined their current (renamed) team -- the leave will have been registered in TeamRename, -- and the join will have been skipped; -- nothing has to happen here to conclude a tenure -- param to specify is `changed_on_team_rename` for both here & TeamRename if not row.ChangedOnTeamRename then h.storeOneRosterChangeRow(mw.clone(row), 'Start', 'End') end row.N_LineInNews = row.N_LineInNews + 1 h.storeOneRosterChangeRow(mw.clone(row), 'End', 'Start') end

function h.storeOneRosterChangeRow(rowCopy, when, notWhen) for i, v in ipairs(util_news.PLAYER_STATUSES) do		-- legacy value-for-each-part rowCopy[v .. notWhen] = nil end -- one value, and direction says leave or join h.copyWhenArgsToRowCopy(rowCopy, when) util_news.storeRosterChangesRow(rowCopy) end

--- -- News Sentence Creation & Printing --- function h.getNewsData(args, listOfPlayers) util_table.merge(args, h.getNewsSentences(args, listOfPlayers)) local ret = util_table.merge(		util_news.getNewsCargoFieldsFromArgs(args),		h.getSpecializedNewsData(args.team, listOfPlayers)	) return ret end

function h.getSpecializedNewsData(team, listOfPlayers) local ret = { Players = util_table.concat(util_table.extractValueToList(listOfPlayers, 'Player')), Subject = m_team.teamlinkname(team), SubjectType = 'Team', }	return ret end

function h.getNewsSentences(args, listOfPlayers) local playersByPreload = h.groupPlayersByPreload(listOfPlayers) local playersAsSentences = util_map.dictInPlace(playersByPreload, h.getSentencePart, args) local ret = { Sentence = h.getSentenceParts(playersAsSentences, args) }	return util_map.inPlace(ret, util_table.concatDict, ' ') end

function h.groupPlayersByPreload(listOfPlayers) local ret = {} for _, player in ipairs(listOfPlayers) do		h.addPlayerToPreloadOutput(ret, player.Preload or 'unknown', player) end util_sort.dictByKeys(ret, 'sort_key_sentence', true) h.sortEachPreload(ret) return ret end

function h.addPlayerToPreloadOutput(ret, preload, player) local key = h.getCustomKey(preload, player) util_table.initDict(ret, key) ret[key].preload = preload ret[key].sort_key_sentence = h.getSortKeySentence(ret, player) ret[key].custom_text = player.custom_text ret[key].class = SENTENCES.lookup[preload or 'unknown'].class util_table.initDict(ret[key], player.Player, player) end

function h.getSortKeySentence(sentenceData, player) if not sentenceData.sort_key_sentence then return player.sort_key_sentence end return math.min(player.sort_key_sentence, sentenceData.sort_key_sentence) end

function h.getCustomKey(preload, player) -- modify the key to add any information that requires different information so that -- we never have anything problematically grouped together local key = player.custom_text or preload if player.sentence_group then key = player.sentence_group .. key end if not SENTENCES.lookup[preload or 'unknown'] then error(i18n.print('error_invalidPreloadComputed', preload)) end key = key .. (player.LoanedTo or player.LoanedFrom or '') if preload == 'extended' then key = key .. (player.contract_until or '') end if not SENTENCES.lookup[preload or 'unknown'].respect_joining_key then return key end for _, modifier in ipairs { 'sub', 'trainee', 'rejoin' } do		if player[modifier] then key = key .. modifier end end return key end

function h.sortEachPreload(playersByPreload) for _, preload in ipairs(playersByPreload) do		util_sort.dictByKeys(			playersByPreload[preload],			{ 'sort_key_role', 'sort_key_player' },			{ true, true }		) end end

function h.getSentenceParts(playersAsSentences, args) -- returns ordered dict local prepend = { 'prepend', prepend = util_text.ucfirst(args.custom_prepend) } local append = { 'append', append = util_text.ucfirst(args.custom_append) } return util_table.mergeDicts(prepend, playersAsSentences, append) end

function h.getSentencePart(row, args) if row.class then return h.makeSentencePartWithClass(row, args) end return h.getSentencePartText(row, args) end

function h.makeSentencePartWithClass(row, args) local span = mw.html.create('span') :addClass('rosterchange-' .. row.class) h.printSentencePartText(span, row, args) return tostring(span) end

function h.printSentencePartText(span, row, args) span:wikitext(h.getSentencePartText(row, args)) end

function h.getSentencePartText(row, args) local text = h.getTextForReplacements(row, args) local replacements = h.getReplacements(row, args) return util_sentence.makeReplacements(text, replacements) end

function h.getTextForReplacements(row, args) if args.custom then return args.custom end if row.custom_text then return row.custom_text end local preload = args.sentencetype or row.preload if not SENTENCES.lookup[preload or 'unknown'] then error(i18n.print('error_unknownPreload', row.preload)) end return SENTENCES.lookup[preload or 'unknown'].sentence end

function h.getReplacements(players, args) local firstPlayer = players[players[1]] local ret = { PLAYERS = util_sentence.players(players), PLAYERS_POSS = util_sentence.playersPossessive(players), JOIN = h.getJoinText(players, ''), JOINING = h.getJoinText(players, 'ing'), ADDED = h.getAddedText(firstPlayer), AS = h.getJoinSuffix(firstPlayer), PLAYERS_WITH_SWAP = util_sentence.playersWithSwap(players), PLAYERS_POSS_WITH_SWAP = util_sentence.playersPossessiveWithSwap(players), LOANED_TO = m_team.mediumplainlinked(firstPlayer.LoanedTo), LOANED_FROM = m_team.mediumplainlinked(firstPlayer.LoanedFrom), CONTRACT_EXTEND_UNTIL = h.getUntilDateSpecific(firstPlayer, 'contractUntil'), DRAFT_WINDOW_UNTIL = h.getUntilDateSpecific(firstPlayer, 'draftWindowUntil'), ASSISTANCE = firstPlayer.assistance and ' with assistance finding a new team' or '', EVENT = firstPlayer.Event and util_text.intLink(firstPlayer.EventLink, firstPlayer.Event), AT_EVENT = h.getAtEventText(firstPlayer), REASON = h.getReason(firstPlayer.Reason), REPLACING = h.getReplacing(players), REMAIN_FOR = h.getRemainFor(firstPlayer), BECAUSE_COMMA = h.getBecauseComma(players, firstPlayer.Reason), PHASE = h.getPhase(firstPlayer.Phase), LEAVE_DATE = h.getLeaveDate(firstPlayer.leave_date), ALREADY_JOINED = h.getAlreadyJoined(firstPlayer.AlreadyJoined), SISTER_TEAM = m_team.rightmediumlinked(firstPlayer.SisterTeam), }	util_table.merge(		ret,		util_map.arrayToLookupSafe( SENTENCES.to_conjugate, util_sentence.getConjugation, #players)		) return ret end

function h.getJoinText(players, ing) return h.getJoinWord(players, ing) .. h.getJoinSuffix(players[players[1]]) end

function h.getJoinWord(players, ing) if players[players[1]].rejoin then return util_sentence.getConjugation('rejoin' .. ing, #players) end return util_sentence.getConjugation('join' .. ing, #players) end

function h.getAddedText(firstPlayer) if firstPlayer.rejoin then return i18n.default('readded') end return i18n.default('added') end

function h.getUntilDateSpecific(firstPlayer, formatKey) if not firstPlayer.contract_until then return '' end if firstPlayer.contract_until:find('^%d%d%d%d$') then return i18n.default('extendThrough', firstPlayer.contract_until) end return i18n.default(		formatKey,		util_time.strToDateStrFuzzy(firstPlayer.contract_until)	) end

function h.getReplacing(players) local replacing = util_table.extractValueFromDictToList(players, 'Replacing') if #replacing == 0 then return '' end return (', replacing %s%s'):format(		util_table.printList( util_map.inPlace(replacing, util_esports.playerLinked) ),		#players > 1 and ', respectively' or ''	) end

function h.getRemainFor(firstPlayer) if not firstPlayer.remain_for then return '' end return i18n.default(		'remainFor',		util_text.intLinkOrText(firstPlayer.remain_for_link, firstPlayer.remain_for)	) end

function h.getAtEventText(firstPlayer) if not firstPlayer.Event then return '' end return i18n.default(		'atEvent',		util_text.intLink(firstPlayer.EventLink, firstPlayer.Event)	) end

function h.getReason(reason) if not reason then return '' end return (' because %s'):format(reason) end

function h.getBecauseComma(players, reason) return reason and #players > 1 and ',' or '' end

function h.getPhase(phase) if not phase then return '' end return ('%s of '):format(phase) end

function h.getLeaveDate(date) if not date then return '' end return i18n.default('leaveDate', date) end

function h.getAlreadyJoined(team) if not team then return '' end return m_team.rightmediumlinked(team) end

function h.getJoinSuffix(player) return h.getJoinRoleModifier(player) .. h.getJoinStatusModifier(player) end

function h.getJoinRoleModifier(player) for _, v in ipairs(JOIN_MODIFIERS) do		if player[v] then return i18n.default(('store_joins_as_%s'):format(v)) end end return '' end

function h.getJoinStatusModifier(player) if player.Status == 'trial' then return i18n.default('store_joins_on_trial') end return '' end

function h.makeNewsOutput(newsData) local output = mw.html.create util_html.printColspanCell(output, newsData.SentenceWithDate, #COLUMNS) return output end

return p