Module:Translate

From AEGIS
Revision as of 17:27, 28 March 2024 by Pooka (Pookaw) (talk | contribs) (1 revision imported)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Module documentation[view][edit][history][purge]
This documentation is transcluded from Module:Translate/doc. Changes can be proposed in the talk page.

This module allows templates and modules to be easily translated as part of the multilingual templates and modules project. Instead of storing English text in a module or a template, Translate module allows modules to be designed language-neutral, and store multilingual text in the /i18n.json subpage. This way your module or template will use those translated strings (messages), or if the message has not yet been translated, will fallback to English. When someone updates the translation table, your page will automatically update (might take some time, or you can purge it), but no change in the template or module is needed on any of the wikis. This process is very similar to MediaWiki's localisation, and supports all standard localization conventions such as {{PLURAL|...}} and other parameters.

See also


require( 'strict' )

local Translate = {}

local metatable = {}
local methodtable = {}

metatable.__index = methodtable

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

--- Cache table containing i18n data at the 'data' key, and a key map at the 'keys' key
--- The table is keyed by the i18n.json module name
local cache = {}
local i18nDataset = 'Module:Translate/i18n.json'


--- Uses the current title as the base and appends '/i18n.json'
---
--- @param dataset table
--- @return string|nil
local function guessDataset( dataset )
    if mw.ustring.find( dataset, ':', 1, true ) then
        return dataset
    elseif type( dataset ) == 'string' then
        return mw.ustring.format( 'Module:%s/i18n.json', dataset )
    end

    return nil
end


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

    local data = mw.loadJsonData( dataset )
    local keys = {}
    for index, row in ipairs( data.data ) do
        keys[ row[ 1 ] ] = index
    end

    cache[ dataset ] = {
        data = data,
        keys = keys
    }

    return cache[ dataset ]
end


--- Retrieves a message from a dataset and formats it according to parameters and language
---
--- @param dataset string
--- @param key string
--- @param params table
--- @param lang string
local function formatMessage( dataset, key, params, lang )
    local data = load( dataset )

    if data.keys[ key ] == nil then
        error( formatMessage( i18nDataset, 'error_bad_msgkey', { key, dataset }, mw.getContentLanguage():getCode() ) )
    end

    local msg = data.data.data[ data.keys[ key ] ][ 2 ]

    if msg == nil then
        error( formatMessage( i18nDataset, 'error_bad_msgkey', { key, dataset }, mw.getContentLanguage():getCode() ) )
    end

    msg = msg[ lang ] or error( mw.ustring.format( 'Language "%s" not found for key "%s"', lang, key ) )

    local result = mw.message.newRawMessage( msg, unpack( params or {} ) )

    return result:plain()
end


--- Translates a message
---
--- @param dataset string
--- @param key string
--- @return string
function methodtable.format( dataset, key, ... )
    dataset = guessDataset( dataset )

    checkType('format', 1, dataset, 'string')
    checkType('format', 2, key, 'string')

    local lang = mw.getContentLanguage():getCode()

    return formatMessage( dataset, key, {...}, lang )
end


--- Translates a message in a given language
---
--- @param lang string
--- @param dataset string
--- @param key string
--- @return string
function methodtable.formatInLanguage( lang, dataset, key, ... )
    dataset = guessDataset( dataset )

    checkType('formatInLanguage', 1, lang, 'string')
    checkType('formatInLanguage', 2, dataset, 'string')
    checkType('formatInLanguage', 3, key, 'string')

    return formatMessage( dataset, key, {...}, lang )
end


--- Wrapper function for Translate.getTemplateData that wraps the output in a <templatedata> tag
---
--- @param frame table Current MW Frame
--- @return string
function Translate.doc( frame )
    local dataset = frame.args[ 1 ] or frame.args[ 'dataset' ] or ( mw.title.getCurrentTitle().prefixedText .. '/i18n.json' )

    return frame:extensionTag( 'templatedata', Translate.getTemplateData( dataset ) )
end


--- Base logic taken from Module:TNT
--- Iterates through all user settable arguments and outputs json usable in a <templatedata> tag
---
--- @param dataset string The data.json page from which the arguments are taken
--- @return string Json
function Translate.getTemplateData( dataset )
    local data = load( guessDataset( dataset ) )
    local instance = Translate:new( dataset )

    local names = {}
    for _, field in ipairs( data.data.schema.fields ) do
        table.insert( names, field.name )
    end

    local numOnly = true
    local params = {}
    local paramOrder = {}

    for _, row in ipairs( data.data.data ) do
        local newVal = {}
        local name

        if row[ 1 ]:sub( 1, 3 ) == 'ARG' then
            for pos, columnName in ipairs( names ) do
                if columnName == 'id' then
                    name = instance.format( dataset, row[ pos ] )
                elseif columnName ~= 'message' then
                    newVal[ columnName ] = row[ pos ]

                    -- Allow to share examples and label
                    if ( columnName == 'example' or columnName == 'label' ) and type( row[ pos ] ) == 'string' then
                        newVal[ columnName ] = {
                            de = row[ pos ],
                            en = row[ pos ],
                        }
                    end
                end
            end

            if name and newVal[ 'type' ] ~= nil then
                if type( name ) ~= "number" and ( type( name ) ~= "string" or not mw.ustring.match( name, "^%d+$" ) ) then
                    numOnly = false
                end

                params[ name ] = newVal

                table.insert( paramOrder, name )

                -- TODO: Limit this to a specified subset of url args
                if row[ 1 ]:sub( -3 ) == 'Url' then
                    for i = 1, 4 do
                        local tmp = {}
                        for k, v in pairs( newVal ) do
                            if type( v ) == 'table' then
                                tmp[ k ] = {}
                                for k1, v1 in pairs( v ) do
                                    tmp[ k ][ k1 ] = v1
                                end
                            else
                                tmp[ k ] = v
                            end
                        end

                        local nameI = name .. tostring( i )
                        tmp[ 'required' ] = false
                        tmp[ 'suggested' ] = false
                        params[ nameI ] = tmp

                        table.insert( paramOrder, nameI )
                    end
                end
            end
        end
    end

    -- Work around json encoding treating {"1":{...}} as an [{...}]
    if numOnly then
        params['zzz123']=''
    end

    local json = mw.text.jsonEncode({
        params = params,
        paramOrder = paramOrder,
        description = data.template_description,
    })

    if numOnly then
        json = mw.ustring.gsub( json,'"zzz123":"",?', "" )
    end

    return json
end


--- Calls TNT with the given key
---
--- @param dataset string The i18n.json page
--- @param config table The calling module's config
--- @param key string The translation key
--- @param addSuffix boolean|nil Adds a language suffix if config.smw_multilingual_text is true
--- @return string If the key was not found in the .tab page, the key is returned
function methodtable.translate( self, dataset, config, key, addSuffix, ... )
    checkType( 'Module:Translate.translate', 1, self, 'table' )
    checkType( 'Module:Translate.translate', 2, dataset, 'string' )
    checkType( 'Module:Translate.translate', 3, config, 'table' )
    checkType( 'Module:Translate.translate', 4, key, 'string' )
    checkType( 'Module:Translate.translate', 5, addSuffix, 'boolean', true )

	addSuffix = addSuffix or false
	local success, translation

	local function multilingualIfActive( input )
		if addSuffix and config.smw_multilingual_text == true then
			return mw.ustring.format( '%s@%s', input, config.module_lang or mw.getContentLanguage():getCode() )
		end

		return input
	end

	if config.module_lang ~= nil then
		success, translation = pcall( self.formatInLanguage, config.module_lang, dataset, key or '', ... )
	else
		success, translation = pcall( self.format, dataset, key or '', ... )
	end

	if not success or translation == nil then
        local title = mw.title.new( guessDataset( dataset ) )

        if not title.exists then
            error( mw.ustring.format( 'I18N table "%s" does not exist!', dataset ), 3 )
        end

		return nil
	end

	return multilingualIfActive( translation )
end


--- New Instance
---
--- @return table Translate
function Translate.new( self, dataset )
    local instance = {
        dataset = dataset or nil
    }

    setmetatable( instance, metatable )

    return instance
end


return Translate