Модуль:Languages: различия между версиями
Admin (обсуждение | вклад) м 1 версия импортирована |
Admin (обсуждение | вклад) м 1 версия импортирована |
||
| (не показана 1 промежуточная версия 1 участника) | |||
| Строка 2: | Строка 2: | ||
-- загрузка модуля данных с таблицей языков | -- загрузка модуля данных с таблицей языков | ||
-- Module:Languages/data. The expected layout of the record is: | |||
-- langData[1] → abbreviation (e.g. 'англ.') | |||
-- langData[2] → article title in Russian (e.g. 'Английский язык') | |||
-- langData[3] → prepositional form (optional) | |||
-- langData[4] → genitive form (optional) | |||
local languages = mw.loadData('Module:Languages/data') | local languages = mw.loadData('Module:Languages/data') | ||
local languagesEn = mw.loadData('Module:Languages/data-en') | |||
local wikidataLanguageCodes = mw.loadData('Module:Wikidata/Language-codes') | |||
local getArgs = require('Module:Arguments').getArgs | |||
local p = {} | local p = {} | ||
-- forward declaration so helpers above can call it | |||
local transformLangCase | |||
-- | -- константы | ||
local CATEGORY_UNKNOWN_LANG = | |||
'[[Category:Википедия:Статьи с нераспознанным языком]]' | |||
local CATEGORY_UNKNOWN_LANG_REF = | |||
'[[Category:Википедия:Статьи с нераспознанным языком (ref)]]' | |||
local CATEGORY_ALIAS_CODE = | |||
'[[Category:Википедия:Статьи с использованием алиасов языковых кодов]]' | |||
local LOG_NO_LANG_DATA = 'Language description for code %s not found' | |||
local DEFAULT_REF_CLASS = 'ref-info' | |||
local TITLE_PREFIX = 'на ' | |||
-- Returns true when the string is either nil or an empty string. | |||
-- This helper is used to guard early-exit branches and avoid | |||
-- calling mw.ustring functions on invalid values. | |||
local function isEmpty(s) | local function isEmpty(s) | ||
return s == nil or s == '' | return s == nil or s == '' | ||
end | end | ||
-- | -- Returns bracket characters based on variant type. | ||
local function | -- Parameters: | ||
return ( | -- variant - 'square' for square brackets, 'none' for no brackets, otherwise round brackets | ||
-- Returns: | |||
-- openB, closeB - opening and closing bracket characters | |||
local function getBrackets(variant) | |||
if variant == 'square' then | |||
return '[', ']' | |||
elseif variant == 'none' then | |||
return '', '' | |||
else | |||
return '(', ')' | |||
end | |||
end | |||
-- Generates HTML error message for unknown language codes or Wikidata IDs. | |||
-- Parameters: | |||
-- type - 'lang' for language codes, 'wikidata' for Wikidata IDs | |||
-- code - the unknown code/ID to display in error | |||
local function generateError(type, code) | |||
if type == 'wikidata' then | |||
return '<span class="error">Неизвестный Wikidata ID: ' .. mw.text.nowiki(code) .. '</span>' | |||
else | |||
return '<span class="error">Неизвестный языковой код: ' | |||
.. mw.text.nowiki(code) | |||
.. '. Обратитесь на [[Обсуждение модуля:Languages|специальную страницу]] для добавления данного кода.</span>' | |||
end | |||
end | |||
-- Builds a wiki link from a language record coming from | |||
-- Returns string like '[[Английский язык|англ.]]'. | |||
local function buildLanguageLink(langData) | |||
if not langData then | |||
return '' | |||
end | |||
return '[[' .. langData[2] .. '|' .. langData[1] .. ']]' | |||
end | |||
-- Builds a HTML span used as an inline language reference. | |||
-- Parameters: | |||
-- text — abbreviation to show inside brackets. | |||
-- title — tooltip text (full wording in the target case). | |||
-- variant — 'square' to use [ ... ], otherwise round ( ... ). | |||
-- includeNbsp — when false no leading NBSP is added; otherwise NBSP. | |||
-- Returns a string: ' <span class="ref-info" title="...">(англ.)</span>' | |||
local function buildRefSpan(text, title, variant, includeNbsp, className) | |||
local openB, closeB = getBrackets(variant) | |||
local prefix = (includeNbsp == false) and '' or ' ' | |||
local encodedClassName = mw.text.encode(className or DEFAULT_REF_CLASS) | |||
local encodedTitle = mw.text.encode(title or '') | |||
return prefix | |||
.. '<span class="' | |||
.. encodedClassName | |||
.. '" title="' | |||
.. TITLE_PREFIX | |||
.. encodedTitle | |||
.. '" style="cursor:help;">' | |||
.. openB | |||
.. (text or '') | |||
.. closeB | |||
.. '</span>' | |||
end | |||
-- A thin wrapper around Module:Arguments.getArgs with the | |||
-- options we use consistently across entry points: | |||
-- trim = true — trims whitespace | |||
-- removeBlanks = false — preserves explicit empty parameters | |||
local function getInvokeArgs(frame) | |||
return getArgs(frame, { trim = true, removeBlanks = false, frameOnly = true }) | |||
end | |||
-- Looks up a language record by code in Module:Languages/data. | |||
-- Normalizes case to lowercase. Returns two values: | |||
-- 1) record table or nil when code is unknown | |||
-- 2) normalized code (lowercase) | |||
local function getLanguageData(code) | |||
if isEmpty(code) then | |||
return nil | |||
end | |||
local normalized_code = mw.ustring.lower(code) | |||
local record = languages[normalized_code] | |||
if not record then -- take "parent" language name, if normalized_code is a language variant | |||
record = languages[normalized_code:match("^[^-]+")] | |||
end | |||
return record, normalized_code | |||
end | |||
-- Returns tracking category for alias code usage. | |||
-- Parameters: | |||
-- code - the language code to check | |||
-- Returns: | |||
-- category string or empty string | |||
local function getCodeCategory(code) | |||
if isEmpty(code) then | |||
return '' | |||
end | |||
local normalized = mw.ustring.lower(code) | |||
if languages.aliases[normalized] then | |||
return CATEGORY_ALIAS_CODE | |||
end | |||
return '' | |||
end | |||
-- Resolves language record and normalized code with unified error handling. | |||
-- Returns: | |||
-- l, normalized when code exists in data | |||
-- nil, nil, errorOrEmptyString when code is empty/unknown ('' or error html) | |||
local function resolveLanguage(code, showError) | |||
if isEmpty(code) then | |||
return nil, nil, '' | |||
end | |||
local l, normalized = getLanguageData(code) | |||
if l then | |||
return l, normalized | |||
end | |||
return nil, nil, (showError and generateError('lang', code) or '') | |||
end | end | ||
-- | -- Given a language code, returns: | ||
local function | -- • rendered link to the language article using its abbreviation, and | ||
-- • normalized code to be used in the 'lang' attribute. | |||
if l | -- If the code is unknown but non-empty, returns the raw code and empty lang. | ||
-- If the input is empty, returns two empty strings. | |||
local function buildLanguageLinkForCode(code) | |||
local l, normalized_code = resolveLanguage(code, false) | |||
if l then | |||
return buildLanguageLink(l), (normalized_code or '') | |||
elseif not isEmpty(code) then | |||
return code, '' | |||
else | |||
return '', '' | |||
end | end | ||
end | end | ||
function | -- Renders a ref span for a known ISO 639 code. Returns nil when code | ||
return | -- is unknown so caller can decide on a fallback strategy. | ||
local function buildRefForCode(code, variant, includeNbsp, className, mode) | |||
local l, normalized = resolveLanguage(code, false) | |||
if not l then | |||
return nil | |||
end | |||
local text = (mode == 'iso') and normalized or l[1] | |||
return buildRefSpan( | |||
text, | |||
transformLangCase(normalized or code, 'prepositional'), | |||
variant, | |||
includeNbsp, | |||
className | |||
) | |||
end | |||
-- Fallback renderer for unknown codes via Template:ref-<code>, with | |||
-- categorization. Uses 'und' when a specific template doesn't exist. | |||
local function renderUnknownRef(code, showError, refVariant, customClass, includeNbsp) | |||
if showError then | |||
return generateError('lang', code) | |||
end | |||
-- Generate HTML for unknown language reference | |||
local openB, closeB = getBrackets(refVariant) | |||
local className = customClass or DEFAULT_REF_CLASS | |||
local title = TITLE_PREFIX .. 'неопределённом языке' | |||
local text = 'неопр.' | |||
local nbsp = includeNbsp and ' ' or '' | |||
local encodedClassName = mw.text.encode(className) | |||
local encodedTitle = mw.text.encode(title) | |||
return nbsp .. '<small class="' .. encodedClassName .. '" style="cursor:help;" title="' .. encodedTitle .. '">' .. openB .. text .. closeB .. '</small>' .. CATEGORY_UNKNOWN_LANG_REF | |||
end | end | ||
function | -- Internal implementation of the language-name case transformation. | ||
local l = | -- Computes either prepositional ('на английском языке') or | ||
if l == | -- genitive ('английского языка') forms. If specific forms are | ||
mw. | -- provided in Module:Languages/data via l[3]/l[4], they win. | ||
-- Otherwise, applies a set of heuristics to generate the forms. | |||
local function _transformLangCaseImpl(code, case) | |||
-- Resolve the language record; unknown code gets a readable placeholder | |||
local l = getLanguageData(code) | |||
if not l then | |||
return '<неизвестный код ' .. code .. '>' | |||
end | |||
-- Prefer explicit forms from data when present: | |||
-- l[3] = prepositional, l[4] = genitive | |||
if case == 'prepositional' and l[3] then | |||
return l[3] | |||
end | |||
if case == 'genitive' and l[4] then | |||
return l[4] | |||
end | |||
-- Derive a base noun phrase: | |||
-- 1) take explicit Russian title if present (l[2]), | |||
-- otherwise fetch the language name via mw.language | |||
-- 2) lowercase everything for stable pattern processing | |||
-- 3) strip the word "язык" and optional parenthetical "(язык)" | |||
local ln = mw.ustring | |||
.lower(l[2] or mw.language.fetchLanguageName(code, 'ru')) | |||
:gsub('%s+язык%s+', ' ') | |||
:gsub('%s*%(?язык%)?%s*', '') | |||
if not ln then | |||
-- Final fallback string when even a base name is unavailable | |||
local fallback = ( | |||
case == 'prepositional' and ('языке с ISO-кодом ' .. code .. ' (?)') | |||
or ('языка с ISO-кодом ' .. code .. ' (?)') | |||
) | |||
return fallback | |||
end | |||
-- Special-case: names ending with "лингва" (e.g., "линга франка") | |||
-- only need a final vowel change: а → е/ы depending on target case | |||
if ln:match('.*лингва$') then | |||
local r = ln:gsub('а$', (case == 'prepositional' and 'е' or 'ы')) | |||
return r | |||
end | |||
-- Adjectival language names ("...ский", "...цкий", "...ный"), | |||
-- optionally followed by a parenthetical qualifier, require | |||
-- morphological agreement. The rules below implement common | |||
-- Russian inflection patterns: | |||
-- - "нЫЙ/нИЙ" → "нОМ/нЕМ" (prep.) or "нОГО/нЕГО" (gen.) | |||
-- - "СКИЙ/ЦКИЙ" → "СКОМ/ЦКОМ" (prep.) or "СКОГО/ЦКОГО" (gen.) | |||
if | |||
mw.ustring.match(ln, '[сц]кий$') | |||
or ln:match('ный$') | |||
or mw.ustring.match(ln, '[сц]кий%s%b()$') | |||
or ln:match('ный%s%b()$') | |||
then | |||
ln = mw.ustring | |||
.gsub( | |||
mw.ustring | |||
.gsub(ln, 'н([ыи])й(%A)', function(y, s) | |||
-- "нЫЙ/нИЙ" endings: pick target case and keep following separator | |||
return ( | |||
case == 'prepositional' and (y == 'ы' and 'ном' or 'нем') | |||
or (y == 'ы' and 'ного' or 'него') | |||
) .. s | |||
end) | |||
:gsub('ный$', (case == 'prepositional' and 'ном' or 'ного')), | |||
'([сц]к)ий(%A)', | |||
(case == 'prepositional' and '%1ом%2' or '%1ого%2') | |||
) | |||
:gsub('ский$', (case == 'prepositional' and 'ском' or 'ского')) | |||
:gsub('цкий$', (case == 'prepositional' and 'цком' or 'цкого')) | |||
-- If the name ends with a qualifier in parentheses and the case ending | |||
-- did not land immediately before the closing paren, append | |||
-- "языке/языка" before the qualifier to keep a natural phrasing. | |||
local hasClosingParen = ln:match('%)$') ~= nil | |||
local endsWithCase1 = ln:match(case == 'prepositional' and 'ом%)$' or 'ого%)$') ~= nil | |||
local endsWithCase2 = ln:match(case == 'prepositional' and 'нем%)$' or 'него%)$') | |||
~= nil | |||
if hasClosingParen and not (endsWithCase1 or endsWithCase2) then | |||
local r, s = ln:gsub( | |||
'(%s)(%b())$', | |||
(case == 'prepositional' and '%1языке%1%2' or '%1языка%1%2') | |||
) | |||
if s == 1 then | |||
return r | |||
end | |||
end | |||
-- Default phrasing for adjectival names | |||
local result = (case == 'prepositional' and (ln .. ' языке') or (ln .. ' языка')) | |||
return result | |||
else | |||
-- Non‑adjectival names (base nouns): prepend "языке/языка" | |||
local result = (case == 'prepositional' and ('языке ' .. ln) or ('языка ' .. ln)) | |||
return result | |||
end | end | ||
end | end | ||
function p.getWikidataRefHtmlFrame( frame ) | -- Public wrapper kept for backwards compatibility with existing | ||
return p.getWikidataRefHtml( | -- callers in this module and other modules. Do not move above | ||
-- function definitions to avoid upvalue shadowing. | |||
function transformLangCase(code, case) | |||
return _transformLangCaseImpl(code, case) | |||
end | |||
-- export for other modules that `require("Module:Languages").transformLangCase` | |||
p.transformLangCase = transformLangCase | |||
-- P U B L I C E N T R Y P O I N T S | |||
-- Entry point: builds a ref span by Wikidata item id coming from #invoke. | |||
-- Delegates to p.getWikidataRefHtml after parsing the argument. | |||
function p.getWikidataRefHtmlFrame(frame) | |||
local args = getInvokeArgs(frame) | |||
local showError = args['error'] and true or false | |||
return p.getWikidataRefHtml(args[1] or '', showError) | |||
end | end | ||
-- Builds a ref span from a Wikidata item id of a language. | |||
-- Uses a preloaded mapping Module:Wikidata/Language-codes to | |||
local code = | -- resolve item → ISO 639 code, then renders the standard ref span. | ||
function p.getWikidataRefHtml(wikidataItemId, showError) | |||
local code = wikidataLanguageCodes[wikidataItemId] | |||
if code == nil then | if code == nil then | ||
mw.log( 'Language code not found for ' .. wikidataItemId ) | if showError then | ||
return | return generateError('wikidata', wikidataItemId) | ||
end | |||
mw.log('Language code not found for ' .. wikidataItemId) | |||
return '' | |||
end | |||
local rendered = buildRefForCode(code, nil, false, nil, '') | |||
if not rendered then | |||
if showError then | |||
return generateError('lang', code) | |||
end | |||
mw.log(string.format(LOG_NO_LANG_DATA, tostring(code))) | |||
return '' | |||
end | end | ||
return | -- Добавляем категорию если код является алиасом | ||
return rendered .. getCodeCategory(code) | |||
end | end | ||
-- | -- Entry point: returns a language abbreviation by ISO 639 code. | ||
-- Unknown/empty codes produce an empty string. | |||
function p.abbr(frame) | function p.abbr(frame) | ||
local | local args = getInvokeArgs(frame) | ||
local code = args[1] or '' | |||
return | local showError = args['error'] and true or false | ||
local l, _normalized, err = resolveLanguage(code, showError) | |||
if l then | |||
return l[1] .. getCodeCategory(code) | |||
end | end | ||
return err or '' | |||
end | end | ||
-- | -- Entry point: returns the Russian article title (full language name) | ||
-- by ISO 639 code. Unknown/empty codes produce an empty string. | |||
-- When output=en, returns English language name from data_en. | |||
function p.name(frame) | function p.name(frame) | ||
local | local args = getInvokeArgs(frame) | ||
if code | local code = args[1] or '' | ||
local mode = args['mode'] or '' | |||
local showError = args['error'] and true or false | |||
if not isEmpty(code) then | |||
if mode == 'en' then | |||
local _, normalized = getLanguageData(code) | |||
local l = normalized and languagesEn[normalized] or nil | |||
if l then | |||
return l[1] .. getCodeCategory(code) | |||
elseif showError then | |||
return generateError('lang', code) | |||
end | |||
else | |||
local l = getLanguageData(code) | |||
if l then | |||
return l[2] .. getCodeCategory(code) | |||
elseif showError then | |||
return generateError('lang', code) | |||
end | |||
end | |||
end | end | ||
return '' | |||
end | end | ||
-- | -- TODO: Вынести в отдельный модуль, так как логика сложная не для этого модуля, тут не только работа с языками | ||
-- | |||
-- Entry point: builds a comma-separated list of pairs | |||
-- (language link + supplied text). The input is a flat sequence: | |||
-- code1 | text1 | code2 | text2 | ... | |||
-- If text for the last code is missing, only the language link is emitted. | |||
-- Unknown codes are marked with a category and wrapped in a span. | |||
function p.list(frame) | function p.list(frame) | ||
local args = getInvokeArgs(frame) | |||
local curr_lang = nil | |||
local noItalic = args['noItalic'] | |||
local parts = {} | |||
local categories = {} | |||
-- Итерируемся по позиционным аргументам по порядку | |||
local | local maxIndex = 0 | ||
for k, _ in pairs(args) do | |||
if type(k) == 'number' and k > maxIndex then | |||
maxIndex = k | |||
end | |||
end | |||
-- Use ipairs for sequential iteration, but handle gaps by checking maxIndex | |||
for i = 1, maxIndex do | |||
local v = args[i] | |||
local value = v or '' | |||
if curr_lang == nil then | if curr_lang == nil then | ||
if value ~= '' then | |||
curr_lang = value | |||
end | |||
else | else | ||
if value ~= '' then | |||
local link, lang_code = buildLanguageLinkForCode(curr_lang) | |||
local category = getCodeCategory(curr_lang) | |||
if category ~= '' then | |||
categories[category] = true | |||
end | |||
local list_item | local list_item | ||
if lang_code ~= '' then | if lang_code ~= '' then | ||
--- Temporary fix for [[Шаблон:L6e]] (see [[Служебная:Постоянная ссылка/151687926#Опять наинжинерили]]) | |||
local italic = noItalic == nil and not languages.no_italics[lang_code] or false | |||
local style = italic and "style='font-style:italic'" or '' | |||
list_item = link | |||
.. " <span dir='auto'" .. style .. "lang='" | |||
.. lang_code | |||
.. "'>" | |||
.. value | |||
.. '</span>' | |||
else | |||
list_item = link | |||
.. " <span class='unknown-foreign-lang'>" | |||
.. value | |||
.. '</span>' | |||
.. CATEGORY_UNKNOWN_LANG | |||
end | |||
table.insert(parts, list_item) | |||
else | else | ||
local link, _ = buildLanguageLinkForCode(curr_lang) | |||
local category = getCodeCategory(curr_lang) | |||
if category ~= '' then | |||
categories[category] = true | |||
end | |||
table.insert(parts, link) | |||
end | end | ||
curr_lang = nil | curr_lang = nil | ||
end | end | ||
end | end | ||
if curr_lang ~= nil then | if curr_lang ~= nil then | ||
local link, _ = buildLanguageLinkForCode(curr_lang) | |||
local category = getCodeCategory(curr_lang) | |||
if category ~= '' then | |||
categories[category] = true | |||
end | |||
table.insert(parts, link) | |||
end | |||
if #parts == 0 then | |||
return nil | |||
end | |||
local result = table.concat(parts, ', ') | |||
for cat, _ in pairs(categories) do | |||
result = result .. cat | |||
end | |||
return result | |||
end | end | ||
-- | -- Entry point: builds a sequence of inline language references | ||
-- for the provided ISO 639 codes. Positional args are codes. | |||
-- When parameter 'в' is present, square brackets are used. | |||
-- For unknown codes tries Template:ref-<code>, otherwise uses 'und'. | |||
function p.list_ref(frame) | function p.list_ref(frame) | ||
local | local args = getInvokeArgs(frame) | ||
local | local brackets = args['brackets'] or args['в'] | ||
local | local mode = args['mode'] or '' | ||
local showError = args['error'] and true or false | |||
if | local includeNbsp = args['includeNbsp'] ~= 'false' and args['includeNbsp'] ~= false | ||
local refVariant | |||
if brackets == 'none' then | |||
refVariant = 'none' | |||
elseif brackets == nil or brackets == '' or brackets == '()' then | |||
refVariant = nil -- round | |||
elseif brackets == '[]' or brackets == 'в' or brackets == 'square' or brackets == '1' or brackets == true then | |||
refVariant = 'square' | |||
else | else | ||
refVariant = nil | |||
end | end | ||
for | -- We rely strictly on named params for brackets/mode; only numeric args are language codes. | ||
local code = | -- But we need to filter out positional args that were used as options | ||
local | local chunks = {} | ||
if l | local customClass = args['class'] or nil | ||
local join = not isEmpty(args['join']) | |||
local joinTexts, joinTitles = {}, {} | |||
local categories = {} | |||
-- Process positional arguments as language codes | |||
-- Use ipairs to iterate through args, but skip known named parameters | |||
for idx, code in ipairs(args) do | |||
-- Skip if this is a known named parameter | |||
local isNamedParam = false | |||
if code == brackets or code == tostring(brackets) then | |||
isNamedParam = true | |||
elseif code == mode then | |||
isNamedParam = true | |||
elseif code == customClass then | |||
isNamedParam = true | |||
elseif code == 'join' or code == 'error' or code == 'class' then | |||
isNamedParam = true | |||
end | |||
if not isNamedParam and code and not isEmpty(code) then | |||
local normalized = mw.ustring.lower(code) | |||
local category = getCodeCategory(normalized) | |||
if category ~= '' then | |||
categories[category] = true | |||
end | |||
if join then | |||
local l = getLanguageData(normalized) | |||
if l then | |||
local text = mode == 'iso' and normalized or l[1] | |||
table.insert(joinTexts, text) | |||
table.insert(joinTitles, transformLangCase(normalized, 'prepositional')) | |||
else | |||
table.insert(chunks, renderUnknownRef(normalized, showError, refVariant, customClass, includeNbsp)) | |||
end | |||
else | else | ||
local rendered = buildRefForCode(normalized, refVariant, includeNbsp, customClass, mode) | |||
table.insert(chunks, rendered or renderUnknownRef(normalized, showError, refVariant, customClass, includeNbsp)) | |||
end | end | ||
end | end | ||
end | end | ||
return result | if join and #joinTexts > 0 then | ||
local text = table.concat(joinTexts, ', ') | |||
local title = table.concat(joinTitles, ', ') | |||
table.insert(chunks, buildRefSpan(text, title, refVariant, includeNbsp, customClass)) | |||
end | |||
local result = table.concat(chunks) | |||
for cat, _ in pairs(categories) do | |||
result = result .. cat | |||
end | |||
return result | |||
end | end | ||
-- | -- args: | ||
function p. | -- 1 — language code | ||
-- case|form — 'l2'|'l3'|'l4' or 'nom'|'gen'|'prep' or 'р'|'п' (default 'l2') | |||
-- 'р' → genitive, 'п' → prepositional | |||
-- mode — 'en' or 'iso' to override language output | |||
-- error — when truthy, returns error html for unknown code | |||
function p.transform(frame) | |||
local args = getInvokeArgs(frame) | |||
local code = args[1] or '' | |||
-- Treat empty strings as absent to ensure proper fallback to 'l2' | |||
local form = args['case'] | |||
if isEmpty(form) then | |||
form = args['form'] | |||
end | |||
if isEmpty(form) then | |||
form = 'l2' | |||
end | |||
local mode = args['mode'] or '' | |||
local showError = args['error'] and true or false | |||
if isEmpty(code) then | |||
return '' | |||
end | |||
local l, normalized = getLanguageData(code) | |||
local normCode = normalized or '' | |||
local category = getCodeCategory(code) | |||
-- Output overrides | |||
if mode == 'iso' then | |||
return normCode .. category | |||
elseif mode == 'en' then | |||
local e = languagesEn[normCode] | |||
if e then | |||
return e[1] .. category | |||
end | |||
return showError and generateError('lang', code) or '' | |||
end | |||
-- Russian forms | |||
if not l then | |||
return showError and generateError('lang', code) or '' | |||
end | |||
local f = mw.ustring.lower(form) | |||
if f == 'l2' or f == 'nom' or f == 'nominative' then | |||
return (l[2] and mw.ustring.lower(l[2]) or '') .. category | |||
elseif f == 'l3' or f == 'gen' or f == 'genitive' or f == 'р' then | |||
return transformLangCase(normCode, 'genitive') .. category | |||
elseif f == 'l4' or f == 'prep' or f == 'prepositional' or f == 'п' then | |||
return transformLangCase(normCode, 'prepositional') .. category | |||
else | |||
-- default to nominative (lowercase) | |||
return (l[2] and mw.ustring.lower(l[2]) or '') .. category | |||
end | end | ||
end | end | ||
-- Entry point: returns the prepositional form for a language code. | |||
function p.transform_lang(frame) | function p.transform_lang(frame) | ||
return p. | local args = getInvokeArgs(frame) | ||
local code = args[1] or '' | |||
local showError = args['error'] and true or false | |||
local l, normalized, err = resolveLanguage(code, showError) | |||
if l then | |||
return transformLangCase(normalized or code, 'prepositional') .. getCodeCategory(code) | |||
end | |||
return err or '' | |||
end | |||
-- Entry point: returns the genitive form for a language code. | |||
function p.transform_lang_genitive(frame) | |||
local args = getInvokeArgs(frame) | |||
local code = args[1] or '' | |||
local showError = args['error'] and true or false | |||
local l, normalized, err = resolveLanguage(code, showError) | |||
if l then | |||
return transformLangCase(normalized or code, 'genitive') .. getCodeCategory(code) | |||
end | |||
return err or '' | |||
end | end | ||
return p | return p | ||
Текущая версия от 14:35, 22 июня 2026
Для документации этого модуля может быть создана страница Модуль:Languages/doc
-- Модуль для работы с языками ISO 639
-- загрузка модуля данных с таблицей языков
-- Module:Languages/data. The expected layout of the record is:
-- langData[1] → abbreviation (e.g. 'англ.')
-- langData[2] → article title in Russian (e.g. 'Английский язык')
-- langData[3] → prepositional form (optional)
-- langData[4] → genitive form (optional)
local languages = mw.loadData('Module:Languages/data')
local languagesEn = mw.loadData('Module:Languages/data-en')
local wikidataLanguageCodes = mw.loadData('Module:Wikidata/Language-codes')
local getArgs = require('Module:Arguments').getArgs
local p = {}
-- forward declaration so helpers above can call it
local transformLangCase
-- константы
local CATEGORY_UNKNOWN_LANG =
'[[Category:Википедия:Статьи с нераспознанным языком]]'
local CATEGORY_UNKNOWN_LANG_REF =
'[[Category:Википедия:Статьи с нераспознанным языком (ref)]]'
local CATEGORY_ALIAS_CODE =
'[[Category:Википедия:Статьи с использованием алиасов языковых кодов]]'
local LOG_NO_LANG_DATA = 'Language description for code %s not found'
local DEFAULT_REF_CLASS = 'ref-info'
local TITLE_PREFIX = 'на '
-- Returns true when the string is either nil or an empty string.
-- This helper is used to guard early-exit branches and avoid
-- calling mw.ustring functions on invalid values.
local function isEmpty(s)
return s == nil or s == ''
end
-- Returns bracket characters based on variant type.
-- Parameters:
-- variant - 'square' for square brackets, 'none' for no brackets, otherwise round brackets
-- Returns:
-- openB, closeB - opening and closing bracket characters
local function getBrackets(variant)
if variant == 'square' then
return '[', ']'
elseif variant == 'none' then
return '', ''
else
return '(', ')'
end
end
-- Generates HTML error message for unknown language codes or Wikidata IDs.
-- Parameters:
-- type - 'lang' for language codes, 'wikidata' for Wikidata IDs
-- code - the unknown code/ID to display in error
local function generateError(type, code)
if type == 'wikidata' then
return '<span class="error">Неизвестный Wikidata ID: ' .. mw.text.nowiki(code) .. '</span>'
else
return '<span class="error">Неизвестный языковой код: '
.. mw.text.nowiki(code)
.. '. Обратитесь на [[Обсуждение модуля:Languages|специальную страницу]] для добавления данного кода.</span>'
end
end
-- Builds a wiki link from a language record coming from
-- Returns string like '[[Английский язык|англ.]]'.
local function buildLanguageLink(langData)
if not langData then
return ''
end
return '[[' .. langData[2] .. '|' .. langData[1] .. ']]'
end
-- Builds a HTML span used as an inline language reference.
-- Parameters:
-- text — abbreviation to show inside brackets.
-- title — tooltip text (full wording in the target case).
-- variant — 'square' to use [ ... ], otherwise round ( ... ).
-- includeNbsp — when false no leading NBSP is added; otherwise NBSP.
-- Returns a string: ' <span class="ref-info" title="...">(англ.)</span>'
local function buildRefSpan(text, title, variant, includeNbsp, className)
local openB, closeB = getBrackets(variant)
local prefix = (includeNbsp == false) and '' or ' '
local encodedClassName = mw.text.encode(className or DEFAULT_REF_CLASS)
local encodedTitle = mw.text.encode(title or '')
return prefix
.. '<span class="'
.. encodedClassName
.. '" title="'
.. TITLE_PREFIX
.. encodedTitle
.. '" style="cursor:help;">'
.. openB
.. (text or '')
.. closeB
.. '</span>'
end
-- A thin wrapper around Module:Arguments.getArgs with the
-- options we use consistently across entry points:
-- trim = true — trims whitespace
-- removeBlanks = false — preserves explicit empty parameters
local function getInvokeArgs(frame)
return getArgs(frame, { trim = true, removeBlanks = false, frameOnly = true })
end
-- Looks up a language record by code in Module:Languages/data.
-- Normalizes case to lowercase. Returns two values:
-- 1) record table or nil when code is unknown
-- 2) normalized code (lowercase)
local function getLanguageData(code)
if isEmpty(code) then
return nil
end
local normalized_code = mw.ustring.lower(code)
local record = languages[normalized_code]
if not record then -- take "parent" language name, if normalized_code is a language variant
record = languages[normalized_code:match("^[^-]+")]
end
return record, normalized_code
end
-- Returns tracking category for alias code usage.
-- Parameters:
-- code - the language code to check
-- Returns:
-- category string or empty string
local function getCodeCategory(code)
if isEmpty(code) then
return ''
end
local normalized = mw.ustring.lower(code)
if languages.aliases[normalized] then
return CATEGORY_ALIAS_CODE
end
return ''
end
-- Resolves language record and normalized code with unified error handling.
-- Returns:
-- l, normalized when code exists in data
-- nil, nil, errorOrEmptyString when code is empty/unknown ('' or error html)
local function resolveLanguage(code, showError)
if isEmpty(code) then
return nil, nil, ''
end
local l, normalized = getLanguageData(code)
if l then
return l, normalized
end
return nil, nil, (showError and generateError('lang', code) or '')
end
-- Given a language code, returns:
-- • rendered link to the language article using its abbreviation, and
-- • normalized code to be used in the 'lang' attribute.
-- If the code is unknown but non-empty, returns the raw code and empty lang.
-- If the input is empty, returns two empty strings.
local function buildLanguageLinkForCode(code)
local l, normalized_code = resolveLanguage(code, false)
if l then
return buildLanguageLink(l), (normalized_code or '')
elseif not isEmpty(code) then
return code, ''
else
return '', ''
end
end
-- Renders a ref span for a known ISO 639 code. Returns nil when code
-- is unknown so caller can decide on a fallback strategy.
local function buildRefForCode(code, variant, includeNbsp, className, mode)
local l, normalized = resolveLanguage(code, false)
if not l then
return nil
end
local text = (mode == 'iso') and normalized or l[1]
return buildRefSpan(
text,
transformLangCase(normalized or code, 'prepositional'),
variant,
includeNbsp,
className
)
end
-- Fallback renderer for unknown codes via Template:ref-<code>, with
-- categorization. Uses 'und' when a specific template doesn't exist.
local function renderUnknownRef(code, showError, refVariant, customClass, includeNbsp)
if showError then
return generateError('lang', code)
end
-- Generate HTML for unknown language reference
local openB, closeB = getBrackets(refVariant)
local className = customClass or DEFAULT_REF_CLASS
local title = TITLE_PREFIX .. 'неопределённом языке'
local text = 'неопр.'
local nbsp = includeNbsp and ' ' or ''
local encodedClassName = mw.text.encode(className)
local encodedTitle = mw.text.encode(title)
return nbsp .. '<small class="' .. encodedClassName .. '" style="cursor:help;" title="' .. encodedTitle .. '">' .. openB .. text .. closeB .. '</small>' .. CATEGORY_UNKNOWN_LANG_REF
end
-- Internal implementation of the language-name case transformation.
-- Computes either prepositional ('на английском языке') or
-- genitive ('английского языка') forms. If specific forms are
-- provided in Module:Languages/data via l[3]/l[4], they win.
-- Otherwise, applies a set of heuristics to generate the forms.
local function _transformLangCaseImpl(code, case)
-- Resolve the language record; unknown code gets a readable placeholder
local l = getLanguageData(code)
if not l then
return '<неизвестный код ' .. code .. '>'
end
-- Prefer explicit forms from data when present:
-- l[3] = prepositional, l[4] = genitive
if case == 'prepositional' and l[3] then
return l[3]
end
if case == 'genitive' and l[4] then
return l[4]
end
-- Derive a base noun phrase:
-- 1) take explicit Russian title if present (l[2]),
-- otherwise fetch the language name via mw.language
-- 2) lowercase everything for stable pattern processing
-- 3) strip the word "язык" and optional parenthetical "(язык)"
local ln = mw.ustring
.lower(l[2] or mw.language.fetchLanguageName(code, 'ru'))
:gsub('%s+язык%s+', ' ')
:gsub('%s*%(?язык%)?%s*', '')
if not ln then
-- Final fallback string when even a base name is unavailable
local fallback = (
case == 'prepositional' and ('языке с ISO-кодом ' .. code .. ' (?)')
or ('языка с ISO-кодом ' .. code .. ' (?)')
)
return fallback
end
-- Special-case: names ending with "лингва" (e.g., "линга франка")
-- only need a final vowel change: а → е/ы depending on target case
if ln:match('.*лингва$') then
local r = ln:gsub('а$', (case == 'prepositional' and 'е' or 'ы'))
return r
end
-- Adjectival language names ("...ский", "...цкий", "...ный"),
-- optionally followed by a parenthetical qualifier, require
-- morphological agreement. The rules below implement common
-- Russian inflection patterns:
-- - "нЫЙ/нИЙ" → "нОМ/нЕМ" (prep.) or "нОГО/нЕГО" (gen.)
-- - "СКИЙ/ЦКИЙ" → "СКОМ/ЦКОМ" (prep.) or "СКОГО/ЦКОГО" (gen.)
if
mw.ustring.match(ln, '[сц]кий$')
or ln:match('ный$')
or mw.ustring.match(ln, '[сц]кий%s%b()$')
or ln:match('ный%s%b()$')
then
ln = mw.ustring
.gsub(
mw.ustring
.gsub(ln, 'н([ыи])й(%A)', function(y, s)
-- "нЫЙ/нИЙ" endings: pick target case and keep following separator
return (
case == 'prepositional' and (y == 'ы' and 'ном' or 'нем')
or (y == 'ы' and 'ного' or 'него')
) .. s
end)
:gsub('ный$', (case == 'prepositional' and 'ном' or 'ного')),
'([сц]к)ий(%A)',
(case == 'prepositional' and '%1ом%2' or '%1ого%2')
)
:gsub('ский$', (case == 'prepositional' and 'ском' or 'ского'))
:gsub('цкий$', (case == 'prepositional' and 'цком' or 'цкого'))
-- If the name ends with a qualifier in parentheses and the case ending
-- did not land immediately before the closing paren, append
-- "языке/языка" before the qualifier to keep a natural phrasing.
local hasClosingParen = ln:match('%)$') ~= nil
local endsWithCase1 = ln:match(case == 'prepositional' and 'ом%)$' or 'ого%)$') ~= nil
local endsWithCase2 = ln:match(case == 'prepositional' and 'нем%)$' or 'него%)$')
~= nil
if hasClosingParen and not (endsWithCase1 or endsWithCase2) then
local r, s = ln:gsub(
'(%s)(%b())$',
(case == 'prepositional' and '%1языке%1%2' or '%1языка%1%2')
)
if s == 1 then
return r
end
end
-- Default phrasing for adjectival names
local result = (case == 'prepositional' and (ln .. ' языке') or (ln .. ' языка'))
return result
else
-- Non‑adjectival names (base nouns): prepend "языке/языка"
local result = (case == 'prepositional' and ('языке ' .. ln) or ('языка ' .. ln))
return result
end
end
-- Public wrapper kept for backwards compatibility with existing
-- callers in this module and other modules. Do not move above
-- function definitions to avoid upvalue shadowing.
function transformLangCase(code, case)
return _transformLangCaseImpl(code, case)
end
-- export for other modules that `require("Module:Languages").transformLangCase`
p.transformLangCase = transformLangCase
-- P U B L I C E N T R Y P O I N T S
-- Entry point: builds a ref span by Wikidata item id coming from #invoke.
-- Delegates to p.getWikidataRefHtml after parsing the argument.
function p.getWikidataRefHtmlFrame(frame)
local args = getInvokeArgs(frame)
local showError = args['error'] and true or false
return p.getWikidataRefHtml(args[1] or '', showError)
end
-- Builds a ref span from a Wikidata item id of a language.
-- Uses a preloaded mapping Module:Wikidata/Language-codes to
-- resolve item → ISO 639 code, then renders the standard ref span.
function p.getWikidataRefHtml(wikidataItemId, showError)
local code = wikidataLanguageCodes[wikidataItemId]
if code == nil then
if showError then
return generateError('wikidata', wikidataItemId)
end
mw.log('Language code not found for ' .. wikidataItemId)
return ''
end
local rendered = buildRefForCode(code, nil, false, nil, '')
if not rendered then
if showError then
return generateError('lang', code)
end
mw.log(string.format(LOG_NO_LANG_DATA, tostring(code)))
return ''
end
-- Добавляем категорию если код является алиасом
return rendered .. getCodeCategory(code)
end
-- Entry point: returns a language abbreviation by ISO 639 code.
-- Unknown/empty codes produce an empty string.
function p.abbr(frame)
local args = getInvokeArgs(frame)
local code = args[1] or ''
local showError = args['error'] and true or false
local l, _normalized, err = resolveLanguage(code, showError)
if l then
return l[1] .. getCodeCategory(code)
end
return err or ''
end
-- Entry point: returns the Russian article title (full language name)
-- by ISO 639 code. Unknown/empty codes produce an empty string.
-- When output=en, returns English language name from data_en.
function p.name(frame)
local args = getInvokeArgs(frame)
local code = args[1] or ''
local mode = args['mode'] or ''
local showError = args['error'] and true or false
if not isEmpty(code) then
if mode == 'en' then
local _, normalized = getLanguageData(code)
local l = normalized and languagesEn[normalized] or nil
if l then
return l[1] .. getCodeCategory(code)
elseif showError then
return generateError('lang', code)
end
else
local l = getLanguageData(code)
if l then
return l[2] .. getCodeCategory(code)
elseif showError then
return generateError('lang', code)
end
end
end
return ''
end
-- TODO: Вынести в отдельный модуль, так как логика сложная не для этого модуля, тут не только работа с языками
--
-- Entry point: builds a comma-separated list of pairs
-- (language link + supplied text). The input is a flat sequence:
-- code1 | text1 | code2 | text2 | ...
-- If text for the last code is missing, only the language link is emitted.
-- Unknown codes are marked with a category and wrapped in a span.
function p.list(frame)
local args = getInvokeArgs(frame)
local curr_lang = nil
local noItalic = args['noItalic']
local parts = {}
local categories = {}
-- Итерируемся по позиционным аргументам по порядку
local maxIndex = 0
for k, _ in pairs(args) do
if type(k) == 'number' and k > maxIndex then
maxIndex = k
end
end
-- Use ipairs for sequential iteration, but handle gaps by checking maxIndex
for i = 1, maxIndex do
local v = args[i]
local value = v or ''
if curr_lang == nil then
if value ~= '' then
curr_lang = value
end
else
if value ~= '' then
local link, lang_code = buildLanguageLinkForCode(curr_lang)
local category = getCodeCategory(curr_lang)
if category ~= '' then
categories[category] = true
end
local list_item
if lang_code ~= '' then
--- Temporary fix for [[Шаблон:L6e]] (see [[Служебная:Постоянная ссылка/151687926#Опять наинжинерили]])
local italic = noItalic == nil and not languages.no_italics[lang_code] or false
local style = italic and "style='font-style:italic'" or ''
list_item = link
.. " <span dir='auto'" .. style .. "lang='"
.. lang_code
.. "'>"
.. value
.. '</span>'
else
list_item = link
.. " <span class='unknown-foreign-lang'>"
.. value
.. '</span>'
.. CATEGORY_UNKNOWN_LANG
end
table.insert(parts, list_item)
else
local link, _ = buildLanguageLinkForCode(curr_lang)
local category = getCodeCategory(curr_lang)
if category ~= '' then
categories[category] = true
end
table.insert(parts, link)
end
curr_lang = nil
end
end
if curr_lang ~= nil then
local link, _ = buildLanguageLinkForCode(curr_lang)
local category = getCodeCategory(curr_lang)
if category ~= '' then
categories[category] = true
end
table.insert(parts, link)
end
if #parts == 0 then
return nil
end
local result = table.concat(parts, ', ')
for cat, _ in pairs(categories) do
result = result .. cat
end
return result
end
-- Entry point: builds a sequence of inline language references
-- for the provided ISO 639 codes. Positional args are codes.
-- When parameter 'в' is present, square brackets are used.
-- For unknown codes tries Template:ref-<code>, otherwise uses 'und'.
function p.list_ref(frame)
local args = getInvokeArgs(frame)
local brackets = args['brackets'] or args['в']
local mode = args['mode'] or ''
local showError = args['error'] and true or false
local includeNbsp = args['includeNbsp'] ~= 'false' and args['includeNbsp'] ~= false
local refVariant
if brackets == 'none' then
refVariant = 'none'
elseif brackets == nil or brackets == '' or brackets == '()' then
refVariant = nil -- round
elseif brackets == '[]' or brackets == 'в' or brackets == 'square' or brackets == '1' or brackets == true then
refVariant = 'square'
else
refVariant = nil
end
-- We rely strictly on named params for brackets/mode; only numeric args are language codes.
-- But we need to filter out positional args that were used as options
local chunks = {}
local customClass = args['class'] or nil
local join = not isEmpty(args['join'])
local joinTexts, joinTitles = {}, {}
local categories = {}
-- Process positional arguments as language codes
-- Use ipairs to iterate through args, but skip known named parameters
for idx, code in ipairs(args) do
-- Skip if this is a known named parameter
local isNamedParam = false
if code == brackets or code == tostring(brackets) then
isNamedParam = true
elseif code == mode then
isNamedParam = true
elseif code == customClass then
isNamedParam = true
elseif code == 'join' or code == 'error' or code == 'class' then
isNamedParam = true
end
if not isNamedParam and code and not isEmpty(code) then
local normalized = mw.ustring.lower(code)
local category = getCodeCategory(normalized)
if category ~= '' then
categories[category] = true
end
if join then
local l = getLanguageData(normalized)
if l then
local text = mode == 'iso' and normalized or l[1]
table.insert(joinTexts, text)
table.insert(joinTitles, transformLangCase(normalized, 'prepositional'))
else
table.insert(chunks, renderUnknownRef(normalized, showError, refVariant, customClass, includeNbsp))
end
else
local rendered = buildRefForCode(normalized, refVariant, includeNbsp, customClass, mode)
table.insert(chunks, rendered or renderUnknownRef(normalized, showError, refVariant, customClass, includeNbsp))
end
end
end
if join and #joinTexts > 0 then
local text = table.concat(joinTexts, ', ')
local title = table.concat(joinTitles, ', ')
table.insert(chunks, buildRefSpan(text, title, refVariant, includeNbsp, customClass))
end
local result = table.concat(chunks)
for cat, _ in pairs(categories) do
result = result .. cat
end
return result
end
-- args:
-- 1 — language code
-- case|form — 'l2'|'l3'|'l4' or 'nom'|'gen'|'prep' or 'р'|'п' (default 'l2')
-- 'р' → genitive, 'п' → prepositional
-- mode — 'en' or 'iso' to override language output
-- error — when truthy, returns error html for unknown code
function p.transform(frame)
local args = getInvokeArgs(frame)
local code = args[1] or ''
-- Treat empty strings as absent to ensure proper fallback to 'l2'
local form = args['case']
if isEmpty(form) then
form = args['form']
end
if isEmpty(form) then
form = 'l2'
end
local mode = args['mode'] or ''
local showError = args['error'] and true or false
if isEmpty(code) then
return ''
end
local l, normalized = getLanguageData(code)
local normCode = normalized or ''
local category = getCodeCategory(code)
-- Output overrides
if mode == 'iso' then
return normCode .. category
elseif mode == 'en' then
local e = languagesEn[normCode]
if e then
return e[1] .. category
end
return showError and generateError('lang', code) or ''
end
-- Russian forms
if not l then
return showError and generateError('lang', code) or ''
end
local f = mw.ustring.lower(form)
if f == 'l2' or f == 'nom' or f == 'nominative' then
return (l[2] and mw.ustring.lower(l[2]) or '') .. category
elseif f == 'l3' or f == 'gen' or f == 'genitive' or f == 'р' then
return transformLangCase(normCode, 'genitive') .. category
elseif f == 'l4' or f == 'prep' or f == 'prepositional' or f == 'п' then
return transformLangCase(normCode, 'prepositional') .. category
else
-- default to nominative (lowercase)
return (l[2] and mw.ustring.lower(l[2]) or '') .. category
end
end
-- Entry point: returns the prepositional form for a language code.
function p.transform_lang(frame)
local args = getInvokeArgs(frame)
local code = args[1] or ''
local showError = args['error'] and true or false
local l, normalized, err = resolveLanguage(code, showError)
if l then
return transformLangCase(normalized or code, 'prepositional') .. getCodeCategory(code)
end
return err or ''
end
-- Entry point: returns the genitive form for a language code.
function p.transform_lang_genitive(frame)
local args = getInvokeArgs(frame)
local code = args[1] or ''
local showError = args['error'] and true or false
local l, normalized, err = resolveLanguage(code, showError)
if l then
return transformLangCase(normalized or code, 'genitive') .. getCodeCategory(code)
end
return err or ''
end
return p