Fandom Developers Wiki
m (Let's try passing a language argument in a different way)
m (Reverted edits by The crewmates among us (talk) to last revision by MUNEME)
Tag: Rollback
 
(61 intermediate revisions by 6 users not shown)
Line 1: Line 1:
  +
--- I18n library for message storage in Lua datastores.
-- <nowiki>
 
  +
-- The module is designed to enable message separation from modules &
-- I18n storage module for FANDOM.
 
  +
-- templates. It has support for handling language fallbacks. This
-- @module i18n
 
  +
-- module is a Lua port of [[I18n-js]] and i18n modules that can be loaded
-- @version 1.3.3
 
  +
-- by it are editable through [[I18nEdit]].
-- @usage require("Dev:I18n")
 
  +
--
-- @author Speedit
 
-- @author KockaAdmiralac
+
-- @module i18n
-- @release alpha; untested
+
-- @version 1.4.0
-- @todo Fallbacks and unit testing.
+
-- @require Module:Entrypoint
  +
-- @require Module:Fallbacklist
local i18n = {}
 
  +
-- @author [[User:KockaAdmiralac|KockaAdmiralac]]
  +
-- @author [[User:Speedit|Speedit]]
  +
-- @attribution [[User:Cqm|Cqm]]
  +
-- @release stable
  +
-- @see [[I18n|I18n guide]]
  +
-- @see [[I18n-js]]
  +
-- @see [[I18nEdit]]
  +
-- <nowiki>
  +
local i18n, _i18n = {}, {}
   
-- Module variables & dependencies.
+
-- Module variables & dependencies.
 
local title = mw.title.getCurrentTitle()
 
local title = mw.title.getCurrentTitle()
 
local fallbacks = require('Dev:Fallbacklist')
 
local fallbacks = require('Dev:Fallbacklist')
  +
local entrypoint = require('Dev:Entrypoint')
  +
local uselang
   
  +
--- Argument substitution as $n where n > 0.
-- I18n datastore class.
 
-- @section i18nd
+
-- @function _i18n.handleArgs
  +
-- @param {string} msg Message to substitute arguments into.
local i18nd = {}
 
  +
-- @param {table} args Arguments table to substitute.
 
  +
-- @return {string} Resulting message.
-- Datastore language getter.
 
  +
-- @local
-- @name i18nd:getLang
 
  +
function _i18n.handleArgs(msg, args)
-- @return {string} Default language (datastore messages).
 
  +
for i, a in ipairs(args) do
function i18nd:getLang()
 
  +
msg = (string.gsub(msg, '%$' .. tostring(i), tostring(a)))
return self.defaultLang
 
  +
end
  +
return msg
 
end
 
end
   
  +
--- Checks whether a language code is valid.
-- Datastore language setter to user language.
 
-- @name i18nd:useUserLang
+
-- @function _i18n.isValidCode
-- @return {self} Self instance.
+
-- @param {string} code Language code to check.
  +
-- @return {boolean} Whether the language code is valid.
function i18nd:useUserLang()
 
  +
-- @local
self.defaultLang = i18n.getLang() or self.defaultLang
 
  +
function _i18n.isValidCode(code)
return self
 
  +
return type(code) == 'string' and #mw.language.fetchLanguageName(code) ~= 0
 
end
 
end
   
  +
--- Checks whether a message contains unprocessed wikitext.
-- Datastore language setter to user language.
 
  +
-- Used to optimise message getter by not preprocessing pure text.
-- @name i18nd:useContentLang
 
-- @return {self} Self instance.
+
-- @function _i18n.isWikitext
  +
-- @param {string} msg Message to check.
function i18nd:useContentLang()
 
  +
-- @return {boolean} Whether the message contains wikitext.
self.defaultLang = mw.language.getContentLanguage():getCode()
 
  +
function _i18n.isWikitext(msg)
return self
 
  +
return
  +
type(msg) == 'string' and
  +
(
  +
msg:find('%-%-%-%-') or
  +
msg:find('%f[^\n%z][;:*#] ') or
  +
msg:find('%f[^\n%z]==* *[^\n|]+ =*=%f[\n]') or
  +
msg:find('%b<>') or msg:find('\'\'') or
  +
msg:find('%[%b[]%]') or msg:find('{%b{}}')
  +
)
 
end
 
end
   
  +
--- I18n datastore class.
-- Datastore language setter to specificed language.
 
  +
-- This is used to control language translation and access to individual
-- @name i18nd:useLang
 
  +
-- messages. The datastore instance provides language and message
-- @param {string} code Language code to use.
 
  +
-- getter-setter methods, which can be used to internationalize Lua modules.
-- @return {self} Self instance.
 
  +
-- The language methods (any ending in `Lang`) are all **chainable**.
function i18nd:useLang(code)
 
  +
-- @type Data
self.defaultLang = isValidCode(code)
 
  +
local Data = {}
and code
 
  +
Data.__index = Data
or self.defaultLang
 
return self
 
end
 
   
-- Datastore temporary language setter to user language.
+
--- Datastore message getter utility.
  +
-- This method returns localized messages from the datastore corresponding
-- @name i18nd:inUserLang
 
  +
-- to a `key`. These messages may have `$n` parameters, which can be
-- @return {self} Self instance.
 
  +
-- replaced by optional argument strings supplied by the `msg` call.
function i18nd:inUserLang()
 
  +
--
self.tempLang = i18n.getLang() or i18nd.tempLang
 
  +
-- This function supports [[Lua reference manual#named_arguments|named
return self
 
  +
-- arguments]]. The named argument syntax is more versatile despite its
end
 
  +
-- verbosity; it can be used to select message language & source(s).
 
  +
-- @function Data:msg
-- Datastore temporary language setter to user language.
 
  +
-- @usage
-- @name i18nd:inContentLang
 
  +
--
-- @return {self} Self instance.
 
  +
-- ds:msg{
function i18nd:inContentLang()
 
  +
-- key = 'message-name',
self.tempLang = mw.language.getContentLanguage():getCode()
 
  +
-- lang = '',
return self
 
  +
-- args = {...},
end
 
  +
-- sources = {}
 
  +
-- }
-- Datastore temporary language setter to specificed language.
 
  +
--
-- @name i18nd:inLang
 
  +
-- @usage
-- @param {string} code Language code to use.
 
  +
--
-- @return {self} Self instance.
 
  +
-- ds:msg('message-name', ...)
function i18nd:inLang(code)
 
  +
--
self.tempLang = isValidCode(code)
 
  +
-- @param {string|table} opts Message configuration or key.
and code
 
  +
-- @param[opt] {string} opts.key Message key to return from the
or self.tempLang
 
  +
-- datastore.
return self
 
  +
-- @param[opt] {table} opts.args Arguments to substitute into the
end
 
  +
-- message (`$n`).
 
  +
-- @param[opt] {table} opts.sources Source names to limit to (see
-- Datastore temporary source setter to specificed datastore.
 
-- @name i18nd:fromSource
+
-- `Data:fromSources`).
-- @param {string} ... Source name(s) to use.
+
-- @param[opt] {table} opts.lang Temporary language to use (see
-- @return {self} Self instance.
+
-- `Data:inLang`).
  +
-- @param[opt] {string} ... Arguments to substitute into the message
function i18nd:fromSource(...)
 
  +
-- (`$n`).
local c = select('#', ...)
 
  +
-- @error[115] {string} 'missing arguments in Data:msg'
if c ~= 0 then
 
  +
-- @return {string} Localised datastore message or `'<key>'`.
self.tempSources = {}
 
  +
function Data:msg(opts, ...)
for i = 1, c do
 
local n = select(i, ...)
 
if type(n) == 'string' and type(self._sources[n]) == 'number' then
 
self.tempSources[n] = self._sources[n]
 
end
 
end
 
end
 
return self
 
end
 
 
-- Datastore message utility.
 
-- @name i18nd:msg
 
-- @param {string|table} opts Message configuration or key.
 
-- @param[opt] {string} opts.key Message key to return.
 
-- @param[opt] {table} opts.args Arguments to substitute.
 
-- @param[opt] {table} opts.sources Source names to limit to.
 
-- @param[opt] {table} opts.lang Temporary language to use.
 
-- @param[opt] {string} ... Arguments to substitute.
 
-- @return {string} Message key or '<key>'.
 
-- @todo Better fallback system with [[Dev:Fallbacklist]].
 
function i18nd:msg(opts, ...)
 
 
local frame = mw.getCurrentFrame()
 
local frame = mw.getCurrentFrame()
 
-- Argument normalization.
 
-- Argument normalization.
 
if not self or not opts then
 
if not self or not opts then
error('missing arguments in i18nd:msg')
+
error('missing arguments in Data:msg')
 
end
 
end
 
local key = type(opts) == 'table' and opts.key or opts
 
local key = type(opts) == 'table' and opts.key or opts
Line 147: Line 148:
 
-- Handling argument substitution from Lua.
 
-- Handling argument substitution from Lua.
 
if msg and source_i[i] and #args > 0 then
 
if msg and source_i[i] and #args > 0 then
msg = handleArgs(msg, args)
+
msg = _i18n.handleArgs(msg, args)
 
end
 
end
 
if msg and source_i[i] and lang ~= 'qqx' then
 
if msg and source_i[i] and lang ~= 'qqx' then
return frame
+
return frame and _i18n.isWikitext(msg)
 
and frame:preprocess(mw.text.trim(msg))
 
and frame:preprocess(mw.text.trim(msg))
 
or mw.text.trim(msg)
 
or mw.text.trim(msg)
Line 158: Line 159:
 
end
 
end
   
  +
--- Datastore template parameter getter utility.
-- Argument substitution as $n where n > 0.
 
  +
-- This method, given a table of arguments, tries to find a parameter's
-- @param {string} msg Message to substitute arguments into.
 
  +
-- localized name in the datastore and returns its value, or nil if
-- @param {table} args Arguments table to substitute.
 
  +
-- not present.
-- @return {string} Resulting message.
 
  +
--
function handleArgs(msg, args)
 
  +
-- This method always uses the wiki's content language.
for i, a in ipairs(args) do
 
  +
-- @function Data:parameter
msg = (string.gsub(msg, '%$' .. i, a))
 
  +
-- @param {string} parameter Parameter's key in the datastore
  +
-- @param {table} args Arguments to find the parameter in
  +
-- @error[176] {string} 'missing arguments in Data:parameter'
  +
-- @return {string|nil} Parameter's value or nil if not present
  +
function Data:parameter(key, args)
  +
-- Argument normalization.
  +
if not self or not key or not args then
  +
error('missing arguments in Data:parameter')
  +
end
  +
local contentLang = mw.language.getContentLanguage():getCode()
  +
-- Message fetching.
  +
for i, messages in ipairs(self._messages) do
  +
local msg = (messages[contentLang] or {})[key]
  +
if msg ~= nil and args[msg] ~= nil then
  +
return args[msg]
  +
end
  +
for _, l in ipairs((fallbacks[contentLang] or {})) do
  +
if msg == nil or args[msg] == nil then
  +
-- Check next fallback.
  +
msg = (messages[l] or {})[key]
  +
else
  +
-- A localized message was found.
  +
return args[msg]
  +
end
  +
end
  +
-- Fallback to English.
  +
msg = messages.en[key]
  +
if msg ~= nil and args[msg] ~= nil then
  +
return args[msg]
  +
end
 
end
 
end
return msg
 
 
end
 
end
   
  +
--- Datastore temporary source setter to a specificed subset of datastores.
-- Checks whether a language code is valid.
 
  +
-- By default, messages are fetched from the datastore in the same
-- @param {string} code Language code to check
 
  +
-- order of priority as `i18n.loadMessages`.
-- @return {bool} Whether the language code is valid.
 
function isValidCode(code)
+
-- @function Data:fromSource
  +
-- @param {string} ... Source name(s) to use.
return type(code) == 'string' and #mw.language.fetchLanguageName(code) ~= 0
 
  +
-- @return {Data} Datastore instance.
  +
function Data:fromSource(...)
  +
local c = select('#', ...)
  +
if c ~= 0 then
  +
self.tempSources = {}
  +
for i = 1, c do
  +
local n = select(i, ...)
  +
if type(n) == 'string' and type(self._sources[n]) == 'number' then
  +
self.tempSources[n] = self._sources[n]
  +
end
  +
end
  +
end
  +
return self
 
end
 
end
   
  +
--- Datastore default language getter.
-- Language code function.
 
-- @usage {{#invoke:i18n|getLang}}
+
-- @function Data:getLang
-- @return {string} code Language code.
+
-- @return {string} Default language to serve datastore messages in.
function i18n.getLang()
+
function Data:getLang()
  +
return self.defaultLang
local code = mw.language.getContentLanguage():getCode()
 
local frame = mw.getCurrentFrame()
 
local subPage = title.subpageText
 
local uselang
 
-- Language argument test.
 
if isValidCode( ( (frame or {}).args or {}).uselang or '') then
 
code = frame.args.uselang
 
elseif isValidCode( ( (frame and frame.getParent(frame) or {}).args or {}).uselang or '') then
 
code = frame:getParent().args.uselang
 
-- Subpage language test.
 
elseif title.isSubpage and isValidCode(subPage) then
 
code = isValidCode(subPage) and subPage or code
 
-- User language test.
 
elseif frame then
 
uselang = frame:preprocess('{{int:lang}}')
 
code = mw.text.decode(uselang) == '<lang>'
 
and code
 
or uselang
 
end
 
return code
 
 
end
 
end
   
  +
--- Datastore language setter to `wgUserLanguage`.
-- I18n message datastore loader.
 
  +
-- @function Data:useUserLang
-- @param {string} source ROOTPAGENAME/path of target i18n submodule.
 
  +
-- @return {Data} Datastore instance.
-- @usage require('Dev:Install').loadMessages(source)
 
  +
-- @note Scribunto only registers `wgUserLanguage` when an
-- @raise 'no source supplied to i18n.loadMessages'
 
  +
-- invocation is at the top of the call stack.
-- @return {table} i18n I18n datastore instance.
 
  +
function Data:useUserLang()
  +
self.defaultLang = i18n.getLang() or self.defaultLang
  +
return self
  +
end
  +
  +
--- Datastore language setter to `wgContentLanguage`.
  +
-- @function Data:useContentLang
  +
-- @return {Data} Datastore instance.
  +
function Data:useContentLang()
  +
self.defaultLang = mw.language.getContentLanguage():getCode()
  +
return self
  +
end
  +
  +
--- Datastore language setter to specificed language.
  +
-- @function Data:useLang
  +
-- @param {string} code Language code to use.
  +
-- @return {Data} Datastore instance.
  +
function Data:useLang(code)
  +
self.defaultLang = _i18n.isValidCode(code)
  +
and code
  +
or self.defaultLang
  +
return self
  +
end
  +
  +
--- Temporary datastore language setter to `wgUserLanguage`.
  +
-- The datastore language reverts to the default language in the next
  +
-- @{Data:msg} call.
  +
-- @function Data:inUserLang
  +
-- @return {Data} Datastore instance.
  +
function Data:inUserLang()
  +
self.tempLang = i18n.getLang() or self.tempLang
  +
return self
  +
end
  +
  +
--- Temporary datastore language setter to `wgContentLanguage`.
  +
-- Only affects the next @{Data:msg} call.
  +
-- @function Data:inContentLang
  +
-- @return {Data} Datastore instance.
  +
function Data:inContentLang()
  +
self.tempLang = mw.language.getContentLanguage():getCode()
  +
return self
  +
end
  +
  +
--- Temporary datastore language setter to a specificed language.
  +
-- Only affects the next @{Data:msg} call.
  +
-- @function Data:inLang
  +
-- @param {string} code Language code to use.
  +
-- @return {Data} Datastore instance.
  +
function Data:inLang(code)
  +
self.tempLang = _i18n.isValidCode(code)
  +
and code
  +
or self.tempLang
  +
return self
  +
end
  +
  +
-- Package functions.
  +
  +
--- Localized message getter by key.
  +
-- Can be used to fetch messages in a specific language code through `uselang`
  +
-- parameter. Extra numbered parameters can be supplied for substitution into
  +
-- the datastore message.
  +
-- @function i18n.getMsg
  +
-- @param {table} frame Frame table from invocation.
  +
-- @param {table} frame.args Metatable containing arguments.
  +
-- @param {string} frame.args[1] ROOTPAGENAME of i18n submodule.
  +
-- @param {string} frame.args[2] Key of i18n message.
  +
-- @param[opt] {string} frame.args.lang Default language of message.
  +
-- @error[271] 'missing arguments in i18n.getMsg'
  +
-- @return {string} I18n message in localised language.
  +
-- @usage {{i18n|getMsg|source|key|arg1|arg2|uselang {{=}} code}}
  +
function i18n.getMsg(frame)
  +
if
  +
not frame or
  +
not frame.args or
  +
not frame.args[1] or
  +
not frame.args[2]
  +
then
  +
error('missing arguments in i18n.getMsg')
  +
end
  +
local source = frame.args[1]
  +
local key = frame.args[2]
  +
-- Pass through extra arguments.
  +
local repl = {}
  +
for i, a in ipairs(frame.args) do
  +
if i >= 3 then
  +
repl[i-2] = a
  +
end
  +
end
  +
-- Load message data.
  +
local ds = i18n.loadMessages(source)
  +
-- Pass through language argument.
  +
ds:inLang(frame.args.uselang)
  +
-- Return message.
  +
return ds:msg { key = key, args = repl }
  +
end
  +
  +
--- I18n message datastore loader.
  +
-- @function i18n.loadMessages
  +
-- @param {string} ... ROOTPAGENAME/path for target i18n
  +
-- submodules.
  +
-- @error[322] {string} 'no source supplied to i18n.loadMessages'
  +
-- @return {table} I18n datastore instance.
  +
-- @usage require('Dev:I18n').loadMessages('1', '2')
 
function i18n.loadMessages(...)
 
function i18n.loadMessages(...)
 
local ds
 
local ds
Line 219: Line 346:
 
-- Instantiate datastore.
 
-- Instantiate datastore.
 
ds = {}
 
ds = {}
i18nd.__index = i18nd
 
setmetatable(ds, i18nd)
 
-- Set default language.
 
ds.defaultLang = i18n.getLang()
 
 
ds._messages = {}
 
ds._messages = {}
  +
-- Set default language.
  +
setmetatable(ds, Data)
  +
ds:useUserLang()
 
end
 
end
 
source = string.gsub(source, '^.', mw.ustring.upper)
 
source = string.gsub(source, '^.', mw.ustring.upper)
Line 242: Line 368:
 
end
 
end
   
  +
--- Language code getter.
-- I18n message function.
 
  +
-- Can validate a template's language code through `uselang` parameter.
-- @param {table} frame Frame table from invocation.
 
-- @param {string} frame.args[1] ROOTPAGENAME of i18n submodule.
+
-- @function i18n.getLang
-- @param {string} frame.args[2] Key of i18n message.
+
-- @usage {{i18n|getLang|uselang {{=}} code}}
  +
-- @return {string} Language code.
-- @param {string} frame.args.lang Default language of message (optional).
 
  +
function i18n.getLang()
-- @usage {{#invoke:i18n|getMsg|source|key}}
 
  +
local frame = mw.getCurrentFrame() or {}
-- @raise 'missing arguments in i18n.getMsg'
 
  +
local parentFrame = frame.getParent and frame:getParent() or {}
-- @return {string} msg I18n message in localised language.
 
  +
function i18n.getMsg(frame)
 
  +
local code = mw.language.getContentLanguage():getCode()
if
 
  +
local subPage = title.subpageText
not frame or
 
  +
not frame.args or
 
  +
-- Language argument test.
not frame.args[1] or
 
  +
local langOverride =
not frame.args[2]
 
  +
(frame.args or {}).uselang or
then
 
  +
(parentFrame.args or {}).uselang
error('missing arguments in i18n.getMsg')
 
  +
if _i18n.isValidCode(langOverride) then
end
 
local source = frame.args[1]
+
code = langOverride
  +
local key = frame.args[2]
 
-- Pass through extra arguments.
+
-- Subpage language test.
  +
elseif title.isSubpage and _i18n.isValidCode(subPage) then
local repl = {}
 
  +
code = _i18n.isValidCode(subPage) and subPage or code
for i, a in ipairs(frame.args) do
 
  +
if i >= 3 then
 
  +
-- User language test.
repl[i-2] = a
 
  +
elseif parentFrame.preprocess or frame.preprocess then
  +
uselang = uselang
  +
or parentFrame.preprocess
  +
and parentFrame:preprocess('{{int:lang}}')
  +
or frame:preprocess('{{int:lang}}')
  +
local decodedLang = mw.text.decode(uselang)
  +
if decodedLang ~= '<lang>' and decodedLang ~= '⧼lang⧽' then
  +
code = decodedLang == '(lang)'
  +
and 'qqx'
  +
or uselang
 
end
 
end
 
end
 
end
  +
-- Load message data.
 
  +
return code
local i18nd = i18n.loadMessages(source)
 
-- Pass through language argument.
 
i18nd:inLang(frame.args.lang)
 
-- Return message.
 
return i18nd:msg { key = key, args = repl, lang = lang }
 
 
end
 
end
   
-- Template wrapper for [[Template:I18n]].
+
--- Template wrapper for [[Template:I18n]].
-- @param {table} frameChild Frame invocation object.
+
-- @function i18n.main
-- @usage {{#invoke:i18n|main}}
+
-- @param {table} frame Frame invocation object.
  +
-- @return {string} Module output in template context.
function i18n.main(frameChild)
 
  +
-- @usage {{#invoke:i18n|main}}
local frame = frameChild:getParent()
 
  +
i18n.main = entrypoint(i18n)
local args = {}
 
for p, v in pairs(frame.args) do args[p] = v end
 
-- Extract function name as first argument.
 
local fn_name = args[1] and table.remove(args, 1)
 
-- Check for function argument.
 
if fn_name == nil then
 
error((mw.ustring.gsub(mw.ustring.match(mw.message.new('scribunto-common-nofunction'):plain(), ':%s(.*)%p$'), '^.', mw.ustring.lower)))
 
end
 
-- Check function exists.
 
if i18n[fn_name] == nil then
 
error((mw.ustring.gsub(mw.ustring.match(mw.message.new('scribunto-common-nosuchfunction'):plain(), ':%s(.*)%p$'), '^.', mw.ustring.lower)))
 
end
 
-- Execute function if it does.
 
frame.args = args
 
return i18n[fn_name](frame)
 
end
 
   
 
return i18n
 
return i18n

Latest revision as of 17:54, 27 September 2022

--- I18n library for message storage in Lua datastores.
--  The module is designed to enable message separation from modules &
--  templates. It has support for handling language fallbacks. This
--  module is a Lua port of [[I18n-js]] and i18n modules that can be loaded
--  by it are editable through [[I18nEdit]].
--  
--  @module         i18n
--  @version        1.4.0
--  @require        Module:Entrypoint
--  @require        Module:Fallbacklist
--  @author         [[User:KockaAdmiralac|KockaAdmiralac]]
--  @author         [[User:Speedit|Speedit]]
--  @attribution    [[User:Cqm|Cqm]]
--  @release        stable
--  @see            [[I18n|I18n guide]]
--  @see            [[I18n-js]]
--  @see            [[I18nEdit]]
--  <nowiki>
local i18n, _i18n = {}, {}

--  Module variables & dependencies.
local title = mw.title.getCurrentTitle()
local fallbacks = require('Dev:Fallbacklist')
local entrypoint = require('Dev:Entrypoint')
local uselang

--- Argument substitution as $n where n > 0.
--  @function           _i18n.handleArgs
--  @param              {string} msg Message to substitute arguments into.
--  @param              {table} args Arguments table to substitute.
--  @return             {string} Resulting message.
--  @local
function _i18n.handleArgs(msg, args)
    for i, a in ipairs(args) do
        msg = (string.gsub(msg, '%$' .. tostring(i), tostring(a)))
    end
    return msg
end

--- Checks whether a language code is valid.
--  @function           _i18n.isValidCode
--  @param              {string} code Language code to check.
--  @return             {boolean} Whether the language code is valid.
--  @local
function _i18n.isValidCode(code)
    return type(code) == 'string' and #mw.language.fetchLanguageName(code) ~= 0
end

--- Checks whether a message contains unprocessed wikitext.
--  Used to optimise message getter by not preprocessing pure text.
--  @function           _i18n.isWikitext
--  @param              {string} msg Message to check.
--  @return             {boolean} Whether the message contains wikitext.
function _i18n.isWikitext(msg)
    return
        type(msg) == 'string' and
        (
            msg:find('%-%-%-%-') or
            msg:find('%f[^\n%z][;:*#] ') or
            msg:find('%f[^\n%z]==* *[^\n|]+ =*=%f[\n]') or
            msg:find('%b<>') or msg:find('\'\'') or
            msg:find('%[%b[]%]') or msg:find('{%b{}}')
        )
end

--- I18n datastore class.
--  This is used to control language translation and access to individual
--  messages. The datastore instance provides language and message
--  getter-setter methods, which can be used to internationalize Lua modules.
--  The language methods (any ending in `Lang`) are all **chainable**.
--  @type            Data
local Data = {}
Data.__index = Data

--- Datastore message getter utility.
--  This method returns localized messages from the datastore corresponding
--  to a `key`. These messages may have `$n` parameters, which can be
--  replaced by optional argument strings supplied by the `msg` call.
--  
--  This function supports [[Lua reference manual#named_arguments|named
--  arguments]]. The named argument syntax is more versatile despite its
--  verbosity; it can be used to select message language & source(s).
--  @function           Data:msg
--  @usage
--  
--      ds:msg{
--          key = 'message-name',
--          lang = '',
--          args = {...},
--          sources = {}
--      }
--  
--  @usage
--  
--      ds:msg('message-name', ...)
--  
--  @param              {string|table} opts Message configuration or key.
--  @param[opt]         {string} opts.key Message key to return from the
--                      datastore.
--  @param[opt]         {table} opts.args Arguments to substitute into the
--                      message (`$n`).
--  @param[opt]         {table} opts.sources Source names to limit to (see
--                      `Data:fromSources`).
--  @param[opt]         {table} opts.lang Temporary language to use (see
--                      `Data:inLang`).
--  @param[opt]         {string} ... Arguments to substitute into the message
--                      (`$n`).
--  @error[115]         {string} 'missing arguments in Data:msg'
--  @return             {string} Localised datastore message or `'<key>'`.
function Data:msg(opts, ...)
    local frame = mw.getCurrentFrame()
    -- Argument normalization.
    if not self or not opts then
        error('missing arguments in Data:msg')
    end
    local key = type(opts) == 'table' and opts.key or opts
    local args = opts.args or {...}
    -- Configuration parameters.
    if opts.sources then
        self:fromSources(unpack(opts.sources))
    end
    if opts.lang then
        self:inLang(opts.lang)
    end
    -- Source handling.
    local source_n = self.tempSources or self._sources
    local source_i = {}
    for n, i in pairs(source_n) do
        source_i[i] = n
    end
    self.tempSources = nil
    -- Language handling.
    local lang = self.tempLang or self.defaultLang
    self.tempLang = nil
    -- Message fetching.
    local msg
    for i, messages in ipairs(self._messages) do
        -- Message data.
        local msg = (messages[lang] or {})[key]
        -- Fallback support (experimental).
        for _, l in ipairs((fallbacks[lang] or {})) do
            if msg == nil then
                msg = (messages[l] or {})[key]
            end
        end
        -- Internal fallback to 'en'.
        msg = msg ~= nil and msg or messages.en[key]
        -- Handling argument substitution from Lua.
        if msg and source_i[i] and #args > 0 then
            msg = _i18n.handleArgs(msg, args)
        end
        if msg and source_i[i] and lang ~= 'qqx' then
            return frame and _i18n.isWikitext(msg)
                and frame:preprocess(mw.text.trim(msg))
                or  mw.text.trim(msg)
        end
    end
    return mw.text.nowiki('<' .. key .. '>')
end

--- Datastore template parameter getter utility.
--  This method, given a table of arguments, tries to find a parameter's
--  localized name in the datastore and returns its value, or nil if
--  not present.
--
--  This method always uses the wiki's content language.
--  @function           Data:parameter
--  @param              {string} parameter Parameter's key in the datastore
--  @param              {table} args Arguments to find the parameter in
--  @error[176]         {string} 'missing arguments in Data:parameter'
--  @return             {string|nil} Parameter's value or nil if not present
function Data:parameter(key, args)
    -- Argument normalization.
    if not self or not key or not args then
        error('missing arguments in Data:parameter')
    end
    local contentLang = mw.language.getContentLanguage():getCode()
    -- Message fetching.
    for i, messages in ipairs(self._messages) do
        local msg = (messages[contentLang] or {})[key]
        if msg ~= nil and args[msg] ~= nil then
            return args[msg]
        end
        for _, l in ipairs((fallbacks[contentLang] or {})) do
            if msg == nil or args[msg] == nil then
                -- Check next fallback.
                msg = (messages[l] or {})[key]
            else
                -- A localized message was found.
                return args[msg]
            end
        end
        -- Fallback to English.
        msg = messages.en[key]
        if msg ~= nil and args[msg] ~= nil then
            return args[msg]
        end
    end
end

--- Datastore temporary source setter to a specificed subset of datastores.
--  By default, messages are fetched from the datastore in the same
--  order of priority as `i18n.loadMessages`.
--  @function           Data:fromSource
--  @param              {string} ... Source name(s) to use.
--  @return             {Data} Datastore instance.
function Data:fromSource(...)
    local c = select('#', ...)
    if c ~= 0 then
        self.tempSources = {}
        for i = 1, c do
            local n = select(i, ...)
            if type(n) == 'string' and type(self._sources[n]) == 'number' then
                self.tempSources[n] = self._sources[n]
            end
        end
    end
    return self
end

--- Datastore default language getter.
--  @function           Data:getLang
--  @return             {string} Default language to serve datastore messages in.
function Data:getLang()
    return self.defaultLang
end

--- Datastore language setter to `wgUserLanguage`.
--  @function           Data:useUserLang
--  @return             {Data} Datastore instance.
--  @note               Scribunto only registers `wgUserLanguage` when an
--                      invocation is at the top of the call stack.
function Data:useUserLang()
    self.defaultLang = i18n.getLang() or self.defaultLang
    return self
end

--- Datastore language setter to `wgContentLanguage`.
--  @function           Data:useContentLang
--  @return             {Data} Datastore instance.
function Data:useContentLang()
    self.defaultLang = mw.language.getContentLanguage():getCode()
    return self
end

--- Datastore language setter to specificed language.
--  @function           Data:useLang
--  @param              {string} code Language code to use.
--  @return             {Data} Datastore instance.
function Data:useLang(code)
    self.defaultLang = _i18n.isValidCode(code)
        and code
        or  self.defaultLang
    return self
end

--- Temporary datastore language setter to `wgUserLanguage`.
--  The datastore language reverts to the default language in the next
--  @{Data:msg} call.
--  @function           Data:inUserLang
--  @return             {Data} Datastore instance.
function Data:inUserLang()
    self.tempLang = i18n.getLang() or self.tempLang
    return self
end

--- Temporary datastore language setter to `wgContentLanguage`.
--  Only affects the next @{Data:msg} call.
--  @function           Data:inContentLang
--  @return             {Data} Datastore instance.
function Data:inContentLang()
    self.tempLang = mw.language.getContentLanguage():getCode()
    return self
end

--- Temporary datastore language setter to a specificed language.
--  Only affects the next @{Data:msg} call.
--  @function           Data:inLang
--  @param              {string} code Language code to use.
--  @return             {Data} Datastore instance.
function Data:inLang(code)
    self.tempLang = _i18n.isValidCode(code)
        and code
        or  self.tempLang
    return self
end

--  Package functions.

--- Localized message getter by key.
--  Can be used to fetch messages in a specific language code through `uselang`
--  parameter. Extra numbered parameters can be supplied for substitution into
--  the datastore message.
--  @function           i18n.getMsg
--  @param              {table} frame Frame table from invocation.
--  @param              {table} frame.args Metatable containing arguments.
--  @param              {string} frame.args[1] ROOTPAGENAME of i18n submodule.
--  @param              {string} frame.args[2] Key of i18n message.
--  @param[opt]         {string} frame.args.lang Default language of message.
--  @error[271]         'missing arguments in i18n.getMsg'
--  @return             {string} I18n message in localised language.
--  @usage              {{i18n|getMsg|source|key|arg1|arg2|uselang {{=}} code}}
function i18n.getMsg(frame)
    if
        not frame or
        not frame.args or
        not frame.args[1] or
        not frame.args[2]
    then
        error('missing arguments in i18n.getMsg')
    end
    local source = frame.args[1]
    local key = frame.args[2]
    -- Pass through extra arguments.
    local repl = {}
    for i, a in ipairs(frame.args) do
        if i >= 3 then
            repl[i-2] = a
        end
    end
    -- Load message data.
    local ds = i18n.loadMessages(source)
    -- Pass through language argument.
    ds:inLang(frame.args.uselang)
    -- Return message.
    return ds:msg { key = key, args = repl }
end
 
--- I18n message datastore loader.
--  @function           i18n.loadMessages
--  @param              {string} ... ROOTPAGENAME/path for target i18n
--                      submodules.
--  @error[322]         {string} 'no source supplied to i18n.loadMessages'
--  @return             {table} I18n datastore instance.
--  @usage              require('Dev:I18n').loadMessages('1', '2')
function i18n.loadMessages(...)
    local ds
    local i = 0
    local s = {}
    for j = 1, select('#', ...) do
        local source = select(j, ...)
        if type(source) == 'string' and source ~= '' then
            i = i + 1
            s[source] = i
            if not ds then
                -- Instantiate datastore.
                ds = {}
                ds._messages = {}
                -- Set default language.
                setmetatable(ds, Data)
                ds:useUserLang()
            end
            source = string.gsub(source, '^.', mw.ustring.upper)
            source = mw.ustring.find(source, ':')
                and source
                or  'Dev:' .. source .. '/i18n'
            ds._messages[i] = mw.loadData(source)
        end
    end
    if not ds then
        error('no source supplied to i18n.loadMessages')
    else
        -- Attach source index map.
        ds._sources = s
        -- Return datastore instance.
        return ds
    end
end

--- Language code getter.
--  Can validate a template's language code through `uselang` parameter.
--  @function           i18n.getLang
--  @usage              {{i18n|getLang|uselang {{=}} code}}
--  @return             {string} Language code.
function i18n.getLang()
    local frame = mw.getCurrentFrame() or {}
    local parentFrame = frame.getParent and frame:getParent() or {}

    local code = mw.language.getContentLanguage():getCode()
    local subPage = title.subpageText

    -- Language argument test.
    local langOverride =
        (frame.args or {}).uselang or
        (parentFrame.args or {}).uselang
    if _i18n.isValidCode(langOverride) then
        code = langOverride

    -- Subpage language test.
    elseif title.isSubpage and _i18n.isValidCode(subPage) then
        code = _i18n.isValidCode(subPage) and subPage or code

    -- User language test.
    elseif parentFrame.preprocess or frame.preprocess then
        uselang = uselang
            or  parentFrame.preprocess
                and parentFrame:preprocess('{{int:lang}}')
                or  frame:preprocess('{{int:lang}}')
        local decodedLang = mw.text.decode(uselang) 
        if decodedLang ~= '<lang>' and decodedLang ~= '⧼lang⧽' then
            code = decodedLang == '(lang)'
                and 'qqx'
                or  uselang
        end
    end

    return code
end

--- Template wrapper for [[Template:I18n]].
--  @function           i18n.main
--  @param              {table} frame Frame invocation object.
--  @return             {string} Module output in template context.
--  @usage              {{#invoke:i18n|main}}
i18n.main = entrypoint(i18n)

return i18n
-- </nowiki>