Jump to content

Module:I18n

විකිපීඩියා වෙතින්

--- 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.1
--  @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.
-- @type Data
local Data = {}
Data.__index = Data

--- Datastore message getter utility.
-- @function Data:msg
-- @param {string|table} opts Message configuration or key.
-- @param[opt] {string} opts.key Message key.
-- @param[opt] {table} opts.args Arguments for `$n` substitution.
-- @param[opt] {table} opts.sources Source names to limit to.
-- @param[opt] {table} opts.lang Temporary language.
-- @param[opt] {string} ... Arguments for `$n` substitution.
-- @error[115] {string} 'missing arguments in Data:msg'
-- @return {string} Localised message or `'<key>'`.
function Data:msg(opts, ...)
    local frame = mw.getCurrentFrame()
    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 {...}
    if opts.sources then
        self:fromSource(unpack(opts.sources))
    end
    if opts.lang then
        self:inLang(opts.lang)
    end
    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
    local lang = self.tempLang or self.defaultLang
    self.tempLang = nil
    local msg
    for i, messages in ipairs(self._messages) do
        msg = (messages[lang] or {})[key]
        for _, l in ipairs(fallbacks[lang] or {}) do
            if msg == nil then
                msg = (messages[l] or {})[key]
            end
        end
        msg = msg or messages.en[key]
        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.
-- @function Data:parameter
-- @param {string} key Parameter 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 value or nil.
function Data:parameter(key, args)
    if not self or not key or not args then
        error('missing arguments in Data:parameter')
    end
    local contentLang = mw.language.getContentLanguage():getCode()
    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
                msg = (messages[l] or {})[key]
            else
                return args[msg]
            end
        end
        msg = messages.en[key]
        if msg ~= nil and args[msg] ~= nil then
            return args[msg]
        end
    end
end

--- Datastore temporary source setter.
-- @function Data:fromSource
-- @param {string} ... Source name(s).
-- @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.
function Data:getLang()
    return self.defaultLang
end

--- Datastore language setter to wgUserLanguage.
-- @function Data:useUserLang
-- @return {Data} 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 specified language.
-- @function Data:useLang
-- @param {string} code Language code.
-- @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.
-- @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.
-- @function Data:inContentLang
-- @return {Data} Datastore instance.
function Data:inContentLang()
    self.tempLang = mw.language.getContentLanguage():getCode()
    return self
end

--- Temporary datastore language setter to specified language.
-- @function Data:inLang
-- @param {string} code Language code.
-- @return {Data} Datastore instance.
function Data:inLang(code)
    self.tempLang = _i18n.isValidCode(code) and code or self.tempLang
    return self
end

--- Localized message getter by key.
-- @function i18n.getMsg
-- @param {table} frame Frame table.
-- @param {string} frame.args[1] ROOTPAGENAME of i18n submodule.
-- @param {string} frame.args[2] Message key.
-- @param[opt] {string} frame.args.lang Default language.
-- @error[271] {string} 'missing arguments in i18n.getMsg'
-- @return {string} Localized message.
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]
    local repl = {}
    for i, a in ipairs(frame.args) do
        if i >= 3 then
            repl[i-2] = a
        end
    end
    local ds = i18n.loadMessages(source)
    ds:inLang(frame.args.uselang)
    return ds:msg { key = key, args = repl }
end

--- I18n message datastore loader.
-- @function i18n.loadMessages
-- @param {string} ... ROOTPAGENAME/path for i18n submodules.
-- @error[322] {string} 'no source supplied to i18n.loadMessages'
-- @return {table} I18n datastore instance.
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
                ds = {}
                ds._messages = {}
                setmetatable(ds, Data)
                ds:useUserLang()
            end
            source = string.gsub(source, '^.', mw.ustring.upper)
            local fullSource = mw.ustring.find(source, ':') and source or 'Module:' .. source .. '/i18n'
            local success, messages = pcall(mw.loadData, fullSource)
            if success then
                -- Handle legacy { i18n = {...} } structure
                if messages.i18n then
                    messages = { en = messages.i18n }
                end
                local msgCopy = {}
                local langSecond = nil
                for lang_id, msgtbl in pairs(messages) do
                    if langSecond == nil then
                        if lang_id == "qqq" or fallbacks[lang_id] then
                            langSecond = false
                        else
                            langSecond = true
                        end
                    end
                    for id_lang, msg in pairs(msgtbl) do
                        if langSecond then
                            msgCopy[id_lang] = msgCopy[id_lang] or {}
                            msgCopy[id_lang][lang_id] = msg
                        else
                            msgCopy[lang_id] = msgCopy[lang_id] or {}
                            msgCopy[lang_id][id_lang] = msg
                        end
                    end
                end
                ds._messages[i] = msgCopy
            end
            local tab = mw.ext.data.get('I18n/' .. source .. '.tab', '_')
            local T = {}
            if not success and not tab then
                mw.log("Warning: i18n for " .. source .. " is missing")
                ds._messages[i] = {}
            else
                if tab and tab.data then
                    for _, row in pairs(tab.data) do
                        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
                end
                if not success then
                    ds._messages[i] = T
                else
                    for lang, msgTbl in pairs(T) do
                        ds._messages[i][lang] = ds._messages[i][lang] or msgTbl
                    end
                end
            end
        end
    end
    if not ds then
        error('no source supplied to i18n.loadMessages')
    end
    ds._sources = s
    return ds
end

--- Language code getter.
-- @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
    local langOverride = (frame.args or {}).uselang or (parentFrame.args or {}).uselang
    if _i18n.isValidCode(langOverride) then
        code = langOverride
    elseif title.isSubpage and _i18n.isValidCode(subPage) then
        code = _i18n.isValidCode(subPage) and subPage or code
    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

--- Merges two tables.
-- @function tableMerge
-- @param {table} t1 Target table.
-- @param {table} t2 Source table.
-- @param {boolean} overwrite Overwrite existing keys.
-- @return {table} Merged table.
-- @local
local function tableMerge(t1, t2, overwrite)
    for k, v in pairs(t2) do
        if type(v) == "table" and type(t1[k]) == "table" then
            tableMerge(t1[k], v, overwrite)
        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 table.
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
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 table.
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.
i18n.main = entrypoint(i18n)

return require("Module:Deprecated")(i18n, 
    {
        ["loadI18n"] = {
            deprecated = true,
            replacement = "use <code>i18n.loadMessages</code>"
        },
        ["loadI18nFrame"] = {
            deprecated = true,
            replacement = "use <code>i18n.loadMessages</code>"
        }
    }
)
-- </nowiki>
"https://si.wikipedia.org/w/index.php?title=Module:I18n&oldid=763994" වෙතින් සම්ප්‍රවේශනය කෙරිණි