본문으로 이동

모듈:Wikidata lexicographical data

위키낱말사전, 말과 글의 누리

이 모듈은 위키데이터의 어휘소(Lexeme) 데이터를 가져와 위키낱말사전 형식에 맞는 사전 항목을 자동으로 생성합니다. 어휘소의 언어, 품사, 어원, 발음, 의미, 예문, 활용 정보 등을 종합하여 완전한 형태의 문서 섹션을 만들어냅니다.

사용법

[편집]

특정 어휘소의 전체 정보를 표시하려면, 위키낱말사전 문서에서 다음과 같이 사용합니다.

{{#invoke:Wikidata lexicographical data|all|어휘소ID}}

  • 어휘소ID: 위키데이터 어휘소의 L-숫자 ID입니다. (예: {L12345})

또한, 각 섹션에 수동으로 노트를 추가할 수 있습니다. 예를 들어, 발음 섹션에 추가 정보를 넣고 싶다면 다음과 같이 사용합니다. 이는 i18n 하위 모듈의 manual_... 테이블에 정의된 키를 사용합니다.

의존성

[편집]

이 모듈은 올바르게 작동하기 위해 다음 모듈들을 필요로 합니다.

  • Module:Wikidata reference format: 위키데이터의 출처 정보를 한국어 위키백과 인용 틀 스타일과 호환되는 각주 형식으로 변환합니다.
  • Module:Arguments: 모듈 호출 시 사용된 인자를 처리하는 공용 모듈입니다.

함수

[편집]

getLemmata

[편집]

function getLemmata( current_lexeme )

어휘소의 모든 표제어(lemma)를 가져와, 슬래시(`/`)로 구분된 문자열로 반환합니다. 이 함수는 termSpan을 사용하여 링크가 없는 일반 텍스트 목록을 생성합니다.

getLinkedLemmata

[편집]

function getLinkedLemmata( current_lexeme )

어휘소의 모든 표제어(lemma)를 가져와, 슬래시(`/`)로 구분된 문자열로 반환합니다. 이 함수는 termLink를 사용하여 클릭 가능한 링크가 포함된 표제어 목록을 생성합니다.

getExamples

[편집]

function getExamples( current_lexeme, sense_id )

특정 의미(Sense)에 대한 사용 예문들을 위키데이터에서 찾아 형식화된 목록으로 반환합니다. 이 함수는 예문(P5831)이 어휘소 자체에 있는지, 또는 의미(Sense)에 직접 연결되어 있는지 모두 확인합니다. 또한 예문 속에서 해당 단어의 특정 활용형을 찾아 굵게 표시하고, 출처(각주) 정보가 있으면 함께 표시합니다.

getLanguageForCategories

[편집]

function getLanguageForCategories ( frame, lang_id )

위키데이터에서는 하나의 언어로 취급되지만, 문자 체계가 여러 개인 언어를 처리하기 위한 함수입니다. 문서 제목에 포함된 문자를 확인하여 내부적으로 사용할 언어 ID를 변경합니다. 힌디어(데바가나리 문자)/우르두어(아랍 문자)를 구분하는 함수로, 한국어 환경에서는 보통 변경 없이 원래 언어 ID를 그대로 반환합니다.

getCategory

[편집]

function getCategory ( frame, current_lexeme )

어휘소의 품사(어휘 범주)에 해당하는 위키낱말사전 분류를 생성합니다. `/i18n` 하위 모듈의 lang_categories 테이블을 '언어-품사-분류명' 매핑 테이블로 사용하여, 정확한 분류 링크를 찾아 품사 이름과 함께 반환합니다.

getMeanings

[편집]

function getMeanings ( frame, args, current_lexeme, references_seen )

어휘소의 모든 의미(Sense)를 종합하여 사전의 '의미' 섹션 전체를 생성하는 함수입니다. 각 의미에 대한 설명(gloss), 문맥 한정자, 인용문, 출처, 이미지, 관련 위키백과 링크, 수동 노트, 사용 예문 등을 모두 가져와 순서가 있는 목록(<ol>)으로 만듭니다.

get_pronunciation_base_form

[편집]

function get_pronunciation_base_form( current_lexeme )

발음 정보를 찾을 때 기준이 될 '대표 어형(base form)'을 결정합니다. 언어별로 발음의 기준이 되는 형태가 다를 수 있으므로, 특정 언어에 대한 규칙을 먼저 적용합니다. 해당하는 규칙이 없으면, 등록된 어형 목록 중 가장 첫 번째를 기본값으로 사용합니다.

getCombines

[편집]

function getCombines ( current_lexeme )

합성어의 구성 요소를 분석하여 반환합니다. P5238(...의 결합) 속성을 찾아, P1545(순서) 한정자에 따라 올바른 순서로 각 구성 요소를 나열합니다. 각 구성 요소에 대해 getEtymology를 재귀적으로 호출하여 그 어원도 함께 표시할 수 있습니다.

getEtymology

[편집]

function getEtymology (frame, current_lexeme, lang_module)

단어의 어원을 추적하여 완전한 어원 정보를 생성합니다. 먼저 getCombines를 호출하여 합성어 여부를 확인하고, P5191(...에서 유래) 속성을 따라 재귀적으로 어원의 뿌리를 찾아 올라갑니다.

getPronunciation

[편집]

function getPronunciation ( frame, args, current_lexeme, lang_module )

단어의 발음 정보를 찾아 목록 형태로 반환합니다. get_pronunciation_base_form을 통해 찾은 대표 어형을 기준으로, P443(발음 오디오)와 P898(IPA 표기) 속성을 찾아 형식에 맞게 가공합니다.

IPA 표기의 경우 언어별 하위 모듈이 존재하는 경우 위키데이터 항목 대신 하위 모듈에서 IPA 정보를 가져오는 로직을 수행합니다.

heading_level

[편집]

function heading_level(text, level)

위키텍스트 단락을 생성하는 도우미 함수입니다.

get_any_notes

[편집]

function get_any_notes(sections, args, keys)

사용자가 모듈 호출 시 직접 입력한 수동 노트들을 찾아 리스트로 반환하는 도우미 함수입니다.

add_specific_notes

[편집]

function add_specific_notes(sections, notes)

주어진 노트 리스트를 최종 결과물(sections)에 추가하는 도우미 함수입니다.

add_any_notes

[편집]

function add_any_notes(sections, args, keys)

사용자 인자에서 특정 키에 해당하는 노트를 찾아 바로 최종 결과물(sections)에 추가하는 도우미 함수입니다. get_any_notesadd_specific_notes의 기능을 합친 것과 같습니다.

p.all

[편집]

function p.all( frame )

이 모듈의 외부 진입점입니다. {{#invoke:...}} 구문을 통해 호출됩니다. 이 함수는 어휘소 ID를 받아, 지금까지 분석한 모든 보조 함수들을 순서대로 호출하여 사전 항목의 각 부분(제목, 어원, 발음, 의미, 활용표, 참고문헌 등)을 조립하고, 최종적으로 완성된 하나의 위키텍스트를 반환합니다. 모듈:module_categorization 221번째 줄에서 Lua 오류: Did not recognize inferred module-type keyword 'lexicographical data' from root pagename 'Wikidata lexicographical data'.

하위 모듈

[편집]

local p = {}
local page_invocation_info = nil -- 페이지 내 모듈 호출 정보를 분석하여 저장

local anchors_module = require("Module:anchors")
local i18n = require('Module:Wikidata lexicographical data/i18n')
local references = require('Module:Wikidata reference format')
local getArgs = require('Module:Arguments').getArgs

base_lang_code_labels = 'ko' -- 아이템 레이블/설명/별칭, 단어 뜻 설명
base_lang_code_forms = 'ko' -- 어휘소 표제어, 어형의 표기
base_lang_wiki = 'kowiki' -- 위키백과 연결

formatter_urls = { -- https://w.wiki/ELuT
    -- 다국어
    ['P11512'] = 'https://ids.clld.org/units/$1',
    ['P11055'] = 'https://diacl.uni-frankfurt.de/Lexeme/Details/$1',
    ['P13258'] = 'https://www.termania.net/slovarji/presisov-vecjezicni-slovar/$1/_',
    ['P5912'] = 'https://oqaasileriffik.gl/dict/?lex=$1',
    -- Q11051 (힌두스탄어)
    ['P11350'] = 'http://udb.gov.pk/result_details.php?word=$1',
    ['P13175'] = 'https://www.cfilt.iitb.ac.in/hindishabdamitra/class/CBSE/1/1/$1/?page=1',
    -- Q56356571 (페르시아어족)
    ['P11328'] = 'https://dehkhoda.ut.ac.ir/fa/dictionary/detail/$1',
    ['P12326'] = 'https://vazhaju.tj/word/_/$1',
    ['P12519'] = 'https://qamosona.com/G3/index.php/term/57,$1.xhtml',
    ['P12845'] = 'https://farhang.ru/lexeme/$1.html',
    -- Q13955 (아랍어)
    ['P11038'] = 'https://ontology.birzeit.edu/lemma/$1',
    ['P11757'] = 'https://ontology.birzeit.edu/wordform/$1',
    ['P11761'] = 'https://corpus.quran.com/qurandictionary.jsp?q=$1',
    ['P12451'] = ' https://ontology.birzeit.edu/lexicalconcept/$1',
    ['P12901'] = 'http://arabiclexicon.hawramani.com/?p=$1',
    -- Q9072 (에스토니아어)
    ['P11138'] = 'https://sonaveeb.ee/worddetails/unif/$1',
    ['P13190'] = 'https://www.letonika.lv/groups/default.aspx?cid=$1&r=10611062',
    -- Q150 (프랑스어)
    ['P11118'] = 'https://www.larousse.fr/dictionnaires/francais/_/$1',
    ['P11284'] = 'https://www.dictionnaire-academie.fr/article/A1$1',
    ['P11285'] = 'https://www.dictionnaire-academie.fr/article/A2$1',
    ['P11286'] = 'https://www.dictionnaire-academie.fr/article/A3$1',
    ['P11287'] = 'https://www.dictionnaire-academie.fr/article/A4$1',
    ['P11288'] = 'https://www.dictionnaire-academie.fr/article/A5$1',
    ['P11289'] = 'https://www.dictionnaire-academie.fr/article/A6$1',
    ['P11290'] = 'https://www.dictionnaire-academie.fr/article/A7$1',
    ['P11291'] = 'https://www.dictionnaire-academie.fr/article/A8$1',
    ['P7732'] = 'https://www.dictionnaire-academie.fr/article/A9$1',
    -- Q58635 (펀자브어)
    ['P7820'] = 'https://wikidata-externalid-url.toolforge.org/?url=https%3A%2F%2Fpunjabipedia.org%2Ftopic.aspx%3Ftxt%3D%251&exp=%28.%2A%29&id=$1',
    ['P7575'] = 'https://wikidata-externalid-url.toolforge.org/?url=https%3A%2F%2Fwww.srigranth.org%2Fservlet%2Fgurbani.dictionary%3FParam%3D%251&exp=%28.%2A%29&id=$1',
    -- Q1860 (영어)
    ['P5275'] = 'http://www.oed.com/view/Entry/$1',
    ['P12510'] = 'https://doi.org/10.1093/OED/$1',
    ['P12739'] = 'https://www.oxfordreference.com/display/10.1093/acref/9780199571123.001.0001/m_en_gb$1',
    ['P12690'] = 'https://www.oxfordreference.com/display/10.1093/acref/9780195392883.001.0001/m_en_us$1',
    ['P12726'] = 'https://www.oxfordreference.com/display/10.1093/acref/9780195418163.001.0001/m_en_ca$1',
    ['P12718'] = 'https://www.oxfordreference.com/display/10.1093/acref/9780195517965.001.0001/m-en_au-msdict-00001-$1',
    ['P12724'] = 'https://www.oxfordreference.com/display/10.1093/acref/9780195584516.001.0001/m-en_nz-msdict-00001-$1',
    ['P11481'] = 'https://greensdictofslang.com/entry/$1',
    ['P12448'] = 'https://www.daredictionary.com/view/dare/$1',
    ['P11130'] = 'https://wikidata-externalid-url.toolforge.org/?url=https%3A%2F%2Fwww.merriam-webster.com%2Fdictionary%2F%251&exp=%28.%2A%29&id=$1',
    ['P11263'] = 'https://wikidata-externalid-url.toolforge.org/?url=https%3A%2F%2Fwww.britannica.com%2Fdictionary%2F%251&exp=%28.%2A%29&id=$1',
    ['P13163'] = 'https://ahdictionary.com/word/search.html?id=$1',
    -- Q188 (독일어)
    ['P11519'] = 'https://www.owid.de/artikel/$1',
    ['P11520'] = 'https://www.owid.de/artikel/$1',
    ['P11521'] = 'https://www.owid.de/artikel/$1',
    ['P11522'] = 'https://www.owid.de/artikel/$1',
    ['P11523'] = 'https://www.owid.de/artikel/$1',
    ['P11524'] = 'https://www.owid.de/artikel/$1',
    ['P8376'] = 'https://www.duden.de/rechtschreibung/$1',
    ['P9384'] = 'https://www.woerterbuchnetz.de/Adelung?lemid=$1',
    ['P9385'] = 'https://www.woerterbuchnetz.de/DWB?lemid=$1',
    ['P9386'] = 'https://www.woerterbuchnetz.de/DWB2?lemid=$1',
    ['P9387'] = 'https://www.woerterbuchnetz.de/GWB?lemid=$1',
    ['P9388'] = 'https://www.woerterbuchnetz.de/Meyers?lemid=$1',
    ['P9389'] = 'https://www.woerterbuchnetz.de/RDWB1?lemid=$1',
    ['P9390'] = 'https://www.woerterbuchnetz.de/Wander?lemid=$1',
    ['P9940'] = 'https://www.dwds.de/wb/$1',
    -- Q9035 (덴마크어)
    ['P10830'] = 'https://ordregister.dk/id/$1',
    ['P10831'] = 'https://ordregister.dk/id/$1',
    ['P9529'] = 'https://ordnet.dk/ddo/ordbog?query=wd&entry_id=$1',
    ['P9530'] = 'https://ordnet.dk/ddo/ordbog?query=wd&mselect=$1',
    ['P12828'] = 'https://iserasuaat.gl/daka/daka?f=lo&l=1&p=$1',
    ['P9962'] = 'https://ordnet.dk/ods/ordbog?query=wd&entry_id=$1',
    -- Q25167 (노르웨이어 보크몰)
    ['P10042'] = 'https://ordbøkene.no/bm/$1',
    ['P9958'] = 'https://naob.no/ordbok/$1',
    -- Q25164 (노르웨이어 뉘노르스크)
    ['P10041'] = 'https://ordbøkene.no/nn/$1',
    -- Q652 (이탈리아어)
    ['P12826'] = 'https://api.tommaseobellini.it/api/text?delta=0&html=true&id=$1',
    ['P12825'] = 'http://tlio.ovi.cnr.it/voci/$1.htm',
    ['P5844'] = 'https://www.treccani.it/vocabolario/$1',
    ['P12420'] = 'https://dizionario.internazionale.it/parola/',
    ['P12862'] = 'https://www.dizionario.rai.it/p.aspx?nID=lemma&lID=$1',
    ['P12885'] = 'https://www.oxfordreference.com/display/10.1093/acref/9780191739569.001.0001/b-it-en-00002-$1',
    -- Q1321 (스페인어)
    ['P12529'] = 'https://wikidata-externalid-url.toolforge.org/?p=12529&url_prefix=https://dle.rae.es/?id=&id=$1',
    ['P12821'] = 'https://www.oxfordreference.com/display/10.1093/acref/9780191739538.001.0001/b-es-en-00008-$1',
    ['P12630'] = 'https://aragonario.aragon.es/words/$1',
    -- Q9027 (스웨덴어)
    ['P8478'] = 'https://www.saob.se/artikel/?unik=$1',
    ['P9837'] = 'https://svenska.se/so/?id=$1',
    ['P11838'] = 'https://svenska.se/saol/?id=$1',
    ['P12032'] = 'https://kaino.kotus.fi/fo/?p=article&fo_id=FO_$1',
    -- Q8752 (바스크어)
    ['P6838'] = 'https://hiztegiak.elhuyar.eus/eu/$1',
    -- Q9176 (한국어)
    ['P11124'] = 'https://krdict.korean.go.kr/kor/dicSearch/SearchView?ParaWordNo=$1', -- 한국어 기초 사전
    ['P11125'] = 'https://stdict.korean.go.kr/search/searchView.do?searchKeywordTo=3&word_no=$1', -- 표준국어대사전
    ['P11298'] = 'https://opendict.korean.go.kr/dictionary/view?sense_no=$1' -- 우리말샘
}

-- 어휘소에서 주어진 문법적 특징을 정확히 하나만 가지고 있는 첫 번째 어형을 반환합니다.
local function formWithSingleGrammaticalFeature( lexeme, item_id )
    for i, form in pairs( lexeme:getForms() ) do
        local grammaticalFeatures = form:getGrammaticalFeatures()
        if #grammaticalFeatures == 1 and grammaticalFeatures[1] == item_id then
            return form
        end
    end
    return nil
end

-- 가능하면 주어진 언어 코드로 된 어형의 표기법을 반환하거나, 그렇지 않으면 첫 번째 표기법을 반환합니다.
-- 예: 한국어 단어의 경우 ko, ko-hani 둘 중 원하는 형태의 표기법을 반환하는 데 사용할 수 있습니다.
local function representationInLanguage( form, language_code )
    for i, representation in pairs( form:getRepresentations() ) do
        if representation[2] == language_code then
            return representation
        end
    end
    return form:getRepresentations()[1]
end

-- 위키데이터 ID를 받아서, 해당 ID와 연결된 한국어 위키백과 문서가 있는지 확인합니다.
-- 문서가 존재한다면, 해당 문서로 연결되는 위키백과 틀을 생성해서 반환합니다.
local function getArticleLinkTemplate(frame, stmt_value)
    template = ''
    stmt_item = mw.wikibase.getEntity(stmt_value)
    if stmt_item:getSitelink(base_lang_wiki) ~= nil then
        template = frame:expandTemplate{
            title=i18n['template_wikipedia'],
            args={stmt_item:getSitelink(base_lang_wiki)}
        }
    end
    return template
end

-- 하나의 단어 의미 항목을 받아서, 그 의미와 관련된 모든 위키백과 문서 링크를 찾고 문자열로 합쳐서 반환합니다.
local function getArticleLinks ( frame, sense )
    article_links = ''
    for i, stmt in pairs(sense:getAllStatements('P5137')) do -- 이 의미에 관한 항목
        stmt_value = stmt.mainsnak.datavalue.value.id
        article_links = article_links .. getArticleLinkTemplate(frame, stmt_value)
    end
    for i, stmt in pairs(sense:getAllStatements('P9970')) do -- 다음을 서술
        stmt_value = stmt.mainsnak.datavalue.value.id
        article_links = article_links .. getArticleLinkTemplate(frame, stmt_value)
    end
    return article_links
end

-- 어휘소 객체를 입력받아 위키데이터에 연결된 모든 외부 사전 사이트 링크를 찾아 목록 형태로 만들어 반환합니다.
local function getExternalLinks ( lexeme )
    local external_links = {}
    for property_id, formatter_url in pairs(formatter_urls) do
        for i, stmt in ipairs(lexeme:getAllStatements(property_id)) do
            property_source = mw.wikibase.getBestStatements(property_id, 'P9073') -- 다음의 곳에서 다뤄짐
            if next(property_source) ~= nil then
                source_name = mw.wikibase.getLabel(property_source[1].mainsnak.datavalue.value.id)
                if source_name == nil then
                    source_name = property_source[1].mainsnak.datavalue.value.id
                end
            else
                source_name = mw.wikibase.getLabel(property_id)
                if source_name == nil then
                    source_name = property_id
                end
            end
            formatted_link = mw.ustring.gsub(mw.ustring.gsub(formatter_url, '$1', stmt.mainsnak.datavalue.value), ' ', '+')
            new_link = '* [' .. formatted_link .. ' ' .. source_name .. ']'
            table.insert(external_links, new_link)
        end
    end
    return table.concat(external_links, '\n')
end

-- 단어와 언어 코드를 받아서, HTML 태그로 감싼 위키링크가 없는 일반 텍스트를 생성합니다.
local function termSpan( term )
    local text = term[1]
    local lang = term[2]
    local dir = mw.language.new( lang ):getDir()
    local span = mw.html.create( 'span' )
    span:attr( 'lang', lang )
        :attr( 'dir', dir )
        :wikitext( text )
    return tostring( span )
end

-- 단어와 언어 코드를 받아서, HTML 태그로 감싼 위키링크 텍스트를 생성합니다.
local function termLink( term )
    local text = term[1]
    local lang = term[2]
    local dir = mw.language.new( lang ):getDir()
    local span = mw.html.create( 'span' )
    span:attr( 'lang', lang )
        :attr( 'dir', dir )
        :wikitext( '[[' .. text .. ']]' )
    return tostring( span )
end

--[==[
어휘소의 모든 표제어(lemma)를 가져와, 슬래시(`/`)로 구분된 문자열로 반환합니다.
이 함수는 `termSpan`을 사용하여 링크가 없는 일반 텍스트 목록을 생성합니다.
]==]
function getLemmata( current_lexeme )
    lemma_string = ''
    for i, rep in pairs(current_lexeme:getLemmas()) do
        if lemma_string == '' then
            lemma_string = termSpan(rep)
        else
            lemma_string = lemma_string .. '/' .. termSpan(rep)
        end
    end
    return lemma_string
end

--[==[
어휘소의 모든 표제어(lemma)를 가져와, 슬래시(`/`)로 구분된 문자열로 반환합니다.
이 함수는 `termLink`를 사용하여 클릭 가능한 링크가 포함된 표제어 목록을 생성합니다.
]==]
function getLinkedLemmata( current_lexeme )
    lemma_string = ''
    for i, rep in pairs(current_lexeme:getLemmas()) do
        if lemma_string == '' then
            lemma_string = termLink(rep)
        else
            lemma_string = lemma_string .. '/' .. termLink(rep)
        end
    end
    return lemma_string
end

--[==[
특정 의미(Sense)에 대한 사용 예문들을 위키데이터에서 찾아 형식화된 목록으로 반환합니다.
이 함수는 예문({P5831})이 어휘소 자체에 있는지, 또는 의미(Sense)에 직접 연결되어 있는지 모두 확인합니다.
또한 예문 속에서 해당 단어의 특정 활용형을 찾아 굵게 표시하고, 출처(각주) 정보가 있으면 함께 표시합니다.
]==]
function getExamples( current_lexeme, sense_id )
	local examples = mw.html.create('dl')
	local tracking_cats = '' -- 추적용 분류를 담을 변수
	
	local function process_statement(stmt)
		-- 이 예문이 현재 다루는 의미에 대한 것인지 확인 (P6072)
		-- 이 한정자가 없으면 어떤 의미에 대한 예문인지 알 수 없으므로 건너뜀
		if not (stmt.qualifiers and stmt.qualifiers['P6072'] and stmt.qualifiers['P6072'][1]) then
            tracking_cats = tracking_cats .. '[[분류:위키데이터 특정 의미 한정어가 누락된 예문]]' -- 예문 P6072 추적용 분류 추가
            return -- 이 예문 처리를 중단하고 다음으로 넘어감
		end
		
		-- 이 예문이 현재 처리 중인 의미와 다르면 건너뜀
        if stmt.qualifiers['P6072'][1].datavalue.value.id ~= sense_id then
            return
        end
        
        local example_text = mw.ustring.gsub(stmt.mainsnak.datavalue.value.text, ' / ','<br/>')
        local example_lang = stmt.mainsnak.datavalue.value.language
        
        -- 2. 예문에 사용된 특정 어형(form)을 찾아 굵게 표시하는 로직 (P5830)
        local example_form_str = nil
        if stmt.qualifiers and stmt.qualifiers['P5830'] and stmt.qualifiers['P5830'][1] then
            local example_form = mw.wikibase.getEntity(stmt.qualifiers['P5830'][1].datavalue.value.id)
            -- 어형의 텍스트를 찾는 대체 로직
            if stmt.qualifiers['P1810'] ~= nil then
                example_form_str = stmt.qualifiers['P1810'][1].datavalue.value
            end
            if example_form_str == nil then
                example_form_str = example_form:getRepresentation(base_lang_code_forms)
            end
            if example_form_str == nil and example_form:getRepresentations()[1] then
                example_form_str = example_form:getRepresentations()[1][1]
            end
            -- 찾은 어형이 있으면 텍스트에서 굵게 표시
            if example_form_str then
                example_text = mw.ustring.gsub(example_text, example_form_str, "'''" .. example_form_str .. "'''")
            end
        else
            -- 사용되고 있는 어형(P5830) 한정어가 없으면 추적 분류 추가 (예문 표시는 계속 진행)
            tracking_cats = tracking_cats .. '[[분류:위키데이터 사용되고 있는 어형 한정어가 없는 예문]]'
        end
        
        local example_str = termSpan({example_text, example_lang})
        local reference_text = ''
        
        -- 3. 참고 문헌이 있을 때만 루프를 실행하도록 수정
        if stmt.references then
            for j, reference in pairs(stmt.references) do
                reference_text = reference_text .. '\n\n' .. references.format(reference)
            end
        end
        
        examples:tag('dd'):wikitext("''" .. example_str .. "''"):done():tag('dd'):css('text-indent', '2em'):wikitext(reference_text)
	end

    -- 어휘소 자체와 의미 아이템 양쪽에서 예문을 검색
    for i, stmt in pairs(current_lexeme:getAllStatements('P5831')) do
        process_statement(stmt)
    end
    for i, stmt in pairs(mw.wikibase.getAllStatements(sense_id, 'P5831')) do
        process_statement(stmt)
    end
    
    -- 최종 결과에 추적용 분류를 추가하여 반환
    return tostring(examples) .. tracking_cats
end

--[==[
위키데이터에서는 하나의 언어로 취급되지만, 문자 체계가 여러 개인 언어를 처리하기 위한 함수입니다.
문서 제목에 포함된 문자를 확인하여 내부적으로 사용할 언어 ID를 변경합니다.
힌디어(데바가나리 문자)/우르두어(아랍 문자)를 구분하는 함수로, 한국어 환경에서는 보통 변경 없이 원래 언어 ID를 그대로 반환합니다.
]==]
function getLanguageForCategories ( frame, lang_id )
    local current_page_title = mw.title.getCurrentTitle().text
    -- বিশেষ ভাষার জন্য (특별한 언어를 위해)
    if lang_id == 'Q11051' and mw.ustring.find( current_page_title, '[؀-ۿ]' ) ~= nil then -- U+0600 - U+06FF
        lang_id = 'Q11051ur'
    elseif lang_id == 'Q11051' and mw.ustring.find( current_page_title, '[ऀ-ॿ]' ) ~= nil then -- U+0900 - U+097F
        lang_id = 'Q11051hi'
    end
    return lang_id
end

--[==[
어휘소의 품사(어휘 범주)에 해당하는 위키낱말사전 분류를 생성합니다.
`/i18n` 하위 모듈의 `lang_categories` 테이블을 '언어-품사-분류명' 매핑 테이블로 사용하여, 정확한 분류 링크를 찾아 품사 이름과 함께 반환합니다.
]==]
function getCategory ( frame, current_lexeme )
    local lang_id = current_lexeme:getLanguage()
    local cat_id = current_lexeme:getLexicalCategory()
    local cat_text = mw.wikibase.getLabelByLang( cat_id, base_lang_code_labels )
    lang_id = getLanguageForCategories(frame , lang_id)
    if i18n['lang_categories'][lang_id] ~= nil then
        if i18n['lang_categories'][lang_id][cat_id] ~= nil then
            cat_text = cat_text .. '[[분류:' .. i18n['lang_categories'][lang_id][cat_id] .. ']]'
        else
            cat_text = cat_text .. '[[분류:' .. i18n['lang_categories'][lang_id]['_'] .. ']]'
        end
    end
    return cat_text
end

-- 위키데이터의 특정 항목을 편집하는 페이지로 바로 연결되는 위키텍스트 아이콘 링크를 생성합니다.
local createicon = function(langcode, entityID, propertyID)
	langcode = langcode or ""
    propertyID = propertyID or ""
	local icon = "&nbsp;<span class='penicon autoconfirmed-show'>[["
	-- "&nbsp;<span data-bridge-edit-flow='overwrite' class='penicon'>[[" -> enable Wikidata Bridge
	.. "File:OOjs UI icon edit-ltr-progressive.svg |frameless |text-top |10px |alt="
	.. i18n['edit_wikidata']
	.. "|link=https://www.wikidata.org/entity/" .. entityID
	if langcode ~= "" then icon = icon .. "?uselang=" .. langcode end
	if propertyID ~= "" then icon = icon .. "#" .. propertyID end
	icon = icon .. "|" .. i18n['edit_wikidata'] .. "]]</span>"
	return icon
end

--[==[
어휘소의 모든 의미(Sense)를 종합하여 사전의 '의미' 섹션 전체를 생성하는 함수입니다.
각 의미에 대한 설명(gloss), 문맥 한정자, 인용문, 출처, 이미지, 관련 위키백과 링크, 수동 노트, 사용 예문 등을 모두 가져와 순서가 있는 목록({<ol>})으로 만듭니다.
]==]
function getMeanings ( frame, args, current_lexeme, references_seen )
    if #current_lexeme:getSenses() == 0 then -- 어휘소에 의미가 등록되지 않은 경우 정의가 필요한 문서 분류 추가
        return createicon(base_lang_code_labels, current_lexeme:getId()) .. "''" .. i18n['text_category_rfdef'] .. "''" .. '[[분류:' .. i18n['category_rfdef'] .. ']]' 
    end
    meanings = mw.html.create( 'ol' )
    for i, sense in pairs(current_lexeme:getSenses()) do
        local gloss_text_parts = {}
        local main_gloss_text = anchors_module.make_anchors({sense:getId()})
        local specifiers = {} -- 의미 한정자 처리
        for k, property_id in ipairs({'P6084', 'P6191', 'P9488'}) do -- 의미가 사용되는 위치, 의미가 사용되는 양식, 사용 분야
            for i, stmt in pairs(sense:getAllStatements(property_id)) do
                stmt_value = stmt.mainsnak.datavalue.value.id
                table.insert(specifiers, mw.wikibase.getLabel(stmt_value, base_lang_code_labels))
            end
        end
        if #specifiers > 0 then
            main_gloss_text = main_gloss_text .. "(''" .. table.concat(specifiers, "'', ''") .. "'') "
        end
        if sense:getGloss( base_lang_code_labels ) ~= nil then -- 기본 언어인 한국어로 된 의미 설명을 가져오려고 시도
            main_gloss_text = main_gloss_text .. sense:getGloss( base_lang_code_labels)
        else
            local other_gloss_text = nil
            local other_gloss_lang = nil
            local item_label_gloss_parts = {}
            for k, stmt in pairs(sense:getAllStatements('P5137')) do -- '이 의미에 해당하는 항목' 한국어 항목이 존재하는 경우
                stmt_value = stmt.mainsnak.datavalue.value.id
                local stmt_label = mw.wikibase.getLabelByLang(stmt_value, base_lang_code_labels)
                if stmt_label ~= nil then
                    table.insert(item_label_gloss_parts, '[[:d:' .. stmt_value .. '|' .. stmt_label .. ']]')
                end
            end
            if #item_label_gloss_parts > 0 then
                other_gloss_text = table.concat(item_label_gloss_parts, '; ')
            end
            if other_gloss_text == nil then
                for i, fallback_lang in ipairs(mw.language.getFallbacksFor( base_lang_code_labels )) do
                    if sense:getGloss( fallback_lang ) ~= nil then
                        other_gloss_text, other_gloss_lang = sense:getGloss( fallback_lang )
                    end
                end
                if other_gloss_lang == nil then
                    local glosses = sense:getGlosses() -- 의미 설명(gloss)이 없는 경우 아이템 라벨을 의미 설명 대신 사용
                    for j, gloss in pairs(glosses) do
                        other_gloss_text = gloss[1]
                        other_gloss_lang = gloss[2]
                        break
                    end
                end
                main_gloss_text = main_gloss_text .. other_gloss_text .. "<sup><em>" .. mw.language.fetchLanguageName(other_gloss_lang, base_lang_code_labels) .. "</em></sup>"
            	-- 아이템 라벨이 없는 경우 대체 언어로 된 의미 설명이 있는지 탐색
            else
                main_gloss_text = main_gloss_text .. "''" .. other_gloss_text .. "''" -- 모두 실패한 경우 등록된 아무 언어의 의미 설명과 해당 언어 표시.
            end
        end
        table.insert(gloss_text_parts, main_gloss_text .. createicon(base_lang_code_labels, sense:getId())) -- 위키데이터 편집 아이콘 추가
        for i, stmt in pairs(sense:getAllStatements('P8394')) do -- 인용 정의
            gloss_quote = termSpan({stmt.mainsnak.datavalue.value.text, stmt.mainsnak.datavalue.value.language})
            if stmt.references[1] ~= nil then
                gloss_quote = '"' .. gloss_quote .. '" ' .. references.format ( stmt.references[1] )
            end
            table.insert(references_seen, stmt.references[1].hash)
            table.insert(gloss_text_parts, frame:extensionTag('ref', gloss_quote))
        end
        for i, stmt in pairs(sense:getAllStatements('P1343')) do -- 다음 문헌에 항목으로 실렸음
            -- TODO: do away with making fake reference objects (TODO: 가짜 참조 객체를 만드는 것을 없앨 것)
            fake_reference = {['hash'] = stmt.id, ['snaks'] = stmt.qualifiers }
            fake_reference.snaks['P248'] = { [1] = stmt.mainsnak }
            table.insert(references_seen, fake_reference.hash)
            table.insert(gloss_text_parts, frame:extensionTag('ref', references.format ( fake_reference )))
        end
        first_sense_image = ''
        sense_images = sense:getAllStatements('P18') -- 그림 속성이 있는 경우 썸네일 추가
        if next(sense_images) ~= nil then
            first_sense_image = sense_images[1].mainsnak.datavalue.value
        end
        if first_sense_image ~= '' then
            table.insert(gloss_text_parts, '[[파일:' .. first_sense_image .. "|thumb|'''" .. getLemmata(current_lexeme) .. "'''—" .. main_gloss_text .. ']]')
        end
        externallinks = getArticleLinks(frame, sense) -- 관련 위키백과 문서 링크를 찾아 추가
        if externallinks ~= '' then
            table.insert(gloss_text_parts, externallinks)
        end
        local new_notes = {} -- 사용자가 모듈 호출 시 직접 추가한 수동 노트가 있으면 표시
        sense_keys = { sense:getId(), string.sub(sense:getId(), string.find(sense:getId(), '-')+1) }
        for i, v in ipairs(sense_keys) do
            if args[v] ~= nil then
                table.insert(new_notes, args[v])
            end
        end
        if #new_notes > 0 then
            for i, v in ipairs(new_notes) do
                if i == 1 then
                    table.insert(gloss_text_parts, '<br/>' .. v)
                else
                    table.insert(gloss_text_parts, v)
                end
            end
        end
        examples = getExamples ( current_lexeme, sense:getId() ) -- 이 의미에 대한 사용 예문 목록 전체 가져오기
        local gloss_text = table.concat(gloss_text_parts, '\n')
        meanings:tag('li'):wikitext(gloss_text):wikitext(examples)
    end
    return meanings
end

--[==[
발음 정보를 찾을 때 기준이 될 '대표 어형(base form)'을 결정합니다.
언어별로 발음의 기준이 되는 형태가 다를 수 있으므로, 특정 언어에 대한 규칙을 먼저 적용합니다.
해당하는 규칙이 없으면, 등록된 어형 목록 중 가장 첫 번째를 기본값으로 사용합니다.
]==]
function get_pronunciation_base_form( current_lexeme )
    base_form = nil
    -- (!) 다른 언어의 단어가 다른 종류의 기본형을 가지고 있다면, 여기에 새로운 if 구문으로 추가할 수 있습니다.
    if current_lexeme:getLanguage() == 'Q9610' then -- (벵골어)
        if current_lexeme:getLexicalCategory() == 'Q1084' then --  (명사)
            base_form = formWithSingleGrammaticalFeature( current_lexeme, 'Q131105' ) -- (주격)
        elseif current_lexeme:getLexicalCategory() == 'Q24905' then -- (동사)
            base_form = formWithSingleGrammaticalFeature( current_lexeme, 'Q1350145' ) -- (동사적 명사)
        end
    end
    if base_form == nil then
        for i, form in pairs(current_lexeme:getForms()) do
            base_form = form
            break
        end
    end
    return base_form
end

--[==[
합성어의 구성 요소를 분석하여 반환합니다.
{P5238}(...의 결합) 속성을 찾아, {P1545}(순서) 한정자에 따라 올바른 순서로 각 구성 요소를 나열합니다.
각 구성 요소에 대해 `getEtymology`를 재귀적으로 호출하여 그 어원도 함께 표시할 수 있습니다.
]==]
function getCombines ( current_lexeme )
    local combines = ''
    local index_mappings = {}
    for i, stmt in pairs(current_lexeme:getAllStatements('P5238')) do -- 어휘소에서 '복합어의 성분' 속성 찾기
        if stmt.qualifiers and stmt.qualifiers['P1545'] ~= nil then -- 순번 한정자가 있는지 확인
            local current_index = tonumber(stmt.qualifiers['P1545'][1].datavalue.value)
            index_mappings[current_index] = stmt
        end
    end
    if #index_mappings ~= 0 then
        -- 순번에 따라 정렬
        table.sort(index_mappings, function(a, b)
            local order_a = tonumber(a.qualifiers['P1545'][1].datavalue.value)
            local order_b = tonumber(b.qualifiers['P1545'][1].datavalue.value)
            return order_a < order_b
        end)
        for i, stmt in ipairs(index_mappings) do
            local part_lexeme_id = stmt.mainsnak.datavalue.value.id
            local part_lexeme = mw.wikibase.getEntity(part_lexeme_id)
            local current_substring = getLinkedLemmata(part_lexeme)
            -- 재귀 호출 시 모든 매개변수를 전달
            local part_etymology = getEtymology(frame, part_lexeme, lang_module)
            if part_etymology and part_etymology ~= '' then
                current_substring = current_substring .. ' (← ' .. part_etymology .. ')'
            end
            if combines == '' then
                combines = current_substring
            else
                combines = combines .. ' + ' .. current_substring
            end
        end
    end
    return combines
end

--[==[
단어의 어원을 추적하여 완전한 어원 정보를 생성합니다.
먼저 `getCombines`를 호출하여 합성어 여부를 확인하고, {P5191}(...에서 유래) 속성을 따라 재귀적으로 어원의 뿌리를 찾아 올라갑니다.
]==]
function getEtymology (frame, current_lexeme, lang_module)
	local etymology_parts = {}
	
	-- 1. 언어별 하위 모듈에 특화된 어원 함수가 있는지 먼저 확인
	if lang_module and lang_module.getEtymology then
        local lang_specific_etym = lang_module.getEtymology(frame, current_lexeme)
        if lang_specific_etym and lang_specific_etym ~= '' then
            return lang_specific_etym
        end
    end
	
	-- 2. 기존의 유래 및 결합 어원 분석 로직
    local other_etymology_string = ''
    -- [수정] getCombines 호출 시 모든 매개변수를 전달합니다.
    local current_combines = getCombines(frame, current_lexeme, lang_module)
    
    local derived_from_stmts = current_lexeme:getAllStatements('P5191')
    if #derived_from_stmts == 0 then
    	other_etymology_string = current_combines	-- '다음에서 유래됨' 속성이 없는 경우 합성어 정보만 반환
    else
    	local derived_parts = {}
    	for i, stmt in pairs(derived_from_stmts) do
            local origin_lexeme = mw.wikibase.getEntity(stmt.mainsnak.datavalue.value.id)
            if origin_lexeme then
                local origin_lexeme_lang = origin_lexeme:getLanguage()
                -- 이 재귀 호출은 이미 정상적으로 모든 인자를 전달하고 있습니다.
                local origin_origin = getEtymology(frame, origin_lexeme, lang_module)
                
                local new_etymology_part
                if origin_origin and origin_origin ~= '' then
                    new_etymology_part = getLinkedLemmata(origin_lexeme) .. ' (' .. mw.wikibase.getLabel(origin_lexeme_lang) .. ') ← ' .. origin_origin
                else
                    new_etymology_part = getLinkedLemmata(origin_lexeme) .. ' (' .. mw.wikibase.getLabel(origin_lexeme_lang) .. ')'
                end
                table.insert(derived_parts, new_etymology_part)
            end
    	end
    	other_etymology_string = table.concat(derived_parts, ' ')

        if current_combines and current_combines ~= '' then
            other_etymology_string = other_etymology_string .. '<br/>(' .. current_combines .. ')'
        end
    end
    
    if other_etymology_string and other_etymology_string ~= '' then
        table.insert(etymology_parts, other_etymology_string)
    end
    
    -- 최종적으로 모든 어원 정보들을 줄바꿈으로 합쳐서 반환
    return table.concat(etymology_parts, '\n')
end

--[==[
단어의 발음 정보를 찾아 목록 형태로 반환합니다.
`get_pronunciation_base_form`을 통해 찾은 대표 어형을 기준으로, {P443}(발음 오디오)와 {P898}(IPA 표기) 속성을 찾아 형식에 맞게 가공합니다.

IPA 표기의 경우 언어별 하위 모듈이 존재하는 경우 위키데이터 항목 대신 하위 모듈에서 IPA 정보를 가져오는 로직을 수행합니다.
]==]
function getPronunciation ( frame, args, current_lexeme, lang_module )
    local pronunciations = {}
    local base_form = get_pronunciation_base_form(current_lexeme)

    -- 대표 어형이 없으면 빈 문자열을 반환하고 종료
    if base_form == nil then
        return ''
    end

    -- 1. IPA 정보 처리 (언어별 하위 모듈 우선)
    if lang_module and lang_module.getPronunciation then
        -- 1a. 언어별 특화 함수가 있으면 하위 문서를 호출하여 IPA 정보를 가져옴
        local ipa_text = lang_module.getPronunciation(frame, args, current_lexeme)
        if ipa_text and ipa_text ~= '' then
            table.insert(pronunciations, ipa_text)
        end
    else
        -- 1b. 특화 함수가 없으면, 위키데이터에서 IPA 표기를 직접 조회
        for i, stmt in pairs(base_form:getAllStatements('P898')) do
            local ipa_string = stmt.mainsnak.datavalue.value
            local specifier_text = ''
            local specifiers = {}
            if stmt.qualifiers and stmt.qualifiers['P5237'] then -- 발음의 종류
                for l, qual in pairs(stmt.qualifiers['P5237']) do
                    local stmt_value = qual.datavalue.value.id
                    table.insert(specifiers, mw.wikibase.getLabel(stmt_value))
                end
            end
            if #specifiers > 0 then
               specifier_text = "(''" .. table.concat(specifiers, "'', ''") .. "'') "
            end
            table.insert(pronunciations, specifier_text .. frame:expandTemplate{
                title = i18n['template_ipa'],
                args = {current_lexeme:getLemmas()[1][2], ipa_string}
            })
        end
    end
    
    -- 2. 오디오 파일 처리
    for i, stmt in pairs(base_form:getAllStatements('P443')) do -- 발음 (음성 파일)
        local pronunciation_file = stmt.mainsnak.datavalue.value
        local specifier_text = ''
        local specifiers = {}
        if stmt.qualifiers and stmt.qualifiers['P5237'] then -- 발음 종류
            for l, qual in pairs(stmt.qualifiers['P5237']) do
                local stmt_value = qual.datavalue.value.id
                table.insert(specifiers, mw.wikibase.getLabel(stmt_value))
            end
        end
        if #specifiers > 0 then
           specifier_text = "(''" .. table.concat(specifiers, "'', ''") .. "'') "
        end
        table.insert(pronunciations, '* ' .. specifier_text .. frame:expandTemplate{
            title= i18n['template_audio'],
            args = {current_lexeme:getLemmas()[1][2], pronunciation_file}
        })
    end

    -- 3. 수집된 모든 발음 정보(IPA, 오디오)를 합쳐서 반환
    return table.concat(pronunciations, '\n')
end

--[==[
위키텍스트 단락을 생성하는 도우미 함수입니다.
]==]
function heading_level(text, level)
    local heading_delimiter = string.rep('=', level)
    return heading_delimiter .. ' ' .. text .. ' ' .. heading_delimiter
end

--[==[
사용자가 모듈 호출 시 직접 입력한 수동 노트들을 찾아 리스트로 반환하는 도우미 함수입니다.
]==]
function get_any_notes(sections, args, keys)
    local notes = {}
    for i, v in ipairs(keys) do
        if args[v] ~= nil then
            table.insert(notes, args[v])
        end
    end
    return notes
end

--[==[
주어진 노트 리스트를 최종 결과물(`sections`)에 추가하는 도우미 함수입니다.
]==]
function add_specific_notes(sections, notes)
    for i, v in ipairs(notes) do
        table.insert(sections, v)
    end
end

--[==[
사용자 인자에서 특정 키에 해당하는 노트를 찾아 바로 최종 결과물(`sections`)에 추가하는 도우미 함수입니다.
`get_any_notes`와 `add_specific_notes`의 기능을 합친 것과 같습니다.
]==]
function add_any_notes(sections, args, keys)
    for i, v in ipairs(keys) do
        if args[v] ~= nil then
            table.insert(sections, args[v])
        end
    end
end

-- 페이지의 위키텍스트를 스캔하여 이 모듈이 총 몇 번, 어떤 순서로, 어떤 언어에 대해 호출되었는지 미리 분석하는 내부 함수입니다.
-- 이 함수는 한 페이지가 렌더링될 때 단 한 번만 실행되어 page_invocation_info 테이블을 채웁니다.
local function _scan_page_for_invocations()
    page_invocation_info = {
        order = {}, -- 호출 순서대로 lexeme ID 저장
        lang_totals = {}, -- 언어별 총 호출 횟수
        lang_sequences = {} -- 언어별 현재 순번 카운터
    }
    
    local title_obj = mw.title.getCurrentTitle()
    local content = title_obj:getContent()
    if not content then return end

    -- 모듈 호출 패턴을 찾음: {{#invoke:모듈명|all|어휘소ID ... }}
    for lexeme_id in mw.ustring.gmatch(content, '{{#invoke:Wikidata lexicographical data%s*|%s*all%s*|%s*([Ll]%d+)') do
        table.insert(page_invocation_info.order, lexeme_id)
        
        -- 각 어휘소의 언어를 미리 확인하여 언어별 총계를 계산
        local lexeme_entity = mw.wikibase.getEntity(lexeme_id)
        if lexeme_entity then
            local lang_id = lexeme_entity:getLanguage()
            page_invocation_info.lang_totals[lang_id] = (page_invocation_info.lang_totals[lang_id] or 0) + 1
        end
    end
end

--[==[
이 모듈의 외부 진입점입니다. { {{#invoke:...}} } 구문을 통해 호출됩니다.
이 함수는 어휘소 ID를 받아, 지금까지 분석한 모든 보조 함수들을 순서대로 호출하여 사전 항목의 각 부분(제목, 어원, 발음, 의미, 활용표, 참고문헌 등)을 조립하고, 최종적으로 완성된 하나의 위키텍스트를 반환합니다.
]==]
function p.all( frame )
    -- 1. 페이지 스캔 (최초 호출 시에만 실행)
    if page_invocation_info == nil then
        _scan_page_for_invocations()
    end

    local args = getArgs(frame, { parentOnly = True })
    local lexeme_id = args[1]
    if not lexeme_id then return '<strong class="error">오류: 어휘소 ID가 지정되지 않았습니다.</strong>' end
    
    local current_lexeme = mw.wikibase.getEntity(lexeme_id)
    if not current_lexeme then return '<strong class="error">오류: 어휘소 ID ' .. lexeme_id .. '를 찾을 수 없습니다.</strong>' end
    
    local lang_id = current_lexeme:getLanguage()

    -- 언어별 현재 순번 및 총 호출 횟수 계산
    page_invocation_info.lang_sequences[lang_id] = (page_invocation_info.lang_sequences[lang_id] or 0) + 1
    local current_sequence_for_lang = page_invocation_info.lang_sequences[lang_id]
    local total_calls_for_lang = page_invocation_info.lang_totals[lang_id] or 0

    -- 언어별 하위 모듈 불러오기
    local lang_code_statements = mw.wikibase.getBestStatements(lang_id, 'P218')
    local lang_code = lang_code_statements and lang_code_statements[1] and lang_code_statements[1].mainsnak.datavalue.value
    
    local lang_module = nil
    if lang_code then
        local success, module_result = pcall(require, '모듈:Wikidata lexicographical data/' .. lang_code)
        if success then
            lang_module = module_result
        end
    end
    
    local sections = {}
    local references_seen = {}
    
    local etymology_content = getEtymology(frame, current_lexeme, lang_module)

    -- 언어별 총 호출 횟수에 따라 구조 분기
    if total_calls_for_lang > 1 then
        -- 어원이 여러 개인 경우
        
        -- 1. 어원 단락
        local etymology_heading_text = i18n['heading_etymology'] .. ' ' .. current_sequence_for_lang
        table.insert(sections, heading_level(etymology_heading_text, 3))
        if etymology_content and etymology_content ~= '' then
            table.insert(sections, etymology_content)
        end
        add_any_notes(sections, args, i18n['manual_etymology'])

        -- 2. 발음 단락
        local pronunciation = getPronunciation ( frame, args, current_lexeme, lang_module )
        if pronunciation and pronunciation ~= '' then
            table.insert(sections, heading_level(i18n['heading_pronunciation'], 4))
            table.insert(sections, pronunciation)
        end
        add_any_notes(sections, args, i18n['manual_pronunciation'])

        -- 3. 품사 단락
        local cat_text = '====' .. getCategory ( frame, current_lexeme ) .. '===='
        table.insert(sections, cat_text)
        
        -- 3a. 품사별 특화 템플릿 호출
        if lang_module and lang_module.get_pos_specific_template then
            local pos_template = lang_module.get_pos_specific_template(frame, args, current_lexeme)
            if pos_template and pos_template ~= '' then
                table.insert(sections, pos_template)
            end
        end
        
        table.insert(sections, frame:expandTemplate{ title= i18n['template_lexeme'], args = {lexeme_id} })
        add_any_notes(sections, args, i18n['manual_category'])
    
        -- 4. 의미 단락
        local meanings = getMeanings ( frame, args, current_lexeme, references_seen )
        if meanings and tostring(meanings) ~= '<ol></ol>' then
            table.insert(sections, heading_level(i18n['heading_meanings'], 5))
            table.insert(sections, tostring(meanings))
        end
        add_any_notes(sections, args, i18n['manual_meaning'])

        -- 5. 나머지 단락
        if lang_module and lang_module.getInflectionTable then
            local inflectionTable = lang_module.getInflectionTable(frame, args, current_lexeme)
            if inflectionTable and inflectionTable ~= '' then
                table.insert(sections, inflectionTable)
            end
        end
        
        local reference_notes = get_any_notes(sections, args, i18n['manual_reference'])
        if #references_seen > 0 or #reference_notes > 0 then
            table.insert(sections, heading_level(i18n['heading_references'], 4))
            table.insert(sections, frame:extensionTag('references'))
            add_specific_notes(sections, reference_notes)
        end
            
        local external_links = getExternalLinks ( current_lexeme )
        if external_links and external_links ~= '' then
            table.insert(sections, heading_level(i18n['heading_external_links'], 4))
            table.insert(sections, external_links)
        end
        add_any_notes(sections, args, i18n['manual_external_link'])

    else
        -- 단일 어원

        -- 1. 어원 단락
        table.insert(sections, heading_level(i18n['heading_etymology'], 3))
        if etymology_content and etymology_content ~= '' then
            table.insert(sections, etymology_content)
        end
        add_any_notes(sections, args, i18n['manual_etymology'])

        -- 2. 발음 단락
        local pronunciation = getPronunciation ( frame, args, current_lexeme, lang_module )
        if pronunciation and pronunciation ~= '' then
            table.insert(sections, heading_level(i18n['heading_pronunciation'], 3))
            table.insert(sections, pronunciation)
        end
        add_any_notes(sections, args, i18n['manual_pronunciation'])

        -- 3. 품사 단락
        local cat_text = '===' .. getCategory ( frame, current_lexeme ) .. '==='
        table.insert(sections, cat_text)

        -- 3a. 품사별 특화 템플릿 호출
        if lang_module and lang_module.get_pos_specific_template then
            local pos_template = lang_module.get_pos_specific_template(frame, args, current_lexeme)
            if pos_template and pos_template ~= '' then
                table.insert(sections, pos_template)
            end
        end
        
        table.insert(sections, frame:expandTemplate{ title= i18n['template_lexeme'], args = {lexeme_id} })
        add_any_notes(sections, args, i18n['manual_category'])
    
        -- 4. 의미 단락
        local meanings = getMeanings ( frame, args, current_lexeme, references_seen )
        if meanings and tostring(meanings) ~= '<ol></ol>' then
            table.insert(sections, heading_level(i18n['heading_meanings'], 4))
            table.insert(sections, tostring(meanings))
        end
        add_any_notes(sections, args, i18n['manual_meaning'])
        
        -- 5. 나머지 단락
        if lang_module and lang_module.getInflectionTable then
            local inflectionTable = lang_module.getInflectionTable(frame, args, current_lexeme)
            if inflectionTable and inflectionTable ~= '' then
                table.insert(sections, inflectionTable)
            end
        end
        
        local reference_notes = get_any_notes(sections, args, i18n['manual_reference'])
        if #references_seen > 0 or #reference_notes > 0 then
            table.insert(sections, heading_level(i18n['heading_references'], 3))
            table.insert(sections, frame:extensionTag('references'))
            add_specific_notes(sections, reference_notes)
        end
            
        local external_links = getExternalLinks ( current_lexeme )
        if external_links and external_links ~= '' then
            table.insert(sections, heading_level(i18n['heading_external_links'], 3))
            table.insert(sections, external_links)
        end
        add_any_notes(sections, args, i18n['manual_external_link'])
    end
    
    return table.concat(sections,"\n\n")
end

return p