м 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 trimstr(s)
-- Parameters:
   return (s:gsub("^%s*(.-)%s*$", "%1"))
--  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: '&nbsp;<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 '&nbsp;'
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 get_lang_data(code)
--  • rendered link to the language article using its abbreviation, and
    local l = languages[code];
--  • normalized code to be used in the 'lang' attribute.
     if l ~= nil then
-- If the code is unknown but non-empty, returns the raw code and empty lang.
        return "[[" .. l[2] .. "|" .. l[1] .. "]]", code
-- If the input is empty, returns two empty strings.
    elseif code ~= "" then
local function buildLanguageLinkForCode(code)
        return code, ""
local l, normalized_code = resolveLanguage(code, false)
    else
     if l then
        return "", ""
return buildLanguageLink(l), (normalized_code or '')
elseif not isEmpty(code) then
return code, ''
else
return '', ''
     end
     end
end
end


function p.getRefHtmlFrame( frame )
-- Renders a ref span for a known ISO 639 code. Returns nil when code
return p.getRefHtml( trimstr( frame.args[1] ) )
-- 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 '&nbsp;' 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 p.getRefHtml( code )
-- Internal implementation of the language-name case transformation.
local l = languages[code];
-- Computes either prepositional ('на английском языке') or
if l == nil then
-- genitive ('английского языка') forms. If specific forms are
mw.log( 'Language description for code ' .. code .. ' not found' )
-- provided in Module:Languages/data via l[3]/l[4], they win.
        return ""
-- Otherwise, applies a set of heuristics to generate the forms.
    else
local function _transformLangCaseImpl(code, case)
        return '<span class="ref-info" title="' .. l[ 2 ] .. '" style="cursor:help">(' .. l[ 1 ] .. ')</span>'
-- Resolve the language record; unknown code gets a readable placeholder
local l = getLanguageData(code)
if not l then
return '&lt;неизвестный код ' .. code .. '&gt;'
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( trimstr( frame.args[1] ) )
-- 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


function p.getWikidataRefHtml( wikidataItemId )
-- Builds a ref span from a Wikidata item id of a language.
local codeByItemId = mw.loadData( "Module:Wikidata/Language-codes" )
-- Uses a preloaded mapping Module:Wikidata/Language-codes to
local code = codeByItemId[ wikidataItemId ];
-- 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 p.getRefHtml( code )
-- Добавляем категорию если код является алиасом
return rendered .. getCodeCategory(code)
end
end


-- принимает zh, возвращает аббревиатуру
-- 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 code = trimstr( frame.args[1] )
local args = getInvokeArgs(frame)
if not isEmpty(code) then
local code = args[1] or ''
return (languages[code] and languages[code][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
end
return err or ''
end
end


-- принимает zh|我|tt|мин, возвращает название статьи из Module:Languages/data
-- 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 code = trimstr( frame.args[1] )
local args = getInvokeArgs(frame)
if code ~= nil and code ~= '' then
local code = args[1] or ''
return (languages[code] and languages[code][2] 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


-- принимает zh|我|tt|мин, возвращает список через запятую
-- 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 curr_lang = nil
local args = getInvokeArgs(frame)
    local result = nil
    local curr_lang = nil
local noItalic = args['noItalic']
local parts = {}
local categories = {}


    for n, v in frame:argumentPairs() do
-- Итерируемся по позиционным аргументам по порядку
local trimmed = trimstr(v)
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 trimmed ~= '' then
if value ~= '' then
-- если язык пропущен, оставим прошлый
curr_lang = value
curr_lang = trimstr(v)
end
end
else
else
if trimmed ~= '' then
if value ~= '' then
local link, lang_code = get_lang_data(curr_lang)
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
list_item = link .. "&nbsp;<span dir='auto' lang='" .. lang_code .. "'>" .. trimmed .. "</span>"
    --- 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
.. "&nbsp;<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
list_item = link .. " <span class='unknown-foreign-lang'>" .. trimmed .. "</span>".."[[Category:Википедия:Статьи с нераспознанным языком]]"
local link, _ = buildLanguageLinkForCode(curr_lang)
local category = getCodeCategory(curr_lang)
if category ~= '' then
categories[category] = true
end
table.insert(parts, link)
end
end
if result == nil then
result = list_item
else
result = result .. ", " .. list_item
end
else
-- Пустой текст — значит, текущий язык совпадает с следующим
local link, lang_code = get_lang_data(curr_lang)
if result == nil then
result = link
else
result = result .. ", " .. link
end
end
curr_lang = nil
curr_lang = nil
       end
       end
     end
     end
      
      
    -- просто #invoke:Languages|list|yue должно возвращать всё же ссылку без текста
     if curr_lang ~= nil then
     if curr_lang ~= nil then
        local link, lang_code = get_lang_data(curr_lang)
local link, _ = buildLanguageLinkForCode(curr_lang)
        if result ~= nil then
local category = getCodeCategory(curr_lang)
            result = result .. ", " .. link
if category ~= '' then
        else
categories[category] = true
            result = link
end
        end
table.insert(parts, link)
    end
end
    return result
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


-- принимает zh|tt, возвращает ref-zh, ref-tt
-- 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 result = ''
local args = getInvokeArgs(frame)
local v = frame.args['в'] or nil
local brackets = args['brackets'] or args['в']
local ref = nil
local mode = args['mode'] or ''
 
local showError = args['error'] and true or false
if v then
local includeNbsp = args['includeNbsp'] ~= 'false' and args['includeNbsp'] ~= false
ref = '&nbsp;<span class="ref-info" style="cursor:help;" title="на %2%">&#091;%1%&#093;</span>'
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
ref = '&nbsp;<span class="ref-info" style="cursor:help;" title="на %2%">(%1%)</span>'
refVariant = nil
end
end


for x, lg in pairs( frame.args ) do
-- We rely strictly on named params for brackets/mode; only numeric args are language codes.
local code = mw.ustring.lower(trimstr(lg))
-- But we need to filter out positional args that were used as options
if code ~= '' and code ~= 'в' then
 
local l = languages[code]
local chunks = {}
if l and l ~= nil then
local customClass = args['class'] or nil
result = result .. mw.ustring.gsub(mw.ustring.gsub(ref, '%%2%%', p._transform_lang(code)), '%%1%%', l[1])
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
if mw.title.new('ref-' .. code, 10).exists == false then
local rendered = buildRefForCode(normalized, refVariant, includeNbsp, customClass, mode)
code = 'und'
table.insert(chunks, rendered or renderUnknownRef(normalized, showError, refVariant, customClass, includeNbsp))
end
result = result .. frame:expandTemplate{ title = 'ref-' .. code } .. '[[Category:Википедия:Статьи с нераспознанным языком (ref)]]'
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._transform_lang(code)
--  1          — language code
if isEmpty(languages[code]) then return '&lt;неизвестный код ' .. code .. '&gt;' end
--  case|form  — 'l2'|'l3'|'l4' or 'nom'|'gen'|'prep' or 'р'|'п' (default 'l2')
    if languages[code][3] then return languages[code][3] end
--                  'р' → genitive, 'п' → prepositional
    local ln = mw.ustring.lower(
--  mode        — 'en' or 'iso' to override language output
        languages[code] and languages[code][2]
--  error      — when truthy, returns error html for unknown code
        or mw.language.fetchLanguageName(code,'ru')
function p.transform(frame)
    ):gsub('%s+язык%s+', ' '):gsub('%s*%(?язык%)?%s*',  '')
local args = getInvokeArgs(frame)
    if not ln then return 'языке с ISO-кодом '..code..' (?)' end
local code = args[1] or ''
    if ln:match('.*лингва$') then return ln:gsub('а$','е') end
-- Treat empty strings as absent to ensure proper fallback to 'l2'
    if mw.ustring.match(ln,'[сц]кий$') or ln:match('ный$') or mw.ustring.match(ln,'[сц]кий%s%b()$') or ln:match('ный%s%b()$')
local form = args['case']
    then
if isEmpty(form) then
        ln = mw.ustring.gsub(
form = args['form']
            mw.ustring.gsub(ln, 'н([ыи])й(%A)', function(y,s) return (y=='ы' and 'ном' or 'нем')..s end)
end
                :gsub('ный$', 'ном'),
if isEmpty(form) then
            '([сц]к)ий(%A)', '%1ом%2'
form = 'l2'
        ):gsub('ский$', 'ском'):gsub('цкий$', 'цком');
end
        if ln:match('%)$') and not (ln:match('ом%)$') or ln:match('нем%)$')) -- «языке» перед уточнением, если оно не склоняется
local mode = args['mode'] or ''
        then  
local showError = args['error'] and true or false
            local r,s=ln:gsub('(%s)(%b())$','%1языке%1%2');
 
            if s==1 then return r end
if isEmpty(code) then
        end
return ''
        return ln..' языке'
end
    else
 
        return 'языке '..ln
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._transform_lang(trimstr(frame.args[1]))
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

Версия от 19:07, 20 февраля 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 '&#091;', '&#093;'
	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: '&nbsp;<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 '&nbsp;'
	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 '&nbsp;' 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 '&lt;неизвестный код ' .. code .. '&gt;'
	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
						.. "&nbsp;<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