Чулоацамага гӀó

Модуль:Languages

Википеди материал

Этот модуль используется в шаблоне {{l6e}} при создании собственно языковых ссылок путём конструкции {{#invoke:Languages|list|множество языков}}. За более подробной документацией насчёт работы шаблона следует идти в его документацию.

Также модуль используется для различных преобразований кодов языков.

4 tests failed.

test_getRefHtml:

Text Expected Actual
N {{#invoke:Languages | getRefHtmlFrame | }} Ошибка скрипта: Функции «getRefHtmlFrame» не существует.
N {{#invoke:Languages | getRefHtmlFrame | QNNN }} Ошибка скрипта: Функции «getRefHtmlFrame» не существует.
N {{#invoke:Languages | getRefHtmlFrame | Q1860 }} (англ.) Ошибка скрипта: Функции «getRefHtmlFrame» не существует.
N {{#invoke:Languages | getRefHtmlFrame | Q7737 }} (рус.) Ошибка скрипта: Функции «getRefHtmlFrame» не существует.




-- Модуль для работы с языками 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 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: '&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]
	return record, normalized_code
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
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]
	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]
			elseif showError then
				return generateError('lang', code)
			end
		else
			local l = getLanguageData(code)
			if l then
				return l[2]
			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 parts = {}

	-- Итерируемся по позиционным аргументам по порядку
	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 list_item
			if lang_code ~= '' then
					list_item = link
						.. "&nbsp;<span dir='auto' 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)
				table.insert(parts, link)
			end
		curr_lang = nil
      end
    end
    
    if curr_lang ~= nil then
		local link, _ = buildLanguageLinkForCode(curr_lang)
		table.insert(parts, link)
	end
	if #parts == 0 then
		return nil
	end
	return table.concat(parts, ', ')
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 = {}, {}

	-- 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)
			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

    return table.concat(chunks)
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 ''

	-- Output overrides
	if mode == 'iso' then
		return normCode
	elseif mode == 'en' then
		local e = languagesEn[normCode]
		if e then
			return e[1]
		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 ''
	elseif f == 'l3' or f == 'gen' or f == 'genitive' or f == 'р' then
		return transformLangCase(normCode, 'genitive')
	elseif f == 'l4' or f == 'prep' or f == 'prepositional' or f == 'п' then
		return transformLangCase(normCode, 'prepositional')
	else
		-- default to nominative (lowercase)
		return l[2] and mw.ustring.lower(l[2]) or ''
    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')
	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')
	end
	return err or ''
end

return p