Module:I18n/consolefriendly

From Coral Island Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:I18n/consolefriendly/doc

-- <nowiki>
-- I18n storage module for FANDOM.
-- @module      i18n
-- @version     1.3.3
-- @usage       require("Dev:I18n")
-- @author      Speedit
-- @author      KockaAdmiralac
-- @release     alpha; untested
-- @todo        Fallbacks and unit testing.
local i18n = {}

-- local keeper of locality
-- keeps local functions local and defends them against nasty globals
local _i18n = {}

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

-- I18n datastore class.
-- @section     i18nd
local i18nd = {}

-- converts things to human-frienldy strings
-- https://stackoverflow.com/a/27028488
-- @param           {object} o thing to dump
-- @returns         {string} dumped thing
function dump(o)
   if type(o) == 'table' then
      local s = '{ '
      for k,v in pairs(o) do
         if type(k) ~= 'number' then k = '"'..k..'"' end
         s = s .. '['..k..'] = ' .. dump(v) .. ','
      end
      return s .. '} '
   else
      return tostring(o)
   end
end-- dump

-- Datastore language getter.
-- @name        i18nd:getLang
-- @return      {string} Default language (datastore messages).
function i18nd:getLang()
    return self.defaultLang
end

-- Datastore language setter to user language.
-- @name        i18nd:useUserLang
-- @return      {self} Self instance.
function i18nd:useUserLang()
    self.defaultLang = i18n.getLang() or self.defaultLang
    return self
end

-- Datastore language setter to user language.
-- @name        i18nd:useContentLang
-- @return      {self} Self instance.
function i18nd:useContentLang()
    self.defaultLang = mw.language.getContentLanguage():getCode()
    return self
end

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

-- Datastore temporary language setter to user language.
-- @name        i18nd:inUserLang
-- @return      {self} Self instance.
function i18nd:inUserLang()
    self.tempLang = i18n.getLang() or i18nd.tempLang
    return self
end

-- Datastore temporary language setter to user language.
-- @name        i18nd:inContentLang
-- @return      {self} Self instance.
function i18nd:inContentLang()
    self.tempLang = mw.language.getContentLanguage():getCode()
    return self
end

-- Datastore temporary language setter to specificed language.
-- @name        i18nd:inLang
-- @param       {string} code Language code to use.
-- @return      {self} Self instance.
function i18nd:inLang(code)
    self.tempLang = _i18n.isValidCode(code)
        and code
        or  self.tempLang
    return self
end

-- Datastore temporary source setter to specificed datastore.
-- @name        i18nd:fromSource
-- @param       {string} ... Source name(s) to use.
-- @return      {self} Self instance.
function i18nd: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 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()
    -- Argument normalization.
    if not self or not opts then
        error('missing arguments in i18nd: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
            -- debug
            --mw.log('i18n.msg pre-handleargs msg&args', dump(msg), dump(args))
            -- not debug
            msg = _i18n.handleArgs(msg, args)
            -- debug
            --mw.log('i18n.msg post-handleargs msg&args', dump(msg), dump(args))
        end
        if msg and source_i[i] and lang ~= 'qqx' then
            -- debug
            --mw.log('i18n.msg', msg, dump(msg))
            return frame
                and frame.preprocess and frame:preprocess(mw.text.trim(msg))
                or  mw.text.trim(msg)
        end
    end
    return mw.text.nowiki('<' .. key .. '>')
end

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

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

-- Language code function.
-- @usage       {{#invoke:i18n|getLang}}
-- @return      {string} code Language code.
function i18n.getLang()
    local code = mw.language.getContentLanguage():getCode()
    local frame = mw.getCurrentFrame() or {}
    local subPage = title.subpageText
    local uselang
    -- Language argument test.
    if _i18n.isValidCode( ( (frame or {}).args or {}).uselang or '') then
        code = frame.args.uselang
    elseif _i18n.isValidCode( ( (frame and (frame.getParent and frame:getParent(frame) or {}) or {}).args or {}).uselang or '') then
        code = frame:getParent().args.uselang
    -- Subpage language test.
    elseif title.isSubpage then
        code = _i18n.isValidCode(subPage) and subPage or code
    -- User language test.
    elseif frame then
        -- use en if no frame found
        uselang = frame.preprocess and frame:preprocess('{{int:lang}}') or 'en'
        code = mw.text.decode(uselang) == '<lang>'
            and code
            or  uselang
    end
    return code
end

-- I18n message datastore loader.
-- @param       {string} source ROOTPAGENAME/path of target i18n submodule.
-- @usage       require('Dev:Install').loadMessages(source)
-- @raise       'no source supplied to i18n.loadMessages'
-- @return      {table} i18n 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
                -- Instantiate datastore.
                ds = {}
                i18nd.__index = i18nd
                setmetatable(ds, i18nd)
                -- Set default language.
                ds.defaultLang = i18n.getLang()
                ds._messages = {}
            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

-- I18n message function.
-- @param       {table} frame Frame table from invocation.
-- @param       {string} frame.args[1] ROOTPAGENAME of i18n submodule.
-- @param       {string} frame.args[2] Key of i18n message.
-- @param       {string} frame.args.lang Default language of message (optional).
-- @usage       {{#invoke:i18n|getMsg|source|key}}
-- @raise       'missing arguments in i18n.getMsg'
-- @return      {string} msg I18n message in localised language.
function i18n.getMsg(frame)
    local args = frame and frame.args or frame
    if
        not frame or
        not args or
        not args[1] or
        not args[2]
    then
        error('missing arguments in i18n.getMsg')
    end
    local source = args[1]
    local key = args[2]
    -- Pass through extra arguments.
    local repl = {}
    for i, a in ipairs(args) do
        if i >= 3 then
            repl[i-2] = a
        end
    end
    -- Load message data.
    local i18nd = i18n.loadMessages(source)
    -- Pass through language argument.
    i18nd:inLang(args.lang)
    -- Return message.
    return i18nd:msg { key = key, args = repl }
end

-- Template wrapper for [[Template:I18n]].
-- @param               {table} frameChild Frame invocation object.
-- @usage               {{#invoke:i18n|main}}
function i18n.main(frameChild)
    -- anti-console defense
    local frame = frameChild.getParent and frameChild:getParent() or (frameChild or {})
    local args = {}
    -- there is no .parents and .args in the console
    local frameChildArgs = frameChild.args and frameChild.args or frameChild
    for p, v in pairs(frameChildArgs) 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
-- </nowiki>