Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:I18n: Difference between revisions

From Poets.Wiki
Created page with "--- 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 Data:i18n/ subpages on --..."
 
m Protected "Module:I18n": Initial wiki setup ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite))
 
(One intermediate revision by the same user not shown)
Line 1: Line 1:
--- I18n library for message storage in Lua datastores.
require( 'strict' )
--  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 [[Special:PrefixIndex/commons: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]]
--  @author        [[wikia:dev:User:Speedit|Speedit]]
--  @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 i18n = {}
local title = mw.title.getCurrentTitle()
 
local fallbacks = require('Module:Fallbacklist')
local metatable = {}
local entrypoint = require('Module:Entrypoint')
local methodtable = {}
local uselang
 
metatable.__index = methodtable
 
local libraryUtil = require( 'libraryUtil' )
local checkType = libraryUtil.checkType
 
--- Cache table containing i18n data
--- e.g. cache['en']['SMW'] will get you the SMW table in English
local cache = {}
 
--- Cache language codes for reuse
local languages = {}


--- 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.
--- Retrieve dataset namespace from key prefix
-- @function          _i18n.isValidCode
---
-- @param             {string} code Language code to check.
--- @param key string The translation key
-- @return             {boolean} Whether the language code is valid.
--- @return string
--  @local
local function getNamespace( key )
function _i18n.isValidCode(code)
     local namespace = string.match( key, '([^_]*)' )
     return type(code) == 'string' and #mw.language.fetchLanguageName(code) ~= 0
    return namespace
end
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.
--- Retrieve a list of applicable language codes
--  This is used to control language translation and access to individual
---
-- messages. The datastore instance provides language and message
--- @return table
-- getter-setter methods, which can be used to internationalize Lua modules.
local function getLanguageCodes()
--  The language methods (any ending in `Lang`) are all **chainable**.
    if #languages > 0 then return languages end
--  @type            Data
    local mwlang = mw.language.getContentLanguage()
local Data = {}
    local langCodes = { mwlang:getCode() }
Data.__index = Data


--- Datastore message getter utility.
     local fallbackLangCodes = mwlang:getFallbackLanguages()
--  This method returns localized messages from the datastore corresponding
     if next( fallbackLangCodes ) ~= nil then
--  to a `key`. These messages may have `$n` parameters, which can be
         for _, fallbackLangCode in pairs( fallbackLangCodes ) do
--  replaced by optional argument strings supplied by the `msg` call.
             table.insert( langCodes, fallbackLangCode )
-- 
--  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
     end
     end
     return mw.text.nowiki('&#x29FC;' .. key .. '&#x29FD;')
 
     --mw.log( string.format( '🌐 [i18n] Setting language chain: %s', table.concat( langCodes, '→' ) ) )
    return langCodes
end
end


--- Datastore template parameter getter utility.
 
--  This method, given a table of arguments, tries to find a parameter's
--- Loads a dataset and saves it to the cache
--  localized name in the datastore and returns its value, or nil if
---
--  not present.
--- @param lang string
--
--- @param namespace string
--  This method always uses the wiki's content language.
--- @return table|nil { data = "The dataset", keys = "Translation key mapped to index" }
-- @function          Data:parameter
local function load( lang, namespace )
--  @param             {string} parameter Parameter's key in the datastore
     -- Init language cache if it does not exist
-- @param             {table} args Arguments to find the parameter in
     if cache[ lang ] == nil then
--  @error[176]        {string} 'missing arguments in Data:parameter'
         cache[ lang ] = {}
-- @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
     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.
     if cache[ lang ][ namespace ] then
--  By default, messages are fetched from the datastore in the same
        return cache[ lang ][ namespace ]
--  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
     end
    return self
end


--- Datastore default language getter.
    local datasetName = string.format( 'Module:i18n/%s/%s.json', namespace, lang )
--  @function          Data:getLang
     local success, data = pcall( mw.loadJsonData, datasetName )
--  @return            {string} Default language to serve datastore messages in.
function Data:getLang()
     return self.defaultLang
end


--- Datastore language setter to `wgUserLanguage`.
    if not success then
--  @function          Data:useUserLang
        --mw.log( string.format( '🚨 [i18n] Loading dataset[%s][%s]: %s not found on wiki', lang, namespace, datasetName ) )
--  @return            {Data} Datastore instance.
        -- Cache the empty result so we do not run mw.loadJsonData again
-- @note              Scribunto only registers `wgUserLanguage` when an
        cache[ lang ][ namespace ] = {}
--                      invocation is at the top of the call stack.
        return
function Data:useUserLang()
    end
    self.defaultLang = i18n.getLang() or self.defaultLang
    return self
end


--- Datastore language setter to `wgContentLanguage`.
    cache[ lang ][ namespace ] = data
-- @function          Data:useContentLang
    --mw.log( string.format( '⌛ [i18n] Loading dataset[%s][%s]: %s', lang, namespace, datasetName ) )
--  @return            {Data} Datastore instance.
function Data:useContentLang()
    self.defaultLang = mw.language.getContentLanguage():getCode()
    return self
end


--- Datastore language setter to specificed language.
     return cache[ lang ][ namespace ]
--  @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
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`.
--- Returns translated message (or key if returnKey is enabled)
-- Only affects the next @{Data:msg} call.
---
-- @function          Data:inContentLang
--- @param key string The translation key
-- @return             {Data} Datastore instance.
--- @param options table|nil Optional options
function Data:inContentLang()
--- @return string|nil
     self.tempLang = mw.language.getContentLanguage():getCode()
function methodtable.translate( self, key, options )
     return self
     options = options or {
end
        ['returnKey'] = true
     }


--- Temporary datastore language setter to a specificed language.
    checkType( 'Module:i18n.translate', 1, self, 'table' )
--  Only affects the next @{Data:msg} call.
    checkType( 'Module:i18n.translate', 2, key, 'string' )
--  @function          Data:inLang
     checkType( 'Module:i18n.translate', 3, options, 'table' )
--  @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.
    --mw.log( string.format( '🔍 [i18n] Looking for message: %s', key ) )


--- Localized message getter by key.
    local namespace = getNamespace( key )
--  Can be used to fetch messages in a specific language code through `uselang`
    if namespace == nil then
-- parameter. Extra numbered parameters can be supplied for substitution into
        -- No namespace found error
-- the datastore message.
        --mw.log( string.format( '❌ [i18n] Namespace cannot be found from: %s', key ) )
--  @function          i18n.getMsg
        if options['returnKey'] == true then
--  @param              {table} frame Frame table from invocation.
            return key
--  @param              {table} frame.args Metatable containing arguments.
         else
--  @param              {string} frame.args[1] ROOTPAGENAME of i18n submodule.
             return
--  @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
     end
     end
    -- Load message data.
 
     local ds = i18n.loadMessages(source)
     languages = getLanguageCodes()
    -- Pass through language argument.
 
     ds:inLang(frame.args.uselang)
     local message
    -- Return message.
     local i = 1
     return ds:msg { key = key, args = repl }
 
end
     while ( message == nil and i <= #languages ) do
         local dataset = load( languages[ i ], namespace )
--- I18n message datastore loader.
         if dataset then
--  @function          i18n.loadMessages
             local match = dataset[ key ]
--  @param              {string} ... ROOTPAGENAME/path for target i18n
             if match then
--                      submodules.
                 message = match
--  @error[322]        {string} 'no source supplied to i18n.loadMessages'
                 --mw.log( string.format( '✅ [i18n] Found message: %s', message ) )
--  @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
         end
        i = i + 1
     end
     end
     if not ds then
 
         error('no source supplied to i18n.loadMessages')
     if message == nil then
    else
         --mw.log( string.format( '❌ [i18n] Could not found message: %s', key ) )
         -- Attach source index map.
         if options['returnKey'] == true then
        ds._sources = s
            message = key
        -- Return datastore instance.
         end
         return ds
     end
     end
    return message
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()
--- New Instance
     local subPage = title.subpageText
---
--- @return table i18n
function i18n.new( self )
     local instance = {}


     -- Language argument test.
     setmetatable( instance, metatable )
    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
     return instance
end
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
return i18n
-- </nowiki>

Latest revision as of 18:30, 3 March 2025

This page uses Creative Commons Licensed content from Wikipedia (view authors).

require( 'strict' )

local i18n = {}

local metatable = {}
local methodtable = {}

metatable.__index = methodtable

local libraryUtil = require( 'libraryUtil' )
local checkType = libraryUtil.checkType

--- Cache table containing i18n data
--- e.g. cache['en']['SMW'] will get you the SMW table in English
local cache = {}

--- Cache language codes for reuse
local languages = {}


--- Retrieve dataset namespace from key prefix
---
--- @param key string The translation key
--- @return string
local function getNamespace( key )
    local namespace = string.match( key, '([^_]*)' )
    return namespace
end


--- Retrieve a list of applicable language codes
---
--- @return table
local function getLanguageCodes()
    if #languages > 0 then return languages end
    local mwlang = mw.language.getContentLanguage()
    local langCodes = { mwlang:getCode() }

    local fallbackLangCodes = mwlang:getFallbackLanguages()
    if next( fallbackLangCodes ) ~= nil then
        for _, fallbackLangCode in pairs( fallbackLangCodes ) do
            table.insert( langCodes, fallbackLangCode )
        end
    end

    --mw.log( string.format( '🌐 [i18n] Setting language chain: %s', table.concat( langCodes, '→' ) ) )
    return langCodes
end


--- Loads a dataset and saves it to the cache
---
--- @param lang string
--- @param namespace string
--- @return table|nil { data = "The dataset", keys = "Translation key mapped to index" }
local function load( lang, namespace )
    -- Init language cache if it does not exist
    if cache[ lang ] == nil then
        cache[ lang ] = {}
    end

    if cache[ lang ][ namespace ] then
        return cache[ lang ][ namespace ]
    end

    local datasetName = string.format( 'Module:i18n/%s/%s.json', namespace, lang )
    local success, data = pcall( mw.loadJsonData, datasetName )

    if not success then
        --mw.log( string.format( '🚨 [i18n] Loading dataset[%s][%s]: %s not found on wiki', lang, namespace, datasetName ) )
        -- Cache the empty result so we do not run mw.loadJsonData again
        cache[ lang ][ namespace ] = {}
        return
    end

    cache[ lang ][ namespace ] = data
    --mw.log( string.format( '⌛ [i18n] Loading dataset[%s][%s]: %s', lang, namespace, datasetName ) )

    return cache[ lang ][ namespace ]
end


--- Returns translated message (or key if returnKey is enabled)
---
--- @param key string The translation key
--- @param options table|nil Optional options
--- @return string|nil
function methodtable.translate( self, key, options )
    options = options or {
        ['returnKey'] = true
    }

    checkType( 'Module:i18n.translate', 1, self, 'table' )
    checkType( 'Module:i18n.translate', 2, key, 'string' )
    checkType( 'Module:i18n.translate', 3, options, 'table' )

    --mw.log( string.format( '🔍 [i18n] Looking for message: %s', key ) )

    local namespace = getNamespace( key )
    if namespace == nil then
        -- No namespace found error
        --mw.log( string.format( '❌ [i18n] Namespace cannot be found from: %s', key ) )
        if options['returnKey'] == true then
            return key
        else
            return
        end
    end

    languages = getLanguageCodes()

    local message
    local i = 1

    while ( message == nil and i <= #languages ) do
        local dataset = load( languages[ i ], namespace )
        if dataset then
            local match = dataset[ key ]
            if match then
                message = match
                --mw.log( string.format( '✅ [i18n] Found message: %s', message ) )
            end
        end
        i = i + 1
    end

    if message == nil then
        --mw.log( string.format( '❌ [i18n] Could not found message: %s', key ) )
        if options['returnKey'] == true then
            message = key
        end
    end

    return message
end


--- New Instance
---
--- @return table i18n
function i18n.new( self )
    local instance = {}

    setmetatable( instance, metatable )

    return instance
end


return i18n