m (Let's try passing a language argument in a different way) |
m (Reverted edits by The crewmates among us (talk) to last revision by MUNEME) Tag: Rollback |
||
(61 intermediate revisions by 6 users not shown) | |||
Line 1: | Line 1: | ||
+ | --- I18n library for message storage in Lua datastores. |
||
− | -- <nowiki> |
||
+ | -- The module is designed to enable message separation from modules & |
||
− | -- I18n storage module for FANDOM. |
||
+ | -- templates. It has support for handling language fallbacks. This |
||
− | -- @module i18n |
||
+ | -- module is a Lua port of [[I18n-js]] and i18n modules that can be loaded |
||
− | -- @version 1.3.3 |
||
+ | -- by it are editable through [[I18nEdit]]. |
||
− | -- @usage require("Dev:I18n") |
||
+ | -- |
||
− | -- @author Speedit |
||
− | -- @ |
+ | -- @module i18n |
− | -- @ |
+ | -- @version 1.4.0 |
− | -- @ |
+ | -- @require Module:Entrypoint |
+ | -- @require Module:Fallbacklist |
||
− | local i18n = {} |
||
+ | -- @author [[User:KockaAdmiralac|KockaAdmiralac]] |
||
+ | -- @author [[User:Speedit|Speedit]] |
||
+ | -- @attribution [[User:Cqm|Cqm]] |
||
+ | -- @release stable |
||
+ | -- @see [[I18n|I18n guide]] |
||
+ | -- @see [[I18n-js]] |
||
+ | -- @see [[I18nEdit]] |
||
+ | -- <nowiki> |
||
+ | local i18n, _i18n = {}, {} |
||
− | -- Module variables & dependencies. |
+ | -- Module variables & dependencies. |
local title = mw.title.getCurrentTitle() |
local title = mw.title.getCurrentTitle() |
||
local fallbacks = require('Dev:Fallbacklist') |
local fallbacks = require('Dev:Fallbacklist') |
||
+ | local entrypoint = require('Dev:Entrypoint') |
||
+ | local uselang |
||
+ | --- Argument substitution as $n where n > 0. |
||
− | -- I18n datastore class. |
||
− | -- @ |
+ | -- @function _i18n.handleArgs |
+ | -- @param {string} msg Message to substitute arguments into. |
||
− | local i18nd = {} |
||
+ | -- @param {table} args Arguments table to substitute. |
||
− | |||
+ | -- @return {string} Resulting message. |
||
− | -- Datastore language getter. |
||
+ | -- @local |
||
− | -- @name i18nd:getLang |
||
+ | function _i18n.handleArgs(msg, args) |
||
− | -- @return {string} Default language (datastore messages). |
||
+ | for i, a in ipairs(args) do |
||
− | function i18nd:getLang() |
||
+ | msg = (string.gsub(msg, '%$' .. tostring(i), tostring(a))) |
||
− | return self.defaultLang |
||
+ | end |
||
+ | return msg |
||
end |
end |
||
+ | --- Checks whether a language code is valid. |
||
− | -- Datastore language setter to user language. |
||
− | -- @ |
+ | -- @function _i18n.isValidCode |
− | -- @ |
+ | -- @param {string} code Language code to check. |
+ | -- @return {boolean} Whether the language code is valid. |
||
− | function i18nd:useUserLang() |
||
+ | -- @local |
||
− | self.defaultLang = i18n.getLang() or self.defaultLang |
||
+ | function _i18n.isValidCode(code) |
||
− | return self |
||
+ | return type(code) == 'string' and #mw.language.fetchLanguageName(code) ~= 0 |
||
end |
end |
||
+ | --- Checks whether a message contains unprocessed wikitext. |
||
− | -- Datastore language setter to user language. |
||
+ | -- Used to optimise message getter by not preprocessing pure text. |
||
− | -- @name i18nd:useContentLang |
||
− | -- @ |
+ | -- @function _i18n.isWikitext |
+ | -- @param {string} msg Message to check. |
||
− | function i18nd:useContentLang() |
||
+ | -- @return {boolean} Whether the message contains wikitext. |
||
− | self.defaultLang = mw.language.getContentLanguage():getCode() |
||
+ | function _i18n.isWikitext(msg) |
||
− | return self |
||
+ | 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 |
end |
||
+ | --- I18n datastore class. |
||
− | -- Datastore language setter to specificed language. |
||
+ | -- This is used to control language translation and access to individual |
||
− | -- @name i18nd:useLang |
||
+ | -- messages. The datastore instance provides language and message |
||
− | -- @param {string} code Language code to use. |
||
+ | -- getter-setter methods, which can be used to internationalize Lua modules. |
||
− | -- @return {self} Self instance. |
||
+ | -- The language methods (any ending in `Lang`) are all **chainable**. |
||
− | function i18nd:useLang(code) |
||
+ | -- @type Data |
||
− | self.defaultLang = isValidCode(code) |
||
+ | local Data = {} |
||
− | and code |
||
+ | Data.__index = Data |
||
− | or self.defaultLang |
||
− | return self |
||
− | end |
||
− | -- Datastore |
+ | --- Datastore message getter utility. |
+ | -- This method returns localized messages from the datastore corresponding |
||
− | -- @name i18nd:inUserLang |
||
+ | -- to a `key`. These messages may have `$n` parameters, which can be |
||
− | -- @return {self} Self instance. |
||
+ | -- replaced by optional argument strings supplied by the `msg` call. |
||
− | function i18nd:inUserLang() |
||
+ | -- |
||
− | self.tempLang = i18n.getLang() or i18nd.tempLang |
||
+ | -- This function supports [[Lua reference manual#named_arguments|named |
||
− | return self |
||
+ | -- arguments]]. The named argument syntax is more versatile despite its |
||
− | end |
||
+ | -- verbosity; it can be used to select message language & source(s). |
||
− | |||
+ | -- @function Data:msg |
||
− | -- Datastore temporary language setter to user language. |
||
+ | -- @usage |
||
− | -- @name i18nd:inContentLang |
||
+ | -- |
||
− | -- @return {self} Self instance. |
||
+ | -- ds:msg{ |
||
− | function i18nd:inContentLang() |
||
+ | -- key = 'message-name', |
||
− | self.tempLang = mw.language.getContentLanguage():getCode() |
||
+ | -- lang = '', |
||
− | return self |
||
+ | -- args = {...}, |
||
− | end |
||
+ | -- sources = {} |
||
− | |||
+ | -- } |
||
− | -- Datastore temporary language setter to specificed language. |
||
+ | -- |
||
− | -- @name i18nd:inLang |
||
+ | -- @usage |
||
− | -- @param {string} code Language code to use. |
||
+ | -- |
||
− | -- @return {self} Self instance. |
||
+ | -- ds:msg('message-name', ...) |
||
− | function i18nd:inLang(code) |
||
+ | -- |
||
− | self.tempLang = isValidCode(code) |
||
+ | -- @param {string|table} opts Message configuration or key. |
||
− | and code |
||
+ | -- @param[opt] {string} opts.key Message key to return from the |
||
− | or self.tempLang |
||
+ | -- datastore. |
||
− | return self |
||
+ | -- @param[opt] {table} opts.args Arguments to substitute into the |
||
− | end |
||
+ | -- message (`$n`). |
||
− | |||
+ | -- @param[opt] {table} opts.sources Source names to limit to (see |
||
− | -- Datastore temporary source setter to specificed datastore. |
||
− | -- |
+ | -- `Data:fromSources`). |
− | -- @param { |
+ | -- @param[opt] {table} opts.lang Temporary language to use (see |
− | -- |
+ | -- `Data:inLang`). |
+ | -- @param[opt] {string} ... Arguments to substitute into the message |
||
− | function i18nd:fromSource(...) |
||
+ | -- (`$n`). |
||
− | local c = select('#', ...) |
||
+ | -- @error[115] {string} 'missing arguments in Data:msg' |
||
− | if c ~= 0 then |
||
+ | -- @return {string} Localised datastore message or `'<key>'`. |
||
− | self.tempSources = {} |
||
+ | function Data:msg(opts, ...) |
||
− | 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() |
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 |
+ | 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 |
||
Line 147: | Line 148: | ||
-- 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 = 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 |
+ | 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) |
||
Line 158: | Line 159: | ||
end |
end |
||
+ | --- Datastore template parameter getter utility. |
||
− | -- Argument substitution as $n where n > 0. |
||
+ | -- This method, given a table of arguments, tries to find a parameter's |
||
− | -- @param {string} msg Message to substitute arguments into. |
||
+ | -- localized name in the datastore and returns its value, or nil if |
||
− | -- @param {table} args Arguments table to substitute. |
||
+ | -- not present. |
||
− | -- @return {string} Resulting message. |
||
+ | -- |
||
− | function handleArgs(msg, args) |
||
+ | -- This method always uses the wiki's content language. |
||
− | for i, a in ipairs(args) do |
||
+ | -- @function Data:parameter |
||
− | msg = (string.gsub(msg, '%$' .. i, a)) |
||
+ | -- @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 |
||
− | return msg |
||
end |
end |
||
+ | --- Datastore temporary source setter to a specificed subset of datastores. |
||
− | -- Checks whether a language code is valid. |
||
+ | -- By default, messages are fetched from the datastore in the same |
||
− | -- @param {string} code Language code to check |
||
+ | -- order of priority as `i18n.loadMessages`. |
||
− | -- @return {bool} Whether the language code is valid. |
||
− | function |
+ | -- @function Data:fromSource |
+ | -- @param {string} ... Source name(s) to use. |
||
− | return type(code) == 'string' and #mw.language.fetchLanguageName(code) ~= 0 |
||
+ | -- @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 |
end |
||
+ | --- Datastore default language getter. |
||
− | -- Language code function. |
||
− | -- @ |
+ | -- @function Data:getLang |
− | -- @return {string} |
+ | -- @return {string} Default language to serve datastore messages in. |
− | function |
+ | function Data:getLang() |
+ | return self.defaultLang |
||
− | local code = mw.language.getContentLanguage():getCode() |
||
− | local frame = mw.getCurrentFrame() |
||
− | local subPage = title.subpageText |
||
− | local uselang |
||
− | -- Language argument test. |
||
− | if isValidCode( ( (frame or {}).args or {}).uselang or '') then |
||
− | code = frame.args.uselang |
||
− | elseif isValidCode( ( (frame and frame.getParent(frame) or {}).args or {}).uselang or '') then |
||
− | code = frame:getParent().args.uselang |
||
− | -- Subpage language test. |
||
− | elseif title.isSubpage and isValidCode(subPage) then |
||
− | code = isValidCode(subPage) and subPage or code |
||
− | -- User language test. |
||
− | elseif frame then |
||
− | uselang = frame:preprocess('{{int:lang}}') |
||
− | code = mw.text.decode(uselang) == '<lang>' |
||
− | and code |
||
− | or uselang |
||
− | end |
||
− | return code |
||
end |
end |
||
+ | --- Datastore language setter to `wgUserLanguage`. |
||
− | -- I18n message datastore loader. |
||
+ | -- @function Data:useUserLang |
||
− | -- @param {string} source ROOTPAGENAME/path of target i18n submodule. |
||
+ | -- @return {Data} Datastore instance. |
||
− | -- @usage require('Dev:Install').loadMessages(source) |
||
+ | -- @note Scribunto only registers `wgUserLanguage` when an |
||
− | -- @raise 'no source supplied to i18n.loadMessages' |
||
+ | -- invocation is at the top of the call stack. |
||
− | -- @return {table} i18n I18n 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 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. |
||
+ | -- @usage {{i18n|getMsg|source|key|arg1|arg2|uselang {{=}} code}} |
||
+ | 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('Dev:I18n').loadMessages('1', '2') |
||
function i18n.loadMessages(...) |
function i18n.loadMessages(...) |
||
local ds |
local ds |
||
Line 219: | Line 346: | ||
-- Instantiate datastore. |
-- Instantiate datastore. |
||
ds = {} |
ds = {} |
||
− | i18nd.__index = i18nd |
||
− | setmetatable(ds, i18nd) |
||
− | -- Set default language. |
||
− | ds.defaultLang = i18n.getLang() |
||
ds._messages = {} |
ds._messages = {} |
||
+ | -- Set default language. |
||
+ | setmetatable(ds, Data) |
||
+ | ds:useUserLang() |
||
end |
end |
||
source = string.gsub(source, '^.', mw.ustring.upper) |
source = string.gsub(source, '^.', mw.ustring.upper) |
||
Line 242: | Line 368: | ||
end |
end |
||
+ | --- Language code getter. |
||
− | -- I18n message function. |
||
+ | -- Can validate a template's language code through `uselang` parameter. |
||
− | -- @param {table} frame Frame table from invocation. |
||
− | -- @ |
+ | -- @function i18n.getLang |
− | -- @ |
+ | -- @usage {{i18n|getLang|uselang {{=}} code}} |
+ | -- @return {string} Language code. |
||
− | -- @param {string} frame.args.lang Default language of message (optional). |
||
+ | function i18n.getLang() |
||
− | -- @usage {{#invoke:i18n|getMsg|source|key}} |
||
+ | local frame = mw.getCurrentFrame() or {} |
||
− | -- @raise 'missing arguments in i18n.getMsg' |
||
+ | local parentFrame = frame.getParent and frame:getParent() or {} |
||
− | -- @return {string} msg I18n message in localised language. |
||
+ | |||
− | function i18n.getMsg(frame) |
||
+ | local code = mw.language.getContentLanguage():getCode() |
||
− | if |
||
+ | local subPage = title.subpageText |
||
− | not frame or |
||
+ | |||
− | not frame.args or |
||
+ | -- Language argument test. |
||
− | not frame.args[1] or |
||
+ | local langOverride = |
||
− | not frame.args[2] |
||
+ | (frame.args or {}).uselang or |
||
− | then |
||
+ | (parentFrame.args or {}).uselang |
||
− | error('missing arguments in i18n.getMsg') |
||
+ | if _i18n.isValidCode(langOverride) then |
||
− | end |
||
− | + | code = langOverride |
|
+ | |||
− | local key = frame.args[2] |
||
− | -- |
+ | -- Subpage language test. |
+ | elseif title.isSubpage and _i18n.isValidCode(subPage) then |
||
− | local repl = {} |
||
+ | code = _i18n.isValidCode(subPage) and subPage or code |
||
− | for i, a in ipairs(frame.args) do |
||
+ | |||
− | if i >= 3 then |
||
+ | -- User language test. |
||
− | repl[i-2] = a |
||
+ | 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 |
||
end |
end |
||
+ | |||
− | -- Load message data. |
||
+ | return code |
||
− | local i18nd = i18n.loadMessages(source) |
||
− | -- Pass through language argument. |
||
− | i18nd:inLang(frame.args.lang) |
||
− | -- Return message. |
||
− | return i18nd:msg { key = key, args = repl, lang = lang } |
||
end |
end |
||
− | -- Template wrapper for [[Template:I18n]]. |
+ | --- Template wrapper for [[Template:I18n]]. |
− | -- @ |
+ | -- @function i18n.main |
− | -- @ |
+ | -- @param {table} frame Frame invocation object. |
+ | -- @return {string} Module output in template context. |
||
− | function i18n.main(frameChild) |
||
+ | -- @usage {{#invoke:i18n|main}} |
||
− | local frame = frameChild:getParent() |
||
+ | i18n.main = entrypoint(i18n) |
||
− | local args = {} |
||
− | for p, v in pairs(frame.args) 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 |
return i18n |
Latest revision as of 17:54, 27 September 2022
See also
- Module:I18n/consolefriendly
- Module:I18n/date
- Module:I18n/doc
- Module:I18n/testcases
- Module:I18n/testcases/doc
--- 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 [[I18n-js]] and i18n modules that can be loaded
-- by it are editable through [[I18nEdit]].
--
-- @module i18n
-- @version 1.4.0
-- @require Module:Entrypoint
-- @require Module:Fallbacklist
-- @author [[User:KockaAdmiralac|KockaAdmiralac]]
-- @author [[User:Speedit|Speedit]]
-- @attribution [[User:Cqm|Cqm]]
-- @release stable
-- @see [[I18n|I18n guide]]
-- @see [[I18n-js]]
-- @see [[I18nEdit]]
-- <nowiki>
local i18n, _i18n = {}, {}
-- Module variables & dependencies.
local title = mw.title.getCurrentTitle()
local fallbacks = require('Dev:Fallbacklist')
local entrypoint = require('Dev: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 [[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('<' .. key .. '>')
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.
-- @usage {{i18n|getMsg|source|key|arg1|arg2|uselang {{=}} code}}
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('Dev: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)
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
--- Language code getter.
-- Can validate a template's language code through `uselang` parameter.
-- @function i18n.getLang
-- @usage {{i18n|getLang|uselang {{=}} code}}
-- @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
--- Template wrapper for [[Template:I18n]].
-- @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>