Module:DateI18n

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules


Summary

This module is intended for creating of date strings in any language. It serves as a back-end of {{Date}} template. See documentation of that template for documentation.

The internationalization of the date formats can be found at Data:DateI18n.tab and Data:I18n/MonthCases.tab. Module:DateI18n is called from Module:ISOdate.

Using this module from templates

Date

This module should only be called from {{Date}} template. Please call that template to access this module. Also see {{Date}} template for full documentation

Usage:

{{#invoke:DateI18n|Date|year=...|month=...|day=...|hour=...|minute=...|second=...|tzhour=...|tzmin=...|lang=...}}

Parameters:

year, month, day, hour, minute, second
(most are optional) parameters specifying part of the date
tzhour, tzmin
(optional and rarely used) time zone offset from UTC
lang
(optional) language to be used to display the date. If not specified language of the user will be used
class
(optional) Allows setting of the HTML class of the time node where the date is included. This is useful for microformats. The default value is, for legacy reasons, "dtstart" (used by hCalendar). See the microformats project. "class=" will remove all metadata.
case
(optional) By default each language uses preferred form of the date, which typically has month in nominative or genitive grammatical case. With case parameter one can overwrite the preferred case with some other one used by a given language. Parser function {{#time}} stores nominative and genitive forms, others are stored in Data:I18n/MonthCases.tab. This functionality is mostly used by Module:Complex date.
trim_year
trim_year parameter

Example:

{{#invoke:DateI18n|Date|year=1990|month=Oct|day=01|lang=en}} produces 1 October 1990

Using this module from Lua code

In order to use the functions in this module from another Lua module you first have to import this module.

Example:

local DateMod = require('Module:DateI18n')

_Date

Usage:

date_string = DateMod._Date({year,month,day,hour,minute, second},lang)

I18n tables

This module stores all the language specific settings in two files in "Data" namespace: Data:DateI18n.tab and Data:I18n/MonthCases.tab

Translation table with different forms of dates. Outputs are in the format used by {{#time}} parser function. There are several date formats supported, which depend on the data provided

For marking litteral text (notably with Basic Latin letters) that must be ouput as is (and not recognized as possible placeholders for formatting individual date fields values), you may use straight ASCII single quotes within all specified formats (instead of the straight ASCII double quotes, normally required for litteral text in forms given to the {{#time:}} parser function).

If a specified format needs to embed an apostrophe to be returned litterally (and not to be recognized as the alternative delimiter now reserved for marking litterals for {{#time:}}), the curly apostrophe-quote character U+2019 (’) must be used instead, or any other apostrophe-like character suitable for the target language: the ASCII single quote is ambiguous in all languages (and can also cause problems with Mediawiki syntax for marking bold/italic styles surrounding the generated date) and there are other preferred characters.

Note that formats 'YMDHMS' and 'YMDHM' are legacy aliases present in Data:DateI18n.tab. They have two 'M' in them: the first 'M' means month, the second 'M' means minute only after the 'H' meaning hour. These aliases have been kept in the tabular data, for compatiblity with some other modules that would expect these two legacy keys. For adding more formats showing time with fields for hour, minute and second, distinctive lower case letters 'hms' should be used.

Date formats
Format string Meaning Comments
YMDhms or YMDHMS "YYYY-MM-DD, hh:mm:ss" format for:
year, month name, day of the month, hour, minute, and second
Most items in the list use format created from the YMD format, with hour, minutes and seconds added with ", HH:MM:SS".
All languages using this format do not have to be listed, as they default to this simple rule used in English.
YMDhm or YMDHM "YYYY-MM-DD, hh:mm" format for:
year, month name, day of the month, hour, and minute
Most items in the list use format created from the YMD format, with hour and minutes added with ", HH:MM".
All languages using this format do not have to be listed, as they default to to this simple rule used in English.
YMD "YYYY-MM-DD" format for:
year, month name, and day of the month
Languages using the same format as English do not need to be listed.
YM "YYYY-MM" format for:
year and month name
Most items in the list use the format created from the M format, with the year added with " YYYY" and created from the Y format.
All languages using this format do not have to be listed, as they default to to this simple rule used in English.
Y "YYYY" format for:
the year only
Most languages show year as a simple number, those do not have to be listed as they will default to the same format as English.
MDhms "MM-DD, hh:mm:ss" format for:
month name, day of the month, hour, minute, and second
Most items in the list use the format created from the MD format, with hour, minutes and seconds added with ", HH:MM:SS".
All languages using this format do not have to be listed, as they default to this simple rule used in English.
MDhm "MM-DD, hh:mm" format for:
month name, day of the month, hour, and minute
Most items in the list use the format created from the MD format, with hour, minutes and seconds added with ", HH:MM".
All languages using this format do not have to be listed, as they default to this simple rule used in English.
MD "MM-DD" format for:
month name and day of the month
Most items in the list use the format created from the M format, with the day number prepended with "DD ".
Some language prefer placing the day of the month after the month name. Some languages need to use a genitive form for the month name, or will use different forms for specific days of the month
All languages using this format do not have to be listed, as they default to this simple rule used in English.
M "MM" format for:
the month name only
Languages may have multiple forms to be listed for specific months, depending on their grammatical case of use.
Special cases

Some languages like Catalan, Basque, French, or Irish use different forms for dates, depending on the day of the month.

The format of the Tabular Data (and its eiting interface) allowed only simple strings to be stored in multi-language arrays, not JSON subtables. So the following is still not accepted in Tabular data pages on this wiki, even though it is a valid JSON syntax:

[
    ...
    "en": "j' 'F' 'Y",
    ...
    "fr" : { "default": "j' 'F' 'Y", "d01": "j'<sup>er</sup> 'F' 'Y" },
    ...
]

In order to overcome that, we stored the JSON subtable inside translation strings (already surrounded by ASCII double quotes required by JSON for the main tabular data) by using curly quotes instead of ASCII quotes for strings in the embedded JSON table; that might look like:

[
    ...
    "en": "j' 'F' 'Y",
    ...
    "fr" : "{ ‟default”: ‟j' 'F' 'Y”, ‟d01”: ‟j'<sup>er</sup> 'F' 'Y” }",
    ...
]

This string is converted by replacing curly double quotes, any one in U+201C..U+201F (“”„‟), found in tabular data with regular ASCII straight double-quotes as:

{ "default": "j' 'F' 'Y", "d01": "j'<sup>er</sup> 'F' 'Y" }

which is then parsable as valid JSON and converted into a Lua dictionary table.

Using curly double quotes is supported only in string values embedding JSON data (i.e. a table delimited by braces "{}" inside the string) containing at least the field named ‟default”. For all other formats (using simple strings), you must use ASCII double quotes to delimit them, and curly double quotes will be recognized as part of the litteral text in the ouput.

Then the field value representing the day of the month of the given date is converted into a key by prepending ‟d” to the 2-digit integer for that day. If this key is found in the parsed dictionary, then the form specified by the value associated to that key will be used, otherwise we will use the form specified by the value associated to the ‟default” key.

An alternative (as long as Tabular data only allows simple strings in translations) using normal JSON syntax within values would have required escaping with a prepended backslash each one of these embedded double quotes (which would be less readable and more errorprone to edit with the current user interface for tabular data on this wiki):

[
    ...
    "en": "j' 'F' 'Y",
    ...
    "fr": "{ \"default\": \"j' 'F' 'Y\", \"d01\": \"j'<sup>er</sup> 'F' 'Y\" }",
    ...
]

In all cases, you must use ASCII single quotes (instead of ASCII double quotes) for marking litteral text to display as is, and you must use curly apostrophes (inside or outside marked litterals) for any apostrophe to display as is.

Grammatical cases of month names in different languages used by Module:DateI18n and Module:Complex_date.

See Also

Modules related to internationalization (i18n) of dates
{| class="wikitable sortable" border="1"
Module Name Uses Module Used by Module Used by Template Comment
Module:DateI18n Module:I18n/date Module:ISOdate Template:Date Create date string in any language
Module:ISOdate Module:DateI18n Module:Complex date Template:ISOdate & Template:ISOyear Parse YYYY-MM-DD date string
Module:Roman Module:Complex date
Module:Ordinal
Template:Roman Create Roman numerals
Module:Ordinal Module:I18n/ordinal
Module:Roman
Module:Complex date Template:Ordinal Create Ordinal numerals
Module:Complex date Module:I18n/complex date
Module:ISOdate
Module:Roman
Module:Ordinal
Template:Other date (not deployed yet) Create complex date phrases in many languages

|}

Code

--[[  
  __  __           _       _        ____        _       ___ _  ___        
 |  \/  | ___   __| |_   _| | ___ _|  _ \  __ _| |_ ___|_ _/ |( _ ) _ __  
 | |\/| |/ _ \ / _` | | | | |/ _ (_) | | |/ _` | __/ _ \| || |/ _ \| '_ \ 
 | |  | | (_) | (_| | |_| | |  __/_| |_| | (_| | ||  __/| || | (_) | | | |
 |_|  |_|\___/ \__,_|\__,_|_|\___(_)____/ \__,_|\__\___|___|_|\___/|_| |_|
  
This module is intended for processing of date strings.

Please do not modify this code without applying the changes first at Module:DateI18n/sandbox
and testing at Module:DateI18n/sandbox/testcases and Module talk:DateI18n/sandbox/testcases.

Authors and maintainers:
* User:Parent5446 - original version of the function mimicking Template:ISOdate
* User:Jarekt - original version of the functions mimicking Template:Date 
]]

-- =======================================
-- === Dependencies ======================
-- =======================================

require('strict')

-- =======================================
-- === Local Functions ===================
-- =======================================

------------------------------------------------------------------------------
--[[ (copied from Module:Core)
Function allowing for consistent treatment of boolean-like wikitext input.
Inputs:
  1) val - value to be evaluated, outputs as a function of values:
		true  : true  (boolean), 1 (number), or strings: "yes", "y", "true", "1"
		false : false (boolean), 0 (number), or strings: "no", "n", "false", "0"
  2) default - value to return otherwise
See Also: It works similarly to Module:Yesno
]]
local function yesno(val, default)
	if type(val) == 'boolean' then
		return val
	elseif type(val) == 'number' then
		val = tostring(val)
	end
	if type(val) == 'string' then
		local LUT = {
			yes=true , y=true , ['true'] =true , t=true , ['1']=true , on =true,
			no =false, n=false, ['false']=false, f=false, ['0']=false, off=false }
	    val = LUT[mw.ustring.lower(val)]  -- put in lower case
	    if (val~=nil) then
			return val
		end
    end
    return default
end


---------------------------------------------------------------------------------------
-- trim leading zeros in years prior to year 1000
-- INPUT:
--  * datestr   - translated date string 
--  * lang      - language of translation
-- OUTPUT:
--  * datestr - updated date string 

local function trimYear(datestr, year, lang)
	local yearStr0, yearStr1, yearStr2, zeroStr
	yearStr0 = string.format('%04i', year ) -- 4 digit year in standard form "0123"
	yearStr1 = mw.language.new(lang):formatDate( 'Y', yearStr0) -- same as calling {{#time}} parser function
	--yearStr1 = mw.getCurrentFrame():callParserFunction( "#time", { 'Y', yearStr0, lang } ) -- translate to a language 
	if yearStr0==yearStr1 then -- most of languages use standard form of year 
		yearStr2 = tostring(year)
	else -- some languages use different characters for numbers
		yearStr2 = yearStr1
		zeroStr = mw.ustring.sub(yearStr1,1,1) -- get "0" in whatever language
		for i=1,3 do -- trim leading zeros
			if mw.ustring.sub(yearStr2,1,1)==zeroStr then
				yearStr2 = mw.ustring.sub(yearStr2, 2, 5-i)
			else
				break
			end
		end
	end
	return string.gsub(datestr, yearStr1, yearStr2 ) -- in datestr replace long year with trimmed one
end

---------------------------------------------------------------------------------------
-- Look up proper format string to be passed to {{#time}} parser function
-- INPUTS:
--  * datecode: YMDhms, YMDhm, YMD, YM, Y, MDhms, MDhm, MD, or M
--  * day     : Number between 1 and 31 (not needed for most languages)
--  * lang    : language
-- OUTPUT:
--  * dFormat : input to {{#time}} function
local function getDateFormat(datecode, day, lang)
	local function parseFormat(dFormat, day)
		if dFormat:find('default') and #dFormat>10 then
			-- Special (and messy) case of dFormat code depending on a day number, where data is a
			-- JSON-encoded table {”default”:”*”,”dDD”:”*”} including fields for specific 2-digit days.
			-- Change curly double quotes (possibly used for easier editing in tabular data) in dFormat
			-- to straight ASCII double quotes (required for parsing of this JSON-encoded table).
			local D = mw.text.jsonDecode(mw.ustring.gsub(dFormat, '[„“‟”]', '"')) --com = mw.dumpObject(D)
			-- If the desired day is not in that JSON table, then use its "default" case.
			dFormat = D[string.format('d%02i', day)] or D.default
            -- Change ASCII single quotes to ASCII double quotes used for {{#time}} marking.
            -- Apostrophes needed in plain-text must not use ASCII single quotes but curly apostrophe
            -- e.g. { ‟default”: ‟j”, ‟d01”: ‟j’'o'” }, not { ‟default”: ‟j”, ‟d01”: ‟j''o'” }.
		end
		dFormat = dFormat:gsub("'", '"')
		return dFormat
	end
	
	local T = {}
	local tab = mw.ext.data.get('DateI18n.tab', lang)
	for _, row in pairs(tab.data) do -- convert the output into a dictionary table
		local id, _, msg = unpack(row)
		T[id] = msg
	end
    -- Compatibility of legacy data using 'HMS' or 'HM', where 'M' is ambiguous
    T.YMDhms = T.YMDhms or T.YMDHMS
    T.YMDhm  = T.YMDhm  or T.YMDHM
    datecode = datecode == 'YMDHMS' and 'YMDhms' or datecode == 'YMDHM' and 'YMDhm' or datecode

	local dFormat = T[datecode]
	if dFormat == 'default' and (datecode == 'YMDhms' or datecode == 'YMDhm')  then 
		-- For most languages adding hour:minute:second is done by adding ", HH:ii:ss to the 
		-- day precission date, those languages are skipped in DateI18n.tab and default to 
		-- English which stores word "default"
		dFormat = parseFormat(T['YMD'], day).. ', H:i'
		if datecode == 'YMDhms' then
			dFormat = dFormat .. ':s'
		end
	else
		dFormat = parseFormat(dFormat, day)
	end
	return dFormat
end

---------------------------------------------------------------------------------------
-- Look up proper format string to be passed to {{#time}} parser function
-- INPUTS:
--  * month : month number
--  * case  : gramatic case abbriviation, like "ins", "loc"
--  * lang  : language
-- OUTPUT:
--  * dFormat : input to {{#time}} function
local function MonthCase(month, case, lang)
	if month == nil or case == nil then
		return nil
	end
	local T = {{},{},{},{},{},{},{},{},{},{},{},{}}
	local tab = mw.ext.data.get('I18n/MonthCases.tab', lang)
	for _, row in pairs(tab.data) do
		local mth, cs, msg = unpack(row)
		T[mth][cs] = msg
	end
	return T[month][case]
end

-- ==================================================
-- === External functions ===========================
-- ==================================================
local p = {}

-- ===========================================================================
-- === Functions accesible from the outside to allow unit-testing
-- === Please do not use directly as they could change in the future
-- ===========================================================================

---------------------------------------------------------------------------------------
-- Single string replacement that ignores part of the string in "..."
function p.strReplace(String, old, new)
	if String:find('"') then
		local T={}
		for i, str in ipairs(mw.text.split( String, '"', true )) do
			if i%2==1 then
				str = str:gsub(old, new, 1)
			end
			table.insert(T, str)
		end
		return table.concat(T,'"')
	else
		return String:gsub(old, new, 1)
	end
end

---------------------------------------------------------------------------------------
-- process datevec
-- INPUT:
--  * datevec - Array of {year,month,day,hour,minute,second, tzhour, tzmin} containing broken 
--    down date-time component strings or numbers
-- OUTPUT:
--  * datenum - same array but holding only numbers or nuls
function p.clean_datevec(datevec)
	-- create datecode based on which variables are provided and check for out-of-bound values
	
	-- check special case of month provided as a name
	local month = datevec[2]
	if type(month) == 'string' and month ~= '' and not tonumber(month) then
		-- When the month is not a number, check if it's a month name in the project's language.
		datevec[2] = mw.getContentLanguage():formatDate('n', month)
	end
		
	-- check bounds
	local maxval = {  1/0, 12, 31, 23, 59, 59,  23, 59 } -- max values (or  1/0=+inf) for year, month, day, hour, minute, second, tzhour, tzmin
	local minval = { -1/0, 01, 01, 00, 00, 00, -23, 00 } -- min values (or -1/0=-inf) for year, month, ...
	local datenum  = {} -- date-time encoded as a vector = [year, month, ... , second, tzhour, tzmin]
	for i = 1, 8 do
        local val = tonumber(datevec[i])
        if val and val >= minval[i] and val <= maxval[i] then -- These tests work with infinite min/max values.
		    datenum[i] = val
		end
	end
	
	-- leap second
	if tonumber(datevec[6]) == 60 then -- leap second '60' is valid only at end of 23:59 UTC, on 30 June or 31 December of specific years
--		datenum[6] = 60 
		local MDhm = table.concat({unpack(datenum,2,5)}, ',')
	    if (MDhm == table.concat({6, 30, 23, 59}, ',')) or (MDhm == table.concat({12, 31, 23, 59}, ',')) then
		   datenum[6] = 60 
	    end
	end
	
	return datenum
end
	
---------------------------------------------------------------------------------------
-- process datevec
-- INPUT:
--  * datenum - Array of {year,month,day,hour,minute,second, tzhour, tzmin} as numbers or nuls
-- OUTPUT:
--  * timeStamp - date string in the format taken by mw.language:formatDate lua function and {{#time}} parser function
--       https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate
--       https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
--  * datecode - a code specifying content of the array where Y' is year, 'M' is month,
--     'D' is day, 'h' is hour, 'm' minute, 's' is second.
--     Output has to be one of YMDhms, YMDhm, YMD, YM, Y, MDhms, MDhm, MD, M.
function p.getTimestamp(datenum)

	-- create datecode based on datenum
	local codes  = { 'Y', 'M', 'D', 'h', 'm', 's'} 
	local datecode = '' -- a string signifying which combination of variables was provided
	for i, c in ipairs(codes) do
		datecode = datecode .. (datenum[i] and c or '') -- if datenum[i] than append codes[i] to datecode
	end

	-- create timestamp string (for example 2000-02-20 02:20:20) based on which variables were provided
	local timeStamp
    -- date starting by a year
	if datecode == 'YMDhms' then
		timeStamp = string.format('%04i-%02i-%02i %02i:%02i:%02i', datenum[1], datenum[2], datenum[3], datenum[4], datenum[5], datenum[6] )
	elseif datecode == 'YMDhm' then
		timeStamp = string.format('%04i-%02i-%02i %02i:%02i', datenum[1], datenum[2], datenum[3], datenum[4], datenum[5] )
	elseif datecode:sub(1,3)=='YMD' then
		timeStamp = string.format('%04i-%02i-%02i', datenum[1], datenum[2], datenum[3] )
		datecode  = 'YMD' -- 'YMDhms', 'YMDhm' and 'YMD' are the only supported format starting with 'YMD'; all others will be converted to 'YMD'.
	elseif datecode:sub(1,2) == 'YM' then
		timeStamp = string.format('%04i-%02i', datenum[1], datenum[2] )
		datecode  = 'YM' 
	elseif datecode:sub(1,1)=='Y' then
		timeStamp = string.format('%04i', datenum[1] )
		datecode  = 'Y' 
    -- date starting by a month (the implied year is 2000)
	elseif datecode== 'MDhms' then
		timeStamp = string.format('%04i-%02i-%02i %02i:%02i:%02i', 2000, datenum[2], datenum[3], datenum[4], datenum[5], datenum[6] )
	elseif datecode == 'MDhm' then
		timeStamp = string.format('%04i-%02i-%02i %02i:%02i', 2000, datenum[2], datenum[3], datenum[4], datenum[5] )
	elseif datecode:sub(1,2) == 'MD' then
		timeStamp = string.format('%04i-%02i-%02i', 2000, datenum[2], datenum[3] )
		datecode = 'MD' -- 'MDhms', 'MDhm' and 'MD' are the only supported format starting with 'MD'; all others will be converted to 'MD'
	elseif datecode:sub(1,1) == 'M' then -- Ambiguous: could mean minutes, but here means month (when parsed as a name/abbrev, not as a number).
		timeStamp = string.format('%04i-%02i-%02i', 2000, datenum[2], 1 )
		datecode  = 'M' 
    -- other possible but unrecognized formats (e.g. 'DHis', 'DHi', 'D', 'His', 'Hi');
    -- note that 'Dh', 'D', 'h', 's' may eventually work, but not 'm' for minute only, which is ambiguous with 'M' for month only.
	else
		timeStamp = nil -- format not supported
	end
	return timeStamp, datecode
end

local function isValidLangCode(lang)
	if not lang then
		return false
	end
	lang = mw.text.trim(lang)
	return lang ~= '' and lang ~= '⧼Lang⧽' and mw.language.isValidCode(lang)
end

-- ===========================================================================
-- === Version of the function to be called from other LUA codes
-- ===========================================================================

--[[ ========================================================================================
Date
 
This function is the core part of the ISOdate template. 
 
Usage:
  local Date = require('Module:DateI18n')._Date
  local dateStr = Date({2020, 12, 30, 12, 20, 11}, lang)
 
Parameters:
  * {year,month,day,hour,minute,second, tzhour, tzmin}: broken down date-time component strings or numbers
		tzhour, tzmin are timezone offsets from UTC, hours and minutes
  * lang: The language to display it in
  * case: Language format (genitive, etc.) for some languages
  * class: CSS class for the <time> node, use "" for no metadata at all
]]
function p._Date(datevec, lang, case, class, trim_year)
	-- make sure inputs are in the right format
	
	-- set language
	if not isValidLangCode(lang) then
		-- get user's chosen language
		-- equivalent to {{int:lang}}
		lang = mw.getCurrentFrame():callParserFunction("int", "lang")
		
		if not isValidLangCode(lang) then
			-- if that doesn't work, use the project language
			-- this is useful on projects which import this module from Commons
			lang = mw.language.getContentLanguage().code
			
			if not isValidLangCode(lang) then
				-- if that doesn't work, use English
				lang = "en"
			end
		end
	end
	if lang == 'be-tarask' then
		lang = 'be-x-old'
	end
	
	-- process datevec and extract timeStamp and datecode strings as well as numeric datenum array
	local datenum  = p.clean_datevec(datevec)
	local year, month, day = datenum[1], datenum[2], datenum[3]
	local timeStamp, datecode = p.getTimestamp(datenum)
	if not timeStamp then -- something went wrong in parserDatevec
		return ''
	end
	-- Commons [[Data:DateI18n.tab]] page stores prefered formats for diferent 
	-- languages and datecodes (specifying year-month-day or just year of month-day, etc)
	-- Look up country specific format input to {{#time}} function
	local dFormat = getDateFormat(datecode, day, lang)

	-- By default the gramatical case is not specified (case=='') allowing the format to be specified 
	-- in [[Data:DateI18n.tab]]. You can overwrite the default grammatical case of the month by 
	-- specifying "case" variable. This is needed mostly by Slavic languages to create more complex 
	-- phrases as it is done in [[c:Module:Complex date]]
	case = case or ''
	if (lang=='qu' or lang=='qug') and case=='nom' then
		-- Special case related to Quechua and Kichwa languages. The form in the I18n is
		--  Genitive case with suffix "pi" added to month names provided by {#time}}
		-- in Nominative case that "pi" should be removed
		-- see https://commons.wikimedia.org/wiki/Template_talk:Date#Quechua from 2014
		dFormat = dFormat:gsub('F"pi"', 'F')
	elseif case == 'gen' then
		dFormat = p.strReplace(dFormat, "F", "xg")
	elseif case == 'nom' then
		dFormat = p.strReplace(dFormat, "xg", "F")
	elseif case ~= '' and month ~= nil then
		-- see is page [[Data:I18n/MonthCases.tab]] on Commons have name of the month 
		-- in specific gramatic case in desired language. If we have it than replace 
		-- "F" and xg" in dFormat
		local monthMsg = MonthCase(month, case, lang)
		if  monthMsg and monthMsg ~= '' then -- make sure it exists
			dFormat = p.strReplace(dFormat, 'F',  '"'..monthMsg..'"') -- replace default month with month name we already looked up
			dFormat = p.strReplace(dFormat, 'xg', '"'..monthMsg..'"')
		end
	end

    -- Translate the date using specified format.
	-- See https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.language:formatDate and 
	-- https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions##time for explanation of the format
	local langObj = mw.language.new(lang)
	local datestr = langObj:formatDate(dFormat, timeStamp) -- same as using {{#time}} parser function
	
	-- Special case related to Thai solar calendar: prior to 1940 new-year was at different time of year,
	-- so just year (datecode == 'Y') is ambiguous and is replaced by "YYYY or YYYY" phrase
	if lang=='th' and datecode=='Y' and year<=1940 then
		datestr = string.format('%04i หรือ %04i', year+542, year+543 ) 
	end
	
	-- If year < 1000 than either keep the date padded to the length of 4 digits or trim it.
	-- Decide if the year will stay padded with zeros (for years in 0-999 range).
	if year and year < 1000 then
		trim_year = yesno(trim_year, trim_year or '100-999')
		if type(trim_year) == 'string' then
			-- If `trim_year` not a simple boolean, then it's a range of dates.
			-- For example '100-999' means to pad 1-or-2-digit years to be 4-digit long, while keeping 3-digit years as is.
			local YMin, YMax = trim_year:match( '(%d+)-(%d+)' )
			trim_year = YMin and year >= tonumber(YMin) and year <= tonumber(YMax)
		end
		if trim_year then
			datestr = trimYear(datestr, year, lang) -- in datestr replace long year with trimmed one
		end
	end

	-- Append a timezone if present (after the hour and minute of the day).
	if datenum[7] and (datecode:sub(1, 5) == 'YMDhm' or datecode:sub(1, 4) == 'MDhm') then
		-- Use {{#time}} parser function to create timezone string, so that we use the correct character set.
		local sign = (datenum[7]<0) and '−' or '+'
		timeStamp = string.format("2000-01-01 %02i:%02i:00", math.abs(datenum[7]), datenum[8] or 0)
		local timezone = langObj:formatDate('H:i', timeStamp) -- same as using {{#time}} parser function
		datestr = string.format("%s %s%s", datestr, sign, timezone )
	end

	-- HTML formating of date string and tagging for microformats (only for absolute dates with a year).
	if class and class ~= '' and class ~= '-' and datecode:sub(1,1) == 'Y' then 
		local pat = '<time class="%s" datetime="%s" lang="%s" dir="%s" style="white-space:nowrap">%s</time>'
		datestr = pat:format(class, timeStamp, lang, langObj:getDir(), datestr)
	end
	return datestr
end

-- ===========================================================================
-- === Version of the function to be called from template namespace
-- ===========================================================================

--[[ ========================================================================================
Date
 
This function is the core part of the ISOdate template. 
 
Usage:
{{#invoke:DateI18n|Date|year=|month=|day=|hour=|minute=|second=|tzhour=|tzmin=|lang=en}}
 
Parameters:
  * year, month, day, hour, minute, second: broken down date-time component strings
  * tzhour, tzmin: timezone offset from UTC, hours and minutes
  * lang: The language to display it in
  * case: Language format (genitive, etc.) for some languages
  * class: CSS class for the <time> node, use "" for no metadata at all
]]
function p.Date(frame)
	-- get args
	local args = {}
	for key, value in pairs(frame.args) do 
		local trimmed_key = string.gsub(string.lower(mw.text.trim(key)), ' ', '_')
		local trimmed_value = mw.text.trim(value)
		if trimmed_key ~= 'class' and trimmed_value == '' then
			trimmed_value = nil
		end
		args[trimmed_key] = trimmed_value
	end
	
	-- default values
	-- Allows to set the html class of the time node where the date is included. This is useful for microformats.
	args.class = args.class or '-'
	if args.class == '' then
		args.class = 'dtstart'
	end
	-- By default, pad one- and two-digit years to be 4 digits long, while keeping three-digit years as-is.
	args.trim_year = args.trim_year or '100-999'
	
	return p._Date(	
		{args.year, args.month, args.day, args.hour, args.minute, args.second, args.tzhour, args.tzmin},
		args.lang,
		args.case,	
		args.class,
		args.trim_year
	)	
end

return p