Jump to content

Module:I18n: Difference between revisions

From Wikipedia, the free encyclopedia
Content deleted Content added
No edit summary
convert indentation spaces to tabs
Line 9: Line 9:
-- Wikimedia Commons.
-- Wikimedia Commons.
--
--
-- @module i18n
-- @module i18n
-- @version 1.4.0
-- @version 1.4.0
-- @require Module:Entrypoint
-- @require Module:Entrypoint
-- @require Module:Fallbacklist
-- @require Module:Fallbacklist
-- @author [[wikia:dev:User:KockaAdmiralac|KockaAdmiralac]] (original Fandom implementation)
-- @author [[wikia:dev:User:KockaAdmiralac|KockaAdmiralac]] (original Fandom implementation)
-- @author [[wikia:dev:User:Speedit|Speedit]] (original Fandom implementation)
-- @author [[wikia:dev:User:Speedit|Speedit]] (original Fandom implementation)
-- @author [[User:Awesome Aasim|Awesome Aasim]] (Wikimedia port)
-- @author [[User:Awesome Aasim|Awesome Aasim]] (Wikimedia port)
-- @attribution [[wikia:dev:User:Cqm|Cqm]]
-- @attribution [[wikia:dev:User:Cqm|Cqm]]
-- @release beta
-- @release beta
-- @see [[wikia:dev:I18n|I18n guide]]
-- @see [[wikia:dev:I18n|I18n guide]]
-- @see [[wikia:dev:I18n-js]]
-- @see [[wikia:dev:I18n-js]]
-- @see [[wikia:dev:I18nEdit]]
-- @see [[wikia:dev:I18nEdit]]
-- <nowiki>
-- <nowiki>
local i18n, _i18n = {}, {}
local i18n, _i18n = {}, {}
Line 31: Line 31:


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


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


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


Line 74: Line 74:
-- getter-setter methods, which can be used to internationalize Lua modules.
-- getter-setter methods, which can be used to internationalize Lua modules.
-- The language methods (any ending in `Lang`) are all **chainable**.
-- The language methods (any ending in `Lang`) are all **chainable**.
-- @type Data
-- @type Data
local Data = {}
local Data = {}
Data.__index = Data
Data.__index = Data
Line 86: Line 86:
-- arguments]]. The named argument syntax is more versatile despite its
-- arguments]]. The named argument syntax is more versatile despite its
-- verbosity; it can be used to select message language & source(s).
-- verbosity; it can be used to select message language & source(s).
-- @function Data:msg
-- @function Data:msg
-- @usage
-- @usage
--
--
-- ds:msg{
-- ds:msg{
-- key = 'message-name',
-- key = 'message-name',
-- lang = '',
-- lang = '',
-- args = {...},
-- args = {...},
-- sources = {}
-- sources = {}
-- }
-- }
--
--
-- @usage
-- @usage
--
--
-- ds:msg('message-name', ...)
-- ds:msg('message-name', ...)
--
--
-- @param {string|table} opts Message configuration or key.
-- @param {string|table} opts Message configuration or key.
-- @param[opt] {string} opts.key Message key to return from the
-- @param[opt] {string} opts.key Message key to return from the
-- datastore.
-- datastore.
-- @param[opt] {table} opts.args Arguments to substitute into the
-- @param[opt] {table} opts.args Arguments to substitute into the
-- message (`$n`).
-- message (`$n`).
-- @param[opt] {table} opts.sources Source names to limit to (see
-- @param[opt] {table} opts.sources Source names to limit to (see
-- `Data:fromSources`).
-- `Data:fromSources`).
-- @param[opt] {table} opts.lang Temporary language to use (see
-- @param[opt] {table} opts.lang Temporary language to use (see
-- `Data:inLang`).
-- `Data:inLang`).
-- @param[opt] {string} ... Arguments to substitute into the message
-- @param[opt] {string} ... Arguments to substitute into the message
-- (`$n`).
-- (`$n`).
-- @error[115] {string} 'missing arguments in Data:msg'
-- @error[115] {string} 'missing arguments in Data:msg'
-- @return {string} Localised datastore message or `'<key>'`.
-- @return {string} Localised datastore message or `'<key>'`.
function Data:msg(opts, ...)
function Data: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 Data: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
local args = opts.args or {...}
local args = opts.args or {...}
-- Configuration parameters.
-- Configuration parameters.
if opts.sources then
if opts.sources then
self:fromSources(unpack(opts.sources))
self:fromSources(unpack(opts.sources))
end
end
if opts.lang then
if opts.lang then
self:inLang(opts.lang)
self:inLang(opts.lang)
end
end
-- Source handling.
-- Source handling.
local source_n = self.tempSources or self._sources
local source_n = self.tempSources or self._sources
local source_i = {}
local source_i = {}
for n, i in pairs(source_n) do
for n, i in pairs(source_n) do
source_i[i] = n
source_i[i] = n
end
end
self.tempSources = nil
self.tempSources = nil
-- Language handling.
-- Language handling.
local lang = self.tempLang or self.defaultLang
local lang = self.tempLang or self.defaultLang
self.tempLang = nil
self.tempLang = nil
-- Message fetching.
-- Message fetching.
local msg
local msg
for i, messages in ipairs(self._messages) do
for i, messages in ipairs(self._messages) do
-- Message data.
-- Message data.
local msg = (messages[lang] or {})[key]
local msg = (messages[lang] or {})[key]
-- Fallback support (experimental).
-- Fallback support (experimental).
for _, l in ipairs((fallbacks[lang] or {})) do
for _, l in ipairs((fallbacks[lang] or {})) do
if msg == nil then
if msg == nil then
msg = (messages[l] or {})[key]
msg = (messages[l] or {})[key]
end
end
end
end
-- Internal fallback to 'en'.
-- Internal fallback to 'en'.
msg = msg ~= nil and msg or messages.en[key]
msg = msg ~= nil and msg or messages.en[key]
-- 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 = _i18n.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 and _i18n.isWikitext(msg)
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)
end
end
end
end
return mw.text.nowiki('&#x29FC;' .. key .. '&#x29FD;')
return mw.text.nowiki('&#x29FC;' .. key .. '&#x29FD;')
end
end


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


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


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


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


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


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


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


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


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


Line 297: Line 297:
-- parameter. Extra numbered parameters can be supplied for substitution into
-- parameter. Extra numbered parameters can be supplied for substitution into
-- the datastore message.
-- the datastore message.
-- @function i18n.getMsg
-- @function i18n.getMsg
-- @param {table} frame Frame table from invocation.
-- @param {table} frame Frame table from invocation.
-- @param {table} frame.args Metatable containing arguments.
-- @param {table} frame.args Metatable containing arguments.
-- @param {string} frame.args[1] ROOTPAGENAME of i18n submodule.
-- @param {string} frame.args[1] ROOTPAGENAME of i18n submodule.
-- @param {string} frame.args[2] Key of i18n message.
-- @param {string} frame.args[2] Key of i18n message.
-- @param[opt] {string} frame.args.lang Default language of message.
-- @param[opt] {string} frame.args.lang Default language of message.
-- @error[271] 'missing arguments in i18n.getMsg'
-- @error[271] 'missing arguments in i18n.getMsg'
-- @return {string} I18n message in localised language.
-- @return {string} I18n message in localised language.
function i18n.getMsg(frame)
function i18n.getMsg(frame)
if
if
not frame or
not frame or
not frame.args or
not frame.args or
not frame.args[1] or
not frame.args[1] or
not frame.args[2]
not frame.args[2]
then
then
error('missing arguments in i18n.getMsg')
error('missing arguments in i18n.getMsg')
end
end
local source = frame.args[1]
local source = frame.args[1]
local key = frame.args[2]
local key = frame.args[2]
-- Pass through extra arguments.
-- Pass through extra arguments.
local repl = {}
local repl = {}
for i, a in ipairs(frame.args) do
for i, a in ipairs(frame.args) do
if i >= 3 then
if i >= 3 then
repl[i-2] = a
repl[i-2] = a
end
end
end
end
-- Load message data.
-- Load message data.
local ds = i18n.loadMessages(source)
local ds = i18n.loadMessages(source)
-- Pass through language argument.
-- Pass through language argument.
ds:inLang(frame.args.uselang)
ds:inLang(frame.args.uselang)
-- Return message.
-- Return message.
return ds:msg { key = key, args = repl }
return ds:msg { key = key, args = repl }
end
end
--- I18n message datastore loader.
--- I18n message datastore loader.
-- @function i18n.loadMessages
-- @function i18n.loadMessages
-- @param {string} ... ROOTPAGENAME/path for target i18n
-- @param {string} ... ROOTPAGENAME/path for target i18n
-- submodules.
-- submodules.
-- @error[322] {string} 'no source supplied to i18n.loadMessages'
-- @error[322] {string} 'no source supplied to i18n.loadMessages'
-- @return {table} I18n datastore instance.
-- @return {table} I18n datastore instance.
-- @usage require('Module:I18n').loadMessages('1', '2')
-- @usage require('Module:I18n').loadMessages('1', '2')
function i18n.loadMessages(...)
function i18n.loadMessages(...)
local ds
local ds
local i = 0
local i = 0
local s = {}
local s = {}
for j = 1, select('#', ...) do
for j = 1, select('#', ...) do
local source = select(j, ...)
local source = select(j, ...)
if type(source) == 'string' and source ~= '' then
if type(source) == 'string' and source ~= '' then
i = i + 1
i = i + 1
s[source] = i
s[source] = i
if not ds then
if not ds then
-- Instantiate datastore.
-- Instantiate datastore.
ds = {}
ds = {}
ds._messages = {}
ds._messages = {}
-- Set default language.
-- Set default language.
setmetatable(ds, Data)
setmetatable(ds, Data)
ds:useUserLang()
ds:useUserLang()
end
end
source = string.gsub(source, '^.', mw.ustring.upper)
source = string.gsub(source, '^.', mw.ustring.upper)
local success, messages = pcall(mw.loadData, mw.ustring.find(source, ':')
local success, messages = pcall(mw.loadData, mw.ustring.find(source, ':')
and source
and source
or 'Module:' .. source .. '/i18n')
or 'Module:' .. source .. '/i18n')
if success then
if success then
ds._messages[i] = messages
ds._messages[i] = messages
else
else
local T = {}
local T = {}
local tab = mw.ext.data.get('I18n/' .. source .. '.tab', '_')
local tab = mw.ext.data.get('I18n/' .. source .. '.tab', '_')
if not tab then error("i18n for " .. source .. " is missing") end
if not tab then error("i18n for " .. source .. " is missing") end
for _, row in pairs(tab.data) do -- convert the output into a dictionary table
for _, row in pairs(tab.data) do -- convert the output into a dictionary table
local id, t = unpack(row)
local id, t = unpack(row)
Line 373: Line 373:
end
end
ds._messages[i] = T
ds._messages[i] = T
end
end
end
end
end
end
if not ds then
if not ds then
error('no source supplied to i18n.loadMessages')
error('no source supplied to i18n.loadMessages')
else
else
-- Attach source index map.
-- Attach source index map.
ds._sources = s
ds._sources = s
-- Return datastore instance.
-- Return datastore instance.
return ds
return ds
end
end
end
end


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


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


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


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


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


return code
return code
end
end


Line 465: Line 465:


--- Wrapper for the module.
--- Wrapper for the module.
-- @function i18n.main
-- @function i18n.main
-- @param {table} frame Frame invocation object.
-- @param {table} frame Frame invocation object.
-- @return {string} Module output in template context.
-- @return {string} Module output in template context.
-- @usage {{#invoke:i18n|main}}
-- @usage {{#invoke:i18n|main}}
i18n.main = entrypoint(i18n)
i18n.main = entrypoint(i18n)



Revision as of 00:10, 21 March 2025

--- 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 [[wikia:dev:I18n-js]] and i18n modules that can be loaded
--  by it are editable through [[wikia:dev:I18nEdit]].
--  
--  On Wikimedia projects, i18n messages are editable 
--  through [[c:Special:PrefixIndex/Data:i18n/|Data:i18n/]] subpages on
--  Wikimedia Commons.
--  
--  @module		 i18n
--  @version		1.4.0
--  @require		Module:Entrypoint
--  @require		Module:Fallbacklist
--  @author		 [[wikia:dev:User:KockaAdmiralac|KockaAdmiralac]] (original Fandom implementation)
--  @author		 [[wikia:dev:User:Speedit|Speedit]] (original Fandom implementation)
--  @author			[[User:Awesome Aasim|Awesome Aasim]] (Wikimedia port)
--  @attribution	[[wikia:dev:User:Cqm|Cqm]]
--  @release		beta
--  @see			[[wikia:dev:I18n|I18n guide]]
--  @see			[[wikia:dev:I18n-js]]
--  @see			[[wikia:dev:I18nEdit]]
--  <nowiki>
local i18n, _i18n = {}, {}

--  Module variables & dependencies.
local title = mw.title.getCurrentTitle()
local fallbacks = require('Module:Fallbacklist')
local entrypoint = require('Module: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 [[mw:Extension:Scribunto/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('&#x29FC;' .. key .. '&#x29FD;')
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.
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('Module: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)
			local success, messages = pcall(mw.loadData, mw.ustring.find(source, ':')
				and source
				or  'Module:' .. source .. '/i18n')
			if success then
				ds._messages[i] = messages
			else
				local T = {}
				local tab = mw.ext.data.get('I18n/' .. source .. '.tab', '_')
				if not tab then error("i18n for " .. source .. " is missing") end
				for _, row in pairs(tab.data) do -- convert the output into a dictionary table
					local id, t = unpack(row)
					for lang, msg in pairs(t) do
						if not T[lang] then T[lang] = {} end
						T[lang][id] = msg
					end
				end
				ds._messages[i] = T
			end
		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
--  @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

-- Credit to http://stackoverflow.com/a/1283608/2644759
-- cc-by-sa 3.0
local function tableMerge(t1, t2, overwrite)
	for k,v in pairs(t2) do
		if type(v) == "table" and type(t1[k]) == "table" then
			-- since type(t1[k]) == type(v) == "table", so t1[k] and v is true
			tableMerge(t1[k], v, overwrite) -- t2[k] == v
		else
			if overwrite or t1[k] == nil then t1[k] = v end
		end
	end
	return t1
end

--- Given an i18n table instantiates the values (deprecated)
--  @function i18n.loadI18n
--  @param {string} name name of module with i18n
--  @param {table} i18n_arg existing i18n
function i18n.loadI18n(name, i18n_arg)
	local exist, res = pcall(require, name)
	if exist and next(res) ~= nil then
		if i18n_arg then
			tableMerge(i18n_arg, res.i18n, true)
		end
	end
	-- give warning module is deprecated if previewing a page or module using it
	return require("Module:If preview").warn(
		"Deprecated function <code>i18n.loadI18n</code> called, use <code>i18n.loadMessages</code> and <code>i18n:msg()</code> instead.<br/>" .. debug.traceback():gsub("\n", "<br/>"):gsub("\t", "&emsp;")
	)
end

--- Loads an i18n for a specific frame (deprecated)
--  @function i18n.loadI18nFrame
--  @param {string} name name of module with i18n
--  @param {table} i18n_arg existing i18n
function i18n.loadI18nFrame(frame, i18n_arg)
	return i18n.loadI18n(frame:getTitle().."/i18n", i18n_arg)
end

--- Wrapper for the module.
--  @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>