Wikikamus
mswiktionary
https://ms.wiktionary.org/wiki/Wikikamus:Laman_Utama
MediaWiki 1.47.0-wmf.2
case-sensitive
Media
Khas
Perbincangan
Pengguna
Perbincangan pengguna
Wikikamus
Perbincangan Wikikamus
Fail
Perbincangan fail
MediaWiki
Perbincangan MediaWiki
Templat
Perbincangan templat
Bantuan
Perbincangan bantuan
Kategori
Perbincangan kategori
Lampiran
Perbincangan lampiran
Rima
Perbincangan rima
Tesaurus
Perbincangan tesaurus
Indeks
Perbincangan indeks
Petikan
Perbincangan petikan
Rekonstruksi
Perbincangan rekonstruksi
Padanan isyarat
Perbincangan padanan isyarat
Konkordans
Perbincangan konkordans
TimedText
TimedText talk
Modul
Perbincangan modul
Acara
Perbincangan acara
مايو
0
9228
342804
263644
2026-05-16T12:29:22Z
Hakimi97
2668
342804
wikitext
text/x-wiki
==Bahasa Arab==
==== Kata nama ====
'''{{ARchar|مَايُو}}''' • (māyu)
# [[Mei]] {{qualifier|Kalendar barat}}
=== Etimologi ===
Akhirnya daripada {{der|ar|la|Maius}}.
=== Tesaurus ===
; Sinonim: {{l|ar|أَيَّار|sc=Arab}}, {{l|ar|ماي|sc=Arab}}.
[[Kategori:Kata nama bahasa Arab]]
[[Kategori:ar:Bulan takwim Masihi]]
m3k4kbfkdcedobjt88l47k2q6c4ewe4
July
0
9394
342806
321226
2026-05-16T12:30:54Z
Hakimi97
2668
342806
wikitext
text/x-wiki
{{also|july}}
==Bahasa Inggeris==
{{wikipedia|lang=en}}
====Kata nama khas====
{{en-knk}}
# [[bulan|Bulan]] ketujuh dalam [[kalendar Masihi]], selepas [[Jun]] dan sebelum [[Ogos]]. Kependekan: '''[[Jul]]''' atau '''[[Jul.]]'''
===Etimologi===
Daripada {{inh|en|enm|Julie}}, {{m|enm|julye}}, {{m|enm|iulius}}, daripada {{der|en|xno|julie}}, daripada {{der|en|fro|jule}}, {{m|fro|juil}}, daripada {{der|en|la|iūlius}} (bulan {{w|Julius Caesar|Gaius Julius Caesar}}), mungkin pemendekan bagi ''*Iovilios'', "diturunkan daripada [[Jove]]".
===Sebutan===
* {{enPR|jo͝o-līʹ}}, {{IPA|en|/d͡ʒʊˈlaɪ/}}
* {{audio|en|en-us-July.ogg|Audio (US)}}
* {{rhymes|en|aɪ}}
===Terbitan===
{{atas3}}
* [[Black July]]
* [[Christmas in July]]
* [[Fourth of July]]
* [[Holiday in July]]
* [[w:en:July 1 marches|July 1 marches]]
* [[w:en:July 7 bombings|July 7 bombings]]
* [[July 20 Plot]]
* [[w:en:July Column|July Column]]
* [[July Cup]]
{{tengah3}}
* [[July Days]]
* [[July Monarchy]]
* [[July Morning]]
* [[July Ordinances]]
* [[July Revolution]]
* [[July Stakes]]
* [[July Ultimatum]]
* [[mid-July]]
{{bawah}}
[[Kategori:en:Bulan takwim Masihi]]
[[Kategori:Eponim bahasa Inggeris]]
hrqo7xnab7dl9e9xplq6q8rn50j7v68
يوليو
0
9396
342802
241535
2026-05-16T12:29:04Z
Hakimi97
2668
342802
wikitext
text/x-wiki
{{ar}}
==== Kata nama ====
<big>'''يُولْيُو'''</big> • (yúlyu)
# [[Julai]] {{qualifier|Kalendar barat}}
=== Tesaurus ===
; Sinonim: [[تموز]], [[يُولِيُوز]].
=== Lihat juga ===
* [[رجب]]
[[Kategori:Kata nama bahasa Arab]]
[[Kategori:ar:Bulan takwim Masihi|7]]
drif8kp7dme79fj1u2fiaxchh5b2pvz
August
0
9451
342805
322511
2026-05-16T12:30:37Z
Hakimi97
2668
342805
wikitext
text/x-wiki
{{also|august}}
==Bahasa Inggeris==
{{wikipedia|lang=en}}
====Kata nama khas====
'''August''' (''jamak'' '''[[Augusts]]''')
# Bulan kelapan [[kalendar Masihi]], selepas [[Julai]] dan sebelum [[September]]. Kependekan: '''[[Aug]]''' atau '''[[Aug.]]'''
# ''Nama berian perempuan'' diterbit daripada bulannya (penggunaan moden jarang).
# ''Nama berian lelaki''.
===Etimologi===
Daripada perkembangan awal {{der|en|enm|August|August(us)}}, dilatinkan semula daripada {{der|en|ang|Agustus}}, daripada {{cog|LL.|Agustus}}, daripada {{der|en|la|augustus||bulan Ogos}}, daripada agnomen {{m|la|Augustus||mulia}} kepada {{w|Augustus|Gaius Julius Caesar Augustus}}, yang berkemungkinan daripada sama ada bahasa Latin Kuno {{m|itc-ola|*augos|t=meningkat}}, daripada {{der|en|itc-pro|*augos}}, daripada bentuk dasar {{der|en|ine-pro|-}} {{m|ine-pro|*h₂ewg-||untuk meningkat}}; atau gubahan {{cog|la|avis||burung}} + {{m|la|garrio|garrīre|t=berceloteh}} yang merujuk kepada ramalan dengan memerhati penerbangan burung, menyanyi, memberi makan atau isi perut.
===Sebutan===
* {{enPR|ôʹgəst}}, {{AFA|en|/ˈɔːɡəst/}}
* {{audio|en|en-us-August.ogg|Audio (US)}}
===Terbitan===
{{atas3}}
* [[august]] {{qualifier|kata kerja}}
* [[August Bank Holiday]]
* {{w|en:August Coup|August Coup}}
* {{w|en:Mid-Autumn Festival|August Moon Festival}}
{{tengah3}}
* {{w|en:August Penguin|August Penguin}}
* [[August plum]]
* {{w|en:August Putsch|August Putsch}}
* {{w|en:August Revolution|August Revolution}}
{{tengah3}}
* [[August rooster]]
* {{w|en:Operation August Storm|Operation August Storm}}
* {{w|en:August Uprising in Georgia|August Uprising in Georgia}}
* [[mid-August]]
{{ter-bawah}}
===Tesaurus===
; Sinonim: [[Augost]] {{qualifier|Inggeris Jamaica}}
===Anagram===
* [[Tausug#Bahasa Inggeris|Tausug]]
[[Kategori:Kata nama khas bahasa Inggeris]]
[[Kategori:Kata nama khas terhitung bahasa Inggeris]]
[[Kategori:en:Bulan takwim Masihi]]
[[Kategori:Nama berian perempuan bahasa Inggeris daripada bahasa Inggeris]]
[[Kategori:Nama berian lelaki bahasa Inggeris daripada bahasa Latin]]
[[Kategori:Eponim bahasa Inggeris]]
==Bahasa Denmark==
====Kata nama khas====
{{head|da|kata nama khas}}
# ''Nama berian lelaki''. Bentuk feminin: [[Augusta]].
===Etimologi===
Daripada {{der|da|la|Augustus}}.
[[Kategori:Nama berian lelaki bahasa Denmark]]
==Bahasa Estonia==
====Kata nama khas====
{{head|et|kata nama khas}}
# ''Nama berian lelaki''.
===Etimologi===
Daripada {{der|et|la|Augustus}}.
[[Kategori:Nama berian lelaki bahasa Estonia]]
==Bahasa Ewe==
====Kata nama khas====
{{head|ee|kata nama khas}}
# [[Ogos]]
{{C|ee|Bulan dalam tahun}}
==Bahasa Finland==
====Kata nama khas====
{{head|fi|kata nama khas}}
# ''Nama berian lelaki''
===Etimologi===
Daripada {{bor|fi|sv|August}}, daripada {{der|fi|la|Augustus}}.
[[Kategori:Nama berian lelaki bahasa Finland]]
==Bahasa Jerman==
====Kata nama====
{{de-noun|m,-:s:es}}
# [[Ogos]] {{gloss|bulan}}
====Kata nama khas====
{{de-proper noun|m,(s)}}
# ''Nama berian lelaki'', serumpun dengan Inggeris [[Augustus]].
===Sebutan===
* {{AFA|de|/aʊ̯ˈɡʊst/}} {{i|bulan}}
* {{AFA|de|/ˈaʊ̯ɡʊst/}} {{i|nama berian}}
* {{audio|de|De-August.ogg|audio}}
===Terbitan===
{{atas3}}
* [[Augustabend]]
* [[Augustabende]]
* [[Augustfeier]]
* [[Augustfeiern]]
* [[Augustferien]]
* [[Augustheu]]
{{tengah3}}
* [[Augustmorgen]]
* [[Augustnacht]]
* [[Augustnächte]]
* [[Augusttag]]
* [[Augusttage]]
* [[Augusttraube]]
{{tengah3}}
* [[Augusttrauben]]
* [[Augustwoche]]
* [[Augustwochen]]
* [[Augustwochenende]]
* [[Augustwochenenden]]
* {{qualifier|entomologi}} [[Eichenzackenrandspanner]] ([[Eichen-Zackenrandspanner]])
{{ter-bawah}}
===Tesaurus===
; Sinonim:
* [[Erntemonat]] {{i|arkaik}}
* [[Erntemond]] {{i|arkaik}}
* [[Ernting]] {{i|arkaik}}
===Keturunan===
* Armenia: [[Ավգուստ]]
* Rusia: [[А́вгуст]]
{{C|de|Bulan dalam tahun}}
[[Kategori:Nama berian bahasa Jerman]]
==Bahasa Luxembourg==
====Kata nama khas====
{{head|lb|kata nama khas}}
# [[Ogos]]
{{C|lb|Bulan dalam tahun}}
==Bahasa Norway==
====Kata nama khas====
{{head|no|kata nama khas}}
# ''Nama berian lelaki''.
===Etimologi===
Daripada {{der|no|la|Augustus}}
[[Kategori:Nama berian lelaki bahasa Norway]]
==Bahasa Prusia Kuno==
====Kata nama khas====
{{head|prg|kata nama khas}}
# [[Ogos]] {{gloss|bulan}}
===Lihat juga===
* [[Daggis]]
{{C|prg|Bulan dalam tahun}}
==Bahasa Scots==
====Kata nama khas====
{{head|sco|kata nama khas}}
# [[Ogos]]
===Etimologi===
Daripada {{der|sco|la|augustus}}.
===Lihat juga===
* {{sense|bulan}} Sebelum: [[Julie]]. Selepas: [[September]]
{{C|sco|Bulan dalam tahun}}
==Bahasa Sweden==
====Kata nama khas====
{{head|sv|kata nama khas}}
# ''Nama berian lelaki''. Bentuk feminin:: [[Augusta]]
===Etimologi===
Daripada {{der|sv|la|Augustus}}.
[[Kategori:Nama berian lelaki bahasa Sweden]]
tizm04yk5jxod0hpfx8fimrs4l6rs4z
أغسطس
0
9454
342803
241544
2026-05-16T12:29:13Z
Hakimi97
2668
342803
wikitext
text/x-wiki
{{ar}}
{{wikipedia|lang=ar}}
==== Kata nama ====
'''أغُسْطُسْ''' • (ʾaġúsṭus)
# [[Ogos]] {{qualifier|Kalendar barat}}
=== Lihat juga ===
* [[شعبان]]
* [[آب]]
[[Kategori:Kata nama bahasa Arab]]
[[Kategori:ar:Bulan takwim Masihi|8]]
hru1x3vlfg55tzcgmvtu9if5yljzth7
September
0
9456
342809
334087
2026-05-16T12:32:15Z
Hakimi97
2668
342809
wikitext
text/x-wiki
{{also|september}}
{{Pautan Projek Wikimedia}}
==Bahasa Melayu==
{{wikipedia}}
===Kata nama khas===
{{ms-knk}}
# Bulan kesembilan dalam tahun Masihi yang mengandungi 30 hari, antara [[Ogos]] dan [[Oktober]].
===Etimologi===
{{bor+|ms|en|September}}, daripada bahasa {{inh|en|enm|-}}, daripada bahasa {{der|en|ang|-}} Akhir, daripada perkataan bahasa {{der|ms|fro|septembre}}, {{der|ms|la|September||bulan ketujuh}}, daripada {{m|la|septem||tujuh}}, daripada {{der|en|ine-pro|*septḿ̥||tujuh}}; + {{der|en|la|-ber}}, daripada {{m|la|-bris}} yang merupakan akhiran bersifat; September merupakan bulan ketujuh dalam kalendar Rom.
===Sebutan===
* {{dewan|sép|tém|ber}}
* {{AFA|ms|[sɛptɛmbə(r)]}}
* {{rhymes|ms|bə(r)|ə(r)}}
===Tulisan Jawi===
{{ARchar|[[سيڤتيمبر]]}}
===Terjemahan===
{{ter-atas|bulan kesembilan kalendar Masihi}}
* Abaza: {{t|abq|сентябрь}}
* Abkhaz: {{t|ab|цәыббра|tr=cwəbbra}}
* Afrikaans: {{t+|af|September}}
* Alabama: {{t|akz|hasiholtina istachákkàali}}
* Albania: {{t|sq|shtator}}
* Alutiiq: {{t|ems|Qakiiyat Iraluat}}
* Amhara: {{t|am|ሰፕቴምበር|tr=septembär|sc=Ethi}}
* Apache:
*: Apache Barat: {{t|apw|Binestʼánchoh}}
* Arab: {{t+|ar|سبْتمْبر|tr=sibtímbir, sibtámbir, sabtámbar|m}}, {{t|ar|ايلول|alt=أيْلولٌ|tr=’éilūl|m}}
*: Arab Mesir: {{t|arz|سبتمبر|m|tr=sebtamber|sc=Arab}}
* Armenia: {{t+|hy|սեպտեմբեր|tr=september}}
*: Armenia Lama: {{t|xcl|սեպտեմբեր|tr=september|sc=Armn}}
* Aromania: {{t|rup|septemvriu}}, {{t|rup|ghismãciuni}}, {{t|rup|stavru}}
* Asturia: {{t+|ast|septiembre|m}}
* Azeri: {{t+|az|sentyabr}}
* Basque: {{t+|eu|irail}}
* Belanda: {{t+|nl|september}}
* Benggali: {{t|bn|সেপ্টেম্বর|tr=Sepţembôr|sc=Beng}}
* Bislama: {{t|bi|septemba}}
* Breton: {{t+|br|Gwengolo}}, miz Gwengolo
* Bulgaria: {{t+|bg|септември|m|tr=septémvri}}
* Burma: {{t+|my|စက်တင်ဘာ|sc=Mymr|tr=settinba}}
* Catalonia: {{t+|ca|setembre|m}}
* Chechen: {{t|ce|Гезгмашан-бутт|sc=Cyrl}}
* Cherokee: {{t|chr|ᏚᎵᏍᏗ|tr=Dulisdi|sc=Cher}}
* Cina:
*: Mandarin: {{t+|cmn|九月|tr=jiǔyuè}}
* Corsican: {{t+|co|settembre}}
* Czech: {{t+|cs|září|n}}
* Dakota: {{t|dak|Wiinapciŋwaŋka}}
* Denmark: {{t+|da|september}}
* Esperanto: {{t+|eo|septembro}}, {{t|eo|Septembro}}
* Ewe: {{t|ee|Anyɔnyɔ}}, {{t|ee|September}}
* Faroe: {{t|fo|september|m}}, {{t|fo|septembur|m}}
* Fiji: {{t|fj|Seviteba}}
* Finland: {{t+|fi|syyskuu}}
* Frisia Barat: {{t+|fy|septimber}}, {{t+|fy|hjerstmoanne}}
* Friulia: {{t|fur|Setembar}}
* Galicia: {{t+|gl|setembro|m}}
* Georgia: {{t+|ka|სექტემბერი|sc=Geor}}
* Greek: {{t+|el|Σεπτέμβριος|m|tr=Septémvrios}}, {{t+|el|Σεπτέμβρης|m|tr=Septémvris}}, {{t|el|Τρυγητής|m|tr=Trygitís}}
* Greenland: {{t+|kl|Septembari}}
* Hawaii: {{t|haw|Kepakemapa}}
* Hindi: {{t+|hi|सितम्बर|tr=sitambar}}
* Hungary: {{t+|hu|szeptember}}
* Ibrani: {{t+|he|סֶפְּטֶמְבֶּר|tr=septémber}}
* Iceland: {{t+|is|september|m}}, {{t|is|septembermánuður|m}}
* Ido: {{t+|io|septembro}}
* Inari Sami: {{t|smn|čohčâmáánu}}
* Indonesia: {{t|id|September}}
* Inggeris: {{t+|en|September}}
* Inggeris Kuno: {{t|ang|hærfestmōnaþ|m}}, {{t|ang|hāliġmōnaþ|m}}
* Interlingua: {{t|ia|septembre}}
* Ireland: {{t+|ga|Meán Fómhair|m}}
* Itali: {{t+|it|settembre|m}}
* Jepun: {{t+|ja|九月|tr=くがつ, kugatsu}}, {{t+|ja|長月|tr=ながつき, nagatsuki}}
* Jerman: {{t+|de|September|m}}, {{t+|de|Scheiding|m}}
* Jerman Rendah:
*: Jerman Rendah Jerman: {{t|nds-de|September|m}}, {{t|nds-de|Harvstmaand|m}}
* Kazakh: {{t+|kk|қыркүйек|tr=qırküyek|sc=Cyrl}}
* Khmer: {{t+|km|កញ្ញា|tr=kăññā}}
* Kiribati: {{t|gil|Tebetembwa|sc=Cyrl}}
* Korea: {{t+|ko|구월|sc=Kore}}
* Kreol Haiti: {{t|ht|septanm}}
* Ladin: {{t|lld|setember}}
* Lao: {{t|lo|ເດືອນກັນຍາ|sc=Laoo}}
* Latin: {{t+|la|september}}
* Latvia: {{t+|lv|septembris|m}}
* Lezgi: {{t|lez|мара|tr=mara|sc=Cyrl}}
* Lithuania: {{t+|lt|rugsėjis|m}}, {{t+|lt|rugsėjo|f}}
* Livonia: {{t|liv|septembõr}}, {{t|liv|sigžkū}}
* Luxembourg: {{t+|lb|September|m}}, {{t+|lb|Hierschtmount|m}}
* Macedonia: {{t+|mk|септември|m|tr=septémvri}}
* Malagasy: {{t+|mg|septambra}}
* Malta: {{t|mt|Settembru}}
* Manchu: (uyun biya)
* Maori: {{t|mi|Hepetema}}
* Montagnais: {{t|moe|ushkau-pishimᵘ}}
* Navajo: {{t|nv|Biniʼantʼą́ą́tsoh}}
* Neapolitan: {{t|nap|settiembre|m}}, {{t|nap|settembre|m}}
* Norway: {{t+|no|september}}
* Novial: {{t|nov|septembre}}
* Occitan: {{t+|oc|setembre|m}}
* Ojibwe: {{t|oj|waatebagaa-giizis}}
* Oriya: {{t|or|ସେପ୍ଟେମ୍ବର|sc=Orya}}
* Ossetia: {{t|os|сентябрь|tr=sentjabr’|sc=Cyrl}}
* Parsi: {{t+|fa|سپتامبر|tr=septâmbr}}
* Perancis: {{t+|fr|septembre|m}}
* Poland: {{t+|pl|wrzesień|m}}
* Portugis: {{t+|pt|setembro|m}}
* Romania: {{t+|ro|septembrie}}, {{qualifier|popular}} {{t+|ro|răpciune}}, {{qualifier|popular}} {{t|ro|vincer}}
* Romansch: {{t|rm|satember|m}}, {{t|rm|settember|m}}
* Rusia: {{t+|ru|сентябрь|m|tr=sentjábr’}}
* Samoa: {{t|sm|setema}}
* Sardinia: {{t|sc|cabidanne}}, {{t|sc|cabidanni}}
* Sepanyol: {{t+|es|septiembre|m}}
* Serbo-Croatia:
*: Cyril: {{t|sh|септембар|m|sc=Cyrl}}
*: Rumi: {{t+|sh|septembar|m}}, {{t+|sh|rujan|m}} {{qualifier|Croatia}}
* Sicilia: {{t+|scn|sittèmmiru}}
* Skolt Sami: {{t|sms|čõhččmään}}
* Slovak: {{t+|sk|september|m}}
* Slovene: {{t|sl|septêmber|m}}
* Sotho: {{t+|st|Lwetse}}
* Sweden: {{t+|sv|september}}
* Tahiti: {{t|ty|tetepa}}
* Tajik: {{t+|tg|сентябр|tr=sentjabr|sc=Cyrl}}
* Telugu: {{t|te|సెప్టెంబరు|tr=sepTeMbaru}}
* Thai: {{t+|th|กันยายน|tr=gan yaa yohn}}
* Tok Pisin: {{t|tpi|septemba}}
* Tonga: {{t|to|sēpitema}}
* Turki: {{t+|tr|eylül}}
* Ukraine: {{t+|uk|вересень|m|tr=véresen’}}
* Venetian: {{t+|vec|setenbre}}
* Volapük: {{t+|vo|setul}}
* Võro: {{t|vro|süküskuu}}
* Wales: {{t+|cy|Medi|m}}
* Wolof: {{t|wo|Sattumbar}}
* Yiddish: {{t+|yi|סעפּטעמבער|m|tr=september|sc=Hebr}}
* Yup'ik: {{t|esu|Amiraayaaq}}
* Zuni: {{t|zun|Mola: Akwapba}}, {{t|zun|Łi'dekwakkya Ts'ana}}
{{ter-bawah}}
[[Kategori:ms:Bulan takwim Masihi]]
==Bahasa Afrikaans==
{{wikipedia|lang=af}}
====Kata nama====
'''September''' (''jamak'' '''[[Septembermaande]]''')
# [[#Bahasa Melayu|September]]
===Sebutan===
* {{AFA|af|[səpˈtem(b)ər, səˈtemər]}}
===Tulisan Arab===
{{ARchar|سِپْتَِمْبِرْ}}
[[Kategori:Kata nama bahasa Afrikaans]]
{{C|af|Bulan dalam tahun}}
==Bahasa Ewe==
==== Kata nama khas====
# [[#Bahasa Melayu|September]]
===Tesaurus===
; Sinonim: [[Anyɔnyɔ]].
[[Kategori:Kata nama khas bahasa Ewe]]
{{C|ee|Bulan dalam tahun}}
==Bahasa Indonesia==
==== Kata nama khas====
# Nama bulan ke-9.
[[Kategori:Kata nama khas bahasa Indonesia]]
{{C|id|Bulan dalam tahun}}
==Bahasa Inggeris==
{{wikipedia|lang=en}}
==== Kata nama khas====
'''September''' (''jamak'' '''[[Septembers]]''')
# Bulan kesembilan [[kalendar Masihi]], selepas [[August]] dan sebelum [[October]]. Kependekan: '''[[Sep]]''' atau '''[[Sep.]]''', '''[[Sept]]''' atau '''[[Sept.]]'''
===Etimologi===
Daripada bahasa {{inh|en|enm|-}}, daripada bahasa {{der|en|ang|-}} Akhir, daripada perkataan bahasa {{der|ms|fro|septembre}}, {{der|ms|la|September||bulan ketujuh}}, daripada {{m|la|septem||tujuh}}, daripada {{der|en|ine-pro|*septḿ̥||tujuh}}; + {{der|en|la|-ber}}, daripada {{m|la|-bris}} yang merupakan akhiran bersifat; September merupakan bulan ketujuh dalam kalendar Rom.
===Sebutan===
* {{a|UK}} {{AFA|en|/sɛpˈtɛmbə/}}
* {{a|US}} {{enPR|sĕp-tĕmʹbər}} {{AFA|en|/sɛpˈtɛmbəɹ/}}
* {{audio|en|en-us-September.ogg|Audio (US)}}
* {{rhymes|en|ɛmbə(r)}}
===Terbitan===
{{atas3}}
* [[w:en:Black September|Black September]]
* [[endless September]]
* [[eternal September]]
* [[Great September]]
* [[it's always September]]
* [[May-September romance]]
* [[mid-September]]
* [[perpetual September]]
* [[September 10th]]
* [[September 11]]
{{tengah3}}
* [[September call-up]]
* {{w|en:September Campaign|September Campaign}}
* {{w|en:September Convention|September Convention}}
* {{w|en:September Dossier|September Dossier}}
* [[Septembered]]
* [[september elm]]
* [[september equinox]]
* [[Septemberer]]
* {{w|en:September Group|September Group}}
{{tengah3}}
* [[Septemberish]], [[Septembrish]]
* [[Septemberism]]
* {{w|en:September Massacres|September Massacres}}
* [[September people]]
* {{w|en:September Six|September Six}}
* {{w|en:Eternal September|September that never ended}}
* [[September thorn]]
* [[Septembrian]]
* [[Septembrist]]
{{ter-bawah}}
===Tesaurus===
; Sinonim: [[Septembre]] {{qualifier|usang}}
===Lihat juga===
* [[9/11]]
[[Kategori:Kata nama khas bahasa Inggeris]]
[[Kategori:Kata nama khas terhitung bahasa Inggeris]]
[[Kategori:en:Bulan takwim Masihi]]
==Bahasa Jerman==
{{wikipedia|lang=de}}
====Kata nama====
'''September''' (''genitif'' '''[[Septembers]]''' ''atau'' '''[[September]]''', ''jamak'' '''[[September]]''')
# [[#Bahasa Melayu|September]]
===Sebutan===
* {{AFA|de|/zɛpˈtɛmbɐ/}}
* {{audio|de|De-September.ogg|Audio}}
* {{audio|de|De-September2.ogg|Audio}}
[[Kategori:Kata nama bahasa Jerman]]
{{C|de|Bulan dalam tahun}}
==Bahasa Luxembourg==
{{wikipedia|lang=lb}}
==== Kata nama khas====
'''September'''
# [[#Bahasa Melayu|September]]
[[Kategori:Kata nama khas bahasa Luxembourg]]
{{C|lb|Bulan dalam tahun}}
==Bahasa Scots==
{{wikipedia|lang=sco}}
==== Kata nama khas====
{{head|sco|kata nama khas}}
# [[#Bahasa Melayu|September]]
===Etimologi===
Daripada {{der|sco|la|september||bulan ketujuh}}.
===Lihat juga===
* {{sense|bulan}} Sebelum: [[August]]. Selepas: [[October]]
{{C|sco|Bulan dalam tahun}}
lit7lsj4tdu1un0u160i9rz58msojqe
October
0
9699
342808
321487
2026-05-16T12:31:49Z
Hakimi97
2668
342808
wikitext
text/x-wiki
{{also|october}}
==Bahasa Inggeris==
{{wikipedia|lang=en}}
===Kata nama khas===
{{en-knk}}
# [[bulan|Bulan]] kesepuluh [[kalendar Masihi]], selepas [[September]] dan sebelum [[November]]. Kependekan: '''[[Oct]]'''; [[Oktober]].
===Etimologi===
{{etymon|en|:inh|enm:<ety:bor<fro:octobre>>}}
Daripada {{inh|en|enm|-}}, dipinjam daripada {{der|en|fro|octobre}}, daripada {{der|en|la|Octōber||bulan kelapan}}, daripada {{der|en|la|octō||eight}}, daripada {{der|en|ine-pro|*oḱtṓw||empat kali dua}}; + {{der|en|la|-ber}}, daripada {{m|la|-bris}}, akhiran pembentuk kata sifat; Oktober ialah bulan kelapan dalam takwim Rom.
===Sebutan===
* {{a|UK}} {{AFA|en|/ɒkˈtəʊbə/}}
* {{a|AS}} {{enPR|äk-tōʹbər}}, {{AFA|en|/ɑkˈtoʊbəɹ/}}
* {{audio|en|en-us-October.ogg|Audio (AS)}}
===Terbitan===
{{atas2}}
* [[mid-October]]
* [[October beer]]
* [[October-bird]]
* [[October effect]]
* [[Octoberfest]]
* [[Octoberist]], [[Octobrist]]
{{tengah2}}
* [[October Revolution]]
* [[October surprise]]
* {{w|October War|lang=en}}
* [[Red October]]
{{ter-bawah}}
===Tesaurus===
; Sinonim: [[Octobre]] {{qualifier|kuno}}.
[[Kategori:Kata nama khas bahasa Inggeris]]
[[Kategori:en:Bulan takwim Masihi]]
==Bahasa Scots==
===Kata nama khas===
{{head|sco|kata nama khas}}
# [[Oktober]]
===Etimologi===
Daripada {{der|sco|la|octōber||yang bulan kelapan}}.
===Sebutan===
* {{AFA|sco|[okˈtobər]}}
===Lihat juga===
* {{sense|bulan}} Sebelum: [[September]]. Selepas: [[November]]
{{C|sco|Bulan dalam tahun}}
buf2577x2aqntazehnhhev1lo0m2pct
Modul:languages/data/3/j
828
9767
342846
265546
2026-05-17T03:51:37Z
PeaceSeekers
3334
342846
Scribunto
text/plain
local m_langdata = require("Module:languages/data")
-- Loaded on demand, as it may not be needed (depending on the data).
local function u(...)
u = require("Module:string utilities").char
return u(...)
end
local c = m_langdata.chars
local p = m_langdata.puaChars
local s = m_langdata.shared
local m = {}
m["jaa"] = {
"Jamamadí",
3053275,
"auf",
"Latn",
}
m["jab"] = {
"Hyam",
35403,
"nic-plc",
"Latn",
}
m["jac"] = {
"Jakaltek",
33393,
"myn",
"Latn",
}
m["jad"] = {
"Jahanka",
3913992,
"dmn-wmn",
"Latn",
}
m["jae"] = {
"Jabem",
3571232,
"poz-ocw",
"Latn",
}
m["jaf"] = {
"Jara",
56289,
"cdc",
"Latn",
}
m["jah"] = {
"Jah Hut",
2742661,
"mkh-asl",
"Latn",
}
m["jaj"] = {
"Zazao",
3574969,
"poz-ocw",
"Latn",
}
m["jak"] = {
"Jakun",
4216968,
"poz-mly",
"Latn",
}
m["jal"] = {
"Yalahatan",
8047298,
"poz-cma",
"Latn",
}
m["jam"] = {
"Kreol Jamaica",
35939,
"crp",
"Latn",
ancestors = "en",
}
m["jan"] = {
"Janday",
6150919,
"aus-pam",
"Latn",
}
m["jao"] = {
"Yanyuwa",
34241,
"aus-pam",
"Latn",
}
m["jaq"] = {
"Yaqay",
8049134,
"ngf",
"Latn",
}
m["jas"] = {
"New Caledonian Javanese",
12953527,
"poz-sus",
"Latn",
ancestors = "jv",
}
m["jat"] = {
"Jakati",
4159744,
"inc-pan",
ancestors = "lah",
}
m["jau"] = {
"Yaur",
8050346,
"poz-hce",
"Latn",
}
m["jax"] = {
"Melayu Jambi",
3915769,
"poz-mly",
"Latn",
}
m["jay"] = {
"Yan-nhangu",
10723405,
"aus-yol",
"Latn",
}
m["jaz"] = {
"Jawe",
3163200,
"poz-cln",
"Latn",
}
m["jbj"] = {
"Arandai",
4784070,
"ngf",
"Latn",
}
m["jbk"] = {
"Barikewa",
nil,
"ngf",
"Latn",
}
m["jbn"] = {
"Nefusa",
36151,
"ber",
}
m["jbo"] = {
"Lojban",
36350,
"art",
"Latn",
type = "appendix-constructed",
}
m["jbr"] = {
"Jofotek-Bromnya",
16886849,
"paa-tkw",
}
m["jbt"] = {
"Jabutí",
2060023,
"sai-mje",
"Latn",
}
m["jbu"] = {
"Jukun Takum",
35447,
"nic-jkn",
"Latn",
}
m["jbw"] = {
"Yawijibaya",
31722921,
"aus-wor",
"Latn",
}
m["jcs"] = {
"Bahasa Isyarat Negara Jamaica",
6127418,
"sgn",
"Latn", -- when documented
}
m["jct"] = {
"Krymchak",
33723,
"trk-kcu",
"Latn, Cyrl",
}
m["jda"] = {
"Jad",
12633440,
"sit-las",
}
m["jdg"] = {
"Jadgali",
13560607,
"inc-snd",
}
m["jdt"] = {
"Judeo-Tat",
56495,
"ira-swi",
"Latn, Cyrl, Hebr",
ancestors = "fa",
translit = "jdt-translit",
}
m["jeb"] = {
"Jebero",
967031,
"sai-cah",
}
m["jee"] = {
"Jerung",
56372,
"sit-kiw",
}
m["jeg"] = {
"Jeng",
5091274,
"mkh-ban",
"Latn",
}
m["jeh"] = {
"Jeh",
3914636,
"mkh-ban",
"Latn",
}
m["jei"] = {
"Yei",
8051326,
}
m["jek"] = {
"Jeri Kuo",
11031936,
"dmn-jje",
"Latn",
}
m["jel"] = {
"Yelmek",
8052020,
}
m["jen"] = {
"Dza",
35558,
"alv-bwj",
}
m["jer"] = {
"Jere",
3915449,
"nic-jer",
}
m["jet"] = {
"Manem",
6748412,
"paa-brd",
}
m["jeu"] = {
"Jonkor Bourmataguil",
56269,
}
m["jgb"] = {
"Ngbee",
7022243,
}
-- "jge" IS TREATED AS "ka", SEE WT:LT
m["jgk"] = {
"Gwak",
17523694,
"nic-jrw",
}
m["jgo"] = {
"Ngomba",
36287,
"bai",
"Latn",
}
m["jhi"] = {
"Jehai",
3176748,
"mkh-asl",
}
m["jhs"] = {
"Bahasa Isyarat Jhankot",
6190889,
"sgn",
}
m["jia"] = {
"Jina",
56297,
}
m["jib"] = {
"Jibu",
3914448,
"nic-jkn",
"Latn",
}
m["jic"] = {
"Tol",
3178609,
"nai-jcq",
"Latn",
}
m["jid"] = {
"Bu",
3913321,
"nic-nin",
"Latn",
}
m["jie"] = {
"Jilbe",
56281,
}
m["jig"] = {
"Jingulu",
6202435,
"aus-mir",
}
m["jih"] = {
"Shangzhai",
25559440,
"sit-rgy",
}
m["jii"] = {
"Jiiddu",
56769,
"cus-som",
"Latn",
}
m["jil"] = {
"Jilim",
6192674,
"ngf-mad",
}
m["jim"] = {
"Jimjimen",
56288,
"cdc-cbm",
"Latn",
}
m["jio"] = {
"Jiamao",
3178570,
nil,
"Latn",
}
m["jiq"] = {
"Khroskyabs",
3118757,
"sit-rgy",
}
m["jit"] = {
"Jita",
6203228,
"bnt-haj",
"Latn",
}
m["jiu"] = {
"Youle Jino",
12952530,
"tbq-jin",
}
m["jiv"] = {
"Shuar",
617291,
"sai-jiv",
"Latn",
}
m["jiy"] = {
"Buyuan Jino",
12952528,
"tbq-jin",
}
m["jje"] = {
"Jeju",
129648,
"qfa-kor",
"Kore",
ancestors = "okm",
translit = "jje-translit",
entry_name = s["Kore-entryname"],
}
m["jjr"] = {
"Zhár",
17523697,
"nic-jrw",
}
m["jka"] = {
"Kaera",
16910923,
"ngf",
"Latn",
}
m["jko"] = {
"Kubo",
12952670,
"ngf",
}
m["jkr"] = {
"Koro (India)",
36162,
"sit-gsi",
}
m["jku"] = {
"Labir",
1990210,
"nic-jrn",
}
m["jle"] = {
"Ngile",
36329,
"alv-tal",
}
m["jls"] = {
"Bahasa Isyarat Jamaica",
6127433,
"sgn",
"Latn", -- when documented
}
m["jma"] = {
"Dima",
5277140,
}
m["jmb"] = {
"Zumbun",
56252,
"cdc-wst",
}
m["jmc"] = {
"Machame",
12952751,
"bnt-chg",
"Latn",
}
m["jmd"] = {
"Yamdena",
8048030,
"poz-cet",
"Latn",
}
m["jmi"] = {
"Jimi",
3502308,
"cdc-wst",
"Latn",
}
m["jml"] = {
"Jumli",
6310993,
"inc-pah",
}
m["jmn"] = {
"Makuri Naga",
6740482,
"sit-aao",
}
m["jmr"] = {
"Kamara",
35561,
"nic-dag",
}
-- "jms" IS TREATED AS "mff", SEE WT:LT
m["jmw"] = {
"Mouwase",
nil,
"ngf",
"Latn",
}
m["jmx"] = {
"Western Juxtlahuaca Mixtec",
12953731,
"omq-mxt",
"Latn",
}
m["jna"] = {
"Jangshung",
12633505,
"sit-kin",
"Takr"
}
m["jnd"] = {
"Jandavra",
6150941,
"inc-wes",
"Arab",
ancestors = "gu",
}
m["jng"] = {
"Yangman",
10723416,
"aus-yng",
}
m["jni"] = {
"Janji",
3915330,
"nic-jer",
}
m["jnj"] = {
"Yemsa",
36873,
"omv",
}
m["jnl"] = {
"Rawat",
7296948,
"sit-gma",
}
m["jns"] = {
"Jaunsari",
6164857,
"him",
}
m["job"] = {
"Joba",
13123409,
"bnt-shh",
}
m["jod"] = {
"Wojenaka",
11029540,
"dmn-mnk",
}
m["jor"] = {
"Jorá",
5393974,
"tup-gua",
"Latn",
}
m["jos"] = {
"Bahasa Isyarat Jordan",
6534917,
"sgn",
"Sgnw",
}
m["jow"] = {
"Jowulu",
3914487,
"dmn-mnw",
"Latn",
}
-- "jpa" IS NOT USED, SEE WT:LT
m["jpr"] = {
"Judeo-Persian",
33367,
"ira-swi",
"Hebr",
ancestors = "fa",
}
m["jqr"] = {
"Jaqaru",
33443,
"sai-aym",
"Latn",
}
m["jra"] = {
"Jarai",
33370,
"cmc",
"Latn",
}
m["jrr"] = {
"Jiru",
6203123,
"nic-jkn",
}
m["jru"] = {
"Japrería",
3441409,
"sai-yuk",
"Latn",
}
m["jsl"] = {
"Bahasa Isyarat Jepun",
35601,
"sgn-jsl",
}
m["jua"] = {
"Júma",
12953587,
"tup-gua",
"Latn",
}
m["jub"] = {
"Wannu",
3914905,
"nic-jkn",
}
m["juc"] = {
"Jurchen",
56731,
"tuw-jrc",
"Jurc, Hani",
}
m["jud"] = {
"Worodougou",
11155821,
"dmn-mnk",
}
m["juh"] = {
"Hone",
5964576,
"nic-jkn",
"Latn",
}
m["jui"] = {
"Ngadjuri",
16897028,
"aus-pam",
"Latn",
}
m["juk"] = {
"Wapan",
3914914,
"nic-jkn",
}
m["jul"] = {
"Jirel",
56863,
"sit-tib",
}
m["jum"] = {
"Jumjum",
11283696,
"sdv",
}
m["jun"] = {
"Juang",
33362,
"mun",
"Orya",
}
m["juo"] = {
"Jiba",
6191995,
"nic-jkn",
}
m["jup"] = {
"Hupdë",
3143384,
"sai-nad",
"Latn",
}
m["jur"] = {
"Jurúna",
4023175,
"tup",
"Latn",
}
m["jus"] = {
"Bahasa Isyarat Jumla",
6310991,
"sgn",
}
m["jut"] = {
"Jutish",
1340322,
"gmq-eas",
"Latn",
ancestors = "da",
}
m["juu"] = {
"Ju",
3914897,
}
m["juw"] = {
"Wãpha",
3914934,
"nic-jkn",
"Latn",
}
m["juy"] = {
"Juray",
6314963,
"mun",
}
m["jvd"] = {
"Javindo",
2719893,
"crp",
"Latn",
}
m["jvn"] = {
"Jawa Caribbean",
11732256,
"poz-sus",
"Latn",
ancestors = "jv",
}
m["jwi"] = {
"Jwira-Pepesa",
35467,
"alv-ctn",
"Latn",
}
-- "jya" IS TREATED AS "sit-sit", "sit-jap", "sit-tsh", "sit-zbu", SEE WT:LT
m["jyy"] = {
"Jaya",
641720,
"csu-bgr",
"Latn",
}
return require("Module:languages").finalizeData(m, "language")
nkhxzp70xmnluyo4obnzkqisfhid7ww
November
0
9800
342807
334086
2026-05-16T12:31:20Z
Hakimi97
2668
342807
wikitext
text/x-wiki
{{also|november|nóvember}}
{{Pautan Projek Wikimedia}}
==Rentas bahasa==
====Simbol====
# Huruf [[N]] di [[abjad ejaan ICAO]].
===Sebutan===
* {{AFA|mul|[noˈvembə]}}
[[Kategori:Simbol rentas bahasa]]
[[Kategori:Abjad ejaan ICAO]]
==Bahasa Melayu==
{{wikipedia|lang=ms}}
===Kata nama khas===
{{ms-knk}}
# Bulan kesebelas dalam tahun Masihi yang mengandungi 30 hari, antara [[Oktober]] dan [[Disember]].
===Etimologi===
Daripada {{bor|ms|en|November}}, daripada {{der|ms|enm}}, daripada {{der|ms|fro|novembre}}, daripada {{der|ms|la|november}}, daripada bahasa Latin {{term|la|''novem''}}, daripada {{der|ms|ine-pro|[[*h₁néwn̥]]}}.
===Sebutan===
* {{dewan|nõ|vém|ber}}
* {{AFA|ms|[novɛmbə(r)]}}
* {{rhymes|ms|bə(r)|ə(r)}}
* {{penyempangan|ms|No|vem|ber}}
* {{audio|ms|November (Malay).ogg}}
===Tulisan Jawi===
{{ARchar|[[نوۏيمبر]]}}
===Terjemahan===
{{ter-atas|bulan kesebelas}}
* Abaza: {{t|abq|ноябрь}}
* Abkhaz: {{t|ab|абҵара}}
* Afrikaans: {{t+|af|November}}
* Alabama: {{t|akz|hasiholtina istapókkòolawah cháffàaka}}, {{t|akz|Nofìmba}}
* Albania: {{t|sq|nëntor}}
* Alutiiq: {{t|ems|Quyawim Iralua}}
* Amhara: {{t|am|ኖቬምበር|sc=Ethi}}
* Apache:
*: Apache Barat: {{t|apw|Kǫʼ Bąąh Náłkʼas}}
* Arab: {{t+|ar|نُوفمْبر|tr=nufímbir, nufámbir, nufámbar|m}}, {{t|ar|تِشرينُ الثّانِي|tr=tišrīnu θ-θāni|m}}
*: Arab Mesir: {{t|arz|نوفمبر|m|tr=novamber|sc=Arab}}
* Aragones: {{t+|an|nobiembre|m}}
* Armenia: {{t+|hy|նոյեմբեր}}
*: Armenian Lama: {{t|xcl|նոյեմբեր}}
* Aromania: {{t|rup|nuembru}}, {{t|rup|brumar}}
* Asturia: {{t+|ast|payares|m}}, {{t+|ast|noviembre}}
* Azeri: {{t+|az|noyabr}}
* Basque: {{t+|eu|azaro}}
* Belanda: {{t+|nl|november}}
* Belarus: {{t|be|лістапа́д|m}}
* Benggali: {{t|bn|নভেম্বর|sc=Beng}}
* Bislama: {{t|bi|novemba}}
* Bulgaria: {{t+|bg|ное́мври|m}}
* Burma: {{t+|my|နိုဝင်ဘာ|sc=Mymr}}
* Catalonia: {{t+|ca|novembre|m}}
* Chechen: {{t|ce|Лахьанан-бутт|sc=Cyrl}}
* Cherokee: {{t|chr|ᏅᏓᏕᏩ|tr=Nvdadewa|sc=Cher}}
* Chuvash: {{t|cv|чӳк}}
* Cina:
*: Mandarin: {{t+|cmn|十一月|tr=shíyīyuè}}
* Cornish: {{t|kw|mys du}}
* Czech: {{t+|cs|listopad|m}}
* Dakota: {{t|dak|Tahecapšuŋwi}}
* Denmark: {{t+|da|november}}
* Esperanto: {{t+|eo|novembro}}, {{t|eo|Novembro}}
* Estonia: {{t+|et|november}}
* Ewe: {{t|ee|Adeɛmekpɔxe}}, {{t|ee|Nɔvember}}
* Faroe: {{t|fo|november|m}}, {{t|fo|novembur|m}}
* Fiji: {{t|fj|Noveba}}
* Finland: {{t+|fi|marraskuu}}
* Frisia Barat: {{t+|fy|novimber}}, {{t+|fy|slachtmoanne}}
* Friulia: {{t|fur|Novembar|m}}
* Gaelik Scotland: {{t|gd|Samhain|f|alt=an t-Samhain}}
* Galicia: {{t+|gl|novembro|m}}, {{t|gl|santos|m}}
* Georgia: {{t+|ka|ნოემბერი}}
* Greek: {{t+|el|Νοέμβριος|m}}, {{t+|el|Νοέμβρης|m}}
* Greenland: {{t+|kl|Novembari}}
* Hawaii: {{t|haw|Nowemapa}}
* Hindi: {{t+|hi|नवम्बर|tr=navambar}}
* Hungary: {{t+|hu|november}}
* Ibrani: {{t+|he|נובמבר|alt=נוֹבֶמְבֶּר|tr=novémber}}
* Iceland: {{t+|is|nóvember|m}}, {{t|is|nóvembermánuður|m}}
* Ido: {{t+|io|novembro}}
* Indonesia: {{t|id|November}}
* Inggeris: {{t+|en|November}}
* Inggeris Kuno: {{t|ang|blōtmōnaþ|m}}
* Interlingua: {{t|ia|novembre}}
* Ireland: {{t+|ga|Samhain|f}}
* Itali: {{t+|it|novembre|m}}
* Jepun: {{t+|ja|十一月|tr=じゅういちがつ, jūichigatsu}}, {{t+|ja|霜月|tr=しもつき, shimotsuki}}
* Jerman: {{t+|de|November|m}}, {{qualifier|arkaik}} {{t+|de|Nebelung|m}}
* Jerman Kasar:
*: Jerman Kasar Jerman: {{t|nds-de|November|m}}
*: Saxon Kasar Belanda: {{t|nds-nl|november|m}}
* Kabuverdianu: {{t|kea|nuvembru|m}} {{qualifier|Badiu|Santiago}}, {{t|kea|n'vembr'|m}} {{qualifier|São Vicente}}
* Kashubia: {{t+|csb|lëstopadnik|m}}
* Kazakh: {{t+|kk|қараша}}
* Khmer: {{t+|km|វិច្ឆិកា|tr=weuchăgā}}
* Kiribati: {{t|gil|Nobembwa|sc=Cyrl}}
* Kreol Haiti: {{t|ht|novanm}}
* Korea: {{t+|ko|십일월|tr=sibilwol|sc=Kore}}
* Ladin: {{t|lld|november}}, {{t|lld|nuvember}}
* Lao: {{t|lo|ເດືອນພະຈິກ|sc=Laoo}}
* Latin: {{t+|la|november}}
* Latvia: {{t+|lv|novembris|m}}
* Lezgi: {{t|lez|цӏехуьл}}
* Limburg: {{t+|li|November}}
* Lithuania: {{t+|lt|lapkritis|m}}
* Livonia: {{t|liv|novembõr}}, {{t|liv|kīlmakū}}
* Luxembourg: {{t+|lb|November|m}}, {{t+|lb|Allerhellgemount|m}}, {{t+|lb|Wantermount|m}}
* Macedonia: {{t+|mk|ное́мври|m}}
* Malta: {{t|mt|Novembru}}
* Manchu: (omšon biya)
* Maori: {{t|mi|Noema}}
* Montagnais: {{t|moe|takuatshi-pishimᵘ}}
* Navajo: {{t|nv|Níłchʼitsʼósí}}
* Neapolitan: {{t|nap|nuvèmbre|m}}
* Norway: {{t+|no|november}}
* Novial: {{t|nov|novembre}}
* Occitan: {{t+|oc|novembre|m}}
* Ojibwe: {{t|oj|gashkadino-giizis}}
* Oriya: {{t|or|ନଭେମ୍ବର|sc=Orya}}
* Ossetia: {{t|os|ноябрь}}
* Parsi: {{t+|fa|نوامبر|tr=novâmbr}}
* Perancis: {{t+|fr|novembre|m}}
* Poland: {{t+|pl|listopad|m}}
* Portugis: {{t+|pt|novembro|m}}
* Romania: {{t+|ro|noiembrie|m}}, {{qualifier|pop}} {{t+|ro|brumar}}
* Romansch: {{t|rm|november|m}}
* Rusia: {{t+|ru|ноя́брь|m}}
* Samoa: {{t|sm|novema}}
* Sardinia: {{t|sc|donniasantu}}, {{t|sc|santandria}}
* Sepanyol: {{t+|es|noviembre|m}}
* Serbo-Croatia:
*: Cyril: {{t|sh|новембар|m|sc=Cyrl}}, {{t|sh|студени|m|sc=Cyrl}}
*: Rumi: {{t+|sh|novembar|m|sc=Latn}}, {{t+|sh|studeni|m|sc=Latn}}
* Sicilia: {{t+|scn|nuvemmiru|m}}
* Skolt Sami: {{t|sms|vuˊvrrmmään}}, {{t|sms|skamm´mään}}
* Slovak: {{t+|sk|november|m}}
* Slovene: {{t|sl|novêmber|m}}
* Sotho: {{t+|st|Pudungwana}}
* Sweden: {{t+|sv|november}}
* Tagalog: {{t|tl|nobyembre}}
* Tahiti: {{t|ty|novema}}
* Tajik: {{t+|tg|ноябр}}
* Tatar: {{t|tt|nöyäber}}
* Telugu: {{t|te|నవంబరు}}
* Thai: {{t+|th|พฤศจิกายน|tr=phrēut sà jì gaa yohn}}
* Tok Pisin: {{t|tpi|novemba}}
* Tonga: {{t|to|nōvema}}
* Turki: {{t+|tr|kasım}}, {{t|tr|Teşrini Sani}} {{qualifier|kuno}}
* Ukraine: {{t+|uk|листопа́д|m}}
* Urdu: {{t|ur|نومبر|tr=nuvembar}}
* Venice: {{t+|vec|novenbre}}
* Vietnam: {{t+|vi|tháng Mười một}}, {{t+|vi|tháng Mười Một}}, {{t+|vi|tháng mười một}}
* Volapük: {{t+|vo|novul}}
* Võro: {{t|vro|märtekuu}}
* Walloon: {{t+|wa|nôvimbe|m}}
* Wales: {{t+|cy|Tachwedd|m}}
* Wolof: {{t|wo|Nowembar}}
* Yiddish: {{t+|yi|נאָוועמבער|m|tr=november, nowember|sc=Hebr}}
* Yup'ik: {{t|esu|Cauyarvik}}
* Zuni: {{t|zun|Kok A:wiyanna}}, {{t|zun|Yachun Kwa'shi'amme}}
{{ter-bawah}}
===Pautan luar===
* {{R:PRPM}}
[[Kategori:ms:Bulan takwim Masihi]]
==Bahasa Afrikaans==
{{wikipedia|lang=af}}
====Kata nama====
'''November''' (''jamak'' '''[[Novembermaande]]''')
# [[#Bahasa Melayu|November]]
===Sebutan===
* {{AFA|af|[nuˈfem(b)ər]}}
===Tulisan Arab===
{{ARchar|نُوفَِمْبِرْ}}
===Tesaurus===
; Sinonim: [[Nov.]], [[Nófember]].
[[Kategori:Kata nama bahasa Afrikaans]]
{{C|af|Bulan dalam tahun}}
==Bahasa Indonesia==
{{wikipedia|lang=id}}
====Kata nama====
# [[#Bahasa Melayu|November]]
===Etimologi===
Daripada {{bor|id|nl|november}}.
[[Kategori:Kata nama bahasa Indonesia]]
{{C|id|Bulan dalam tahun}}
==Bahasa Inggeris==
{{wikipedia|lang=en}}
====Kata nama khas====
{{en-knk}}
# [[bulan|Bulan]] kesebelas [[kalendar Masihi]], selepas [[October]] dan sebelum [[December]]. Kependekan: '''[[Nov]]''' atau '''[[Nov.]]'''
# Huruf ''N'' di [[abjad ejaan ICAO]].
===Etimologi===
Daripada {{inh|en|enm}}, daripada {{bor|en|fro|novembre}}, daripada {{inh|fro|la|november||bulan kesembilan}}, daripada bahasa Latin {{term|la|''novem''}}, daripada {{inh|la|ine-pro|*h₁néwn̥||sembilan}}; + bahasa Latin {{term|la|''-ber''}}, daripada akhiran kata sifat {{term|la|''-bris''}}; November dahulu merupakan bulan kesembilan dalam kalendar Roman.
===Sebutan===
* {{a|UK}} {{AFA|en|/nəʊˈvɛmbə/}}
* {{a|AS}} {{enPR|nō-vĕmʹbər}}, {{AFA|en|/noʊˈvɛmbəɹ/}}
* {{penyempangan|en|No|vem|ber}}
* {{audio|en|en-us-November.ogg|Audio (US)}}
* {{rhymes|en|ɛmbə(r)}}
===Terbitan===
{{der-top|Terbitan}}
* [[mid-November]]
* [[November class]]
* {{w|November Coalition|lang=en}}
* [[November criminal]]
* [[November Eve]]
* {{w|November Group|lang=en}}
{{der-mid}}
* [[Novemberish]]
* [[November moth]]
* [[November Revolution]]
* [[November Uprising]]
* [[Novembery]], [[Novembry]]
* [[Witch of November]]
{{der-bottom}}
===Tesaurus===
; Sinonim: [[Novembre]] {{qualifier|kuno}}
[[Kategori:Kata nama khas terhitung bahasa Inggeris]]
[[Kategori:en:Bulan takwim Masihi]]
==Bahasa Jerman==
{{wikipedia|lang=de}}
====Kata nama====
'''November''' (''genitif'' '''[[Novembers]]''' ''atau'' '''[[November]]''', ''jamak'' '''[[November]]''')
# [[#Bahasa Melayu|November]]
===Sebutan===
* {{AFA|de|/noˈvɛmbɐ/}}
* {{rhymes|de|ɛmbɐ}}
* {{penyempangan|de|No|vem|ber}}
* {{audio|de|De-November.ogg|audio}}
* {{audio|de|De-November2.ogg|audio}}
[[Kategori:Kata nama bahasa Jerman]]
{{C|de|Bulan dalam tahun}}
==Bahasa Luxembourg==
{{wikipedia|lang=lb}}
====Kata nama khas====
'''November'''
# [[#Bahasa Melayu|November]]
[[Kategori:Kata nama khas bahasa Luxembourg]]
{{C|lb|Bulan dalam tahun}}
==Bahasa Scots==
{{wikipedia|lang=sco}}
====Kata nama khas====
# [[November]]
===Etimologi===
Daripada {{bor|sco|la|november||bulan kesembilan}}.
===Sebutan===
* {{AFA|sco|[noːvɛmˈbər]}}
====Lihat juga====
* {{sense|bulan}} Sebelum: [[October]]. Selepas: [[December]]
[[Kategori:Kata nama khas bahasa Scots]]
{{C|sco|Bulan dalam tahun}}
5n5u12vm13zb6dxwjisarj7xrd5au8u
Modul:languages/data/3/f
828
9810
342849
254977
2026-05-17T04:01:54Z
PeaceSeekers
3334
342849
Scribunto
text/plain
local m_langdata = require("Module:languages/data")
-- Loaded on demand, as it may not be needed (depending on the data).
local function u(...)
u = require("Module:string utilities").char
return u(...)
end
local c = m_langdata.chars
local p = m_langdata.puaChars
local s = m_langdata.shared
local m = {}
m["faa"] = {
"Fasu",
3446687,
"paa-kut",
"Latn",
}
m["fab"] = {
"Annobon",
34992,
"crp",
"Latn",
ancestors = "pt",
}
m["fad"] = {
"Wagi",
7959569,
"ngf-mad",
"Latn",
}
m["faf"] = {
"Fagani",
3063759,
"poz-sls",
"Latn",
}
m["fag"] = {
"Finongan",
3450761,
"ngf-fin",
"Latn",
}
m["fah"] = {
"Baissa Fali",
3446632,
"nic-bco",
"Latn",
}
m["fai"] = {
"Faiwol",
3501773,
"ngf-okk",
"Latn",
}
m["faj"] = {
"Faita",
976953,
"ngf-mad",
"Latn",
}
m["fak"] = {
"Fang (Beboid)",
5433811,
"nic-beb",
"Latn",
}
m["fal"] = {
"Fali Selatan",
15637351,
"alv-fli",
"Latn",
}
m["fam"] = {
"Fam",
35290,
"nic-mmb",
"Latn",
}
m["fan"] = {
"Fang (Bantu)",
33484,
"bnt-btb",
"Latn",
}
m["fap"] = {
"Palor",
36318,
"alv-cng",
"Latn",
}
m["far"] = {
"Fataleka",
3067168,
"poz-sls",
"Latn",
}
-- "fat" IS TREATED AS "ak", SEE WT:LT
m["fau"] = {
"Fayu",
5439113,
"paa-lkp",
"Latn",
}
m["fax"] = {
"Fala",
300402,
"roa-ibe",
"Latn",
ancestors = "roa-opt",
}
m["fay"] = {
"Fars Barat Daya",
5228140,
"ira-swi",
}
m["faz"] = {
"Fars Barat Laut",
7060307,
"ira-swi",
}
m["fbl"] = {
"Bikol Albay Barat",
18603801,
"phi",
"Latn",
}
m["fcs"] = {
"Bahasa Isyarat Quebec",
13193,
"sgn",
"Latn", -- when documented
}
m["fer"] = {
"Feroge",
35287,
"nic-ser",
"Latn",
}
m["ffi"] = {
"Foia Foia",
8564176,
"ngf",
"Latn",
}
-- "ffm" IS TREATED AS "ff", SEE WT:LT
m["fgr"] = {
"Fongoro",
3437645,
"csu",
"Latn",
}
m["fia"] = {
"Nobiin",
36503,
"nub",
"Latn, Arab, Copt",
ancestors = "onw",
translit = {
Copt = "Copt-translit",
},
}
m["fie"] = {
"Fyer",
56273,
"cdc-wst",
"Latn",
}
-- "fil" IS TREATED AS "tl", SEE WT:LT
m["fip"] = {
"Fipa",
667747,
"bnt-mwi",
"Latn",
}
m["fir"] = {
"Firan",
3915847,
"nic-plc",
"Latn",
}
m["fit"] = {
"Meänkieli",
13357,
"urj-fin",
"Latn",
ancestors = "fi",
}
m["fiw"] = {
"Fiwaga",
5456292,
"paa-kut",
"Latn",
}
m["fkk"] = {
"Kirya-Konzel",
6416310,
"cdc-cbm",
"Latn",
}
m["fkv"] = {
"Kven",
165795,
"urj-fin",
"Latn",
ancestors = "fi",
}
m["fla"] = {
"Montana Salish",
3111983,
"sal",
"Latn",
}
m["flh"] = {
"Foau",
5463819,
"paa-lkp",
"Latn",
}
m["fli"] = {
"Fali",
56244,
"cdc-cbm",
"Latn",
}
m["fll"] = {
"Fali Utara",
12952419,
"alv-fli",
"Latn",
}
m["fln"] = {
"Pulau Flinders",
3915702,
"aus-pmn",
"Latn",
}
m["flr"] = {
"Fuliiru",
7166821,
"bnt-shh",
"Latn",
}
m["fly"] = {
"Tsotsitaal",
12643960,
"crp",
"Latn",
ancestors = "af",
}
m["fmp"] = {
"Fe'fe'",
35276,
"bai",
"Latn",
}
m["fmu"] = {
"Muria Barat Jauh",
42589412,
"dra-mur",
}
m["fng"] = {
"Fanagalo",
35727,
"crp",
"Latn",
ancestors = "zu",
}
m["fni"] = {
"Fania",
317642,
"alv-bua",
"Latn",
}
m["fod"] = {
"Foodo",
5465566,
"alv-gng",
"Latn",
}
m["foi"] = {
"Foi",
5464146,
"paa-kut",
"Latn",
}
m["fom"] = {
"Foma",
5464911,
"bnt-ske",
"Latn",
ancestors = "khy",
}
m["fon"] = {
"Fon",
33291,
"alv-gbe",
"Latn",
}
m["for"] = {
"Fore",
3077126,
"paa-kag",
"Latn",
}
m["fos"] = {
"Siraya",
716604,
"map",
"Latn",
}
m["fpe"] = {
"Pichinglis",
35288,
"crp",
"Latn",
ancestors = "en",
}
m["fqs"] = {
"Fas",
56320,
"paa",
"Latn",
}
-- "frc" IS TREATED AS "fr" (or as etymology-only), SEE WT:LT
m["frd"] = {
"Fordata",
5468035,
"poz",
"Latn",
}
m["frm"] = {
"Perancis Pertengahan",
1473289,
"roa-oil",
"Latn",
sort_key = s["roa-oil-sortkey"],
}
m["fro"] = {
"Perancis Kuno",
35222,
"roa-oil",
"Latn, Hebr",
sort_key = {Latn = s["roa-oil-sortkey"]},
}
m["frp"] = {
"Franco-Provençal",
15087,
"roa",
"Latn",
sort_key = {
remove_diacritics = c.grave .. c.acute .. c.circ .. c.diaer .. c.cedilla .. "'",
from = {"æ", "œ"},
to = {"ae", "oe"}
},
}
m["frq"] = {
"Forak",
5467173,
"ngf-fin",
"Latn",
}
m["frr"] = {
"Frisia Utara",
28224,
"gmw-fri",
"Latn",
}
-- "frs" IS NOT USED, SEE WT:LT
m["frt"] = {
"Fortsenal",
2666835,
"poz-vnc",
"Latn",
}
m["fse"] = {
"Bahasa Isyarat Finland",
33225,
"sgn",
"Latn", -- when documented
}
m["fsl"] = {
"Bahasa Isyarat Perancis",
33302,
"sgn-fsl",
"Latn", -- when documented
}
m["fss"] = {
"Bahasa Isyarat Finland-Sweden",
5450448,
"sgn",
"Latn", -- when documented
}
-- "fub" IS TREATED AS "ff", SEE WT:LT
-- "fuc" IS TREATED AS "ff", SEE WT:LT
m["fud"] = {
"Futuna Timur",
35334,
"poz-pnp",
"Latn",
}
-- "fue" IS TREATED AS "ff", SEE WT:LT
-- "fuf" IS TREATED AS "ff", SEE WT:LT
-- "fuh" IS TREATED AS "ff", SEE WT:LT
-- "fui" IS TREATED AS "ff", SEE WT:LT
m["fuj"] = {
"Ko",
35693,
"alv-hei",
"Latn",
}
m["fum"] = {
"Fum",
11011870,
"nic-nka",
"Latn",
}
m["fun"] = {
"Fulniô",
774441,
"qfa-iso",
"Latn",
}
-- "fuq" IS TREATED AS "ff", SEE WT:LT
m["fur"] = {
"Friuli",
33441,
"roa-rhe",
"Latn",
}
m["fut"] = {
"Futuna-Aniwa",
3064409,
"poz-pnp",
"Latn",
}
m["fuu"] = {
"Furu",
3441160,
"csu-bkr",
"Latn",
}
-- "fuv" IS TREATED AS "ff", SEE WT:LT
m["fuy"] = {
"Fuyug",
3073472,
"ngf",
"Latn",
}
m["fvr"] = {
"Fur",
33364,
"ssa-fur",
"Latn",
}
m["fwa"] = {
"Fwâi",
3091331,
"poz-cln",
"Latn",
}
m["fwe"] = {
"Fwe",
5511159,
"bnt-bot",
"Latn",
}
return require("Module:languages").finalizeData(m, "language")
g3p71awi780ckf2w77tttlvtcga4l3w
Modul:languages/data/3/i
828
9813
342848
273099
2026-05-17T04:00:38Z
PeaceSeekers
3334
342848
Scribunto
text/plain
local m_langdata = require("Module:languages/data")
-- Loaded on demand, as it may not be needed (depending on the data).
local function u(...)
u = require("Module:string utilities").char
return u(...)
end
local c = m_langdata.chars
local p = m_langdata.puaChars
local s = m_langdata.shared
local m = {}
m["iai"] = {
"Iaai",
282888,
"poz-cln",
"Latn",
}
m["ian"] = {
"Iatmul",
5983460,
"paa-spk",
}
m["iar"] = {
"Purari",
3499934,
"paa",
}
m["iba"] = {
"Iban",
33424,
"poz-mly",
"Latn",
}
m["ibb"] = {
"Ibibio",
33792,
"nic-ief",
"Latn",
}
m["ibd"] = {
"Iwaidja",
1977429,
"aus-wdj",
"Latn",
}
m["ibe"] = {
"Akpes",
35457,
"alv-von",
"Latn",
}
m["ibg"] = {
"Ibanag",
1775596,
"phi",
"Latn",
}
m["ibh"] = {
"Bih",
nil,
"cmc",
"Latn",
}
m["ibl"] = {
"Ibaloi",
3147383,
"phi",
}
m["ibm"] = {
"Agoi",
34727,
"nic-ucr",
"Latn",
}
m["ibn"] = {
"Ibino",
3813281,
"nic-lcr",
"Latn",
}
m["ibr"] = {
"Ibuoro",
3813306,
"nic-ief",
}
m["ibu"] = {
"Ibu",
11732235,
"paa-nha",
}
m["iby"] = {
"Ibani",
11280479,
"ijo",
}
m["ica"] = {
"Ede Ica",
12952405,
"alv-ede",
"Latn",
}
m["ich"] = {
"Etkywan",
3914462,
"nic-jkn",
"Latn",
}
m["icl"] = {
"Bahasa Isyarat Iceland",
3436654,
"sgn",
"Latn", -- when documented
}
m["icr"] = {
"Inggeris Kreol Islander",
2044587,
"crp",
"Latn",
ancestors = "en",
}
m["ida"] = {
"Idakho-Isukha-Tiriki",
12952512,
"bnt-lok",
}
m["idb"] = {
"Indo-Portugis",
6025550,
"crp",
"Latn",
ancestors = "pt",
}
m["idc"] = {
"Idon",
3913366,
"nic-plc",
}
m["idd"] = {
"Ede Idaca",
13123376,
"alv-ede",
"Latn",
}
m["ide"] = {
"Idere",
3813288,
"nic-ief",
}
m["idi"] = {
"Idi",
5988630,
"paa",
"Latn",
}
m["idr"] = {
"Indri",
35662,
"nic-ser",
}
m["ids"] = {
"Idesa",
3913979,
"alv-swd",
"Latn",
ancestors = "oke",
}
m["idt"] = {
"Idaté",
12952511,
"poz-tim",
"Latn",
}
m["idu"] = {
"Idoma",
35478,
"alv-ido",
"Latn",
}
m["ifa"] = {
"Amganad Ifugao",
18748222,
"phi",
"Latn",
}
m["ifb"] = {
"Batad Ifugao",
12953578,
"phi",
"Latn",
}
m["ife"] = {
"Ifè",
33606,
"alv-ede",
"Latn",
entry_name = {remove_diacritics = c.grave .. c.acute .. c.circ .. c.macron .. c.caron},
sort_key = {
remove_diacritics = c.tilde,
from = {"ɖ", "dz", "ɛ", "gb", "kp", "ny", "ŋ", "ɔ", "ts"},
to = {"d" .. p[1], "d" .. p[2], "e" .. p[1], "g" .. p[1], "k" .. p[1], "n" .. p[1], "n" .. p[2], "o" .. p[1], "t" .. p[1]}
},
}
m["iff"] = {
"Ifo",
7902545,
"poz-vns",
"Latn",
}
m["ifk"] = {
"Tuwali Ifugao",
7857158,
"phi",
"Latn",
}
m["ifm"] = {
"Teke-Fuumu",
36603,
"bnt-tek",
}
m["ifu"] = {
"Mayoyao Ifugao",
12953579,
"phi",
"Latn",
}
m["ify"] = {
"Keley-I Kallahan",
3192221,
"phi",
"Latn",
}
m["igb"] = {
"Ebira",
35363,
"alv-nup",
"Latn",
}
m["ige"] = {
"Igede",
35420,
"alv-ido",
"Latn",
}
m["igg"] = {
"Igana",
5991454,
"paa",
"Latn",
}
m["igl"] = {
"Igala",
35513,
"alv-yrd",
"Latn",
entry_name = {remove_diacritics = c.grave .. c.acute .. c.circ .. c.macron .. c.dotabove .. c.caron .. c.lineabove},
sort_key = {
from = {
"ñm", "ñw", -- 3 chars
"ch", "ẹ", "gb", "gw", "kp", "kw", "ny", "ñ", "ọ" -- 2 chars
},
to = {
"n" .. p[3], "n" .. p[4],
"c" .. p[1], "e" .. p[1], "g" .. p[1], "g" .. p[2], "k" .. p[1], "k" .. p[2], "n" .. p[1], "n" .. p[2], "o" .. p[1]
}
},
}
m["igm"] = {
"Kanggape",
6362743,
"paa",
"Latn",
}
m["ign"] = {
"Ignaciano",
3148190,
"awd",
}
m["igo"] = {
"Isebe",
11732248,
"ngf-mad",
}
m["igs"] = {
"Glosa",
1138529,
"art",
type = "appendix-constructed",
}
m["igw"] = {
"Igwe",
3913985,
"alv-yek",
"Latn",
}
m["ihb"] = {
"Pidgin Iha",
12639686,
"crp",
ancestors = "ihp",
}
m["ihi"] = {
"Ihievbe",
3441193,
"alv-eeo",
"Latn",
ancestors = "ema",
}
m["ihp"] = {
"Iha",
5994495,
"ngf",
}
m["ijc"] = {
"Izon",
35483,
"ijo",
"Latn",
}
m["ije"] = {
"Biseni",
35010,
"ijo",
}
m["ijj"] = {
"Ede Ije",
12952406,
"alv-ede",
"Latn",
}
m["ijn"] = {
"Kalabari",
35697,
"ijo",
}
m["ijs"] = {
"Southeast Ijo",
3915854,
"ijo",
"Latn",
}
m["ike"] = {
"Eastern Canadian Inuktitut",
4126517,
"esx-inu",
"Cans",
}
m["iki"] = {
"Iko",
3813290,
"nic-lcr",
"Latn",
}
m["ikk"] = {
"Ika",
35406,
"alv-igb",
"Latn",
}
m["ikl"] = {
"Ikulu",
425973,
"nic-plc",
"Latn",
}
m["iko"] = {
"Olulumo-Ikom",
3914402,
"nic-uce",
"Latn",
}
m["ikp"] = {
"Ikpeshi",
3912777,
"alv-yek",
"Latn",
}
m["ikr"] = {
"Ikaranggal",
5995402,
"aus-pam",
}
m["iks"] = {
"Bahasa Isyarat Inuit",
13360244,
"sgn",
"Latn", -- when documented
}
m["ikt"] = {
"Inuvialuktun",
27990,
"esx-inu",
"Cans, Latn",
}
m["ikv"] = {
"Iku-Gora-Ankwa",
3913940,
"nic-plc",
}
m["ikw"] = {
"Ikwere",
35399,
"alv-igb",
}
m["ikx"] = {
"Ik",
35472,
"ssa-klk",
"Latn",
}
m["ikz"] = {
"Ikizu",
10977626,
"bnt-lok",
"Latn",
}
m["ila"] = {
"Ile Ape",
12473380,
"poz-cet",
}
m["ilb"] = {
"Ila",
10962725,
"bnt-bot",
"Latn",
}
m["ilg"] = {
"Ilgar",
5997810,
"aus-wdj",
"Latn",
}
m["ili"] = {
"Ili Turki",
33627,
"trk-kar",
}
m["ilk"] = {
"Ilongot",
3148787,
"phi",
"Latn",
}
m["ill"] = {
"Iranun",
12953581,
"phi",
"Latn, Arab",
}
m["ilo"] = {
"Ilocano",
35936,
"phi",
"Latn, Tglg",
translit = {
Tglg = "ilo-translit",
},
override_translit = true,
entry_name = {
Latn = {
remove_diacritics = c.grave .. c.acute .. c.circ .. c.diaer,
}
},
sort_key = {
Latn = "tl-sortkey",
},
standardChars = {
Latn = "AaBbKkDdEeGgHhIiLlMmNnOoPpRrSsTtUuWwYy" .. c.punc,
},
}
m["ils"] = {
"International Sign",
35754,
"sgn",
}
m["ilu"] = {
"Ili'uun",
12632888,
"poz-tim",
}
m["ilv"] = {
"Ilue",
3813301,
"nic-lcr",
"Latn",
}
m["ima"] = {
"Mala Malasar",
6740693,
"dra-tam",
}
m["imi"] = {
"Anamgura",
3501881,
"ngf-mad",
}
m["iml"] = {
"Miluk",
3314550,
"nai-coo",
"Latn",
}
m["imn"] = {
"Imonda",
6005721,
"paa-brd",
}
m["imo"] = {
"Imbongu",
12632895,
"ngf-mad",
}
m["imr"] = {
"Imroing",
6008394,
"poz-tim",
}
m["ims"] = {
"Marsian",
1265446,
"itc-sbl",
"Latn",
}
m["imy"] = {
"Milyan",
3832946,
"ine-luw",
"Lyci",
}
m["inb"] = {
"Inga",
35491,
"qwe",
ancestors = "qwe-kch",
}
m["ing"] = {
"Deg Xinag",
27782,
"ath-nor",
"Latn",
}
m["inh"] = {
"Ingush",
33509,
"cau-vay",
"Cyrl, Latn, Arab",
translit = {
Cyrl = "cau-nec-translit",
Arab = "ar-translit",
},
override_translit = true,
display_text = {Cyrl = s["cau-Cyrl-displaytext"]},
entry_name = {
Cyrl = s["cau-Cyrl-entryname"],
Latn = s["cau-Latn-entryname"],
},
sort_key = {
Cyrl = {
from = {"аь", "гӏ", "ё", "кх", "къ", "кӏ", "пӏ", "тӏ", "хь", "хӏ", "цӏ", "чӏ", "яь"},
to = {"а" .. p[1], "г" .. p[1], "е" .. p[1], "к" .. p[1], "к" .. p[2], "к" .. p[3], "п" .. p[1], "т" .. p[1], "х" .. p[1], "х" .. p[2], "ц" .. p[1], "ч" .. p[1], "я" .. p[1]}
},
},
}
m["inj"] = {
"Jungle Inga",
16115012,
"qwe",
ancestors = "qwe-kch",
}
m["inl"] = {
"Bahasa Isyarat Indonesia",
3915477,
"sgn",
"Latn", -- when documented
}
m["inm"] = {
"Minaean",
737784,
"sem-osa",
"Sarb",
translit = "Sarb-translit",
}
m["inn"] = {
"Isinai",
6081098,
"phi",
}
m["ino"] = {
"Inoke-Yate",
6036531,
"paa-kag",
}
m["inp"] = {
"Iñapari",
15338035,
"awd",
"Latn",
}
m["ins"] = {
"Bahasa Isyarat India",
12953486,
"sgn",
}
m["int"] = {
"Intha",
6057507,
"tbq-brm",
ancestors = "obr",
}
m["inz"] = {
"Ineseño",
35443,
"nai-chu",
"Latn",
}
m["ior"] = {
"Inor",
35763,
"sem-eth",
"Ethi",
}
m["iou"] = {
"Tuma-Irumu",
7852460,
"ngf-fin",
"Latn",
}
m["iow"] = {
"Chiwere",
56737,
"sio-msv",
"Latn",
}
m["ipi"] = {
"Ipili",
6065141,
"paa-eng",
}
m["ipo"] = {
"Ipiko",
10566515,
"ngf",
}
m["iqu"] = {
"Iquito",
2669184,
"sai-zap",
"Latn",
}
m["iqw"] = {
"Ikwo",
11926474,
"alv-igb",
"Latn",
ancestors = "izi",
}
m["ire"] = {
"Iresim",
6069398,
"poz-hce",
"Latn",
}
m["irh"] = {
"Irarutu",
3027928,
"poz-cet",
"Latn",
}
m["iri"] = {
"Rigwe",
3912756,
"nic-plc",
"Latn",
}
m["irk"] = {
"Iraqw",
33595,
"cus-sou",
"Latn",
}
m["irn"] = {
"Irantxe",
3409301,
nil,
"Latn",
}
m["irr"] = {
"Ir",
3071880,
"mkh-kat",
}
m["iru"] = {
"Irula",
33363,
"dra-imd",
"Taml",
translit = "ta-translit"
}
m["irx"] = {
"Kamberau",
6356317,
"ngf",
}
m["iry"] = {
"Iraya",
6068356,
"phi",
"Latn",
}
m["isa"] = {
"Isabi",
11732247,
"paa-kag",
}
m["isc"] = {
"Isconahua",
3052971,
"sai-pan",
"Latn",
}
m["isd"] = {
"Isnag",
6085162,
"phi",
"Latn",
}
m["ise"] = {
"Bahasa Isyarat Itali",
375619,
"sgn",
"Latn", -- when documented
}
m["isg"] = {
"Bahasa Isyarat Ireland",
14183,
"sgn",
"Latn", -- when documented
}
m["ish"] = {
"Esan",
35268,
"alv-eeo",
"Latn",
}
m["isi"] = {
"Nkem-Nkum",
36261,
"nic-eko",
"Latn",
}
m["isk"] = {
"Ishkashimi",
33419,
"ira-sgi",
}
m["ism"] = {
"Masimasi",
6783273,
"poz-ocw",
"Latn",
}
m["isn"] = {
"Isanzu",
6078891,
"bnt-tkm",
"Latn",
}
m["iso"] = {
"Isoko",
35414,
"alv-swd",
"Latn",
}
m["isr"] = {
"Bahasa Isyarat Israel",
2911863,
"sgn",
"Sgnw",
}
m["ist"] = {
"Istriot",
35845,
"roa-itd",
"Latn",
}
m["isu"] = {
"Isu",
6089423,
"nic-rnw",
"Latn",
}
m["isv"] = {
"Interslavic",
148971,
"art",
"Latn, Cyrl",
type = "appendix-constructed",
ancestors = "sla-pro",
}
m["itb"] = {
"Binongan Itneg",
12953584,
"phi",
}
m["itd"] = {
"Tidung Selatan",
7049643,
"poz-san",
"Latn",
}
m["ite"] = {
"Itene",
3038640,
"sai-cpc",
"Latn",
}
m["iti"] = {
"Inlaod Itneg",
12953585,
"phi",
}
m["itk"] = {
"Judeo-Itali",
1145414,
"roa-itd",
"Hebr, Latn",
}
m["itl"] = {
"Itelmen",
33624,
"qfa-cka",
"Cyrl, Latn",
entry_name = {
Cyrl = {
from = {"['’]", "[ӅԮ]", "[ӆԯ]", "Ҳ", "ҳ"},
to = {"ʼ", "Ԓ", "ԓ", "Ӽ", "ӽ"}
},
},
sort_key = {
Cyrl = {
from = {
"ӑ", "ё", "кʼ", "ӄʼ", "о̆", "пʼ", "тʼ", "ў", "чʼ", -- 2 chars
"ӄ", "љ", "ԓ", "њ", "ӈ", "ӽ", "ә" -- 1 char
},
to = {
"а" .. p[1], "е" .. p[1], "к" .. p[1], "к" .. p[3], "о" .. p[1], "п" .. p[1], "т" .. p[1], "у" .. p[1], "ч" .. p[1],
"к" .. p[2], "л" .. p[1], "л" .. p[2], "н" .. p[1], "н" .. p[2], "х" .. p[1], "ь" .. p[1]
}
},
},
}
m["itm"] = {
"Itu Mbon Uzo",
10977737,
"nic-ief",
"Latn",
ancestors = "ibr",
}
m["ito"] = {
"Itonama",
950585,
"qfa-iso",
"None",
}
m["itr"] = {
"Iteri",
2083185,
"paa-asa",
}
m["its"] = {
"Itsekiri",
36045,
"alv-edk",
"Latn",
entry_name = {Latn = {remove_diacritics = c.grave .. c.acute .. c.macron}},
sort_key = {
remove_diacritics = c.tilde,
from = {"ẹ", "gb", "gh", "kp", "ọ", "ts", "ṣ"},
to = {"e" .. p[1], "g" .. p[1], "g" .. p[2], "k" .. p[1], "o" .. p[1], "t" .. p[1], "t" .. p[1]}
},
}
m["itt"] = {
"Maeng Itneg",
18748761,
"phi",
}
m["itv"] = {
"Itawit",
3915527,
"phi",
"Latn",
}
m["itw"] = {
"Ito",
11128810,
"nic-ief",
ancestors = "ibr",
}
m["itx"] = {
"Itik",
6094713,
"paa-tkw",
}
m["ity"] = {
"Moyadan Itneg",
12953583,
"phi",
}
m["itz"] = {
"Itzá",
35537,
"myn",
}
m["ium"] = {
"Iu Mien",
2498808,
"hmx-mie",
"Latn",
}
m["ivb"] = {
"Ibatan",
18748212,
"phi",
"Latn",
}
m["ivv"] = {
"Ivatan",
3547080,
"phi",
"Latn",
}
m["iwk"] = {
"I-Wak",
12632789,
"phi",
}
m["iwm"] = {
"Iwam",
3915215,
"paa-spk",
}
m["iwo"] = {
"Iwur",
6101006,
"ngf-okk",
}
m["iws"] = {
"Sepik Iwam",
16893603,
"paa-spk",
}
m["ixc"] = {
"Ixcatec",
56706,
"omq",
}
m["ixl"] = {
"Ixil",
35528,
"myn",
"Latn",
}
m["iya"] = {
"Iyayu",
3913390,
"alv-nwd",
"Latn",
}
m["iyo"] = {
"Mesaka",
36080,
"nic-tiv",
"Latn",
}
m["iyx"] = {
"Yaa",
36909,
"bnt-nze",
"Latn",
}
m["izh"] = {
"Ingria",
33559,
"urj-fin",
"Latn",
sort_key = {
from = {
"š", "ž",
},
to = {
"s" .. p[1], "z" .. p[1],
}
},
}
m["izi"] = {
"Izi-Ezaa-Ikwo-Mgbo",
nil,
"alv-igb",
}
m["izr"] = {
"Izere",
6101921,
"nic-plc",
"Latn",
}
m["izz"] = {
"Izi",
3914387,
"alv-igb",
"Latn",
ancestors = "izi",
}
return require("Module:languages").finalizeData(m, "language")
5znpn41jwyxlwm965ga6tclzb3km6tc
Modul:languages/data/3/x
828
9824
342847
281274
2026-05-17T03:52:49Z
PeaceSeekers
3334
342847
Scribunto
text/plain
local m_langdata = require("Module:languages/data")
-- Loaded on demand, as it may not be needed (depending on the data).
local function u(...)
u = require("Module:string utilities").char
return u(...)
end
local c = m_langdata.chars
local p = m_langdata.puaChars
local s = m_langdata.shared
local m = {}
m["xaa"] = {
"Arab Andalusia",
1137945,
"sem-arb",
"Arab, Latn",
entry_name = {
remove_diacritics = c.kashida .. c.fathatan .. c.dammatan .. c.kasratan .. c.fatha .. c.damma .. c.kasra .. c.shadda .. c.sukun .. c.superalef,
from = {u(0x0671)},
to = {u(0x0627)}
},
}
m["xab"] = {
"Sambe",
36265,
"nic-alu",
"Latn",
}
m["xac"] = {
"Kachari",
3442442,
"tbq-bdg",
}
m["xad"] = {
"Adai",
346744,
}
m["xae"] = {
"Aequian",
930579,
"itc",
}
m["xag"] = {
"Aghwan",
34931,
"cau-esm",
"Aghb",
translit = "Aghb-translit",
override_translit = true,
}
m["xai"] = {
"Kaimbé",
6348017,
}
m["xaj"] = {
"Ararandewára",
nil,
"tup-gua",
"Latn",
}
m["xak"] = {
"Maku",
2032882,
nil,
"Latn",
}
m["xal"] = {
"Kalmyk",
33634,
"xgn-cen",
"Cyrl, xwo-Mong",
ancestors = "xwo",
translit = "xal-translit",
override_translit = true,
sort_key = "xal-sortkey",
}
m["xam"] = {
"ǀXam",
2086145,
"khi-tuu",
"Latn",
}
m["xan"] = {
"Xamtanga",
56527,
"cus-cen",
}
m["xao"] = {
"Khao",
3196077,
"mkh-pal",
}
m["xap"] = {
"Apalachee",
686501,
"nai-mus",
"Latn",
}
m["xaq"] = {
"Aquitanian",
500522,
"euq",
"Latn",
}
m["xar"] = {
"Karami",
11732281,
}
m["xas"] = {
"Kamassian",
35991,
translit = "xas-translit",
"syd",
"Cyrl",
}
m["xat"] = {
"Katawixi",
3440512,
"sai-ktk",
}
m["xau"] = {
"Kauwera",
6378983,
"paa-tkw",
}
m["xav"] = {
"Xavante",
36962,
"sai-cje",
"Latn",
}
m["xaw"] = {
"Kawaiisu",
56338,
"azc-num",
"Latn",
}
m["xay"] = {
"Kayan Mahakam",
25337171,
}
m["xbb"] = {
"Lower Burdekin",
6693353,
}
m["xbc"] = {
"Baktria",
756651,
"ira-sbc",
"Grek, Mani",
translit = "xbc-translit",
entry_name = {
from = {"Þ", "þ"},
to = {"Ϸ", "ϸ"}
},
}
m["xbd"] = {
"Bindal",
4913975,
}
m["xbe"] = {
"Bigambal",
16841801,
"aus-pam", --unclassified within
}
m["xbg"] = {
"Bunganditj",
4997615,
}
m["xbi"] = {
"Kombio",
6428259,
"qfa-tor",
"Latn",
}
m["xbj"] = {
"Birrpayi",
nil,
}
m["xbm"] = {
"Breton Pertengahan",
787610,
"cel-bry",
"Latn",
ancestors = "obt",
}
m["xbn"] = {
"Kenaboi",
6388752,
}
m["xbo"] = {
"Bulgar",
36880,
"trk-ogr",
"Arab, Grek",
}
m["xbp"] = {
"Bibbulman",
22918391,
}
m["xbr"] = {
"Kambera",
3053279,
"poz-cet",
"Latn",
}
m["xbw"] = {
"Kambiwá",
9006744,
}
m["xby"] = {
"Butchulla",
31752631,
}
m["xcb"] = {
"Cumbric",
35965,
"cel-bry",
}
m["xcc"] = {
"Camunic",
489011,
nil,
"Ital",
translit = "Ital-translit",
}
m["xce"] = {
"Celtiberian",
37012,
"cel",
"Latn",
}
m["xch"] = {
"Chemakum",
56397,
"chi",
"Latn",
}
m["xcl"] = {
"Armenia Kuno",
181074,
"hyx",
"Armn",
translit = "Armn-translit",
override_translit = true,
entry_name = {
remove_diacritics = "՞՜՛՟",
from = {"եւ"},
to = {"և"}
},
}
m["xcm"] = {
"Comecrudo",
609808,
"nai-pak",
}
m["xcn"] = {
"Cotoname",
56889,
"nai-pak",
}
m["xco"] = {
"Khwarezm",
33138,
"ira-sbc",
"Arab, Armi, Chrs, Phlv, Sogd",
translit = {Chrs = "Chrs-translit"},
}
m["xcr"] = {
"Carian",
35929,
"ine-ana",
"Cari",
}
m["xct"] = {
"Tibet Klasik",
5128314,
"sit-tib",
"Tibt, Hani, Marc, Mong, mnc-Mong, xwo-Mong, Phag, Tang, Zanb",
translit = {
Tibt = "Tibt-translit",
Mong = "Mong-translit",
["mnc-Mong"] = "mnc-translit",
["xwo-Mong"] = "xwo-translit",
Tang = "txg-translit",
},
override_translit = true,
display_text = {
Tibt = s["Tibt-displaytext"],
Mong = s["Mong-displaytext"],
},
entry_name = {
Tibt = s["Tibt-entryname"],
Mong = s["Mong-entryname"],
},
sort_key = {
Tibt = "Tibt-sortkey",
Hani = "Hani-sortkey",
},
}
m["xcu"] = {
"Curonian",
35857,
"bat",
"Latn",
}
m["xcv"] = {
"Chuva",
3516641,
"qfa-yuk",
"Cyrl",
translit = "xcv-translit"
}
m["xcw"] = {
"Coahuilteco",
2008062,
"nai-pak",
}
m["xcy"] = {
"Cayuse",
2472016,
}
m["xda"] = {
"Darkinjung",
5223660,
"aus-yuk",
"Latn",
}
m["xdc"] = {
"Dacian",
682547,
"ine",
"Latn",
}
m["xdk"] = {
"Dharug",
1166814,
"aus-yuk",
"Latn",
}
m["xdm"] = {
"Edom",
2363529,
"sem-can",
"Phnx",
translit = "Phnx-translit",
}
m["xdq"] = {
"Kaitag",
1990659,
"cau-drg",
"Cyrl",
translit = {Cyrl = "dar-translit"},
override_translit = true,
display_text = {Cyrl = s["cau-Cyrl-displaytext"]},
entry_name = {
Cyrl = s["cau-Cyrl-entryname"],
Latn = s["cau-Latn-entryname"],
},
sort_key = {
Cyrl = {
from = {
"къкъ", "хьхь", -- 4 chars
"гъ", "гь", "гӏ", "ё", "къ", "кь", "кӏ", "пп", "пӏ", "сс", "тт", "тӏ", "хх", "хъ", "хь", "хӏ", "цц", "цӏ", "чч", "чӏ" -- 2 chars
},
to = {
"к" .. p[2], "х" .. p[4],
"г" .. p[1], "г" .. p[2], "г" .. p[3], "е" .. p[1], "к" .. p[1], "к" .. p[3], "к" .. p[4], "п" .. p[1], "п" .. p[2], "с" .. p[1], "т" .. p[1], "т" .. p[2], "х" .. p[1], "х" .. p[2], "х" .. p[3], "х" .. p[5], "ц" .. p[1], "ц" .. p[2], "ч" .. p[1], "ч" .. p[2]
}
},
},
}
m["xdy"] = {
"Malayic Dayak",
3514892,
}
m["xeb"] = {
"Ebla",
35345,
"sem-eas",
"Xsux",
}
m["xed"] = {
"Hdi",
56246,
"cdc-cbm",
"Latn",
}
m["xeg"] = {
"ǁXegwi",
3509732,
"khi-tuu",
"Latn",
}
m["xel"] = {
"Kelo",
6386412,
"sdv-eje",
}
m["xem"] = {
"Kembayan",
6386874,
}
m["xep"] = {
"Epi-Olmec",
nil,
}
m["xer"] = {
"Xerénte",
3073436,
"sai-cje",
"Latn",
}
m["xes"] = {
"Kesawai",
6394907,
"ngf-mad",
"Latn",
}
m["xet"] = {
"Xetá",
2980404,
"tup-gua",
"Latn",
}
m["xeu"] = {
"Keoru-Ahia",
11732313,
"ngf",
}
m["xfa"] = {
"Falisci",
35669,
"itc",
"Ital, Latn",
translit = "Ital-translit",
entry_name = {remove_diacritics = c.macron .. c.breve .. c.diaer},
}
m["xga"] = {
"Galatia",
27403,
"cel",
"Latn, Grek",
ancestors = "cel-gau",
}
m["xgb"] = {
"Gbin",
16934745,
"dmn-mse",
"Latn",
}
m["xgd"] = {
"Gudang",
5614528,
}
m["xgf"] = {
"Gabrielino-Fernandeño",
56387,
"azc-tak",
"Latn",
}
m["xgg"] = {
"Goreng",
nil,
}
m["xgi"] = {
"Garingbal",
nil,
}
m["xgl"] = {
"Galindan",
1190494,
"bat",
"Latn",
}
m["xgm"] = {
"Darumbal",
16954400,
}
m["xgr"] = {
"Garza",
3098656,
"nai-pak",
}
m["xgu"] = {
"Unggumi",
62000004,
"aus-wor",
"Latn",
}
m["xgw"] = {
"Guwa",
5621992,
}
m["xha"] = {
"Harami",
41506724,
nil,
"Sarb",
translit = "Sarb-translit",
}
m["xhc"] = {
"Hun",
35959,
}
m["xhd"] = {
"Hadrami",
1032453,
"sem-osa",
"Sarb",
translit = "Sarb-translit",
}
m["xhe"] = {
"Khetrani",
2614111,
"inc-pan",
ancestors = "lah",
}
m["xhm"] = {
"Khmer Pertengahan",
25226861,
"mkh-kmr",
"Latn, Khmr", --and also Pallava
ancestors = "okz",
}
m["xhr"] = {
"Hernican",
5908773,
"itc-sbl",
"Ital",
}
m["xht"] = {
"Hatti",
31107,
"qfa-iso",
"Xsux",
}
m["xhu"] = {
"Hurri",
35740,
"qfa-hur",
"Xsux, Ugar",
}
m["xhv"] = {
"Khua",
22970290,
"mkh-kat",
}
m["xib"] = {
"Iberia",
855215,
"qfa-iso",
"Latn, Ibrn",
}
m["xii"] = {
"Xiri",
36876,
}
m["xin"] = {
"Xinca",
1546494,
"nai-xin",
"Latn",
}
m["xil"] = {
"Illyria",
35976,
"ine",
type = "reconstructed",
}
m["xir"] = {
"Xiriâna",
2028772,
"awd",
"Latn",
}
m["xis"] = {
"Kisan",
nil,
}
m["xiv"] = {
"Bahasa Lembah Indus",
3428279,
nil,
"Inds",
}
m["xiy"] = {
"Xipaya",
13226,
"tup",
}
m["xjb"] = {
"Minjungbal",
nil,
"aus-pam",
"Latn",
}
m["xka"] = {
"Kalkoti",
3877551,
"inc-dar",
"xka-Arab",
}
m["xkb"] = {
"Manigri-Kambolé Ede Nago",
36042,
"alv-ede",
}
m["xkc"] = {
"Khoini",
6401919,
"xme-ttc",
ancestors = "xme-ttc-wes",
}
m["xkd"] = {
"Kayan Mendalam",
12952597,
}
m["xke"] = {
"Kereho",
6437086,
"poz",
"Latn",
}
m["xkf"] = {
"Khengkha",
3695207,
"sit-ebo",
"Tibt",
translit = "Tibt-translit",
override_translit = true,
display_text = s["Tibt-displaytext"],
entry_name = s["Tibt-entryname"],
sort_key = "Tibt-sortkey",
}
m["xkg"] = {
"Kagoro",
11159524,
"dmn-wmn",
}
m["xki"] = {
"Bahasa Isyarat Kenya",
6392859,
"sgn",
}
m["xkj"] = {
"Kajali",
14916876,
"xme-ttc",
ancestors = "xme-ttc-cen",
}
m["xkk"] = {
"Kaco'",
6344767,
"mkh",
}
m["xkl"] = {
"Bakung",
6736761,
"poz-swa",
"Latn",
}
m["xkn"] = {
"Kayan Sungai Kayan",
12473395,
"poz",
"Latn",
}
m["xko"] = {
"Kiorr",
6414519,
"mkh-pal",
}
m["xkp"] = {
"Kabatei",
34165,
"xme-ttc",
ancestors = "xme-ttc-cen",
}
m["xkq"] = {
"Koroni",
3199000,
"poz-btk",
}
m["xkr"] = {
"Xakriabá",
3073441,
"sai-cje",
"Latn",
}
m["xks"] = {
"Kumbewaha",
6443722,
}
m["xkt"] = {
"Kantosi",
35651,
"nic-dag",
}
m["xku"] = {
"Kaamba",
11042324,
"bnt-kng",
}
m["xkv"] = {
"Kgalagadi",
2088743,
"bnt-sts",
"Latn",
}
m["xkw"] = {
"Kembra",
12953627,
"paa-pau",
}
m["xkx"] = {
"Karore",
6373260,
"poz-ocw",
}
m["xky"] = {
"Uma' Lasan",
nil,
"poz-swa",
}
m["xkz"] = {
"Kurtöp",
3695193,
"sit-ebo",
"Tibt, Latn",
translit = {Tibt = "Tibt-translit"},
display_text = {Tibt = s["Tibt-displaytext"]},
entry_name = {Tibt = s["Tibt-entryname"]},
sort_key = {Tibt = "Tibt-sortkey"},
}
m["xla"] = {
"Kamula",
10957277,
"ngf",
}
m["xlb"] = {
"Loup B",
13108281,
"alg-eas",
"Latn",
}
m["xlc"] = {
"Lycia",
35969,
"ine-ana",
"Lyci",
translit = "Lyci-translit",
}
m["xld"] = {
"Lydia",
36095,
"ine-ana",
"Lydi",
translit = "Lydi-translit",
}
m["xle"] = {
"Lemnos",
36203,
"qfa-tyn",
"Ital",
translit = "Ital-translit",
}
m["xlg"] = {
"Liguria Purba",
36104,
"ine",
}
m["xli"] = {
"Liburni",
35835,
"ine",
}
--xln is etymology-only
m["xlo"] = {
"Loup A",
27921265,
"alg-eas",
"Latn",
}
m["xlp"] = {
"Lepontii",
35993,
"cel",
"Ital",
translit = "Ital-translit",
}
m["xls"] = {
"Lusitania",
35960,
"ine",
"Latn",
}
m["xlu"] = {
"Luwiya",
12634577,
"ine-ana",
"Xsux, Hluw",
}
m["xly"] = {
"Elymi",
35329,
nil,
"Grek",
}
m["xmb"] = {
"Mbonga",
36064,
"nic-jrn",
"Latn",
}
m["xmc"] = {
"Makhuwa-Marrevone",
11127231,
"bnt-mak",
ancestors = "vmw",
}
m["xmd"] = {
"Mbudum",
6799790,
"cdc-cbm",
"Latn",
}
m["xmf"] = {
"Mingrelia",
13359,
"ccs-zan",
"Geor",
translit = "Geor-translit",
override_translit = true,
}
m["xmg"] = {
"Mengaka",
36017,
"bai",
"Latn",
}
m["xmh"] = {
"Kugu-Muminh",
10549849,
"aus-pmn",
"Latn",
}
m["xmj"] = {
"Majera",
6737666,
"cdc-cbm",
"Latn",
}
m["xmk"] = {
"Macedonia Purba",
35974,
"grk",
"Polyt",
translit = "grc-translit",
entry_name = {remove_diacritics = c.macron .. c.breve},
sort_key = s["Grek-sortkey"],
}
m["xml"] = {
"Bahasa Isyarat Malaysia",
33420,
"sgn",
}
m["xmm"] = {
"Melayu Manado",
1068112,
"crp",
"Latn",
}
m["xmo"] = {
"Morerebi",
12953749,
"tup",
"Latn",
}
m["xmp"] = {
"Kuku-Mu'inh",
10549852,
nil,
"Latn",
}
m["xmq"] = {
"Kuku-Mangk",
10549851,
"aus-pam",
"Latn",
}
m["xmr"] = {
"Meroe",
13366,
"afa",
"Mero, Merc, Latn", -- we have entries in Latn
translit = "xmr-translit",
}
m["xms"] = {
"Bahasa Isyarat Maghribi",
6913107,
"sgn",
}
m["xmt"] = {
"Matbat",
6786187,
"poz-hce",
}
m["xmu"] = {
"Kamu",
6359779,
}
m["xmx"] = {
"Maden",
12952756,
"poz-hce",
}
m["xmy"] = {
"Mayaguduna",
3436736,
}
m["xmz"] = {
"Mori Bawah",
3324069,
"poz-btk",
"Latn",
}
m["xna"] = {
"Arab Utara Purba",
1472213,
"sem",
"Narb",
translit = "Narb-translit",
}
m["xnb"] = {
"Kanakanabu",
172244,
"map",
"Latn",
}
m["xng"] = {
"Mongol Pertengahan",
2582455,
"xgn",
"Mong, Phag, Hani, Arab, Armn",
translit = {Mong = "Mong-translit"},
display_text = {Mong = s["Mong-displaytext"]},
entry_name = {Mong = s["Mong-entryname"]},
sort_key = {Hani = "Hani-sortkey"},
}
m["xnh"] = {
"Kuanhua",
6441084,
"mkh-pal",
}
m["xni"] = {
"Ngarigu",
7022072,
"aus-yuk",
}
m["xnk"] = {
"Nganakarti",
33087049,
}
m["xnn"] = {
"Kankanay Utara",
12953609,
"phi",
}
-- "xno" IS TREATED AS "fro", SEE WT:LT
m["xnr"] = {
"Kangri",
2331560,
"him",
"Deva, Takr, fa-Arab",
ancestors = "doi",
translit = "hi-translit",
}
m["xns"] = {
"Kanashi",
6360672,
"sit-whm",
}
m["xnt"] = {
"Narragansett",
3336118,
"alg-eas",
"Latn",
entry_name = {remove_diacritics = c.grave .. c.acute .. c.tilde .. c.macron},
}
m["xnu"] = {
"Nukunul",
7068904,
}
m["xny"] = {
"Nyiyaparli",
16919427,
"aus-nga",
"Latn",
}
m["xoc"] = {
"O'chi'chi'",
3813833,
"nic-cde",
"Latn",
}
m["xod"] = {
"Kokoda",
6426734,
"ngf-sbh",
}
m["xog"] = {
"Soga",
33784,
"bnt-nyg",
"Latn",
}
m["xoi"] = {
"Kominimung",
6428352,
"paa",
"Latn",
}
m["xok"] = {
"Xokleng",
3027930,
"sai-sje",
}
m["xom"] = {
"Komo",
56681,
"ssa-kom",
}
m["xon"] = {
"Konkomba",
35674,
"nic-grm",
"Latn",
}
m["xoo"] = { -- contrast kzw, sai-kat, sai-xoc
"Xukurú",
9096758,
}
m["xop"] = {
"Kopar",
11732346,
}
m["xor"] = {
"Korubo",
3199022,
}
m["xow"] = {
"Kowaki",
6434920,
"ngf-mad",
}
m["xpa"] = {
"Pirriya",
16978087,
}
m["xpb"] = {
"Pyemmairre",
7262964,
nil,
"Latn",
}
m["xpc"] = {
"Pecheneg",
877881,
"trk",
}
m["xpd"] = {
"Paredarerme",
7136678,
nil,
"Latn",
}
m["xpe"] = {
"Liberia Kpelle",
20527226,
"dmn-msw",
ancestors = "kpe",
}
m["xpf"] = {
"Tasmania Tenggara",
7068421,
nil,
"Latn",
}
m["xpg"] = {
"Phrygia",
36751,
"ine",
"Grek",
translit = "grc-translit",
}
m["xph"] = {
"Tyerrernotepanner",
7859815,
nil,
"Latn",
}
m["xpi"] = {
"Pict",
856383,
"cel",
"Ogam, Latn",
}
m["xpj"] = {
"Mpalitjanh",
6928192,
"aus-pam",
}
m["xpk"] = {
"Kulina",
6443027,
"sai-pan",
}
m["xpl"] = {
"Port Sorell",
7230944,
nil,
"Latn",
}
m["xpm"] = {
"Pumpokol",
2991985,
"qfa-yen",
"Latn",
}
m["xpn"] = {
"Kapinawá",
6366667,
}
m["xpo"] = {
"Pochutec",
2427341,
"azc-nah",
"Latn",
}
m["xpp"] = {
"Puyo-Paekche",
nil,
}
m["xpq"] = {
"Mohegan-Pequot",
3319130,
"alg-eas",
"Latn",
}
m["xpr"] = {
"Parthia",
25953,
"ira-mpr",
"Prti, Mani, Phlv",
translit = {
Prti = "Prti-translit",
Mani = "Mani-translit",
},
}
m["xps"] = {
"Pisidia",
36580,
"ine-ana",
}
m["xpu"] = {
"Punik",
535958,
"sem-can",
"Phnx, Latn, Grek",
ancestors = "phn",
translit = {Phnx = "Phnx-translit"},
}
m["xpv"] = {
"Tommeginne",
7819095,
nil,
"Latn",
}
m["xpw"] = {
"Peerapper",
7160431,
nil,
"Latn",
}
m["xpx"] = {
"Toogee",
7824008,
nil,
"Latn",
}
m["xpy"] = {
"Buyeo",
5003359,
"qfa-kor",
"Hani",
sort_key = "Hani-sortkey",
}
m["xpz"] = {
"Pulau Bruny",
4979601,
nil,
"Latn",
}
m["xqa"] = {
"Karakhanid",
nil,
"trk-kar",
"Arab",
entry_name = "ar-entryname",
}
m["xqt"] = {
"Qatabanian",
384101,
"sem-osa",
"Sarb",
translit = "Sarb-translit",
}
m["xra"] = {
"Krahô",
3199549,
"sai-nje",
"Latn",
}
m["xrb"] = {
"Karaboro Timur",
35716,
"alv-krb",
}
m["xrd"] = {
"Gundungurra",
nil,
}
m["xre"] = {
"Kreye",
3199686,
"sai-nje",
}
m["xrg"] = {
"Minang",
22893424,
}
m["xri"] = {
"Krikati-Timbira",
3199710,
}
m["xrm"] = {
"Armazic",
7599646,
}
m["xrn"] = {
"Arin",
34088,
"qfa-yen",
"Latn",
}
m["xrq"] = {
"Karranga",
6373349,
nil,
"Latn",
}
m["xrr"] = {
"Raetic",
36689,
nil,
"Ital",
translit = "Ital-translit",
}
m["xrt"] = {
"Aranama-Tamique",
2859505,
}
m["xru"] = {
"Marriammu",
10577724,
"aus-dal",
}
m["xrw"] = {
"Karawa",
6368857,
"paa-spk",
}
m["xsa"] = {
"Sabaean",
1070391,
"sem-osa",
"Sarb",
translit = "Sarb-translit",
}
m["xsb"] = {
"Sambal",
2592378,
"phi",
"Latn",
}
m["xsd"] = {
"Sidetic",
36659,
"ine-ana",
}
m["xse"] = {
"Sempan",
3504358,
}
m["xsh"] = {
"Shamang",
3914876,
"nic-plc",
}
m["xsi"] = {
"Sio",
3485100,
"poz-ocw",
}
m["xsj"] = {
"Subi",
7631298,
"bnt-haj",
}
m["xsl"] = {
"Slavey Selatan",
28552,
"ath-nor",
"Latn",
}
m["xsm"] = {
"Kasem",
35552,
"nic-gnn",
}
m["xsn"] = {
"Sanga (Nigeria)",
3915334,
"nic-jer",
"Latn",
}
m["xso"] = {
"Solano",
2474492,
nil,
"Latn",
}
m["xsp"] = {
"Silopi",
7515533,
"ngf-mad",
}
m["xsq"] = {
"Makhuwa-Saka",
11008159,
"bnt-mak",
ancestors = "vmw",
}
m["xsr"] = {
"Sherpa",
36612,
"sit-tib",
"Tibt, Deva",
ancestors = "xct",
translit = {
Tibt = "Tibt-translit",
Deva = "xsr-Deva-translit",
},
override_translit = true,
display_text = {Tibt = s["Tibt-displaytext"]},
entry_name = {Tibt = s["Tibt-entryname"]},
sort_key = {Tibt = "Tibt-sortkey"},
}
m["xss"] = {
"Assan",
34089,
"qfa-yen",
"Latn",
}
m["xsu"] = {
"Sanumá",
251728,
"sai-ynm",
"Latn",
}
m["xsv"] = {
"Sudovian",
35603,
"bat",
"Latn",
}
m["xsy"] = {
"Saisiyat",
716695,
"map",
"Latn",
}
m["xta"] = {
"Alcozauca Mixtec",
25559587,
"omq-mxt",
"Latn",
}
m["xtb"] = {
"Chazumba Mixtec",
12182838,
"omq-mxt",
"Latn",
}
m["xtc"] = {
"Kadugli",
3407136,
"qfa-kad",
"Latn",
}
m["xtd"] = {
"Diuxi-Tilantongo Mixtec",
7802048,
"omq-mxt",
"Latn",
}
m["xte"] = {
"Ketengban",
10990152,
}
m["xth"] = {
"Yitha Yitha",
nil,
}
m["xti"] = {
"Sinicahua Mixtec",
12953733,
"omq-mxt",
"Latn",
}
m["xtj"] = {
"San Juan Teita Mixtec",
32093049,
"omq-mxt",
"Latn",
}
m["xtl"] = {
"Tijaltepec Mixtec",
12953738,
"omq-mxt",
"Latn",
}
m["xtm"] = {
"Mixtec Magdalena Peñasco",
7179700,
"omq-mxt",
"Latn",
}
m["xtn"] = {
"Mixtec Tlaxiaco Utara",
25559585,
"omq-mxt",
"Latn",
}
m["xto"] = {
"Tocharia A",
2827041,
"ine-toc",
"Latn",
wikipedia_article = "Tocharian languages", -- wikidata id has no associated article
}
m["xtp"] = {
"Mixtec San Miguel Piedras",
7414970,
"omq-mxt",
"Latn",
}
m["xtq"] = {
"Tumshuq",
nil,
"xsc-sak",
"Brah, Khar",
translit = "Brah-translit",
}
m["xtr"] = {
"Tripuri Awal",
nil,
}
m["xts"] = {
"Mixtec Sindihui",
13583581,
"omq-mxt",
"Latn",
}
m["xtt"] = {
"Mixtec Tacahua",
7673668,
"omq-mxt",
"Latn",
}
m["xtu"] = {
"Mixtec Cuyamecalco",
12953726,
"omq-mxt",
"Latn",
}
m["xtv"] = {
"Thawa",
7711494,
}
m["xtw"] = {
"Tawandê",
nil,
"sai-nmk",
"Latn",
}
m["xty"] = {
"Mixtec Yoloxochitl",
8054817,
"omq-mxt",
"Latn",
}
m["xtz"] = {
"Tasmania",
530739,
nil,
"Latn",
}
m["xua"] = {
"Kurumba Alu",
12952679,
"dra",
}
m["xub"] = {
"Kurumba Betta",
16841033,
"dra",
"Knda, Mlym, Taml",
}
m["xud"] = {
"Umiida",
61999874,
"aus-wor",
"Latn",
}
m["xug"] = {
"Kunigami",
56558,
"jpx-ryu",
"Jpan",
translit = s["Jpan-translit"],
sort_key = s["Jpan-sortkey"],
}
m["xuj"] = {
"Jennu Kurumba",
21282543,
"dra",
}
m["xul"] = {
"Ngunawal",
7022712,
"aus-yuk",
"Latn",
}
m["xum"] = {
"Umbri",
36957,
"itc-sbl",
"Ital, Latn",
translit = "Ital-translit",
}
m["xun"] = {
"Unggaranggu",
61999823,
"aus-wor",
"Latn",
}
m["xuo"] = {
"Kuo",
6445233,
"alv-mbm",
}
m["xup"] = {
"Upper Umpqua",
20607,
"ath-pco",
"Latn",
}
m["xur"] = {
"Urartian",
36934,
"qfa-hur",
"Xsux",
}
m["xut"] = {
"Kuthant",
6448417,
}
m["xuu"] = {
"Khwe",
28305,
"khi-kal",
"Latn",
}
m["xve"] = {
"Venetic",
36871,
"ine",
"Ital",
translit = "Ital-translit",
}
-- m["xvi"] = { "Kamviri", 1193495, "nur-nor", Arab } moved to etym-only code
m["xvn"] = {
"Vandalic",
36835,
"gme",
"Latn",
}
m["xvo"] = {
"Volscian",
622110,
"itc-sbl",
"Latn",
}
m["xvs"] = {
"Vestinian",
2576407,
"itc",
"Latn",
}
m["xwa"] = {
"Kwaza",
3200839,
}
m["xwc"] = {
"Woccon",
3569569,
"nai-cat",
"Latn",
}
m["xwd"] = {
"Wadi Wadi",
7959249,
}
m["xwe"] = {
"Xwela Gbe",
36887,
"alv-pph",
}
m["xwg"] = {
"Kwegu",
56723,
"sdv",
}
m["xwj"] = {
"Wajuk",
33110188,
}
m["xwk"] = {
"Wangkumara",
7967891,
"aus-pam",
"Latn",
}
m["xwl"] = {
"Gbe Xwla Barat",
36924,
"alv-pph",
"Latn",
}
m["xwo"] = {
"Oirat Bertulis",
56959,
"xgn-cen",
"xwo-Mong",
translit = "xwo-translit",
}
m["xwr"] = {
"Kwerba Mamberamo",
6450325,
"paa-tkw",
}
m["xww"] = {
"Wemba-Wemba",
18472819,
"aus-pam",
"Latn",
}
m["xxb"] = {
"Boro",
16844787,
nil,
"Latn",
}
m["xxk"] = {
"Ke'o",
3195346,
}
m["xxm"] = {
"Minkin",
6867836,
}
m["xxr"] = {
"Koropó",
6432560,
}
m["xxt"] = {
"Tambora",
36711,
"paa",
"Latn",
}
m["xya"] = {
"Yaygir",
8050525,
"aus-pam",
}
m["xyb"] = {
"Yandjibara",
nil,
nil,
"Latn",
}
m["xyl"] = {
"Yalakalore",
12645352,
"sai-nmk",
"Latn",
}
m["xyt"] = {
"Mayi-Thakurti",
47004719,
"aus-pam",
"Latn",
}
m["xyy"] = {
"Yorta Yorta",
8055849,
"aus-pam",
"Latn",
}
m["xzh"] = {
"Zhang-Zhung",
3437292,
"sit-alm",
"xzh-Tibt, Marc",
display_text = {["xzh-Tibt"] = s["Tibt-displaytext"]},
entry_name = {["xzh-Tibt"] = s["Tibt-entryname"]},
}
m["xzm"] = {
"Zemgalia",
47631,
"bat",
}
m["xzp"] = {
"Zapotec Purba",
nil,
}
return require("Module:languages").finalizeData(m, "language")
n40topjbk5dwq74kd70arslduv875oh
Templat:af-bulan kalendar Masihi
10
11122
342812
225993
2026-05-16T12:36:59Z
Hakimi97
2668
342812
wikitext
text/x-wiki
<onlyinclude><div id="rank" align="center">
{| border="1" cellpadding="4" cellspacing="0" style="margin: 1em 1em 1em 0; background: #f9f9f9; border: 1px #aaa solid; border-collapse: collapse;"
! colspan=12 | <div class="center">Die twaalf [[maand|maande]] van die Gregoriaanse [[jaar]]</div>
|-
| bgcolor="#afafaf" style="text-align: center; width: 5em" | {{l|af|Januarie}}<br>{{l|af|یَنِڤَارِي}}
| bgcolor="#bfbfbf" style="text-align: center; width: 5em" | {{l|af|Februarie}}<br>{{l|af|فَِبِرْڤَارِي}}
| bgcolor="#afafaf" style="text-align: center; width: 5em" | {{l|af|Maart}}<br>{{l|af|مَارْتْ}}
| bgcolor="#bfbfbf" style="text-align: center; width: 5em" | {{l|af|April}}<br>{{l|af|آپْرِلْ}}
| bgcolor="#cfcfcf" style="text-align: center; width: 5em" | {{l|af|Mei}}<br>{{l|af|مَيْ}}
| bgcolor="#cfcfcf" style="text-align: center; width: 5em" | {{l|af|Junie}}<br>{{l|af|یُینِي}}
| bgcolor="#cfcfcf" style="text-align: center; width: 5em" | {{l|af|Julie}}<br>{{l|af|یُیلِي}}
| bgcolor="#cfcfcf" style="text-align: center; width: 5em" | {{l|af|Augustus}}<br>{{l|af|اَوْخِوسْتِوسْ}}
| bgcolor="#bfbfbf" style="text-align: center; width: 5em" | {{l|af|September}}<br>{{l|af|سِپْتَِمْبِرْ}}
| bgcolor="#afafaf" style="text-align: center; width: 5em" | {{l|af|Oktober}}<br>{{l|af|اَُوكْتُوَبِرْ}}
| bgcolor="#bfbfbf" style="text-align: center; width: 5em" | {{l|af|November}}<br>{{l|af|نُوفَِمْبِرْ}}
| bgcolor="#afafaf" style="text-align: center; width: 5em" | {{l|af|Desember}}<br>{{l|af|دِیسَِمْبِرْ|sc=Arab}} {{l|af|دَِیسَِمْبِرْ|sc=Arab}}
|}</div>
[[Kategori:af:Bulan takwim Masihi]]
</onlyinclude>
<noinclude>
[[Kategori:Templat|af-kalendar]]
fjxa2ylhhue7t6anblm12s1sl8v6yk8
Modul:category tree/topic/Communication
828
11523
342796
281456
2026-05-16T12:11:57Z
Hakimi97
2668
342796
Scribunto
text/plain
local labels = {}
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
-- FIXME: Lookup langs in the language list.
for _, lang_etc in ipairs {
"Arab", {"Cina", "Bahasa-bahasa Cina"}, "Inggeris", "Jerman", "Jepun", "Okinawa",
"Portugis", "Sepanyol", "Vietnam", {"Melayu", "Bahasa-bahasa Melayik"},
} do
if type(lang_etc) ~= "table" then
lang_etc = {lang_etc}
end
local lang, desc = unpack(lang_etc)
desc = desc or ("[[:Kategori:Bahasa %s|bahasa %s]]"):format(lang, lang)
labels[lang] = {
type = "berkenaan",
description = "=" .. desc,
parents = {"bahasa-bahasa"},
}
end
labels["komunikasi"] = {
type = "berkenaan",
description = "default",
parents = {"Semua topik"},
}
labels["huruf"] = {
type = "nama",
description = "default",
parents = {"sistem tulisan"},
}
labels["bahasa buatan"] = { -- distinguish from "cat:constructed languages" family category
type = "nama",
description = "={{w|constructed language}}s",
parents = {"bahasa-bahasa"},
}
labels["bahasa badan"] = {
type = "berkenaan",
description = "default",
parents = {"bahasa", "nonverbal communication"},
}
labels["penyiaran"] = {
type = "berkenaan",
description = "default",
parents = {"media", "telekomunikasi"},
}
labels["Komponen aksara Cina"] = {
type = "set",
description = "=[[komponen|Komponen]] [[aksara]] [[Cina]].",
parents = {"Huruf, simbol dan tanda baca"},
}
labels["tanda diakritik"] = {
type = "set",
description = "default",
parents = {"Huruf, simbol dan tanda baca"},
}
labels["dialek"] = {
type = "set",
description = "default",
parents = {"bahasa"},
}
labels["dictation"] = {
type = "berkenaan",
description = "default",
parents = {"komunikasi"},
}
labels["bahasa pupus"] = {
type = "nama",
description = "default",
parents = {"bahasa-bahasa"},
}
labels["bahasa isyarat"] = {
type = "nama",
description = "default",
parents = {"bahasa-bahasa"},
}
labels["facial expressions"] = {
type = "set",
description = "default",
parents = {"nonverbal communication", "face"},
}
labels["kiasan"] = {
type = "set",
description = "=[[figure of speech|figures of speech]]",
parents = {"retorik"},
}
labels["bendera"] = {
type = "berkenaan,name,type",
description = "default",
parents = {"komunikasi"},
}
labels["jargon"] = {
type = "berkenaan",
description = "default",
parents = {"bahasa"},
}
labels["aksara Han"] = {
type = "berkenaan",
description = "default",
parents = {"sistem tulisan"},
}
labels["bahasa"] = {
type = "berkenaan",
description = "default",
parents = {"komunikasi"},
}
labels["keluarga bahasa"] = {
type = "nama",
description = "Topik berkenaan [[keluarga bahasa]], termasuklah yang diterima dan yang bersifat kontroversi.",
parents = {"bahasa", "nama"},
}
labels["bahasa-bahasa"] = {
type = "nama",
description = "default",
parents = {"bahasa", "nama"},
}
labels["Huruf, simbol dan tanda baca"] = {
type = "set",
description = "=[[letter]]s, [[symbol]]s, and [[punctuation]]",
parents = {"Ortografi"},
}
labels["logical fallacies"] = {
type = "set",
description = "=[[logical fallacy|logical fallacies]], clearly defined errors in reasoning used to support or refute an argument",
additional = "{{also|Kategori:{{{langcode}}}:biases}}",
parents = {"retorik", "logic"},
}
labels["media"] = {
type = "berkenaan",
description = "default",
parents = {"komunikasi"},
}
labels["telefon bimbit"] = {
type = "berkenaan,set",
description = "default",
parents = {"telefoni"},
}
labels["nonverbal communication"] = {
type = "berkenaan",
description = "default",
parents = {"komunikasi"},
}
labels["ortografi"] = {
type = "berkenaan",
description = "default",
parents = {"penulisan"},
}
labels["palaeography"] = {
type = "berkenaan",
description = "default",
parents = {"penulisan"},
}
labels["pos"] = {
type = "berkenaan",
description = "=[[post#Noun|post]] or [[mail#Noun|mail]]",
parents = {"komunikasi"},
}
labels["postal abbreviations"] = {
type = "nama",
description = "default",
parents = {"pos"},
}
labels["public relations"] = {
type = "berkenaan",
description = "default no singularize",
parents = {"komunikasi"},
}
labels["tanda baca"] = {
type = "set",
description = "default",
parents = {"Huruf, simbol dan tanda baca"},
}
labels["radio"] = {
type = "berkenaan",
description = "default",
parents = {"telekomunikasi"},
}
labels["retorik"] = {
type = "berkenaan",
description = "default",
parents = {"bahasa"},
}
labels["signs"] = {
type = "berkenaan,name,type",
description = "default",
parents = {"komunikasi"},
}
labels["sociolects"] = {
type = "nama",
description = "default",
parents = {"bahasa"},
}
labels["simbol"] = {
type = "set",
description = "=[[symbol]]s, especially [[mathematical]] and [[scientific]] symbols",
additional = "Most symbols have equivalent meanings in many languages and can therefore be found in [[:Category:Translingual symbols]].",
parents = {"Huruf, simbol dan tanda baca"},
}
labels["bercakap"] = {
type = "berkenaan",
description = "default",
parents = {"bahasa", "tingkah laku manusia"},
}
labels["telekomunikasi"] = {
type = "berkenaan",
description = "default no singularize",
parents = {"komunikasi", "teknologi"},
}
labels["telegrafi"] = {
type = "berkenaan",
description = "default",
parents = {"telekomunikasi", "elektronik"},
wpcat = true,
commonscat = true,
}
labels["telefoni"] = {
type = "berkenaan",
description = "default",
parents = {"telekomunikasi", "elektronik"},
}
labels["bermesej"] = {
type = "berkenaan",
description = "default",
parents = {"telekomunikasi"},
}
labels["textual division"] = {
type = "berkenaan",
description = "default",
parents = {"penulisan"},
}
labels["tipografi"] = {
type = "berkenaan",
description = "default",
parents = {"penulisan", "percetakan"},
}
labels["penulisan"] = {
type = "berkenaan",
description = "default",
parents = {"bahasa", "tingkah laku manusia"},
}
labels["sistem tulisan"] = {
type = "set",
description = "default",
parents = {"penulisan"},
}
return labels
cjlofl7ldcty9627g1ibrq2is2urz62
Modul:category tree/topic/People
828
11537
342831
335837
2026-05-16T14:03:42Z
Hakimi97
2668
342831
Scribunto
text/plain
local labels = {}
labels["orang"] = {
description = "default",
parents = {"manusia"},
}
labels["orang perempuan"] = {
type = "set",
description = "default",
parents = {"orang", "perempuan"},
}
labels["orang lelaki"] = {
type = "set",
description = "default",
parents = {"orang", "lelaki"},
}
labels["demonim Armenia"] = {
description = "{{{langname}}} terms related to Armenian [[demonym]]s, i.e., [[demonym]]s relating to places in [[Armenia]], as well as Armenian diaspora communities abroad.",
parents = {"demonim", "Armenia"},
}
labels["atlet"] = {
description = "default",
parents = {"pekerjaan", "olahraga"},
}
labels["pengarang"] = {
description = "default",
parents = {"orang", "kesusasteraan"},
}
labels["Barack Obama"] = {
description = "{{{langname}}} terms related to [[w:Barack Obama|Barack Obama]], [[president]] of the [[United States]] from 2009 until present.",
parents = {"individuals", "politics", "United States of America"},
}
labels["bayi"] = {
description = "Istilah berkenaan [[bayi]] dalam bahasa {{{langname}}}.",
parents = {"kanak-kanak"},
}
labels["kanak-kanak"] = {
description = "Istilah berkenaan [[kanak-kanak]] dalam bahasa {{{langname}}}.",
parents = {"orang"},
}
labels["demonim"] = {
type = "set",
description = "[[demonim|Demonim]], gelaran terhadap kumpulan penetap sesuatu tempat dalam bahasa {{{langname}}}.",
parents = {"orang", "nama"},
}
labels["E. E. Smith"] = {
description = "{{{langname}}} terms and phrases coined by [[w:E. E. Smith|E. E. Smith]], or otherwise derived from or related to his works.",
parents = {"authors", "American fiction", "science fiction", "kesusasteraan"},
}
labels["ethnicity"] = {
description = "default",
parents = {"orang"},
}
labels["keluarga"] = {
description = "default",
parents = {"orang"},
}
labels["ahli keluarga"] = {
type = "set",
description = "Gelaran ahli-ahli keluarga dalam bahasa {{{langname}}} termasuk [[saudara]], [[mentua]] dll.",
parents = {"keluarga"},
}
labels["ahli keluarga perempuan"] = {
type = "set",
description = "Gelaran ahli-ahli keluarga [[perempuan]] dalam bahasa {{{langname}}} termasuk [[saudara]], [[mentua]] dll.",
parents = {"ahli keluarga", "orang perempuan"},
}
labels["ahli keluarga lelaki"] = {
type = "set",
description = "Gelaran ahli-ahli keluarga [[lelaki]] dalam bahasa {{{langname}}} termasuk [[saudara]], [[mentua]] dll.",
parents = {"ahli keluarga", "orang lelaki"},
}
labels["peminat"] = {
description = "Istilah gelaran para peminat penggiat dan karya seni dalam bahasa {{{langname}}}.",
parents = {"orang", "fandom"},
}
labels["George W. Bush"] = {
description = "{{{langname}}} terms related to [[w:George W. Bush|George W. Bush]], [[president]] of the [[United States]] from 2001 to 2009.",
parents = {"individuals", "politics", "United States of America"},
}
labels["ketua negara"] = {
type = "set",
description = "Gelaran [[ketua negara]] dalam bahasa {{{langname}}}.",
parents = {"pekerjaan", "pemerintahan"},
}
labels["pekerjaan kesihatan"] = {
type = "set",
description = "default-set",
parents = {"pekerjaan", "penjagaan kesihatan"},
}
labels["individu"] = {
type = "set",
description = "{{{langname}}} names of [[individual]]s.",
parents = {"orang"},
}
labels["Isaac Asimov"] = {
description = "{{{langname}}} terms and phrases coined by [[w:Isaac Asimov|Isaac Asimov]], or otherwise derived from or related to his works.",
parents = {"authors", "American fiction", "science fiction", "kesusasteraan"},
}
labels["J. R. R. Tolkien"] = {
description = "{{{langname}}} terms related to author [[w:J. R. R. Tolkien|J. R. R. Tolkien]] and his works.",
parents = {"authors", "British fiction", "fantasy", "kesusasteraan"},
}
labels["Latvian demonyms"] = {
description = "{{{langname}}} terms related to Latvian [[demonym]]s, i.e., [[demonym]]s relating to places in [[Latvia]].",
parents = {"demonim", "Latvia"},
}
labels["Lewis Carroll"] = {
description = "{{{langname}}} terms and phrases coined by [[w:Lewis Carroll|Lewis Carroll]], or otherwise derived from his works.",
parents = {"authors", "British fiction", "fantasy", "kesusasteraan"},
}
labels["military ranks"] = {
type = "set",
description = "{{{langname}}} terms for [[rank]]s of the [[military]].",
parents = {"pekerjaan", "ketenteraan"},
}
labels["kerakyatan"] = {
type = "set",
description = "default-set",
parents = {"demonim", "orang"},
}
labels["kebangsawanan"] = {
description = "default",
parents = {"orang"},
}
labels["pekerjaan"] = {
type = "set",
description = "Istilah [[pekerjaan]] dalam bahasa {{{langname}}}",
parents = {"orang"},
}
labels["ibu bapa"] = {
description = "default",
parents = {"keluarga"},
}
labels["ahli sains"] = {
type = "set",
description = "Gelaran-gelaran [[ahli sains]] dalam bahasa {{{langname}}}",
parents = {"pekerjaan", "sains"},
}
labels["gelaran"] = {
description = "[[gelaran|Gelaran-gelaran]] orang dalam bahasa {{{langname}}}.",
parents = {"orang"},
}
labels["tribes"] = {
description = "default",
parents = {"demonim", "orang"},
}
labels["William Shakespeare"] = {
description = "{{{langname}}} terms related to the British author William Shakespeare or his works.",
parents = {"authors"},
}
return labels
7wb4jzot9zc177alsvu4t6565vo78zx
Modul:category tree/topic/Time
828
12242
342798
335863
2026-05-16T12:23:56Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/87310303|87310303]])
342798
Scribunto
text/plain
local labels = {}
labels["masa"] = {
type = "berkenaan",
description = "default",
parents = {"semua topik"},
}
labels["bulan takwim Anglo-Saxon"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Anglo-Saxon calendar}}.",
parents = {"bulan dalam tahun", "bulan qamari"},
}
labels["bulan takwim Armenia"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Armenian calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["takwim"] = {
type = "berkenaan",
description = "default with the",
parents = {"penjagaan masa"},
}
labels["abad"] = {
type = "nama",
description = "default",
parents = {"takwim"},
}
labels["bulan takwim Benggala"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Bengali calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["tanda zodiak Burma"] = {
type = "nama",
description = "{{{langname}}} names for the signs of the {{w|Burmese zodiac}}.",
parents = {"astrologi", "takwim"},
}
labels["cabang bumi Cina"] = {
type = "nama",
description = "=[[Chinese]] [[earthly branch]]es",
parents = {"istilah kitaran seksagenari Cina"},
}
labels["batang langit Cina"] = {
type = "nama",
description = "=[[Chinese]] [[heavenly stem]]s",
parents = {"istilah kitaran seksagenari Cina"},
}
labels["bulan Cina"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["istilah kitaran seksagenari Cina"] = {
type = "nama",
description = "=[[Chinese]] {{w|sexagenary cycle}} [[term]]s",
parents = {"takwim"},
}
labels["tanda zodiak Cina"] = {
type = "nama",
description = "default wikify",
parents = {"astrologi", "takwim", "mitologi Cina"},
}
labels["Krismas"] = {
type = "berkenaan",
description = "default no singularize",
parents = {"cuti", "Kristian"},
}
labels["hari takwim Hindu"] = {
type = "nama",
description = "{{{langname}}} names for the [[day]]s of the {{w|Hindu calendar}}.",
parents = {"kejadian berkala", "Hinduisme"},
}
labels["hari dalam minggu"] = {
type = "nama",
description = "=the [[Appendix:Days of the week|days of the week]]",
parents = {"kejadian berkala"},
}
labels["dekad"] = {
type = "nama",
description = "default",
parents = {"takwim"},
}
labels["Paskah"] = {
type = "berkenaan",
description = "default",
parents = {"cuti", "Kristian"},
}
labels["bulan takwim Mesir"] = {
type = "nama",
description = "{{{langname}}} names for the months of the ancient {{w|Egyptian calendar}}.",
parents = {"Mesir Purba", "bulan dalam tahun"},
}
labels["festival"] = {
type = "nama,jenis",
description = "default",
parents = {"perayaan"},
}
labels["bulan takwim Gregory"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["Halloween"] = {
type = "berkenaan",
description = "default",
parents = {"cuti"},
}
labels["Hanukkah"] = {
type = "berkenaan",
description = "default no singularize",
parents = {"cuti", "Yudaisme"},
}
labels["bulan takwim Ibrani"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Hebrew calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["tahun Jovian Hindu"] = {
type = "nama",
description = "{{{langname}}} names for years in the cycle of [[Jovian year]]s in traditional calendars of [[Hinduism]].",
additional = "These are based on the movement of the planet Jupiter rather than that of the sun.",
parents = {"takwim", "Hinduisme"},
}
labels["bulan takwim qamari Hindu"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Hindu calendar|Hindu lunar calendar}}.",
parents = {"bulan dalam tahun", "Hinduisme"},
}
labels["bulan takwim suria Hindu"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Hindu calendar|Hindu solar calendar}}.",
parents = {"bulan dalam tahun", "Hinduisme"},
}
labels["cuti"] = {
type = "nama,jenis",
description = "default",
parents = {"perayaan"},
}
labels["bulan takwim Iceland"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Icelandic calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan takwim Hijrah"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun", "Islam"},
}
labels["bulan takwim Jepun"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Japanese calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan takwim Jawa"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Javanese calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan Kojoda"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Yoruba calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan qamari"] = {
type = "set",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["tanda hari Mesoamerika"] = {
type = "nama",
description = "default",
parents = {"simbol", "takwim"},
wp = "Aztec calendar",
}
labels["bulan dalam tahun"] = {
type = "set",
preceding = "{{also|Appendix:Months of the year}}",
description = "{{{langname}}} names of the [[month]]s of the [[year]].",
parents = {"kejadian berkala"},
wpcat = true,
commonscat = true,
}
labels["bulan takwim Nanakshahi"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Nanakshahi calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan takwim Norse"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["perayaan"] = {
type = "set",
description = "default",
parents = {"takwim"},
}
labels["kejadian berkala"] = {
type = "set",
description = "=occurrences that repeat at certain intervals of time",
parents = {"masa"},
}
labels["bulan Parsi"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["Ramadan"] = {
type = "berkenaan",
description = "default",
parents = {"perayaan", "Islam", "bulan takwim Hijrah"},
}
labels["musim"] = {
type = "set",
description = "default",
parents = {"kejadian berkala", "alam semula jadi"},
}
labels["Sinterklaas"] = {
type = "berkenaan",
description = "default no singularize",
parents = {"cuti", "tokoh mitologi"},
}
labels["istilah suria Cina"] = {
type = "set",
description = "default",
parents = {"takwim", "Matahari"},
}
labels["Tahun Baru"] = {
type = "berkenaan",
description = "=the [[Gregorian]] [[New Year]]",
parents = {"cuti"},
}
labels["bulan Kristian Syria"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["bulan takwim Tamil"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Tamil calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["zon masa"] = {
type = "nama",
description = "default",
parents = {"penjagaan masa"},
}
labels["Hari Kesyukuran"] = {
type = "berkenaan",
description = "default",
parents = {"cuti"},
}
labels["penjagaan masa"] = {
type = "berkenaan",
description = "default",
parents = {"masa"},
}
labels["waktu dalam sehari"] = {
type = "set",
description = "=[[time]]s of the [[day]]",
parents = {"kejadian berkala", "penjagaan masa"},
}
labels["hari"] = {
type = "berkenaan",
description = "=the [[day]] ([[etymologically]] or [[semantically]])",
additional = "'''NOTE''': Do not include days of the week within this category. These should be in [[:Category:{{{langcode}}}:Days of the week]].",
parents = {"masa"},
}
labels["malam"] = {
type = "berkenaan",
description = "=the [[night]] ([[etymologically]] or [[semantically]])",
parents = {"masa", "kegelapan"},
}
labels["tahun"] = {
type = "jenis",
description = "default",
parents = {"kejadian berkala"},
}
labels["masa lampau"] = {
type = "berkenaan",
description = "{{{langname}}} terms for the [[past]], or for events in the [[past]].",
additional = "NOTE: Do NOT include words that are exclusively verbs, or words related to the field of history or historiography, within this category.",
parents = {"masa"},
}
labels["masa kini"] = {
type = "berkenaan",
description = "{{{langname}}} terms for the [[present]], or for events in the [[present]].",
additional = "NOTE: Do NOT include words that are exclusively verbs, or words related to the field of history or historiography, within this category.",
parents = {"masa"},
}
labels["masa depan"] = {
type = "berkenaan",
description = "{{{langname}}} terms for the [[future]], or for events in the [[future]].",
additional = "NOTE: Do NOT include words that are exclusively verbs, or words related to the field of history or historiography, within this category.",
parents = {"masa"},
}
labels["pengembaraan masa"] = {
type = "berkenaan",
description = "default",
parents = {"masa", "fiksyen sains", "pengembaraan", "kerelatifan"},
}
return labels
7ij6f42a3r2y3g5jtbihtc8s4v93vyn
342799
342798
2026-05-16T12:27:48Z
Hakimi97
2668
342799
Scribunto
text/plain
local labels = {}
labels["masa"] = {
type = "berkenaan",
description = "default",
parents = {"semua topik"},
}
labels["bulan takwim Anglo-Saxon"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Anglo-Saxon calendar}}.",
parents = {"bulan dalam tahun", "bulan qamari"},
}
labels["bulan takwim Armenia"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Armenian calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["takwim"] = {
type = "berkenaan",
description = "default with the",
parents = {"penjagaan masa"},
}
labels["abad"] = {
type = "nama",
description = "default",
parents = {"takwim"},
}
labels["bulan takwim Benggala"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Bengali calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["tanda zodiak Burma"] = {
type = "nama",
description = "{{{langname}}} names for the signs of the {{w|Burmese zodiac}}.",
parents = {"astrologi", "takwim"},
}
labels["cabang bumi Cina"] = {
type = "nama",
description = "=[[Chinese]] [[earthly branch]]es",
parents = {"istilah kitaran seksagenari Cina"},
}
labels["batang langit Cina"] = {
type = "nama",
description = "=[[Chinese]] [[heavenly stem]]s",
parents = {"istilah kitaran seksagenari Cina"},
}
labels["bulan Cina"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["istilah kitaran seksagenari Cina"] = {
type = "nama",
description = "=[[Chinese]] {{w|sexagenary cycle}} [[term]]s",
parents = {"takwim"},
}
labels["tanda zodiak Cina"] = {
type = "nama",
description = "default wikify",
parents = {"astrologi", "takwim", "mitologi Cina"},
}
labels["Krismas"] = {
type = "berkenaan",
description = "default no singularize",
parents = {"cuti", "Kristian"},
}
labels["hari takwim Hindu"] = {
type = "nama",
description = "{{{langname}}} names for the [[day]]s of the {{w|Hindu calendar}}.",
parents = {"kejadian berkala", "Hinduisme"},
}
labels["hari dalam minggu"] = {
type = "nama",
description = "=the [[Appendix:Days of the week|days of the week]]",
parents = {"kejadian berkala"},
}
labels["dekad"] = {
type = "nama",
description = "default",
parents = {"takwim"},
}
labels["Paskah"] = {
type = "berkenaan",
description = "default",
parents = {"cuti", "Kristian"},
}
labels["bulan takwim Mesir"] = {
type = "nama",
description = "{{{langname}}} names for the months of the ancient {{w|Egyptian calendar}}.",
parents = {"Mesir Purba", "bulan dalam tahun"},
}
labels["festival"] = {
type = "nama,jenis",
description = "default",
parents = {"perayaan"},
}
labels["bulan takwim Masihi"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["Halloween"] = {
type = "berkenaan",
description = "default",
parents = {"cuti"},
}
labels["Hanukkah"] = {
type = "berkenaan",
description = "default no singularize",
parents = {"cuti", "Yudaisme"},
}
labels["bulan takwim Ibrani"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Hebrew calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["tahun Jovian Hindu"] = {
type = "nama",
description = "{{{langname}}} names for years in the cycle of [[Jovian year]]s in traditional calendars of [[Hinduism]].",
additional = "These are based on the movement of the planet Jupiter rather than that of the sun.",
parents = {"takwim", "Hinduisme"},
}
labels["bulan takwim qamari Hindu"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Hindu calendar|Hindu lunar calendar}}.",
parents = {"bulan dalam tahun", "Hinduisme"},
}
labels["bulan takwim suria Hindu"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Hindu calendar|Hindu solar calendar}}.",
parents = {"bulan dalam tahun", "Hinduisme"},
}
labels["cuti"] = {
type = "nama,jenis",
description = "default",
parents = {"perayaan"},
}
labels["bulan takwim Iceland"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Icelandic calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan takwim Hijrah"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun", "Islam"},
}
labels["bulan takwim Jepun"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Japanese calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan takwim Jawa"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Javanese calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan Kojoda"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Yoruba calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan qamari"] = {
type = "set",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["tanda hari Mesoamerika"] = {
type = "nama",
description = "default",
parents = {"simbol", "takwim"},
wp = "Aztec calendar",
}
labels["bulan dalam tahun"] = {
type = "set",
preceding = "{{also|Appendix:Months of the year}}",
description = "{{{langname}}} names of the [[month]]s of the [[year]].",
parents = {"kejadian berkala"},
wpcat = true,
commonscat = true,
}
labels["bulan takwim Nanakshahi"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Nanakshahi calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["bulan takwim Norse"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["perayaan"] = {
type = "set",
description = "default",
parents = {"takwim"},
}
labels["kejadian berkala"] = {
type = "set",
description = "=occurrences that repeat at certain intervals of time",
parents = {"masa"},
}
labels["bulan Parsi"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["Ramadan"] = {
type = "berkenaan",
description = "default",
parents = {"perayaan", "Islam", "bulan takwim Hijrah"},
}
labels["musim"] = {
type = "set",
description = "default",
parents = {"kejadian berkala", "alam semula jadi"},
}
labels["Sinterklaas"] = {
type = "berkenaan",
description = "default no singularize",
parents = {"cuti", "tokoh mitologi"},
}
labels["istilah suria Cina"] = {
type = "set",
description = "default",
parents = {"takwim", "Matahari"},
}
labels["Tahun Baru Masihi"] = {
type = "berkenaan",
description = "=the [[Gregorian]] [[New Year]]",
parents = {"cuti"},
}
labels["bulan Kristian Syria"] = {
type = "nama",
description = "default",
parents = {"bulan dalam tahun"},
}
labels["bulan takwim Tamil"] = {
type = "nama",
description = "{{{langname}}} names for the months of the {{w|Tamil calendar}}.",
parents = {"bulan dalam tahun"},
}
labels["zon masa"] = {
type = "nama",
description = "default",
parents = {"penjagaan masa"},
}
labels["Hari Kesyukuran"] = {
type = "berkenaan",
description = "default",
parents = {"cuti"},
}
labels["penjagaan masa"] = {
type = "berkenaan",
description = "default",
parents = {"masa"},
}
labels["waktu dalam sehari"] = {
type = "set",
description = "=[[time]]s of the [[day]]",
parents = {"kejadian berkala", "penjagaan masa"},
}
labels["hari"] = {
type = "berkenaan",
description = "=the [[day]] ([[etymologically]] or [[semantically]])",
additional = "'''NOTE''': Do not include days of the week within this category. These should be in [[:Category:{{{langcode}}}:Days of the week]].",
parents = {"masa"},
}
labels["malam"] = {
type = "berkenaan",
description = "=the [[night]] ([[etymologically]] or [[semantically]])",
parents = {"masa", "kegelapan"},
}
labels["tahun"] = {
type = "jenis",
description = "default",
parents = {"kejadian berkala"},
}
labels["masa lampau"] = {
type = "berkenaan",
description = "{{{langname}}} terms for the [[past]], or for events in the [[past]].",
additional = "NOTE: Do NOT include words that are exclusively verbs, or words related to the field of history or historiography, within this category.",
parents = {"masa"},
}
labels["masa kini"] = {
type = "berkenaan",
description = "{{{langname}}} terms for the [[present]], or for events in the [[present]].",
additional = "NOTE: Do NOT include words that are exclusively verbs, or words related to the field of history or historiography, within this category.",
parents = {"masa"},
}
labels["masa depan"] = {
type = "berkenaan",
description = "{{{langname}}} terms for the [[future]], or for events in the [[future]].",
additional = "NOTE: Do NOT include words that are exclusively verbs, or words related to the field of history or historiography, within this category.",
parents = {"masa"},
}
labels["pengembaraan masa"] = {
type = "berkenaan",
description = "default",
parents = {"masa", "fiksyen sains", "pengembaraan", "kerelatifan"},
}
return labels
7br6sihki71gk6akid7qrd0m8weil6f
معجون أسنان
0
16203
342862
323387
2026-05-17T08:48:04Z
Hakimi97
2668
342862
wikitext
text/x-wiki
==Bahasa Arab==
===Kata nama===
{{ar-kn|مَعْجُون أَسْنان|m}}
# [[ubat gigi]]
===Etimologi===
{{af|ar|مَعْجُون|أَسْنان}}.
===Sebutan===
* {{ar-AFA|مَعْجُون أَسْنان}}
===Deklensi===
{{ar-dekl-kn|مَعْجُون|mod=أَسْنان|idafa=yes}}
===Rujukan===
* {{R:ar:Freytag}}
* {{R:ar:Baranov}}
* {{R:ar:Lane}}
* {{R:ar:Wehr-3}}
{{topik|ar|Kesihatan gigi|Kelengkapan dandanan diri}}
osb46y5sveu8wsv6irrksmq89f79a5o
toothpaste
0
16204
342863
332318
2026-05-17T08:48:48Z
Hakimi97
2668
342863
wikitext
text/x-wiki
==Bahasa Inggeris==
{{wikipedia|lang=en}}
===Kata nama===
{{en-kn|~}}
# [[ubat gigi]].
===Etimologi===
Daripada {{compound|en|tooth|paste|t1=gigi|t2=pes}}. Banding {{cog|stq|Tuskepasta||ubat gigi}}, {{cog|fy|toskpoetserguod||ubat gigi}}, {{cog|nl|tandpasta||ubat gigi}}, {{cog|de|Zahnpasta}}, {{m|de|Zahnpaste||ubat gigi}}, {{cog|da|tandpasta||ubat gigi}}, {{cog|sv|tandpasta||ubat gigi}}, {{cog|is|tannkrem||ubat gigi}}.
===Sebutan===
* {{a|RP}} {{AFA|en|/ˈtuːθpeɪst/}}
===Terbitan===
* {{l|en|toothpaste is out of the tube}}
* {{l|en|toothpastey}}
* {{l|en|toothpasty}}
[[Kategori:Kata majmuk endosentrik bahasa Inggeris]]
[[Kategori:en:Kesihatan gigi]]
[[Kategori:en:Kelengkapan dandanan diri]]
pmuuokerxavj4yn6prm6i28q75ny0u0
牙膏
0
16205
342855
323742
2026-05-17T08:42:24Z
Hakimi97
2668
342855
wikitext
text/x-wiki
==Bahasa Cina==
{{zh-forms}}
===Kata nama===
{{zh-kn}}
# [[ubat gigi]] {{zh-mw|m:管|c:枝}}
#: {{zh-x|擳 牙膏|memicit '''ubat gigi'''|C}}
===Sebutan===
{{zh-pron
|m=yágāo,er=y
|c=ngaa4 gou1
|g=nga4 gau1
|h=pfs=ngà-kâu;gd=nga2 gau1
|md=ngāi-gŏ̤
|mn=gê-ko
|mn-t=ghê5 go1
|w=3nga kau
|ma=Zh-yágao.ogg
|cat=n
}}
===Tesaurus===
====Sinonim====
{{zh-dial}}
===Lihat juga===
* {{zh-l|牙刷}}
{{zh-cat|Kesihatan gigi|Kelengkapan dandanan diri}}
60ihgvrl65akctr0jzqomgsqdpuecl9
Modul:category tree/topic/Islam
828
22341
342832
335862
2026-05-16T14:04:44Z
Hakimi97
2668
Membuang label "Ramadan" kerana sudah ditakrifkan di [[Modul:category tree/topic/Time]]
342832
Scribunto
text/plain
local labels = {}
labels["Islam"] = {
type = "set",
description = "Istilah berkenaan agama dan masyarakat [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"agama"},
wpcat = true,
}
-- Darjat pertama
labels["Allah"] = {
description = "Istilah berkaitan [[Allah]] dalam bahasa {{{langname}}}.",
parents = {"Islam", "tuhan"},
}
labels["makhluk dalam Islam"] = {
description = "Istilah berkenaan makhluk-makhluk dalam agama [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"Islam"},
}
labels["pengajian Islam"] = {
description = "Istilah berkenaan ilmu dan pengajian [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"Islam", "teologi"},
}
labels["ibadat Islam"] = {
description = "Istilah berkenaan ibadat dalam agama [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"Islam", "sembahyang"},
}
labels["akhirat"] = {
description = "Istilah berkenaan [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"Islam", "alam selepas mati"},
}
labels["Al-Quran"] = {
description = "Istilah berkenaan [[Al-Quran]] dalam bahasa {{{langname}}}.",
parents = {"Islam", "buku"},
}
labels["hadis"] = {
description = "Istilah berkenaan ilmu dan pengajian [[hadis]] dalam bahasa {{{langname}}}.",
parents = {"Islam", "Muhammad"},
}
labels["orang Islam"] = {
description = "Istilah berkenaan orang [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"Islam", "orang"},
}
labels["budaya Islam"] = {
description = "Istilah berkenaan kebudayaan [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"Islam", "budaya"},
}
-- Darjat kedua - Pengajian Islam
labels["akidah Islam"] = {
description = "Istilah berkenaan konsep-konsep [[akidah]] Islam dalam bahasa {{{langname}}}.",
parents = {"pengajian Islam", "Allah"},
}
labels["fiqah"] = {
description = "Istilah berkenaan konsep-konsep [[fiqh]] dalam bahasa {{{langname}}}.",
parents = {"pengajian Islam"},
}
labels["usul fiqah"] = {
description = "Istilah berkenaan konsep-konsep [[usul fiqh]] dalam bahasa {{{langname}}}.",
parents = {"fiqah"},
}
labels["mazhab Islam"] = {
description = "Istilah [[mazhab|mazhab-mazhab]] Islam dalam bahasa {{{langname}}}.",
parents = {"fiqh"},
}
labels["perkahwinan Islam"] = {
description = "Istilah berkenaan konsep-konsep [[perkahwinan]] dalam Islam dalam bahasa {{{langname}}}.",
parents = {"pengajian Islam"},
}
labels["syariat"] = {
description = "Istilah berkenaan syariah, perundangan dan jenayah [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"pengajian Islam"},
}
labels["kesufian"] = {
description = "default",
parents = {"mistisisme", "Islam"},
}
-- Darjat kedua - Orang Islam
labels["Muhammad"] = {
description = "Istilah berkenaan Nabi [[Muhammad]] SAW dalam bahasa {{{langname}}}.",
parents = {"orang Islam"},
}
-- Darjat kedua - Ibadat dalam Islam
labels["solat"] = {
description = "Istilah berkenaan [[solat]] dalam Islam dalam bahasa {{{langname}}}.",
parents = {"ibadat Islam"},
}
labels["zakat"] = {
description = "Istilah berkenaan konsep-konsep [[zakat]] dalam Islam dalam bahasa {{{langname}}}.",
parents = {"ibadat Islam", "kewangan Islam"},
}
labels["puasa Islam"] = {
description = "Istilah berkenaan [[puasa]] dalam Islam dalam bahasa {{{langname}}}.",
parents = {"ibadat Islam"},
}
labels["haji dan umrah"] = {
description = "Istilah berkenaan ibadah [[haji]] dan [[umrah]] dalam Islam dalam bahasa {{{langname}}}.",
parents = {"ibadat Islam"},
}
-- Darjat kedua - Al-Quran
labels["tajwid"] = {
description = "Istilah berkenaan hukum [[tajwid]] dalam bahasa {{{langname}}}.",
parents = {"Al-Quran"},
}
labels["taranum"] = {
description = "Istilah-istilah ilmu [[taranum]] dalam bahasa {{{langname}}}.",
parents = {"Al-Quran"},
}
labels["surah"] = {
description = "Nama-nama [[surah]] Al-Quran dalam bahasa {{{langname}}}.",
parents = {"Al-Quran"},
}
-- Darjat kedua - Hadis
-- Darjat kedua - Orang Islam
labels["nabi dan rasul dalam Islam"] = {
description = "Nama-nama [[nabi]] dan [[rasul]] dalam Islam dalam bahasa {{{langname}}}.",
parents = {"orang Islam"},
}
labels["agamawan Islam"] = {
description = "Nama-nama [[agamawan]] Islam dalam bahasa {{{langname}}}.",
parents = {"orang Islam"},
}
-- Darjat kedua - Budaya
labels["muzik Islam"] = {
description = "Istilah berkenaan ibadat dan budaya berkenaan bulan [[Ramadan]] dalam bahasa {{{langname}}}.",
parents = {"budaya Islam", "genre muzik"},
}
labels["perayaan Islam"] = {
description = "Istilah berkenaan hari-hari kebesaran dan perayaan [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"budaya Islam"},
}
labels["khat Islam"] = {
description = "Istilah berkenaan seni [[khat]] [[Islam]] dalam bahasa {{{langname}}}.",
parents = {"budaya Islam", "seni khat"},
}
labels["Aidilfitri"] = {
description = "Istilah berkenaan [[Aidilfitri]] dalam bahasa {{{langname}}}.",
parents = {"perayaan Islam"},
}
return labels
8bq8rktdntsxjdjuf0aj2nry7fbqek3
Modul:parameters/data
828
33722
342815
223595
2026-05-16T12:44:12Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/88995850|88995850]])
342815
Scribunto
text/plain
local list_to_set = require("Module:table").listToSet
local alias_of_2 = {alias_of = 2}
local boolean = {type = "boolean"}
local empty_list = {}
local list = {list = true}
local list_allow_holes_separate_no_index = {list = true, allow_holes = true, separate_no_index = true}
local required = {required = true}
local required_default_ = {required = true, default = ""}
local required_lang_default_und = {required = true, type = "language", default = "und"}
local type_labels = {type = "labels"}
local type_qualifier = {type = "qualifier"}
local type_references = {type = "references"}
local m = {}
-- [[Module:anchors]]
m["anchor"] = {
[1] = {required = true, list = true, disallow_holes = true},
}
m["senseid"] = {
[1] = required_lang_default_und,
[2] = required_default_,
id = alias_of_2,
tag = {set = list_to_set{"li", "p"}, default = "li"},
}
m["etymid"] = {
[1] = required_lang_default_und,
[2] = required_default_,
id = alias_of_2,
}
-- [[Module:etymon]]
m["etymon"] = {
[1] = required_lang_default_und,
[2] = {list = true, disallow_holes = true},
id = true,
title = true,
tree = boolean,
text = true,
exnihilo = boolean,
etydate = true,
rfe = true,
pos = true,
notree = {default = false, type = "boolean"},
nocat = {default = false, type = "boolean"},
nodot = {default = false, type = "boolean"},
json = {default = false, type = "boolean"},
}
-- [[Module:transclude]]
m["transclude"] = {
[1] = {required = true, type = "language"},
[2] = {list = true, required = true},
id = true,
sort = true,
nogloss = {default = false, type = "boolean"},
no_truncate_gloss = boolean,
include_place_extra_info = boolean,
place_translation_follows = boolean,
place_addl = true,
lb = true,
nolb = true,
nocat = boolean,
to = boolean,
t = list,
indent = true,
dot = boolean,
pagename = true,
}
-- [[Module:translations]]
m["translation"] = {
[1] = required_lang_default_und,
[2] = true,
[3] = list,
alt = true,
id = true,
sc = {type = "script"},
tr = true,
ts = true,
lit = true,
l = type_labels,
ll = type_labels,
q = type_qualifier,
qq = type_qualifier,
ref = type_references,
}
m["t-needed"] = {
[1] = required_lang_default_und,
[2] = {set = list_to_set{"usex", "quote"}},
nocat = boolean,
sort = true,
}
m["ter-atas"] = {
[1] = true,
id = true,
["column-width"] = true,
}
m["ter-atas-juga"] = {
[1] = required,
[2] = list,
id = list_allow_holes_separate_no_index,
["column-width"] = true,
}
m["checktrans-top"] = {
[1] = true,
["column-width"] = true,
}
m["ter-bawah"] = empty_list
m["ter-lihat"] = {
[1] = required,
[2] = list,
id = list_allow_holes_separate_no_index,
}
m["translation subpage"] = empty_list
m["no equivalent translation"] = {
[1] = required_lang_default_und,
noend = boolean,
}
m["no attested translation"] = {
[1] = required_lang_default_und,
noend = boolean,
sort = true,
}
m["not used"] = {
[1] = required_lang_default_und,
[2] = true,
}
return m
9kmrm2euhzpw7kuwy17wu4tc0r8r4yg
Modul:anchors
828
57712
342828
234446
2026-05-16T13:56:39Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/90373122|90373122]])
342828
Scribunto
text/plain
local export = {}
local string_utilities_module = "Module:string utilities"
local anchor_encode = mw.uri.anchorEncode
local concat = table.concat
local insert = table.insert
local language_anchor -- Defined below.
local function decode_entities(...)
decode_entities = require(string_utilities_module).decode_entities
return decode_entities(...)
end
local function encode_entities(...)
encode_entities = require(string_utilities_module).encode_entities
return encode_entities(...)
end
-- Returns the anchor text to be used as the fragment of a link to a language section.
function export.language_anchor(lang, id)
return anchor_encode(lang:getFullName() .. ": " .. id)
end
language_anchor = export.language_anchor
-- Normalizes input text (removes formatting etc.), which can then be used as an anchor in an `id=` field.
function export.normalize_anchor(str)
return decode_entities(anchor_encode(str))
end
function export.make_anchors(ids)
local anchors = {}
for i = 1, #ids do
local id = ids[i]
local el = mw.html.create("span")
:addClass("template-anchor")
:attr("id", anchor_encode(id))
:attr("data-id", id)
insert(anchors, tostring(el))
end
return concat(anchors)
end
function export.senseid(lang, id, tag_name)
-- The following tag is opened but never closed, where is it supposed to be closed?
-- with <li> it doesn't matter, as it is closed automatically.
-- with <p> it is a problem
-- Cannot use mw.html here as it always closes tags
return "<" .. tag_name .. " class=\"senseid\" id=\"" .. language_anchor(lang, id) .. "\" data-lang=\"" .. lang:getCode() .. "\" data-id=\"" .. encode_entities(id) .. "\">"
end
function export.etymid(lang, id)
-- Use a <ul> tag to ensure spacing doesn't get messed up.
local el = mw.html.create("ul")
:addClass("etymid")
:attr("id", language_anchor(lang, id))
:attr("data-lang", lang:getCode())
:attr("data-id", id)
return tostring(el)
end
function export.etymonid(lang, id, opts)
opts = opts or {}
-- Use a <ul> tag to ensure spacing doesn't get messed up.
local el = mw.html.create("ul")
:addClass("etymonid")
:attr("data-lang", lang:getCode())
if id then
el:attr("id", language_anchor(lang, id))
el:attr("data-id", id)
end
if opts.no_tree then
el:attr("data-no-tree", "1")
end
if opts.title then
el:attr("data-title", opts.title)
end
if opts.empty_tree then
el:attr("data-empty-tree", "1")
end
if opts.ety_tree_json then
el:attr("data-ety-tree-json", opts.ety_tree_json)
end
return tostring(el)
end
return export
2o0jqln8y0bxvpfhe66eogs2k88qkap
Modul:etymon
828
57903
342816
264877
2026-05-16T13:37:50Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/90788814|90788814]]), terjemahan belum selesai
342816
Scribunto
text/plain
--[=[
This module implements the {{etymon}} template for structured etymology data on Wiktionary.
It enables the creation of etymology trees and text by parsing etymon chains,
scraping linked pages for their own {{etymon}} data, and recursively building a tree
of derivational relationships.
Authors:
- Original implementation: [[User:Ioaxxere]]
- Full refactor (September 2025): [[User:Fenakhay]] ([[Special:Diff/86717746]])
Modules:
- [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping
- [[Module:etymon/data]]: keyword definitions, configuration, and status constants
- [[Module:etymon/tree]]: etymology tree rendering
- [[Module:etymon/text]]: etymology text generation
- [[Module:etymon/categories]]: category generation logic
]=]
local export = {}
local etymon_data_module = "Module:etymon/data"
local etymon_text_module = "Module:etymon/text"
local etymon_tree_module = "Module:etymon/tree"
local etymon_categories_module = "Module:etymon/categories"
local etymon_descendants_module = "Module:etymon/descendants"
local __state = {
cached_etymon_args = {},
cached_etymon_pages = {},
cached_descendants_checks = {},
senseid_parent_etymon = {},
available_etymon_ids = {},
single_etymons = {},
entry_title = nil,
entry_lang_code = nil,
current_page_has_inline_etymology = false,
current_page_has_redundant_etymology = false,
used_idless_etymon = false,
toplevel_has_inline_etymology = false,
toplevel_redundant_etymology = false,
toplevel_idless_etymon = false,
has_mismatched_id = false,
linked_page_multiple_etymons_idless = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
warnings = {},
}
local loader = require("Module:module loader")
local M = loader.init({
require = {
data = etymon_data_module,
tree = etymon_tree_module,
text = etymon_text_module,
categories = etymon_categories_module,
descendants = etymon_descendants_module,
anchors = "Module:anchors",
etydate = "Module:etydate",
etymology = "Module:etymology",
families = "Module:families",
languages = "Module:languages",
languages_errorgetby = "Module:languages/errorGetBy",
links = "Module:links",
pages = "Module:pages",
parameters = "Module:parameters",
string_utilities = "Module:string utilities",
template_parser = "Module:template parser",
utilities = "Module:utilities",
debug = "Module:debug",
en_utilities = "Module:en-utilities",
parse_utilities = "Module:parse utilities",
references = "Module:references",
track = "Module:debug/track",
template_styles = "Module:TemplateStyles",
script_utilities = "Module:script utilities",
JSON = "Module:JSON",
yesno = "Module:yesno",
},
loadData = {
headword_data = "Module:headword/data",
parameters_data = "Module:parameters/data",
text_allowed = "Module:etymon/data/text_allowed",
},
})
local Util = {}
function Util.format_error(message, preview_only)
if preview_only and not M.pages.is_preview() then
return nil
end
return '<span class="error">' .. message .. '</span>'
end
function Util.add_warning(message, preview_only)
local formatted = Util.format_error(message, preview_only)
if formatted then
table.insert(__state.warnings, formatted)
end
end
function Util.is_text_param_allowed_for_lang(lang)
if not lang or type(lang) ~= "table" then
return false
end
local types = lang.getTypes and lang:getTypes()
if types and types.family then
local code = lang.getCode and lang:getCode()
return code and M.text_allowed.families[code] == true
end
local full_code = lang.getFullCode and lang:getFullCode()
if full_code and M.text_allowed.langs[full_code] then
return true
end
if lang.inFamily then
for family_code in pairs(M.text_allowed.families) do
if lang:inFamily(family_code) then
return true
end
end
end
return false
end
function Util.get_lang(code, no_error)
if no_error then
return M.languages.getByCode(code, nil, true)
end
return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true)
end
function Util.get_family(code)
return M.families.getByCode(code)
end
function Util.get_lang_exception(lang)
-- Families have no language-specific exceptions
if lang.getTypes and lang:getTypes().family then
return nil
end
local code = lang:getCode()
local lang_exceptions = M.data.config.lang_exceptions
if lang_exceptions[code] then
return lang_exceptions[code]
end
for norm_code, exc in pairs(lang_exceptions) do
if exc.normalize_to and code == exc.normalize_to then
return exc
end
if exc.normalize_from_families then
local should_normalize = false
for _, family in ipairs(exc.normalize_from_families) do
if lang:inFamily(family) then
should_normalize = true
break
end
end
if should_normalize and exc.normalize_exclude_families then
for _, family in ipairs(exc.normalize_exclude_families) do
if lang:inFamily(family) then
should_normalize = false
break
end
end
end
if should_normalize then
local ret = {}
for k, v in pairs(exc) do
ret[k] = v
end
ret.suppress_tr = nil
return ret
end
end
end
return nil
end
function Util.get_norm_lang(lang)
local exc = Util.get_lang_exception(lang)
if exc and exc.normalize_to then
return M.languages.getByCode(exc.normalize_to)
end
return lang
end
-- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>)
-- This is needed because Module:parse utilities expects boolean modifiers to have explicit values
function Util.add_boolean_defaults(str, param_mods)
local result = str
for name, spec in pairs(param_mods) do
if spec.type == "boolean" then
-- Replace <name> with <name:1> (but not <name:...> which already has a value)
result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>")
end
end
return result
end
-- Centralized term formatting: handles suppress_term, unknown_term, and regular terms
function Util.format_term(term, is_toplevel, opts)
opts = opts or {}
-- suppress_term (-) returns nil
if term.suppress_term then
return nil
end
local lang = term.lang
local exc = Util.get_lang_exception(lang)
if is_toplevel then
local display_text = term.alt or term.title or ""
local sc = term.sc or lang:findBestScript(display_text)
local bold_text = tostring(mw.html.create("strong")
:addClass("selflink")
:wikitext(display_text))
return M.script_utilities.tag_text(bold_text, lang, sc, "term")
end
local link_params = { lang = lang }
link_params.term = not term.unknown_term and term.title or nil
link_params.alt = term.alt
link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil
if not (exc and exc.suppress_tr) then
link_params.tr = term.tr
link_params.ts = term.ts
else
link_params.suppress_tr = true
end
link_params.lit = (opts.lit ~= "suppress") and term.lit or nil
if opts.gloss ~= "suppress" then
link_params.gloss = term.t
end
if term.g and term.g ~= "" then
local genders = M.string_utilities.split(term.g, ",")
for i = 1, #genders do
genders[i] = M.string_utilities.trim(genders[i])
end
link_params.genders = genders
end
if opts.pos ~= "suppress" then
link_params.pos = term.pos
link_params.ng = term.ng
end
if exc and exc.suppress_tr then
link_params.lit = nil
end
local show_qualifiers
if opts.tree_ql ~= "suppress" then
if term.q then
link_params.q = term.q
end
if term.qq then
link_params.qq = term.qq
end
if term.l then
link_params.l = term.l
end
if term.ll then
link_params.ll = term.ll
end
show_qualifiers = term.q or term.qq or term.l or term.ll
end
return M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
end
local __is_content_page_cached
function Util.is_content_page()
if __is_content_page_cached == nil then
__is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle())
end
return __is_content_page_cached
end
local __page_data_cached
function Util.get_page_data()
if not __page_data_cached then
__page_data_cached = M.headword_data.page
end
return __page_data_cached
end
-- Extract base keyword from param (without modifiers)
local function get_keyword_base(param)
if type(param) ~= "string" then return nil end
local base = param:match("^:?([^<]+)") or param:gsub("^:", "")
return base
end
local function is_keyword(param, allow_colon_less)
if type(param) ~= "string" then return false end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
if allow_colon_less then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
return false
end
local function get_keyword(param, allow_colon_less)
if type(param) ~= "string" then return nil end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
return get_keyword_base(param)
end
if allow_colon_less then
local base = get_keyword_base(param)
if keywords[base] then
return base
end
end
return nil
end
local function normalize_keyword(keyword)
if keyword:sub(1, 1) == ":" then
return keyword
end
return ":" .. keyword
end
-- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries
local function get_canonical_keyword(keyword)
if not keyword then return keyword end
return M.data.keyword_canonical[keyword] or keyword
end
-- Build text/phrase for nominalization with <g:code> (uses data module for codes only).
local function get_nominalization_label_for_g(code)
if not code or code == "" then return nil end
local codes = M.data.nominalization_g_codes
local adj = codes[code]
if not adj and #code == 2 then
local gender_adj = codes[code:sub(1, 1)]
local number_adj = codes[code:sub(2, 2)]
if gender_adj and number_adj then
adj = gender_adj .. " " .. number_adj
end
end
if not adj then return nil end
local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of"
local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false)
return { text = text, phrase = phrase }
end
local EtymonParser = {}
-- Keyword modifier definitions
EtymonParser.keyword_param_mods = {
unc = { type = "boolean" },
ref = {},
text = { restrict = { keywords = { "from", "derived" } } },
lit = { restrict = { keywords = { "affix", "surf", "univerbation" } } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
}
-- Term modifier definitions
EtymonParser.etymon_param_mods = {
id = {},
t = {},
tr = {},
ts = {},
q = {},
qq = {},
l = {},
ll = {},
pos = {},
ng = {},
alt = {},
g = {},
ety = {},
lit = {},
unc = { type = "boolean" },
ref = {},
aftype = { restrict = { keywords = { "affix", "surf", "afeq" } } },
postype = {},
bor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
slbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
lbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
}
local function get_clean_param_mods(param_mods)
local clean = {}
for mod_name, mod_def in pairs(param_mods) do
clean[mod_name] = {}
for key, value in pairs(mod_def) do
if key ~= "restrict" then
clean[mod_name][key] = value
end
end
end
return clean
end
function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods)
for mod_name, mod_value in pairs(modifiers) do
-- Only check restrictions if the modifier has a non-false/nil value
if mod_value then
local mod_def = param_mods[mod_name]
if mod_def and mod_def.restrict and mod_def.restrict.keywords then
local allowed_keywords = mod_def.restrict.keywords
local is_allowed = false
for _, allowed_keyword in ipairs(allowed_keywords) do
if current_keyword == allowed_keyword then
is_allowed = true
break
end
end
if not is_allowed then
local keyword_list = {}
for _, kw in ipairs(allowed_keywords) do
table.insert(keyword_list, ":" .. kw)
end
local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ")
if #keyword_list > 2 then
-- Replace last comma with "or"
keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1")
end
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".")
end
end
end
end
end
-- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>")
function EtymonParser.parse_keyword_modifiers(param)
if type(param) ~= "string" then return nil, {} end
local base_keyword = get_keyword_base(param)
if not base_keyword then return nil, {} end
local canonical_keyword = get_canonical_keyword(base_keyword)
-- Check if there are any modifiers
if not param:find("<", 1, true) then
return canonical_keyword, {}
end
-- Parse modifiers using the same mechanism as etymon parsing
local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods)
local function generate_obj(ignored)
return {}
end
local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""),
{ param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj })
local modifiers = {
unc = parsed.unc or false,
ref = parsed.ref,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
function EtymonParser.parse_balanced_segments(str)
local segments = {}
local current = ""
local depth = 0
local i = 1
while i <= #str do
local char = str:sub(i, i)
if char == "<" then
if depth == 0 and current ~= "" then
table.insert(segments, current)
current = ""
end
depth = depth + 1
current = current .. char
elseif char == ">" then
current = current .. char
depth = depth - 1
if depth == 0 then
table.insert(segments, current)
current = ""
elseif depth < 0 then
error("Unbalanced brackets in etymon: unexpected '>'")
end
else
current = current .. char
end
i = i + 1
end
if depth ~= 0 then
error("Unbalanced brackets in etymon: missing '>'")
end
if current ~= "" then
table.insert(segments, current)
end
return segments
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local segments = EtymonParser.parse_balanced_segments(ety_string)
if #segments == 0 then
error("Empty inline etymology")
end
local keyword = M.string_utilities.trim(segments[1])
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword(get_canonical_keyword(keyword)) }
for i = 2, #segments do
local segment = segments[i]
if segment:sub(1, 1) == "<" and segment:sub(-1) == ">" then
local inner = segment:sub(2, -2)
if inner ~= "" then
table.insert(args, inner)
end
elseif is_keyword(segment, true) then
-- Handle keywords that appear between bracketed segments
table.insert(args, normalize_keyword(get_canonical_keyword(get_keyword(segment, true))))
end
end
return args
end
function EtymonParser.parse_etymon(param, context_lang)
if is_keyword(param) then
return nil
end
if type(param) ~= "string" then
return nil
end
local lang, rest
local is_family = false
local before_bracket = param:match("^([^<]*)") or param
local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$")
if lang_code then
local potential_lang = Util.get_lang(lang_code, true)
if potential_lang then
lang = potential_lang
rest = param:sub(#lang_code + 2)
else
local potential_family = Util.get_family(lang_code)
if potential_family then
lang = potential_family
rest = param:sub(#lang_code + 2)
is_family = true
else
lang = context_lang
rest = param
end
end
else
lang = context_lang
rest = param
end
if rest == "" then
M.track("etymon/term/empty")
elseif rest == "?" then
M.track("etymon/term/question-mark")
elseif rest == "-" then
M.track("etymon/term/hyphen")
end
if rest == "" then
return {
lang = lang,
term = nil,
unknown_term = true,
is_family = is_family,
}
end
if rest == "-" then
return {
lang = lang,
term = nil,
suppress_term = true,
is_family = is_family,
}
end
if not rest:find("<", 1, true) then
return {
lang = lang,
term = M.string_utilities.trim(rest),
is_family = is_family,
}
end
local term_text = rest:match("^([^<]*)") or ""
local is_unknown = (term_text == "")
local is_suppress = (term_text == "-")
local function generate_obj(ignored_term)
return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) }
end
local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods)
local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults,
{ param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj })
if parsed_obj.id and parsed_obj.id:match("^!") then
parsed_obj.id = parsed_obj.id:sub(2)
parsed_obj.override = true
end
parsed_obj.lang = lang
parsed_obj.is_family = is_family
if is_unknown then
parsed_obj.unknown_term = true
elseif is_suppress then
parsed_obj.suppress_term = true
end
return parsed_obj
end
function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code)
-- id is now optional, so only validate if provided
if id then
if mw.ustring.len(id) < 2 then
error("The `id` parameter must have at least two characters.")
end
if id == title or id == Util.get_page_data().pagename then
error("The `id` parameter must not be the same as the page title.")
end
end
local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true }
if pos and not valid_pos[pos] then
error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".")
end
local current_keyword = "from"
local etymons_in_group = {}
local keywords = M.data.keywords
local function checkGroup()
if keywords[current_keyword] and keywords[current_keyword].is_group and current_keyword ~= "affix" and current_keyword ~= "surf" and current_keyword ~= "afeq" and current_keyword ~= "univerbation" and #etymons_in_group <= 1 then
error("Detected `:" .. current_keyword .. "` group with fewer than two etymons.")
end
etymons_in_group = {}
end
local start_index = starts_with_lang_code and 2 or 1
for i = start_index, #args do
local param = args[i]
if type(param) ~= "string" then
elseif param:sub(1, 1) == ":" and not is_keyword(param) then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkGroup()
current_keyword = get_canonical_keyword(get_keyword(param))
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(etymons_in_group, param)
local param_lang = etymon_data.lang
if etymon_data.is_family and current_keyword == "inherited" then
error("`:inh` does not support family codes; use a specific language.")
end
if etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
if current_keyword == "from" and param_lang:getFullCode() ~= lang:getFullCode() then
error("`:from` is for same-language derivation, but language does not match. " ..
"Expected '" .. lang:getFullCode() .. "', got '" .. param_lang:getFullCode() .. "'.")
elseif current_keyword == "inherited" then
M.etymology.check_ancestor(lang, param_lang)
end
-- Check modifier restrictions
EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods)
-- postype must be "root" or "word"
local VALID_POSTYPES = { root = true, word = true }
if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then
error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".")
end
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true)
end
else
table.insert(etymons_in_group, param)
end
end
end
checkGroup()
end
local DataRetriever = {}
-- Given an etymon data, scrape its page and cache the result in the global state object.
function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel)
local content = etymon_title:getContent()
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.REDLINK
return
end
-- Check if the linked page is a redirect. If it is, the template parsing
-- code below will be effectively skipped, and `scrape_page` will be called
-- again on the redirect target (see the bottom of this function)
local lang_section_for_descendants = nil
local redirect_target = etymon_title.redirect_target
if not redirect_target then
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
return
end
lang_section_for_descendants = content
end
local etymon_lang_code = etymon_lang:getFullCode()
local lang_page_key = etymon_lang_code .. ":" .. etymon_page
local found_templates_for_lang = {}
local found_ids = {}
local get_node_class = M.template_parser.class_else_type
-- Look for all {{etymon}} templates within the page content using the template parser
-- This way the same page is never parsed more than once
-- Build a map from senseids to their parent etymonids.
local active_etymon_args = nil
for node in M.template_parser.parse(content):iterate_nodes() do
local node_class = get_node_class(node)
if node_class == "heading" then
-- A new L2 or etymology section acts as a barrier: an {{etymon}} usage
-- used previously cannot be the parent of any subsequent senseids.
-- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above.
if node:get_name():find("^Etymology") then
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if template_name == "etymon" then
local template_args = node:get_arguments()
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
table.insert(found_templates_for_lang, template_args)
if template_args.id then
local etymon_key = lang_page_key .. ":" .. template_args.id
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, template_args.id)
active_etymon_args = template_args
else
-- Store idless etymon with default key
local etymon_key = lang_page_key .. ":*"
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, "*")
active_etymon_args = template_args
end
end
elseif active_etymon_args and template_name == "senseid" then
local template_args = node:get_arguments()
-- This should always be true for proper usages of {{senseid}}.
if template_args[1] == etymon_lang_code and template_args[2] then
local sense_id_key = lang_page_key .. ":" .. template_args[2]
__state.senseid_parent_etymon[sense_id_key] = active_etymon_args
__state.cached_etymon_pages[sense_id_key] = tostring(etymon_page)
end
end
end
end
if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then
M.descendants.cache_page_checks({
lang_section = lang_section_for_descendants,
etymon_lang_code = etymon_lang_code,
found_templates_for_lang = found_templates_for_lang,
entry_title = __state.entry_title,
entry_lang_code = __state.entry_lang_code,
entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil,
cached_descendants_checks = __state.cached_descendants_checks,
lang_page_key = lang_page_key,
redirected_from = redirected_from,
})
end
local id_data_list = {}
for _, args in ipairs(found_templates_for_lang) do
local id = args.id or "*"
table.insert(id_data_list, { id = id, pos = args.pos })
end
__state.available_etymon_ids[lang_page_key] = id_data_list
if #found_templates_for_lang == 1 then
__state.single_etymons[lang_page_key] = found_templates_for_lang[1]
end
if redirected_from and __state.available_etymon_ids[lang_page_key] then
__state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {}
for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do
table.insert(__state.available_etymon_ids[redirected_from], id_data)
end
end
if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then
-- All done!
return
elseif redirect_target and not redirected_from then
-- Try scraping the redirect.
etymon_page = redirect_target.prefixedText
DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel)
__state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id]
else
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
end
end
-- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name.
function DataRetriever.get_etymon_args(etymon_data, is_toplevel)
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
if etymon_data.id then
local key = base_key .. ":" .. etymon_data.id
local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key]
if cached_args == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel)
end
cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh
-- Get etymon_id from parent if this was resolved via senseid
local parent_etymon = __state.senseid_parent_etymon[key]
local resolved_etymon_id = parent_etymon and parent_etymon.id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
end
end
return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check
else
__state.used_idless_etymon = true
if is_toplevel then
__state.toplevel_idless_etymon = true
end
if __state.available_etymon_ids[base_key] == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel)
end
local ids = __state.available_etymon_ids[base_key] or {}
local count = #ids
-- Try to filter by postype if available and we have multiple candidates
if count > 1 and etymon_data.postype then
local matching_ids = {}
for _, id_data in ipairs(ids) do
if id_data.pos == etymon_data.postype then
table.insert(matching_ids, id_data)
end
end
if #matching_ids == 1 then
local matched_id = matching_ids[1].id
local matched_key = base_key .. ":" .. matched_id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id = matched_id },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id = matched_id },
})
end
end
return __state.cached_etymon_args[matched_key], __state.cached_etymon_pages[matched_key], nil, descendants_check
end
end
if count == 1 then
local only_id_data = ids[1]
local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*"
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id_data = only_id_data },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id_data = only_id_data },
})
end
end
return __state.single_etymons[base_key], __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
local page_link = M.links.full_link({
term = page,
lang = norm_lang,
no_generate_forms = true,
}, "term")
local function format_id_hint(id_data, idx)
local id = type(id_data) == "table" and id_data.id or id_data
local pos = type(id_data) == "table" and id_data.pos
if id and id ~= "" and id ~= "*" then
return '"' .. id .. '"'
end
if pos and pos ~= "" then
return "unnamed (|pos=" .. pos .. "|)"
end
return "etymon #" .. idx .. " (no |id= on page)"
end
local id_list = {}
local all_idless = true
local target_has_idless = false
for i, id_data in ipairs(ids) do
local id = type(id_data) == "table" and id_data.id or id_data
if id and id ~= "" and id ~= "*" then
all_idless = false
else
target_has_idless = true
end
table.insert(id_list, format_id_hint(id_data, i))
end
if is_toplevel and target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local any_pos = false
for _, id_data in ipairs(ids) do
local pos = type(id_data) == "table" and id_data.pos
if pos and pos ~= "" then
any_pos = true
break
end
end
local suggestion_text
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. count .. " {{etymon}} templates for "
.. norm_lang:getCanonicalName() .. ")."
if all_idless then
if any_pos then
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. mw.text.listToText(id_list) .. "."
else
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)."
end
else
suggestion_text = " Specify which one with `<id:identifier>` after the term. Options: " .. mw.text.listToText(id_list) .. "."
end
Util.add_warning(lead .. suggestion_text, true)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local TreeBuilder = {}
local function parse_etymon_references(refs_text)
if not refs_text or refs_text == "" then
return ""
end
return M.references.parse_references(refs_text)
end
local function parse_tree_references(node)
if node.ref then
node.parsed_ref = parse_etymon_references(node.ref)
end
if node.children then
for _, container in ipairs(node.children) do
if container.terms then
for _, term in ipairs(container.terms) do
parse_tree_references(term)
end
end
end
end
end
-- Build a unique key for deduplication in the seen table
function TreeBuilder.build_key(lang, title, args)
local norm_lang_code = Util.get_norm_lang(lang):getFullCode()
local is_table = type(args) == "table"
local id = (is_table and args.id) or ""
if title then
return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id
end
if is_table and args.status == M.data.STATUS.INLINE then
local content_parts = {}
for i = 1, #args do
content_parts[i] = tostring(args[i])
end
return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0")
end
return norm_lang_code .. ":*:" .. id
end
function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion)
seen = seen or {}
depth = depth or 0
local is_toplevel = (depth == 0)
if depth > __state.max_depth_reached then
__state.max_depth_reached = depth
end
__state.total_nodes = __state.total_nodes + 1
local lang_code = lang:getCode()
__state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1
local current_id = (type(args) == "table" and args.id) or ""
local key = TreeBuilder.build_key(lang, title, args)
local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK }
if type(args) ~= "table" or seen[key] then
node.status = args or M.data.STATUS.MISSING
-- Mark as duplicate if we've seen this node before
if seen[key] then
node.is_duplicate = true
node.duplicate_key = key
local original_node = seen[key]
if type(original_node) == "table" and original_node.children and #original_node.children > 0 then
node.original_has_children = true
end
end
return node
end
node.status = args.status or M.data.STATUS.OK
seen[key] = node
-- If stop_recursion is set, skip parsing children but check for visible children
if stop_recursion then
local keywords = M.data.keywords
local has_visible_children = false
for i = 2, #args do
local param = args[i]
if type(param) == "string" then
local keyword_base = get_keyword_base(param)
if keyword_base and keywords[keyword_base] then
-- It's a keyword, check if visible in tree (invisible "all" or "tree" = hidden in tree)
local keyword_info = keywords[keyword_base]
local inv = keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
elseif param:sub(1, 1) ~= ":" then
-- It's a term (not a keyword), so there are visible children
has_visible_children = true
break
end
end
end
node.has_visible_children = has_visible_children
return node
end
-- Parse args into keyword containers
local current_keyword = "from"
local current_keyword_modifiers = {}
local current_container = nil
-- Helper to track keyword usage at top level
local function track_keyword_usage(keyword, target_lang, source_lang)
if not is_toplevel then return end
if not __state.toplevel_keyword_stats[keyword] then
__state.toplevel_keyword_stats[keyword] = {
count = 0,
target_langs = {},
source_langs = {},
}
end
local keyword_data = __state.toplevel_keyword_stats[keyword]
keyword_data.count = keyword_data.count + 1
local target_code = target_lang:getCode()
keyword_data.target_langs[target_code] = (keyword_data.target_langs[target_code] or 0) + 1
if source_lang then
local source_code = source_lang:getCode()
keyword_data.source_langs[source_code] = (keyword_data.source_langs[source_code] or 0) + 1
end
end
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
current_container = {
keyword = current_keyword,
keyword_info = M.data.keywords[current_keyword],
keyword_modifiers = current_keyword_modifiers,
terms = {},
}
table.insert(node.children, current_container)
-- Override keyword text/phrase for nominalization with <g:code>
if current_keyword_modifiers.g and current_keyword == "nominalization" then
local labels = get_nominalization_label_for_g(current_keyword_modifiers.g)
if not labels then
local codes = {}
for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end
table.sort(codes)
error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", "))
end
current_container.keyword_info = {}
for k, v in pairs(M.data.keywords[current_keyword]) do current_container.keyword_info[k] = v end
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
end
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
current_keyword = keyword
current_keyword_modifiers = modifiers
current_container = nil -- Force new container for new keyword
elseif type(param) == "string" and param:sub(1, 1) == ":" then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif type(param) == "string" then
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
-- Track keyword usage at top level
track_keyword_usage(current_keyword, lang, etymon_data.lang)
local term_node = {}
-- Handle suppress_term (-) and unknown_term (empty) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
ensure_container()
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1)
else
term_node = {
lang = etymon_data.lang,
children = {},
status = M.data.STATUS.OK,
}
end
term_node.suppress_term = etymon_data.suppress_term
term_node.unknown_term = etymon_data.unknown_term
term_node.is_family = etymon_data.is_family
term_node.is_uncertain = etymon_data.unc
term_node.ref = etymon_data.ref
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.pos = etymon_data.pos
term_node.ng = etymon_data.ng
term_node.lit = etymon_data.lit
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
else
-- Regular term: fetch arguments from page
local etymon_args, page_of, resolved_etymon_id, descendants_check = DataRetriever.get_etymon_args(etymon_data, is_toplevel)
if etymon_data.id and etymon_args == M.data.STATUS.MISSING and not etymon_data.ety then
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
local available_ids = __state.available_etymon_ids[base_key] or {}
if #available_ids > 0 then
__state.has_mismatched_id = true
end
end
-- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing
if etymon_data.ety then
if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then
__state.current_page_has_inline_etymology = true
if is_toplevel then
__state.toplevel_has_inline_etymology = true
end
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
-- Track inline ety keywords too
local inline_keyword = get_keyword(inline_args[2], true)
if inline_keyword and #inline_args >= 3 then
local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang)
if inline_etymon then
track_keyword_usage(inline_keyword, etymon_data.lang, inline_etymon.lang)
end
end
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
etymon_args = inline_args
term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent
else
-- Scraped arguments exist, <ety> is redundant and ignored
__state.current_page_has_redundant_etymology = true
if is_toplevel then
__state.toplevel_redundant_etymology = true
end
end
end
-- Ensure container exists before checking keyword info
ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = current_container.keyword_info
local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories))
term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion)
term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() ..
":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang)
term_node.id = etymon_data.id
term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.pos = etymon_data.pos
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.ng = etymon_data.ng
term_node.ref = etymon_data.ref
term_node.is_uncertain = etymon_data.unc
term_node.override = etymon_data.override
term_node.page_of = page_of
term_node.aftype = etymon_data.aftype
term_node.postype = etymon_data.postype
term_node.bor = etymon_data.bor
term_node.lbor = etymon_data.lbor
term_node.slbor = etymon_data.slbor
term_node.lit = etymon_data.lit
term_node.is_family = etymon_data.is_family
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
term_node.missing_descendants_header, term_node.missing_descendants_entry =
M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check)
end
table.insert(current_container.terms, term_node)
end
end
end
return node
end
-- Convert etymology tree to JSON-serializable table
local function tree_to_json(node)
local obj = {
term = node.title,
lang = node.lang:getCode(),
lang_name = node.lang:getCanonicalName(),
id = (node.id and node.id ~= "") and node.id or nil,
status = node.status,
is_uncertain = node.is_uncertain or nil,
is_duplicate = node.is_duplicate or nil,
gloss = node.t,
transliteration = node.tr,
transcription = node.ts,
alt = node.alt,
g = node.g,
pos = node.pos,
ng = node.ng,
children = {},
}
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if keyword_info then
local container_obj = {
keyword = container.keyword,
keyword_label = keyword_info.text,
keyword_abbrev = keyword_info.abbrev,
is_group = keyword_info.is_group or nil,
is_invisible = keyword_info.invisible or nil,
is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil,
terms = {},
}
for _, term in ipairs(container.terms or {}) do
table.insert(container_obj.terms, tree_to_json(term))
end
table.insert(obj.children, container_obj)
end
end
return obj
end
local function track_ranges(base_key, value, ranges, lang_code)
M.track("etymon/" .. base_key .. "/" .. value)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. value)
end
for _, range in ipairs(ranges) do
local matches = false
if range.min and range.max then
matches = value >= range.min and value <= range.max
elseif range.min then
matches = value >= range.min
elseif range.max then
matches = value <= range.max
elseif range.exact then
matches = value == range.exact
end
if matches then
M.track("etymon/" .. base_key .. "/" .. range.label)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. range.label)
end
break
end
end
end
local function track_title_pagename_mismatch_after_strip(lang)
local lang_code = lang:getCode()
M.track("etymon/title/pagename-mismatch-after-strip-diacritics")
M.track("etymon/lang/" .. lang_code .. "/title/pagename-mismatch-after-strip-diacritics")
end
-- Build and return the etymology data tree for a given term.
function export.get_tree(lang, title, args, options)
options = options or {}
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
if options.validate then
EtymonParser.validate(lang, args, options.id, title, options.pos, false)
end
local lang_code = lang:getCode()
local start_index = (args[1] == lang_code) and 2 or 1
local tree_args = { [1] = lang_code, id = options.id or args.id }
for i = start_index, #args do
table.insert(tree_args, args[i])
end
__state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args
local ety_data_tree = TreeBuilder.build(lang, title, tree_args)
parse_tree_references(ety_data_tree)
if options.json then
return M.JSON.toJSON(tree_to_json(ety_data_tree))
end
return ety_data_tree
end
-- Given a language code, page name and optionally the id= parameter,
-- render the tree and only the etymology tree for the relevant page.
-- Fetches and parses the corresponding {{etymon}} from the requested page,
-- and any further pages needed to render the tree.
-- Parameters can be passed either through the #invoke or as
-- template parameters *through* an #invoke.
function export.render_tree_for_etymon_on_page(frame)
local frame_args = frame.args
local parent_args = frame:getParent().args
local langcode = frame_args[1] or parent_args[1]
local pagename = frame_args[2] or parent_args[2]
local id = frame_args["id"] or parent_args["id"]
local display_title = frame_args["title"] or parent_args["title"]
local parsed_title = mw.title.new(pagename, 0)
local title
if parsed_title.namespace == 0 then
title = M.pages.safe_page_name(parsed_title)
elseif parsed_title.namespace == 118 then
title = "*" .. M.pages.safe_page_name(parsed_title)
else
error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace)
end
local lang = Util.get_lang(langcode)
-- Construct etymon_data for DataRetriever.get_args.
local etymon_data = {
lang = lang,
term = title,
id = id
}
local args, pagename = DataRetriever.get_etymon_args(etymon_data, true)
if args == M.data.STATUS.MISSING then
error("The etymon template was not found (language " ..
langcode ..
", title '" ..
title ..
"'" ..
(id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.")
end
local tree_title = display_title or title
if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then
track_title_pagename_mismatch_after_strip(lang)
end
local ety_data_tree = export.get_tree(lang, tree_title, args, {
validate = true,
id = id,
})
local output = {}
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
return table.concat(output)
end
function export.main(frame)
local parent_args = frame:getParent().args
local args = M.parameters.process(parent_args, M.parameters_data.etymon)
local lang = args[1]
local etymon_args = args[2]
local id = args.id
local title = args.title
local text = args.text
local tree = args.tree
local etydate = args.etydate
local rfe = args.rfe
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == "Reconstruction" then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == "Reconstruction" then
entry_pagename = "*" .. entry_pagename
end
if lang:stripDiacritics(M.links.remove_links(title)) ~= lang:stripDiacritics(M.links.remove_links(entry_pagename)) then
track_title_pagename_mismatch_after_strip(lang)
end
local current_L2 = M.pages.get_current_L2()
if current_L2 then
local norm_lang = Util.get_norm_lang(lang)
local norm_name = norm_lang:getCanonicalName()
if current_L2 ~= norm_name then
local lang_desc = lang:getCode() .. " (" .. lang:getCanonicalName() .. ")"
if norm_lang:getCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", normalized to " .. norm_lang:getCode() .. " (" .. norm_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
end
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
})
if args.json then
return ety_data_tree
end
local output = {}
local text_allowlist_mode = M.text_allowed.default_mode or "off"
if text and text_allowlist_mode ~= "off" and not Util.is_text_param_allowed_for_lang(lang) then
local msg = "Etymology texts (parameter <code>text=</code>) are not allowed for " .. lang:getFullName() ..
"; see [[Template:etymon#Text allowlist|Template:etymon § Text allowlist]] for the list of languages that may use the <code>text=</code> parameter."
if text_allowlist_mode == "error" then
error(msg)
else
Util.add_warning(msg, true)
end
end
local lang_exc = Util.get_lang_exception(lang)
if lang_exc and lang_exc.disallow then
local disallow = lang_exc.disallow
local error_text = " for " .. lang:getFullName()
if disallow.ref then
error_text = error_text .. "; see " .. disallow.ref
else
error_text = error_text .. "."
end
if tree and disallow.tree then
error("Etymology trees are not allowed" .. error_text)
end
if text and disallow.text then
error("Etymology texts are not allowed" .. error_text)
end
end
if etydate then
local etydate_param_mods = {
ref = { list = true, type = "references", allow_holes = true },
refn = { list = true, allow_holes = true },
nocap = { type = "boolean" },
}
local function generate_etydate_obj(etydate_text)
local etydate_specs = {}
for spec in etydate_text:gmatch("[^,]+") do
table.insert(etydate_specs, mw.text.trim(spec))
end
return { [1] = etydate_specs }
end
local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj })
local etydate_args = {
[1] = parsed_etydate[1],
nocap = parsed_etydate.nocap or false,
}
ety_data_tree.etydate = M.etydate.format_etydate(etydate_args, { omit_refs = true })
if parsed_etydate.ref and #parsed_etydate.ref > 0 then
ety_data_tree.etydate_refs = parsed_etydate.ref
end
end
if tree then
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
end
-- Check if there are any visible children in tree (invisible "all" or "tree" = hidden in tree)
local has_visible_children = false
for _, child in ipairs(ety_data_tree.children or {}) do
local child_keyword_info = child.keyword_info
local inv = child_keyword_info and child_keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
end
local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree
local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree))
local anchor = M.anchors.etymonid(lang, id, {
no_tree = args.notree,
title = title,
empty_tree = (not has_visible_children) or tree_disallowed,
ety_tree_json = ety_tree_json,
})
table.insert(output, anchor)
if text then
local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink
if text == "++" then
max_depth, stop_at_blue_link = false, false
elseif text == "+" then
max_depth, stop_at_blue_link = 1, false
elseif text == "*" then
max_depth, stop_at_blue_link = false, true
elseif text:match("^:[^*]+%*$") then
-- Stop at a specific language OR first bluelink after it, e.g., ":ota*"
-- If the target language is a redlink, continue to the first bluelink
local lang_code = text:match("^:([^*]+)%*$")
if lang_code and lang_code ~= "" then
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang_or_bluelink = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
elseif text:sub(1, 1) == ":" then
-- Stop at a specific language, e.g., ":ar" stops at first Arabic term
local lang_code = text:sub(2)
if lang_code ~= "" then
-- Validate the language code
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
local num = tonumber(text)
if num and num >= 1 then
max_depth, stop_at_blue_link = num, false
else
error('Invalid text value "' ..
text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)')
end
end
table.insert(output, M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
}))
end
if rfe then
local rfe_param_mods = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
}
local function generate_rfe_obj(rfe_text)
-- Check if it's a boolean true value
if M.yesno(rfe_text, false) then
return { is_boolean = true }
else
return { text = rfe_text }
end
end
local rfe_with_defaults = Util.add_boolean_defaults(rfe, rfe_param_mods)
local parsed_rfe = M.parse_utilities.parse_inline_modifiers(rfe_with_defaults, {
param_mods = rfe_param_mods,
generate_obj = generate_rfe_obj
})
local rfe_args = {
[1] = lang:getCode(),
nocat = parsed_rfe.nocat,
sort = parsed_rfe.sort,
y = parsed_rfe.y,
m = parsed_rfe.m,
fragment = parsed_rfe.fragment,
section = parsed_rfe.section,
box = parsed_rfe.box,
noes = parsed_rfe.noes,
}
if not parsed_rfe.is_boolean then
rfe_args[2] = parsed_rfe.text
end
table.insert(output, frame:expandTemplate({
title = "rfe",
args = rfe_args
}))
end
if Util.is_content_page() and __state.max_depth_reached > 0 then
local lang_code = lang:getCode()
local depth_ranges = {
{ min = 50, label = "extremely-deep" },
{ min = 20, label = "20+" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ max = 2, label = "1-2" }
}
local node_ranges = {
{ min = 100, label = "extremely-large" },
{ min = 50, label = "50+" },
{ min = 20, max = 49, label = "20-49" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ max = 4, label = "1-4" }
}
local language_ranges = {
{ min = 10, label = "10+" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ exact = 2, label = "2" },
{ exact = 1, label = "1" }
}
track_ranges("depth", __state.max_depth_reached, depth_ranges, lang_code)
track_ranges("nodes", __state.total_nodes, node_ranges, lang_code)
local unique_languages = 0
for _ in pairs(__state.language_count) do
unique_languages = unique_languages + 1
end
track_ranges("unique-languages", unique_languages, language_ranges, lang_code)
if __state.total_nodes == __state.max_depth_reached + 1 then
track_ranges("linear-depth", __state.max_depth_reached, depth_ranges, lang_code)
end
end
local categories = {}
if Util.is_content_page() then
local should_suppress_categories = lang_exc and lang_exc.suppress_categories
if not should_suppress_categories and not args.nocat then
categories = M.categories.render({
data_tree = ety_data_tree,
page_lang = lang,
available_etymon_ids = __state.available_etymon_ids,
senseid_parent_etymon = __state.senseid_parent_etymon,
get_norm_lang_func = Util.get_norm_lang,
lang_exc = lang_exc,
})
end
local target_lang_code = lang:getCode()
for keyword, keyword_data in pairs(__state.toplevel_keyword_stats) do
-- Track keyword globally
M.track("etymon/keyword/" .. keyword)
-- Track keyword per target language
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code)
-- Track keyword per source language
for source_code, count in pairs(keyword_data.source_langs) do
M.track("etymon/keyword/" .. keyword .. "/source/" .. source_code)
-- Track keyword per target+source combination
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code .. "/source/" .. source_code)
end
end
if tree then
table.insert(categories, "Pages with etymology trees")
table.insert(categories, lang:getCanonicalName() .. " entries with etymology trees")
end
if text then table.insert(categories, lang:getCanonicalName() .. " entries with etymology texts") end
if args.exnihilo then table.insert(categories, lang:getCanonicalName() .. " terms coined ex nihilo") end
if __state.toplevel_has_inline_etymology then
table.insert(categories, "Pages with inline etymon for redlinks")
end
if __state.toplevel_redundant_etymology then
table.insert(categories, "Pages with redundant inline etymon")
end
if __state.toplevel_idless_etymon then
table.insert(categories, "Pages using etymon with no ID")
end
if __state.has_mismatched_id then
table.insert(categories, lang:getCanonicalName() .. " entries referencing etymons with mismatched IDs")
end
if __state.linked_page_multiple_etymons_idless then
table.insert(categories,
lang:getCanonicalName() .. " entries referencing pages with multiple etymons missing IDs")
end
end
if #categories > 0 then
table.insert(output, M.categories.format(categories, lang))
end
if __state.warnings then
for i, warning in ipairs(__state.warnings) do
table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n")
end
end
return table.concat(output)
end
return export
sd824iy93rvmo3oroit742p1i4oi4gt
342823
342816
2026-05-16T13:47:59Z
Hakimi97
2668
342823
Scribunto
text/plain
--[=[
This module implements the {{etymon}} template for structured etymology data on Wiktionary.
It enables the creation of etymology trees and text by parsing etymon chains,
scraping linked pages for their own {{etymon}} data, and recursively building a tree
of derivational relationships.
Authors:
- Original implementation: [[User:Ioaxxere]]
- Full refactor (September 2025): [[User:Fenakhay]] ([[Special:Diff/86717746]])
Modules:
- [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping
- [[Module:etymon/data]]: keyword definitions, configuration, and status constants
- [[Module:etymon/tree]]: etymology tree rendering
- [[Module:etymon/text]]: etymology text generation
- [[Module:etymon/categories]]: category generation logic
]=]
local export = {}
local etymon_data_module = "Module:etymon/data"
local etymon_text_module = "Module:etymon/text"
local etymon_tree_module = "Module:etymon/tree"
local etymon_categories_module = "Module:etymon/categories"
local etymon_descendants_module = "Module:etymon/descendants"
local __state = {
cached_etymon_args = {},
cached_etymon_pages = {},
cached_descendants_checks = {},
senseid_parent_etymon = {},
available_etymon_ids = {},
single_etymons = {},
entry_title = nil,
entry_lang_code = nil,
current_page_has_inline_etymology = false,
current_page_has_redundant_etymology = false,
used_idless_etymon = false,
toplevel_has_inline_etymology = false,
toplevel_redundant_etymology = false,
toplevel_idless_etymon = false,
has_mismatched_id = false,
linked_page_multiple_etymons_idless = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
warnings = {},
}
local loader = require("Module:module loader")
local M = loader.init({
require = {
data = etymon_data_module,
tree = etymon_tree_module,
text = etymon_text_module,
categories = etymon_categories_module,
descendants = etymon_descendants_module,
anchors = "Module:anchors",
etydate = "Module:etydate",
etymology = "Module:etymology",
families = "Module:families",
languages = "Module:languages",
languages_errorgetby = "Module:languages/errorGetBy",
links = "Module:links",
pages = "Module:pages",
parameters = "Module:parameters",
string_utilities = "Module:string utilities",
template_parser = "Module:template parser",
utilities = "Module:utilities",
debug = "Module:debug",
en_utilities = "Module:en-utilities",
parse_utilities = "Module:parse utilities",
references = "Module:references",
track = "Module:debug/track",
template_styles = "Module:TemplateStyles",
script_utilities = "Module:script utilities",
JSON = "Module:JSON",
yesno = "Module:yesno",
},
loadData = {
headword_data = "Module:headword/data",
parameters_data = "Module:parameters/data",
text_allowed = "Module:etymon/data/text_allowed",
},
})
local Util = {}
function Util.format_error(message, preview_only)
if preview_only and not M.pages.is_preview() then
return nil
end
return '<span class="error">' .. message .. '</span>'
end
function Util.add_warning(message, preview_only)
local formatted = Util.format_error(message, preview_only)
if formatted then
table.insert(__state.warnings, formatted)
end
end
function Util.is_text_param_allowed_for_lang(lang)
if not lang or type(lang) ~= "table" then
return false
end
local types = lang.getTypes and lang:getTypes()
if types and types.family then
local code = lang.getCode and lang:getCode()
return code and M.text_allowed.families[code] == true
end
local full_code = lang.getFullCode and lang:getFullCode()
if full_code and M.text_allowed.langs[full_code] then
return true
end
if lang.inFamily then
for family_code in pairs(M.text_allowed.families) do
if lang:inFamily(family_code) then
return true
end
end
end
return false
end
function Util.get_lang(code, no_error)
if no_error then
return M.languages.getByCode(code, nil, true)
end
return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true)
end
function Util.get_family(code)
return M.families.getByCode(code)
end
function Util.get_lang_exception(lang)
-- Families have no language-specific exceptions
if lang.getTypes and lang:getTypes().family then
return nil
end
local code = lang:getCode()
local lang_exceptions = M.data.config.lang_exceptions
if lang_exceptions[code] then
return lang_exceptions[code]
end
for norm_code, exc in pairs(lang_exceptions) do
if exc.normalize_to and code == exc.normalize_to then
return exc
end
if exc.normalize_from_families then
local should_normalize = false
for _, family in ipairs(exc.normalize_from_families) do
if lang:inFamily(family) then
should_normalize = true
break
end
end
if should_normalize and exc.normalize_exclude_families then
for _, family in ipairs(exc.normalize_exclude_families) do
if lang:inFamily(family) then
should_normalize = false
break
end
end
end
if should_normalize then
local ret = {}
for k, v in pairs(exc) do
ret[k] = v
end
ret.suppress_tr = nil
return ret
end
end
end
return nil
end
function Util.get_norm_lang(lang)
local exc = Util.get_lang_exception(lang)
if exc and exc.normalize_to then
return M.languages.getByCode(exc.normalize_to)
end
return lang
end
-- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>)
-- This is needed because Module:parse utilities expects boolean modifiers to have explicit values
function Util.add_boolean_defaults(str, param_mods)
local result = str
for name, spec in pairs(param_mods) do
if spec.type == "boolean" then
-- Replace <name> with <name:1> (but not <name:...> which already has a value)
result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>")
end
end
return result
end
-- Centralized term formatting: handles suppress_term, unknown_term, and regular terms
function Util.format_term(term, is_toplevel, opts)
opts = opts or {}
-- suppress_term (-) returns nil
if term.suppress_term then
return nil
end
local lang = term.lang
local exc = Util.get_lang_exception(lang)
if is_toplevel then
local display_text = term.alt or term.title or ""
local sc = term.sc or lang:findBestScript(display_text)
local bold_text = tostring(mw.html.create("strong")
:addClass("selflink")
:wikitext(display_text))
return M.script_utilities.tag_text(bold_text, lang, sc, "term")
end
local link_params = { lang = lang }
link_params.term = not term.unknown_term and term.title or nil
link_params.alt = term.alt
link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil
if not (exc and exc.suppress_tr) then
link_params.tr = term.tr
link_params.ts = term.ts
else
link_params.suppress_tr = true
end
link_params.lit = (opts.lit ~= "suppress") and term.lit or nil
if opts.gloss ~= "suppress" then
link_params.gloss = term.t
end
if term.g and term.g ~= "" then
local genders = M.string_utilities.split(term.g, ",")
for i = 1, #genders do
genders[i] = M.string_utilities.trim(genders[i])
end
link_params.genders = genders
end
if opts.pos ~= "suppress" then
link_params.pos = term.pos
link_params.ng = term.ng
end
if exc and exc.suppress_tr then
link_params.lit = nil
end
local show_qualifiers
if opts.tree_ql ~= "suppress" then
if term.q then
link_params.q = term.q
end
if term.qq then
link_params.qq = term.qq
end
if term.l then
link_params.l = term.l
end
if term.ll then
link_params.ll = term.ll
end
show_qualifiers = term.q or term.qq or term.l or term.ll
end
return M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
end
local __is_content_page_cached
function Util.is_content_page()
if __is_content_page_cached == nil then
__is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle())
end
return __is_content_page_cached
end
local __page_data_cached
function Util.get_page_data()
if not __page_data_cached then
__page_data_cached = M.headword_data.page
end
return __page_data_cached
end
-- Extract base keyword from param (without modifiers)
local function get_keyword_base(param)
if type(param) ~= "string" then return nil end
local base = param:match("^:?([^<]+)") or param:gsub("^:", "")
return base
end
local function is_keyword(param, allow_colon_less)
if type(param) ~= "string" then return false end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
if allow_colon_less then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
return false
end
local function get_keyword(param, allow_colon_less)
if type(param) ~= "string" then return nil end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
return get_keyword_base(param)
end
if allow_colon_less then
local base = get_keyword_base(param)
if keywords[base] then
return base
end
end
return nil
end
local function normalize_keyword(keyword)
if keyword:sub(1, 1) == ":" then
return keyword
end
return ":" .. keyword
end
-- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries
local function get_canonical_keyword(keyword)
if not keyword then return keyword end
return M.data.keyword_canonical[keyword] or keyword
end
-- Build text/phrase for nominalization with <g:code> (uses data module for codes only).
local function get_nominalization_label_for_g(code)
if not code or code == "" then return nil end
local codes = M.data.nominalization_g_codes
local adj = codes[code]
if not adj and #code == 2 then
local gender_adj = codes[code:sub(1, 1)]
local number_adj = codes[code:sub(2, 2)]
if gender_adj and number_adj then
adj = gender_adj .. " " .. number_adj
end
end
if not adj then return nil end
local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of"
local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false)
return { text = text, phrase = phrase }
end
local EtymonParser = {}
-- Keyword modifier definitions
EtymonParser.keyword_param_mods = {
unc = { type = "boolean" },
ref = {},
text = { restrict = { keywords = { "from", "derived" } } },
lit = { restrict = { keywords = { "affix", "surf", "univerbation" } } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
}
-- Term modifier definitions
EtymonParser.etymon_param_mods = {
id = {},
t = {},
tr = {},
ts = {},
q = {},
qq = {},
l = {},
ll = {},
pos = {},
ng = {},
alt = {},
g = {},
ety = {},
lit = {},
unc = { type = "boolean" },
ref = {},
aftype = { restrict = { keywords = { "affix", "surf", "afeq" } } },
postype = {},
bor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
slbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
lbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
}
local function get_clean_param_mods(param_mods)
local clean = {}
for mod_name, mod_def in pairs(param_mods) do
clean[mod_name] = {}
for key, value in pairs(mod_def) do
if key ~= "restrict" then
clean[mod_name][key] = value
end
end
end
return clean
end
function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods)
for mod_name, mod_value in pairs(modifiers) do
-- Only check restrictions if the modifier has a non-false/nil value
if mod_value then
local mod_def = param_mods[mod_name]
if mod_def and mod_def.restrict and mod_def.restrict.keywords then
local allowed_keywords = mod_def.restrict.keywords
local is_allowed = false
for _, allowed_keyword in ipairs(allowed_keywords) do
if current_keyword == allowed_keyword then
is_allowed = true
break
end
end
if not is_allowed then
local keyword_list = {}
for _, kw in ipairs(allowed_keywords) do
table.insert(keyword_list, ":" .. kw)
end
local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ")
if #keyword_list > 2 then
-- Replace last comma with "or"
keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1")
end
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".")
end
end
end
end
end
-- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>")
function EtymonParser.parse_keyword_modifiers(param)
if type(param) ~= "string" then return nil, {} end
local base_keyword = get_keyword_base(param)
if not base_keyword then return nil, {} end
local canonical_keyword = get_canonical_keyword(base_keyword)
-- Check if there are any modifiers
if not param:find("<", 1, true) then
return canonical_keyword, {}
end
-- Parse modifiers using the same mechanism as etymon parsing
local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods)
local function generate_obj(ignored)
return {}
end
local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""),
{ param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj })
local modifiers = {
unc = parsed.unc or false,
ref = parsed.ref,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
function EtymonParser.parse_balanced_segments(str)
local segments = {}
local current = ""
local depth = 0
local i = 1
while i <= #str do
local char = str:sub(i, i)
if char == "<" then
if depth == 0 and current ~= "" then
table.insert(segments, current)
current = ""
end
depth = depth + 1
current = current .. char
elseif char == ">" then
current = current .. char
depth = depth - 1
if depth == 0 then
table.insert(segments, current)
current = ""
elseif depth < 0 then
error("Unbalanced brackets in etymon: unexpected '>'")
end
else
current = current .. char
end
i = i + 1
end
if depth ~= 0 then
error("Unbalanced brackets in etymon: missing '>'")
end
if current ~= "" then
table.insert(segments, current)
end
return segments
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local segments = EtymonParser.parse_balanced_segments(ety_string)
if #segments == 0 then
error("Empty inline etymology")
end
local keyword = M.string_utilities.trim(segments[1])
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword(get_canonical_keyword(keyword)) }
for i = 2, #segments do
local segment = segments[i]
if segment:sub(1, 1) == "<" and segment:sub(-1) == ">" then
local inner = segment:sub(2, -2)
if inner ~= "" then
table.insert(args, inner)
end
elseif is_keyword(segment, true) then
-- Handle keywords that appear between bracketed segments
table.insert(args, normalize_keyword(get_canonical_keyword(get_keyword(segment, true))))
end
end
return args
end
function EtymonParser.parse_etymon(param, context_lang)
if is_keyword(param) then
return nil
end
if type(param) ~= "string" then
return nil
end
local lang, rest
local is_family = false
local before_bracket = param:match("^([^<]*)") or param
local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$")
if lang_code then
local potential_lang = Util.get_lang(lang_code, true)
if potential_lang then
lang = potential_lang
rest = param:sub(#lang_code + 2)
else
local potential_family = Util.get_family(lang_code)
if potential_family then
lang = potential_family
rest = param:sub(#lang_code + 2)
is_family = true
else
lang = context_lang
rest = param
end
end
else
lang = context_lang
rest = param
end
if rest == "" then
M.track("etymon/term/empty")
elseif rest == "?" then
M.track("etymon/term/question-mark")
elseif rest == "-" then
M.track("etymon/term/hyphen")
end
if rest == "" then
return {
lang = lang,
term = nil,
unknown_term = true,
is_family = is_family,
}
end
if rest == "-" then
return {
lang = lang,
term = nil,
suppress_term = true,
is_family = is_family,
}
end
if not rest:find("<", 1, true) then
return {
lang = lang,
term = M.string_utilities.trim(rest),
is_family = is_family,
}
end
local term_text = rest:match("^([^<]*)") or ""
local is_unknown = (term_text == "")
local is_suppress = (term_text == "-")
local function generate_obj(ignored_term)
return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) }
end
local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods)
local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults,
{ param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj })
if parsed_obj.id and parsed_obj.id:match("^!") then
parsed_obj.id = parsed_obj.id:sub(2)
parsed_obj.override = true
end
parsed_obj.lang = lang
parsed_obj.is_family = is_family
if is_unknown then
parsed_obj.unknown_term = true
elseif is_suppress then
parsed_obj.suppress_term = true
end
return parsed_obj
end
function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code)
-- id is now optional, so only validate if provided
if id then
if mw.ustring.len(id) < 2 then
error("The `id` parameter must have at least two characters.")
end
if id == title or id == Util.get_page_data().pagename then
error("The `id` parameter must not be the same as the page title.")
end
end
local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true }
if pos and not valid_pos[pos] then
error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".")
end
local current_keyword = "from"
local etymons_in_group = {}
local keywords = M.data.keywords
local function checkGroup()
if keywords[current_keyword] and keywords[current_keyword].is_group and current_keyword ~= "affix" and current_keyword ~= "surf" and current_keyword ~= "afeq" and current_keyword ~= "univerbation" and #etymons_in_group <= 1 then
error("Detected `:" .. current_keyword .. "` group with fewer than two etymons.")
end
etymons_in_group = {}
end
local start_index = starts_with_lang_code and 2 or 1
for i = start_index, #args do
local param = args[i]
if type(param) ~= "string" then
elseif param:sub(1, 1) == ":" and not is_keyword(param) then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkGroup()
current_keyword = get_canonical_keyword(get_keyword(param))
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(etymons_in_group, param)
local param_lang = etymon_data.lang
if etymon_data.is_family and current_keyword == "inherited" then
error("`:inh` does not support family codes; use a specific language.")
end
if etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
if current_keyword == "from" and param_lang:getFullCode() ~= lang:getFullCode() then
error("`:from` is for same-language derivation, but language does not match. " ..
"Expected '" .. lang:getFullCode() .. "', got '" .. param_lang:getFullCode() .. "'.")
elseif current_keyword == "inherited" then
M.etymology.check_ancestor(lang, param_lang)
end
-- Check modifier restrictions
EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods)
-- postype must be "root" or "word"
local VALID_POSTYPES = { root = true, word = true }
if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then
error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".")
end
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true)
end
else
table.insert(etymons_in_group, param)
end
end
end
checkGroup()
end
local DataRetriever = {}
-- Given an etymon data, scrape its page and cache the result in the global state object.
function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel)
local content = etymon_title:getContent()
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.REDLINK
return
end
-- Check if the linked page is a redirect. If it is, the template parsing
-- code below will be effectively skipped, and `scrape_page` will be called
-- again on the redirect target (see the bottom of this function)
local lang_section_for_descendants = nil
local redirect_target = etymon_title.redirect_target
if not redirect_target then
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
return
end
lang_section_for_descendants = content
end
local etymon_lang_code = etymon_lang:getFullCode()
local lang_page_key = etymon_lang_code .. ":" .. etymon_page
local found_templates_for_lang = {}
local found_ids = {}
local get_node_class = M.template_parser.class_else_type
-- Look for all {{etymon}} templates within the page content using the template parser
-- This way the same page is never parsed more than once
-- Build a map from senseids to their parent etymonids.
local active_etymon_args = nil
for node in M.template_parser.parse(content):iterate_nodes() do
local node_class = get_node_class(node)
if node_class == "heading" then
-- A new L2 or etymology section acts as a barrier: an {{etymon}} usage
-- used previously cannot be the parent of any subsequent senseids.
-- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above.
if node:get_name():find("^Etymology") then
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if template_name == "etymon" then
local template_args = node:get_arguments()
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
table.insert(found_templates_for_lang, template_args)
if template_args.id then
local etymon_key = lang_page_key .. ":" .. template_args.id
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, template_args.id)
active_etymon_args = template_args
else
-- Store idless etymon with default key
local etymon_key = lang_page_key .. ":*"
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, "*")
active_etymon_args = template_args
end
end
elseif active_etymon_args and template_name == "senseid" then
local template_args = node:get_arguments()
-- This should always be true for proper usages of {{senseid}}.
if template_args[1] == etymon_lang_code and template_args[2] then
local sense_id_key = lang_page_key .. ":" .. template_args[2]
__state.senseid_parent_etymon[sense_id_key] = active_etymon_args
__state.cached_etymon_pages[sense_id_key] = tostring(etymon_page)
end
end
end
end
if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then
M.descendants.cache_page_checks({
lang_section = lang_section_for_descendants,
etymon_lang_code = etymon_lang_code,
found_templates_for_lang = found_templates_for_lang,
entry_title = __state.entry_title,
entry_lang_code = __state.entry_lang_code,
entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil,
cached_descendants_checks = __state.cached_descendants_checks,
lang_page_key = lang_page_key,
redirected_from = redirected_from,
})
end
local id_data_list = {}
for _, args in ipairs(found_templates_for_lang) do
local id = args.id or "*"
table.insert(id_data_list, { id = id, pos = args.pos })
end
__state.available_etymon_ids[lang_page_key] = id_data_list
if #found_templates_for_lang == 1 then
__state.single_etymons[lang_page_key] = found_templates_for_lang[1]
end
if redirected_from and __state.available_etymon_ids[lang_page_key] then
__state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {}
for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do
table.insert(__state.available_etymon_ids[redirected_from], id_data)
end
end
if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then
-- All done!
return
elseif redirect_target and not redirected_from then
-- Try scraping the redirect.
etymon_page = redirect_target.prefixedText
DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel)
__state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id]
else
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
end
end
-- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name.
function DataRetriever.get_etymon_args(etymon_data, is_toplevel)
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
if etymon_data.id then
local key = base_key .. ":" .. etymon_data.id
local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key]
if cached_args == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel)
end
cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh
-- Get etymon_id from parent if this was resolved via senseid
local parent_etymon = __state.senseid_parent_etymon[key]
local resolved_etymon_id = parent_etymon and parent_etymon.id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
end
end
return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check
else
__state.used_idless_etymon = true
if is_toplevel then
__state.toplevel_idless_etymon = true
end
if __state.available_etymon_ids[base_key] == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel)
end
local ids = __state.available_etymon_ids[base_key] or {}
local count = #ids
-- Try to filter by postype if available and we have multiple candidates
if count > 1 and etymon_data.postype then
local matching_ids = {}
for _, id_data in ipairs(ids) do
if id_data.pos == etymon_data.postype then
table.insert(matching_ids, id_data)
end
end
if #matching_ids == 1 then
local matched_id = matching_ids[1].id
local matched_key = base_key .. ":" .. matched_id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id = matched_id },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id = matched_id },
})
end
end
return __state.cached_etymon_args[matched_key], __state.cached_etymon_pages[matched_key], nil, descendants_check
end
end
if count == 1 then
local only_id_data = ids[1]
local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*"
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id_data = only_id_data },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id_data = only_id_data },
})
end
end
return __state.single_etymons[base_key], __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
local page_link = M.links.full_link({
term = page,
lang = norm_lang,
no_generate_forms = true,
}, "term")
local function format_id_hint(id_data, idx)
local id = type(id_data) == "table" and id_data.id or id_data
local pos = type(id_data) == "table" and id_data.pos
if id and id ~= "" and id ~= "*" then
return '"' .. id .. '"'
end
if pos and pos ~= "" then
return "unnamed (|pos=" .. pos .. "|)"
end
return "etymon #" .. idx .. " (no |id= on page)"
end
local id_list = {}
local all_idless = true
local target_has_idless = false
for i, id_data in ipairs(ids) do
local id = type(id_data) == "table" and id_data.id or id_data
if id and id ~= "" and id ~= "*" then
all_idless = false
else
target_has_idless = true
end
table.insert(id_list, format_id_hint(id_data, i))
end
if is_toplevel and target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local any_pos = false
for _, id_data in ipairs(ids) do
local pos = type(id_data) == "table" and id_data.pos
if pos and pos ~= "" then
any_pos = true
break
end
end
local suggestion_text
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. count .. " {{etymon}} templates for "
.. norm_lang:getCanonicalName() .. ")."
if all_idless then
if any_pos then
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. mw.text.listToText(id_list) .. "."
else
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)."
end
else
suggestion_text = " Specify which one with `<id:identifier>` after the term. Options: " .. mw.text.listToText(id_list) .. "."
end
Util.add_warning(lead .. suggestion_text, true)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local TreeBuilder = {}
local function parse_etymon_references(refs_text)
if not refs_text or refs_text == "" then
return ""
end
return M.references.parse_references(refs_text)
end
local function parse_tree_references(node)
if node.ref then
node.parsed_ref = parse_etymon_references(node.ref)
end
if node.children then
for _, container in ipairs(node.children) do
if container.terms then
for _, term in ipairs(container.terms) do
parse_tree_references(term)
end
end
end
end
end
-- Build a unique key for deduplication in the seen table
function TreeBuilder.build_key(lang, title, args)
local norm_lang_code = Util.get_norm_lang(lang):getFullCode()
local is_table = type(args) == "table"
local id = (is_table and args.id) or ""
if title then
return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id
end
if is_table and args.status == M.data.STATUS.INLINE then
local content_parts = {}
for i = 1, #args do
content_parts[i] = tostring(args[i])
end
return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0")
end
return norm_lang_code .. ":*:" .. id
end
function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion)
seen = seen or {}
depth = depth or 0
local is_toplevel = (depth == 0)
if depth > __state.max_depth_reached then
__state.max_depth_reached = depth
end
__state.total_nodes = __state.total_nodes + 1
local lang_code = lang:getCode()
__state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1
local current_id = (type(args) == "table" and args.id) or ""
local key = TreeBuilder.build_key(lang, title, args)
local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK }
if type(args) ~= "table" or seen[key] then
node.status = args or M.data.STATUS.MISSING
-- Mark as duplicate if we've seen this node before
if seen[key] then
node.is_duplicate = true
node.duplicate_key = key
local original_node = seen[key]
if type(original_node) == "table" and original_node.children and #original_node.children > 0 then
node.original_has_children = true
end
end
return node
end
node.status = args.status or M.data.STATUS.OK
seen[key] = node
-- If stop_recursion is set, skip parsing children but check for visible children
if stop_recursion then
local keywords = M.data.keywords
local has_visible_children = false
for i = 2, #args do
local param = args[i]
if type(param) == "string" then
local keyword_base = get_keyword_base(param)
if keyword_base and keywords[keyword_base] then
-- It's a keyword, check if visible in tree (invisible "all" or "tree" = hidden in tree)
local keyword_info = keywords[keyword_base]
local inv = keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
elseif param:sub(1, 1) ~= ":" then
-- It's a term (not a keyword), so there are visible children
has_visible_children = true
break
end
end
end
node.has_visible_children = has_visible_children
return node
end
-- Parse args into keyword containers
local current_keyword = "from"
local current_keyword_modifiers = {}
local current_container = nil
-- Helper to track keyword usage at top level
local function track_keyword_usage(keyword, target_lang, source_lang)
if not is_toplevel then return end
if not __state.toplevel_keyword_stats[keyword] then
__state.toplevel_keyword_stats[keyword] = {
count = 0,
target_langs = {},
source_langs = {},
}
end
local keyword_data = __state.toplevel_keyword_stats[keyword]
keyword_data.count = keyword_data.count + 1
local target_code = target_lang:getCode()
keyword_data.target_langs[target_code] = (keyword_data.target_langs[target_code] or 0) + 1
if source_lang then
local source_code = source_lang:getCode()
keyword_data.source_langs[source_code] = (keyword_data.source_langs[source_code] or 0) + 1
end
end
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
current_container = {
keyword = current_keyword,
keyword_info = M.data.keywords[current_keyword],
keyword_modifiers = current_keyword_modifiers,
terms = {},
}
table.insert(node.children, current_container)
-- Override keyword text/phrase for nominalization with <g:code>
if current_keyword_modifiers.g and current_keyword == "nominalization" then
local labels = get_nominalization_label_for_g(current_keyword_modifiers.g)
if not labels then
local codes = {}
for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end
table.sort(codes)
error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", "))
end
current_container.keyword_info = {}
for k, v in pairs(M.data.keywords[current_keyword]) do current_container.keyword_info[k] = v end
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
end
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
current_keyword = keyword
current_keyword_modifiers = modifiers
current_container = nil -- Force new container for new keyword
elseif type(param) == "string" and param:sub(1, 1) == ":" then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif type(param) == "string" then
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
-- Track keyword usage at top level
track_keyword_usage(current_keyword, lang, etymon_data.lang)
local term_node = {}
-- Handle suppress_term (-) and unknown_term (empty) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
ensure_container()
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1)
else
term_node = {
lang = etymon_data.lang,
children = {},
status = M.data.STATUS.OK,
}
end
term_node.suppress_term = etymon_data.suppress_term
term_node.unknown_term = etymon_data.unknown_term
term_node.is_family = etymon_data.is_family
term_node.is_uncertain = etymon_data.unc
term_node.ref = etymon_data.ref
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.pos = etymon_data.pos
term_node.ng = etymon_data.ng
term_node.lit = etymon_data.lit
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
else
-- Regular term: fetch arguments from page
local etymon_args, page_of, resolved_etymon_id, descendants_check = DataRetriever.get_etymon_args(etymon_data, is_toplevel)
if etymon_data.id and etymon_args == M.data.STATUS.MISSING and not etymon_data.ety then
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
local available_ids = __state.available_etymon_ids[base_key] or {}
if #available_ids > 0 then
__state.has_mismatched_id = true
end
end
-- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing
if etymon_data.ety then
if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then
__state.current_page_has_inline_etymology = true
if is_toplevel then
__state.toplevel_has_inline_etymology = true
end
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
-- Track inline ety keywords too
local inline_keyword = get_keyword(inline_args[2], true)
if inline_keyword and #inline_args >= 3 then
local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang)
if inline_etymon then
track_keyword_usage(inline_keyword, etymon_data.lang, inline_etymon.lang)
end
end
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
etymon_args = inline_args
term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent
else
-- Scraped arguments exist, <ety> is redundant and ignored
__state.current_page_has_redundant_etymology = true
if is_toplevel then
__state.toplevel_redundant_etymology = true
end
end
end
-- Ensure container exists before checking keyword info
ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = current_container.keyword_info
local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories))
term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion)
term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() ..
":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang)
term_node.id = etymon_data.id
term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.pos = etymon_data.pos
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.ng = etymon_data.ng
term_node.ref = etymon_data.ref
term_node.is_uncertain = etymon_data.unc
term_node.override = etymon_data.override
term_node.page_of = page_of
term_node.aftype = etymon_data.aftype
term_node.postype = etymon_data.postype
term_node.bor = etymon_data.bor
term_node.lbor = etymon_data.lbor
term_node.slbor = etymon_data.slbor
term_node.lit = etymon_data.lit
term_node.is_family = etymon_data.is_family
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
term_node.missing_descendants_header, term_node.missing_descendants_entry =
M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check)
end
table.insert(current_container.terms, term_node)
end
end
end
return node
end
-- Convert etymology tree to JSON-serializable table
local function tree_to_json(node)
local obj = {
term = node.title,
lang = node.lang:getCode(),
lang_name = node.lang:getCanonicalName(),
id = (node.id and node.id ~= "") and node.id or nil,
status = node.status,
is_uncertain = node.is_uncertain or nil,
is_duplicate = node.is_duplicate or nil,
gloss = node.t,
transliteration = node.tr,
transcription = node.ts,
alt = node.alt,
g = node.g,
pos = node.pos,
ng = node.ng,
children = {},
}
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if keyword_info then
local container_obj = {
keyword = container.keyword,
keyword_label = keyword_info.text,
keyword_abbrev = keyword_info.abbrev,
is_group = keyword_info.is_group or nil,
is_invisible = keyword_info.invisible or nil,
is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil,
terms = {},
}
for _, term in ipairs(container.terms or {}) do
table.insert(container_obj.terms, tree_to_json(term))
end
table.insert(obj.children, container_obj)
end
end
return obj
end
local function track_ranges(base_key, value, ranges, lang_code)
M.track("etymon/" .. base_key .. "/" .. value)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. value)
end
for _, range in ipairs(ranges) do
local matches = false
if range.min and range.max then
matches = value >= range.min and value <= range.max
elseif range.min then
matches = value >= range.min
elseif range.max then
matches = value <= range.max
elseif range.exact then
matches = value == range.exact
end
if matches then
M.track("etymon/" .. base_key .. "/" .. range.label)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. range.label)
end
break
end
end
end
local function track_title_pagename_mismatch_after_strip(lang)
local lang_code = lang:getCode()
M.track("etymon/title/pagename-mismatch-after-strip-diacritics")
M.track("etymon/lang/" .. lang_code .. "/title/pagename-mismatch-after-strip-diacritics")
end
-- Build and return the etymology data tree for a given term.
function export.get_tree(lang, title, args, options)
options = options or {}
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
if options.validate then
EtymonParser.validate(lang, args, options.id, title, options.pos, false)
end
local lang_code = lang:getCode()
local start_index = (args[1] == lang_code) and 2 or 1
local tree_args = { [1] = lang_code, id = options.id or args.id }
for i = start_index, #args do
table.insert(tree_args, args[i])
end
__state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args
local ety_data_tree = TreeBuilder.build(lang, title, tree_args)
parse_tree_references(ety_data_tree)
if options.json then
return M.JSON.toJSON(tree_to_json(ety_data_tree))
end
return ety_data_tree
end
-- Given a language code, page name and optionally the id= parameter,
-- render the tree and only the etymology tree for the relevant page.
-- Fetches and parses the corresponding {{etymon}} from the requested page,
-- and any further pages needed to render the tree.
-- Parameters can be passed either through the #invoke or as
-- template parameters *through* an #invoke.
function export.render_tree_for_etymon_on_page(frame)
local frame_args = frame.args
local parent_args = frame:getParent().args
local langcode = frame_args[1] or parent_args[1]
local pagename = frame_args[2] or parent_args[2]
local id = frame_args["id"] or parent_args["id"]
local display_title = frame_args["title"] or parent_args["title"]
local parsed_title = mw.title.new(pagename, 0)
local title
if parsed_title.namespace == 0 then
title = M.pages.safe_page_name(parsed_title)
elseif parsed_title.namespace == 118 then
title = "*" .. M.pages.safe_page_name(parsed_title)
else
error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace)
end
local lang = Util.get_lang(langcode)
-- Construct etymon_data for DataRetriever.get_args.
local etymon_data = {
lang = lang,
term = title,
id = id
}
local args, pagename = DataRetriever.get_etymon_args(etymon_data, true)
if args == M.data.STATUS.MISSING then
error("The etymon template was not found (language " ..
langcode ..
", title '" ..
title ..
"'" ..
(id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.")
end
local tree_title = display_title or title
if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then
track_title_pagename_mismatch_after_strip(lang)
end
local ety_data_tree = export.get_tree(lang, tree_title, args, {
validate = true,
id = id,
})
local output = {}
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
return table.concat(output)
end
function export.main(frame)
local parent_args = frame:getParent().args
local args = M.parameters.process(parent_args, M.parameters_data.etymon)
local lang = args[1]
local etymon_args = args[2]
local id = args.id
local title = args.title
local text = args.text
local tree = args.tree
local etydate = args.etydate
local rfe = args.rfe
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == "Reconstruction" then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == "Reconstruction" then
entry_pagename = "*" .. entry_pagename
end
if lang:stripDiacritics(M.links.remove_links(title)) ~= lang:stripDiacritics(M.links.remove_links(entry_pagename)) then
track_title_pagename_mismatch_after_strip(lang)
end
local current_L2 = M.pages.get_current_L2()
if current_L2 then
local norm_lang = Util.get_norm_lang(lang)
local norm_name = norm_lang:getCanonicalName()
if current_L2 ~= norm_name then
local lang_desc = lang:getCode() .. " (Bahasa " .. lang:getCanonicalName() .. ")"
if norm_lang:getCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", normalized to " .. norm_lang:getCode() .. " (Bahasa " .. norm_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
end
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
})
if args.json then
return ety_data_tree
end
local output = {}
local text_allowlist_mode = M.text_allowed.default_mode or "off"
if text and text_allowlist_mode ~= "off" and not Util.is_text_param_allowed_for_lang(lang) then
local msg = "Etymology texts (parameter <code>text=</code>) are not allowed for " .. lang:getFullName() ..
"; see [[Template:etymon#Text allowlist|Template:etymon § Text allowlist]] for the list of languages that may use the <code>text=</code> parameter."
if text_allowlist_mode == "error" then
error(msg)
else
Util.add_warning(msg, true)
end
end
local lang_exc = Util.get_lang_exception(lang)
if lang_exc and lang_exc.disallow then
local disallow = lang_exc.disallow
local error_text = " for " .. lang:getFullName()
if disallow.ref then
error_text = error_text .. "; see " .. disallow.ref
else
error_text = error_text .. "."
end
if tree and disallow.tree then
error("Etymology trees are not allowed" .. error_text)
end
if text and disallow.text then
error("Etymology texts are not allowed" .. error_text)
end
end
if etydate then
local etydate_param_mods = {
ref = { list = true, type = "references", allow_holes = true },
refn = { list = true, allow_holes = true },
nocap = { type = "boolean" },
}
local function generate_etydate_obj(etydate_text)
local etydate_specs = {}
for spec in etydate_text:gmatch("[^,]+") do
table.insert(etydate_specs, mw.text.trim(spec))
end
return { [1] = etydate_specs }
end
local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj })
local etydate_args = {
[1] = parsed_etydate[1],
nocap = parsed_etydate.nocap or false,
}
ety_data_tree.etydate = M.etydate.format_etydate(etydate_args, { omit_refs = true })
if parsed_etydate.ref and #parsed_etydate.ref > 0 then
ety_data_tree.etydate_refs = parsed_etydate.ref
end
end
if tree then
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
end
-- Check if there are any visible children in tree (invisible "all" or "tree" = hidden in tree)
local has_visible_children = false
for _, child in ipairs(ety_data_tree.children or {}) do
local child_keyword_info = child.keyword_info
local inv = child_keyword_info and child_keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
end
local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree
local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree))
local anchor = M.anchors.etymonid(lang, id, {
no_tree = args.notree,
title = title,
empty_tree = (not has_visible_children) or tree_disallowed,
ety_tree_json = ety_tree_json,
})
table.insert(output, anchor)
if text then
local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink
if text == "++" then
max_depth, stop_at_blue_link = false, false
elseif text == "+" then
max_depth, stop_at_blue_link = 1, false
elseif text == "*" then
max_depth, stop_at_blue_link = false, true
elseif text:match("^:[^*]+%*$") then
-- Stop at a specific language OR first bluelink after it, e.g., ":ota*"
-- If the target language is a redlink, continue to the first bluelink
local lang_code = text:match("^:([^*]+)%*$")
if lang_code and lang_code ~= "" then
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang_or_bluelink = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
elseif text:sub(1, 1) == ":" then
-- Stop at a specific language, e.g., ":ar" stops at first Arabic term
local lang_code = text:sub(2)
if lang_code ~= "" then
-- Validate the language code
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
local num = tonumber(text)
if num and num >= 1 then
max_depth, stop_at_blue_link = num, false
else
error('Invalid text value "' ..
text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)')
end
end
table.insert(output, M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
}))
end
if rfe then
local rfe_param_mods = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
}
local function generate_rfe_obj(rfe_text)
-- Check if it's a boolean true value
if M.yesno(rfe_text, false) then
return { is_boolean = true }
else
return { text = rfe_text }
end
end
local rfe_with_defaults = Util.add_boolean_defaults(rfe, rfe_param_mods)
local parsed_rfe = M.parse_utilities.parse_inline_modifiers(rfe_with_defaults, {
param_mods = rfe_param_mods,
generate_obj = generate_rfe_obj
})
local rfe_args = {
[1] = lang:getCode(),
nocat = parsed_rfe.nocat,
sort = parsed_rfe.sort,
y = parsed_rfe.y,
m = parsed_rfe.m,
fragment = parsed_rfe.fragment,
section = parsed_rfe.section,
box = parsed_rfe.box,
noes = parsed_rfe.noes,
}
if not parsed_rfe.is_boolean then
rfe_args[2] = parsed_rfe.text
end
table.insert(output, frame:expandTemplate({
title = "rfe",
args = rfe_args
}))
end
if Util.is_content_page() and __state.max_depth_reached > 0 then
local lang_code = lang:getCode()
local depth_ranges = {
{ min = 50, label = "extremely-deep" },
{ min = 20, label = "20+" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ max = 2, label = "1-2" }
}
local node_ranges = {
{ min = 100, label = "extremely-large" },
{ min = 50, label = "50+" },
{ min = 20, max = 49, label = "20-49" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ max = 4, label = "1-4" }
}
local language_ranges = {
{ min = 10, label = "10+" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ exact = 2, label = "2" },
{ exact = 1, label = "1" }
}
track_ranges("depth", __state.max_depth_reached, depth_ranges, lang_code)
track_ranges("nodes", __state.total_nodes, node_ranges, lang_code)
local unique_languages = 0
for _ in pairs(__state.language_count) do
unique_languages = unique_languages + 1
end
track_ranges("unique-languages", unique_languages, language_ranges, lang_code)
if __state.total_nodes == __state.max_depth_reached + 1 then
track_ranges("linear-depth", __state.max_depth_reached, depth_ranges, lang_code)
end
end
local categories = {}
if Util.is_content_page() then
local should_suppress_categories = lang_exc and lang_exc.suppress_categories
if not should_suppress_categories and not args.nocat then
categories = M.categories.render({
data_tree = ety_data_tree,
page_lang = lang,
available_etymon_ids = __state.available_etymon_ids,
senseid_parent_etymon = __state.senseid_parent_etymon,
get_norm_lang_func = Util.get_norm_lang,
lang_exc = lang_exc,
})
end
local target_lang_code = lang:getCode()
for keyword, keyword_data in pairs(__state.toplevel_keyword_stats) do
-- Track keyword globally
M.track("etymon/keyword/" .. keyword)
-- Track keyword per target language
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code)
-- Track keyword per source language
for source_code, count in pairs(keyword_data.source_langs) do
M.track("etymon/keyword/" .. keyword .. "/source/" .. source_code)
-- Track keyword per target+source combination
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code .. "/source/" .. source_code)
end
end
if tree then
table.insert(categories, "Pages with etymology trees")
table.insert(categories, lang:getCanonicalName() .. " entries with etymology trees")
end
if text then table.insert(categories, lang:getCanonicalName() .. " entries with etymology texts") end
if args.exnihilo then table.insert(categories, lang:getCanonicalName() .. " terms coined ex nihilo") end
if __state.toplevel_has_inline_etymology then
table.insert(categories, "Pages with inline etymon for redlinks")
end
if __state.toplevel_redundant_etymology then
table.insert(categories, "Pages with redundant inline etymon")
end
if __state.toplevel_idless_etymon then
table.insert(categories, "Pages using etymon with no ID")
end
if __state.has_mismatched_id then
table.insert(categories, lang:getCanonicalName() .. " entries referencing etymons with mismatched IDs")
end
if __state.linked_page_multiple_etymons_idless then
table.insert(categories,
lang:getCanonicalName() .. " entries referencing pages with multiple etymons missing IDs")
end
end
if #categories > 0 then
table.insert(output, M.categories.format(categories, lang))
end
if __state.warnings then
for i, warning in ipairs(__state.warnings) do
table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n")
end
end
return table.concat(output)
end
return export
atzf01y3q7myy0cwlzlcz4tnhqsss0s
342824
342823
2026-05-16T13:49:08Z
Hakimi97
2668
342824
Scribunto
text/plain
--[=[
This module implements the {{etymon}} template for structured etymology data on Wiktionary.
It enables the creation of etymology trees and text by parsing etymon chains,
scraping linked pages for their own {{etymon}} data, and recursively building a tree
of derivational relationships.
Authors:
- Original implementation: [[User:Ioaxxere]]
- Full refactor (September 2025): [[User:Fenakhay]] ([[Special:Diff/86717746]])
Modules:
- [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping
- [[Module:etymon/data]]: keyword definitions, configuration, and status constants
- [[Module:etymon/tree]]: etymology tree rendering
- [[Module:etymon/text]]: etymology text generation
- [[Module:etymon/categories]]: category generation logic
]=]
local export = {}
local etymon_data_module = "Module:etymon/data"
local etymon_text_module = "Module:etymon/text"
local etymon_tree_module = "Module:etymon/tree"
local etymon_categories_module = "Module:etymon/categories"
local etymon_descendants_module = "Module:etymon/descendants"
local __state = {
cached_etymon_args = {},
cached_etymon_pages = {},
cached_descendants_checks = {},
senseid_parent_etymon = {},
available_etymon_ids = {},
single_etymons = {},
entry_title = nil,
entry_lang_code = nil,
current_page_has_inline_etymology = false,
current_page_has_redundant_etymology = false,
used_idless_etymon = false,
toplevel_has_inline_etymology = false,
toplevel_redundant_etymology = false,
toplevel_idless_etymon = false,
has_mismatched_id = false,
linked_page_multiple_etymons_idless = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
warnings = {},
}
local loader = require("Module:module loader")
local M = loader.init({
require = {
data = etymon_data_module,
tree = etymon_tree_module,
text = etymon_text_module,
categories = etymon_categories_module,
descendants = etymon_descendants_module,
anchors = "Module:anchors",
etydate = "Module:etydate",
etymology = "Module:etymology",
families = "Module:families",
languages = "Module:languages",
languages_errorgetby = "Module:languages/errorGetBy",
links = "Module:links",
pages = "Module:pages",
parameters = "Module:parameters",
string_utilities = "Module:string utilities",
template_parser = "Module:template parser",
utilities = "Module:utilities",
debug = "Module:debug",
en_utilities = "Module:en-utilities",
parse_utilities = "Module:parse utilities",
references = "Module:references",
track = "Module:debug/track",
template_styles = "Module:TemplateStyles",
script_utilities = "Module:script utilities",
JSON = "Module:JSON",
yesno = "Module:yesno",
},
loadData = {
headword_data = "Module:headword/data",
parameters_data = "Module:parameters/data",
text_allowed = "Module:etymon/data/text_allowed",
},
})
local Util = {}
function Util.format_error(message, preview_only)
if preview_only and not M.pages.is_preview() then
return nil
end
return '<span class="error">' .. message .. '</span>'
end
function Util.add_warning(message, preview_only)
local formatted = Util.format_error(message, preview_only)
if formatted then
table.insert(__state.warnings, formatted)
end
end
function Util.is_text_param_allowed_for_lang(lang)
if not lang or type(lang) ~= "table" then
return false
end
local types = lang.getTypes and lang:getTypes()
if types and types.family then
local code = lang.getCode and lang:getCode()
return code and M.text_allowed.families[code] == true
end
local full_code = lang.getFullCode and lang:getFullCode()
if full_code and M.text_allowed.langs[full_code] then
return true
end
if lang.inFamily then
for family_code in pairs(M.text_allowed.families) do
if lang:inFamily(family_code) then
return true
end
end
end
return false
end
function Util.get_lang(code, no_error)
if no_error then
return M.languages.getByCode(code, nil, true)
end
return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true)
end
function Util.get_family(code)
return M.families.getByCode(code)
end
function Util.get_lang_exception(lang)
-- Families have no language-specific exceptions
if lang.getTypes and lang:getTypes().family then
return nil
end
local code = lang:getCode()
local lang_exceptions = M.data.config.lang_exceptions
if lang_exceptions[code] then
return lang_exceptions[code]
end
for norm_code, exc in pairs(lang_exceptions) do
if exc.normalize_to and code == exc.normalize_to then
return exc
end
if exc.normalize_from_families then
local should_normalize = false
for _, family in ipairs(exc.normalize_from_families) do
if lang:inFamily(family) then
should_normalize = true
break
end
end
if should_normalize and exc.normalize_exclude_families then
for _, family in ipairs(exc.normalize_exclude_families) do
if lang:inFamily(family) then
should_normalize = false
break
end
end
end
if should_normalize then
local ret = {}
for k, v in pairs(exc) do
ret[k] = v
end
ret.suppress_tr = nil
return ret
end
end
end
return nil
end
function Util.get_norm_lang(lang)
local exc = Util.get_lang_exception(lang)
if exc and exc.normalize_to then
return M.languages.getByCode(exc.normalize_to)
end
return lang
end
-- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>)
-- This is needed because Module:parse utilities expects boolean modifiers to have explicit values
function Util.add_boolean_defaults(str, param_mods)
local result = str
for name, spec in pairs(param_mods) do
if spec.type == "boolean" then
-- Replace <name> with <name:1> (but not <name:...> which already has a value)
result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>")
end
end
return result
end
-- Centralized term formatting: handles suppress_term, unknown_term, and regular terms
function Util.format_term(term, is_toplevel, opts)
opts = opts or {}
-- suppress_term (-) returns nil
if term.suppress_term then
return nil
end
local lang = term.lang
local exc = Util.get_lang_exception(lang)
if is_toplevel then
local display_text = term.alt or term.title or ""
local sc = term.sc or lang:findBestScript(display_text)
local bold_text = tostring(mw.html.create("strong")
:addClass("selflink")
:wikitext(display_text))
return M.script_utilities.tag_text(bold_text, lang, sc, "term")
end
local link_params = { lang = lang }
link_params.term = not term.unknown_term and term.title or nil
link_params.alt = term.alt
link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil
if not (exc and exc.suppress_tr) then
link_params.tr = term.tr
link_params.ts = term.ts
else
link_params.suppress_tr = true
end
link_params.lit = (opts.lit ~= "suppress") and term.lit or nil
if opts.gloss ~= "suppress" then
link_params.gloss = term.t
end
if term.g and term.g ~= "" then
local genders = M.string_utilities.split(term.g, ",")
for i = 1, #genders do
genders[i] = M.string_utilities.trim(genders[i])
end
link_params.genders = genders
end
if opts.pos ~= "suppress" then
link_params.pos = term.pos
link_params.ng = term.ng
end
if exc and exc.suppress_tr then
link_params.lit = nil
end
local show_qualifiers
if opts.tree_ql ~= "suppress" then
if term.q then
link_params.q = term.q
end
if term.qq then
link_params.qq = term.qq
end
if term.l then
link_params.l = term.l
end
if term.ll then
link_params.ll = term.ll
end
show_qualifiers = term.q or term.qq or term.l or term.ll
end
return M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
end
local __is_content_page_cached
function Util.is_content_page()
if __is_content_page_cached == nil then
__is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle())
end
return __is_content_page_cached
end
local __page_data_cached
function Util.get_page_data()
if not __page_data_cached then
__page_data_cached = M.headword_data.page
end
return __page_data_cached
end
-- Extract base keyword from param (without modifiers)
local function get_keyword_base(param)
if type(param) ~= "string" then return nil end
local base = param:match("^:?([^<]+)") or param:gsub("^:", "")
return base
end
local function is_keyword(param, allow_colon_less)
if type(param) ~= "string" then return false end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
if allow_colon_less then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
return false
end
local function get_keyword(param, allow_colon_less)
if type(param) ~= "string" then return nil end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
return get_keyword_base(param)
end
if allow_colon_less then
local base = get_keyword_base(param)
if keywords[base] then
return base
end
end
return nil
end
local function normalize_keyword(keyword)
if keyword:sub(1, 1) == ":" then
return keyword
end
return ":" .. keyword
end
-- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries
local function get_canonical_keyword(keyword)
if not keyword then return keyword end
return M.data.keyword_canonical[keyword] or keyword
end
-- Build text/phrase for nominalization with <g:code> (uses data module for codes only).
local function get_nominalization_label_for_g(code)
if not code or code == "" then return nil end
local codes = M.data.nominalization_g_codes
local adj = codes[code]
if not adj and #code == 2 then
local gender_adj = codes[code:sub(1, 1)]
local number_adj = codes[code:sub(2, 2)]
if gender_adj and number_adj then
adj = gender_adj .. " " .. number_adj
end
end
if not adj then return nil end
local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of"
local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false)
return { text = text, phrase = phrase }
end
local EtymonParser = {}
-- Keyword modifier definitions
EtymonParser.keyword_param_mods = {
unc = { type = "boolean" },
ref = {},
text = { restrict = { keywords = { "from", "derived" } } },
lit = { restrict = { keywords = { "affix", "surf", "univerbation" } } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
}
-- Term modifier definitions
EtymonParser.etymon_param_mods = {
id = {},
t = {},
tr = {},
ts = {},
q = {},
qq = {},
l = {},
ll = {},
pos = {},
ng = {},
alt = {},
g = {},
ety = {},
lit = {},
unc = { type = "boolean" },
ref = {},
aftype = { restrict = { keywords = { "affix", "surf", "afeq" } } },
postype = {},
bor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
slbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
lbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
}
local function get_clean_param_mods(param_mods)
local clean = {}
for mod_name, mod_def in pairs(param_mods) do
clean[mod_name] = {}
for key, value in pairs(mod_def) do
if key ~= "restrict" then
clean[mod_name][key] = value
end
end
end
return clean
end
function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods)
for mod_name, mod_value in pairs(modifiers) do
-- Only check restrictions if the modifier has a non-false/nil value
if mod_value then
local mod_def = param_mods[mod_name]
if mod_def and mod_def.restrict and mod_def.restrict.keywords then
local allowed_keywords = mod_def.restrict.keywords
local is_allowed = false
for _, allowed_keyword in ipairs(allowed_keywords) do
if current_keyword == allowed_keyword then
is_allowed = true
break
end
end
if not is_allowed then
local keyword_list = {}
for _, kw in ipairs(allowed_keywords) do
table.insert(keyword_list, ":" .. kw)
end
local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ")
if #keyword_list > 2 then
-- Replace last comma with "or"
keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1")
end
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".")
end
end
end
end
end
-- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>")
function EtymonParser.parse_keyword_modifiers(param)
if type(param) ~= "string" then return nil, {} end
local base_keyword = get_keyword_base(param)
if not base_keyword then return nil, {} end
local canonical_keyword = get_canonical_keyword(base_keyword)
-- Check if there are any modifiers
if not param:find("<", 1, true) then
return canonical_keyword, {}
end
-- Parse modifiers using the same mechanism as etymon parsing
local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods)
local function generate_obj(ignored)
return {}
end
local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""),
{ param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj })
local modifiers = {
unc = parsed.unc or false,
ref = parsed.ref,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
function EtymonParser.parse_balanced_segments(str)
local segments = {}
local current = ""
local depth = 0
local i = 1
while i <= #str do
local char = str:sub(i, i)
if char == "<" then
if depth == 0 and current ~= "" then
table.insert(segments, current)
current = ""
end
depth = depth + 1
current = current .. char
elseif char == ">" then
current = current .. char
depth = depth - 1
if depth == 0 then
table.insert(segments, current)
current = ""
elseif depth < 0 then
error("Unbalanced brackets in etymon: unexpected '>'")
end
else
current = current .. char
end
i = i + 1
end
if depth ~= 0 then
error("Unbalanced brackets in etymon: missing '>'")
end
if current ~= "" then
table.insert(segments, current)
end
return segments
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local segments = EtymonParser.parse_balanced_segments(ety_string)
if #segments == 0 then
error("Empty inline etymology")
end
local keyword = M.string_utilities.trim(segments[1])
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword(get_canonical_keyword(keyword)) }
for i = 2, #segments do
local segment = segments[i]
if segment:sub(1, 1) == "<" and segment:sub(-1) == ">" then
local inner = segment:sub(2, -2)
if inner ~= "" then
table.insert(args, inner)
end
elseif is_keyword(segment, true) then
-- Handle keywords that appear between bracketed segments
table.insert(args, normalize_keyword(get_canonical_keyword(get_keyword(segment, true))))
end
end
return args
end
function EtymonParser.parse_etymon(param, context_lang)
if is_keyword(param) then
return nil
end
if type(param) ~= "string" then
return nil
end
local lang, rest
local is_family = false
local before_bracket = param:match("^([^<]*)") or param
local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$")
if lang_code then
local potential_lang = Util.get_lang(lang_code, true)
if potential_lang then
lang = potential_lang
rest = param:sub(#lang_code + 2)
else
local potential_family = Util.get_family(lang_code)
if potential_family then
lang = potential_family
rest = param:sub(#lang_code + 2)
is_family = true
else
lang = context_lang
rest = param
end
end
else
lang = context_lang
rest = param
end
if rest == "" then
M.track("etymon/term/empty")
elseif rest == "?" then
M.track("etymon/term/question-mark")
elseif rest == "-" then
M.track("etymon/term/hyphen")
end
if rest == "" then
return {
lang = lang,
term = nil,
unknown_term = true,
is_family = is_family,
}
end
if rest == "-" then
return {
lang = lang,
term = nil,
suppress_term = true,
is_family = is_family,
}
end
if not rest:find("<", 1, true) then
return {
lang = lang,
term = M.string_utilities.trim(rest),
is_family = is_family,
}
end
local term_text = rest:match("^([^<]*)") or ""
local is_unknown = (term_text == "")
local is_suppress = (term_text == "-")
local function generate_obj(ignored_term)
return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) }
end
local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods)
local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults,
{ param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj })
if parsed_obj.id and parsed_obj.id:match("^!") then
parsed_obj.id = parsed_obj.id:sub(2)
parsed_obj.override = true
end
parsed_obj.lang = lang
parsed_obj.is_family = is_family
if is_unknown then
parsed_obj.unknown_term = true
elseif is_suppress then
parsed_obj.suppress_term = true
end
return parsed_obj
end
function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code)
-- id is now optional, so only validate if provided
if id then
if mw.ustring.len(id) < 2 then
error("The `id` parameter must have at least two characters.")
end
if id == title or id == Util.get_page_data().pagename then
error("The `id` parameter must not be the same as the page title.")
end
end
local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true }
if pos and not valid_pos[pos] then
error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".")
end
local current_keyword = "from"
local etymons_in_group = {}
local keywords = M.data.keywords
local function checkGroup()
if keywords[current_keyword] and keywords[current_keyword].is_group and current_keyword ~= "affix" and current_keyword ~= "surf" and current_keyword ~= "afeq" and current_keyword ~= "univerbation" and #etymons_in_group <= 1 then
error("Detected `:" .. current_keyword .. "` group with fewer than two etymons.")
end
etymons_in_group = {}
end
local start_index = starts_with_lang_code and 2 or 1
for i = start_index, #args do
local param = args[i]
if type(param) ~= "string" then
elseif param:sub(1, 1) == ":" and not is_keyword(param) then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkGroup()
current_keyword = get_canonical_keyword(get_keyword(param))
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(etymons_in_group, param)
local param_lang = etymon_data.lang
if etymon_data.is_family and current_keyword == "inherited" then
error("`:inh` does not support family codes; use a specific language.")
end
if etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
if current_keyword == "from" and param_lang:getFullCode() ~= lang:getFullCode() then
error("`:from` is for same-language derivation, but language does not match. " ..
"Expected '" .. lang:getFullCode() .. "', got '" .. param_lang:getFullCode() .. "'.")
elseif current_keyword == "inherited" then
M.etymology.check_ancestor(lang, param_lang)
end
-- Check modifier restrictions
EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods)
-- postype must be "root" or "word"
local VALID_POSTYPES = { root = true, word = true }
if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then
error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".")
end
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true)
end
else
table.insert(etymons_in_group, param)
end
end
end
checkGroup()
end
local DataRetriever = {}
-- Given an etymon data, scrape its page and cache the result in the global state object.
function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel)
local content = etymon_title:getContent()
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.REDLINK
return
end
-- Check if the linked page is a redirect. If it is, the template parsing
-- code below will be effectively skipped, and `scrape_page` will be called
-- again on the redirect target (see the bottom of this function)
local lang_section_for_descendants = nil
local redirect_target = etymon_title.redirect_target
if not redirect_target then
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
return
end
lang_section_for_descendants = content
end
local etymon_lang_code = etymon_lang:getFullCode()
local lang_page_key = etymon_lang_code .. ":" .. etymon_page
local found_templates_for_lang = {}
local found_ids = {}
local get_node_class = M.template_parser.class_else_type
-- Look for all {{etymon}} templates within the page content using the template parser
-- This way the same page is never parsed more than once
-- Build a map from senseids to their parent etymonids.
local active_etymon_args = nil
for node in M.template_parser.parse(content):iterate_nodes() do
local node_class = get_node_class(node)
if node_class == "heading" then
-- A new L2 or etymology section acts as a barrier: an {{etymon}} usage
-- used previously cannot be the parent of any subsequent senseids.
-- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above.
if node:get_name():find("^Etymology") then
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if template_name == "etymon" then
local template_args = node:get_arguments()
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
table.insert(found_templates_for_lang, template_args)
if template_args.id then
local etymon_key = lang_page_key .. ":" .. template_args.id
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, template_args.id)
active_etymon_args = template_args
else
-- Store idless etymon with default key
local etymon_key = lang_page_key .. ":*"
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, "*")
active_etymon_args = template_args
end
end
elseif active_etymon_args and template_name == "senseid" then
local template_args = node:get_arguments()
-- This should always be true for proper usages of {{senseid}}.
if template_args[1] == etymon_lang_code and template_args[2] then
local sense_id_key = lang_page_key .. ":" .. template_args[2]
__state.senseid_parent_etymon[sense_id_key] = active_etymon_args
__state.cached_etymon_pages[sense_id_key] = tostring(etymon_page)
end
end
end
end
if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then
M.descendants.cache_page_checks({
lang_section = lang_section_for_descendants,
etymon_lang_code = etymon_lang_code,
found_templates_for_lang = found_templates_for_lang,
entry_title = __state.entry_title,
entry_lang_code = __state.entry_lang_code,
entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil,
cached_descendants_checks = __state.cached_descendants_checks,
lang_page_key = lang_page_key,
redirected_from = redirected_from,
})
end
local id_data_list = {}
for _, args in ipairs(found_templates_for_lang) do
local id = args.id or "*"
table.insert(id_data_list, { id = id, pos = args.pos })
end
__state.available_etymon_ids[lang_page_key] = id_data_list
if #found_templates_for_lang == 1 then
__state.single_etymons[lang_page_key] = found_templates_for_lang[1]
end
if redirected_from and __state.available_etymon_ids[lang_page_key] then
__state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {}
for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do
table.insert(__state.available_etymon_ids[redirected_from], id_data)
end
end
if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then
-- All done!
return
elseif redirect_target and not redirected_from then
-- Try scraping the redirect.
etymon_page = redirect_target.prefixedText
DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel)
__state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id]
else
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
end
end
-- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name.
function DataRetriever.get_etymon_args(etymon_data, is_toplevel)
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
if etymon_data.id then
local key = base_key .. ":" .. etymon_data.id
local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key]
if cached_args == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel)
end
cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh
-- Get etymon_id from parent if this was resolved via senseid
local parent_etymon = __state.senseid_parent_etymon[key]
local resolved_etymon_id = parent_etymon and parent_etymon.id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
end
end
return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check
else
__state.used_idless_etymon = true
if is_toplevel then
__state.toplevel_idless_etymon = true
end
if __state.available_etymon_ids[base_key] == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel)
end
local ids = __state.available_etymon_ids[base_key] or {}
local count = #ids
-- Try to filter by postype if available and we have multiple candidates
if count > 1 and etymon_data.postype then
local matching_ids = {}
for _, id_data in ipairs(ids) do
if id_data.pos == etymon_data.postype then
table.insert(matching_ids, id_data)
end
end
if #matching_ids == 1 then
local matched_id = matching_ids[1].id
local matched_key = base_key .. ":" .. matched_id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id = matched_id },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id = matched_id },
})
end
end
return __state.cached_etymon_args[matched_key], __state.cached_etymon_pages[matched_key], nil, descendants_check
end
end
if count == 1 then
local only_id_data = ids[1]
local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*"
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id_data = only_id_data },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id_data = only_id_data },
})
end
end
return __state.single_etymons[base_key], __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
local page_link = M.links.full_link({
term = page,
lang = norm_lang,
no_generate_forms = true,
}, "term")
local function format_id_hint(id_data, idx)
local id = type(id_data) == "table" and id_data.id or id_data
local pos = type(id_data) == "table" and id_data.pos
if id and id ~= "" and id ~= "*" then
return '"' .. id .. '"'
end
if pos and pos ~= "" then
return "unnamed (|pos=" .. pos .. "|)"
end
return "etymon #" .. idx .. " (no |id= on page)"
end
local id_list = {}
local all_idless = true
local target_has_idless = false
for i, id_data in ipairs(ids) do
local id = type(id_data) == "table" and id_data.id or id_data
if id and id ~= "" and id ~= "*" then
all_idless = false
else
target_has_idless = true
end
table.insert(id_list, format_id_hint(id_data, i))
end
if is_toplevel and target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local any_pos = false
for _, id_data in ipairs(ids) do
local pos = type(id_data) == "table" and id_data.pos
if pos and pos ~= "" then
any_pos = true
break
end
end
local suggestion_text
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. count .. " {{etymon}} templates for "
.. norm_lang:getCanonicalName() .. ")."
if all_idless then
if any_pos then
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. mw.text.listToText(id_list) .. "."
else
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)."
end
else
suggestion_text = " Specify which one with `<id:identifier>` after the term. Options: " .. mw.text.listToText(id_list) .. "."
end
Util.add_warning(lead .. suggestion_text, true)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local TreeBuilder = {}
local function parse_etymon_references(refs_text)
if not refs_text or refs_text == "" then
return ""
end
return M.references.parse_references(refs_text)
end
local function parse_tree_references(node)
if node.ref then
node.parsed_ref = parse_etymon_references(node.ref)
end
if node.children then
for _, container in ipairs(node.children) do
if container.terms then
for _, term in ipairs(container.terms) do
parse_tree_references(term)
end
end
end
end
end
-- Build a unique key for deduplication in the seen table
function TreeBuilder.build_key(lang, title, args)
local norm_lang_code = Util.get_norm_lang(lang):getFullCode()
local is_table = type(args) == "table"
local id = (is_table and args.id) or ""
if title then
return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id
end
if is_table and args.status == M.data.STATUS.INLINE then
local content_parts = {}
for i = 1, #args do
content_parts[i] = tostring(args[i])
end
return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0")
end
return norm_lang_code .. ":*:" .. id
end
function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion)
seen = seen or {}
depth = depth or 0
local is_toplevel = (depth == 0)
if depth > __state.max_depth_reached then
__state.max_depth_reached = depth
end
__state.total_nodes = __state.total_nodes + 1
local lang_code = lang:getCode()
__state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1
local current_id = (type(args) == "table" and args.id) or ""
local key = TreeBuilder.build_key(lang, title, args)
local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK }
if type(args) ~= "table" or seen[key] then
node.status = args or M.data.STATUS.MISSING
-- Mark as duplicate if we've seen this node before
if seen[key] then
node.is_duplicate = true
node.duplicate_key = key
local original_node = seen[key]
if type(original_node) == "table" and original_node.children and #original_node.children > 0 then
node.original_has_children = true
end
end
return node
end
node.status = args.status or M.data.STATUS.OK
seen[key] = node
-- If stop_recursion is set, skip parsing children but check for visible children
if stop_recursion then
local keywords = M.data.keywords
local has_visible_children = false
for i = 2, #args do
local param = args[i]
if type(param) == "string" then
local keyword_base = get_keyword_base(param)
if keyword_base and keywords[keyword_base] then
-- It's a keyword, check if visible in tree (invisible "all" or "tree" = hidden in tree)
local keyword_info = keywords[keyword_base]
local inv = keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
elseif param:sub(1, 1) ~= ":" then
-- It's a term (not a keyword), so there are visible children
has_visible_children = true
break
end
end
end
node.has_visible_children = has_visible_children
return node
end
-- Parse args into keyword containers
local current_keyword = "from"
local current_keyword_modifiers = {}
local current_container = nil
-- Helper to track keyword usage at top level
local function track_keyword_usage(keyword, target_lang, source_lang)
if not is_toplevel then return end
if not __state.toplevel_keyword_stats[keyword] then
__state.toplevel_keyword_stats[keyword] = {
count = 0,
target_langs = {},
source_langs = {},
}
end
local keyword_data = __state.toplevel_keyword_stats[keyword]
keyword_data.count = keyword_data.count + 1
local target_code = target_lang:getCode()
keyword_data.target_langs[target_code] = (keyword_data.target_langs[target_code] or 0) + 1
if source_lang then
local source_code = source_lang:getCode()
keyword_data.source_langs[source_code] = (keyword_data.source_langs[source_code] or 0) + 1
end
end
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
current_container = {
keyword = current_keyword,
keyword_info = M.data.keywords[current_keyword],
keyword_modifiers = current_keyword_modifiers,
terms = {},
}
table.insert(node.children, current_container)
-- Override keyword text/phrase for nominalization with <g:code>
if current_keyword_modifiers.g and current_keyword == "nominalization" then
local labels = get_nominalization_label_for_g(current_keyword_modifiers.g)
if not labels then
local codes = {}
for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end
table.sort(codes)
error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", "))
end
current_container.keyword_info = {}
for k, v in pairs(M.data.keywords[current_keyword]) do current_container.keyword_info[k] = v end
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
end
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
current_keyword = keyword
current_keyword_modifiers = modifiers
current_container = nil -- Force new container for new keyword
elseif type(param) == "string" and param:sub(1, 1) == ":" then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif type(param) == "string" then
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
-- Track keyword usage at top level
track_keyword_usage(current_keyword, lang, etymon_data.lang)
local term_node = {}
-- Handle suppress_term (-) and unknown_term (empty) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
ensure_container()
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1)
else
term_node = {
lang = etymon_data.lang,
children = {},
status = M.data.STATUS.OK,
}
end
term_node.suppress_term = etymon_data.suppress_term
term_node.unknown_term = etymon_data.unknown_term
term_node.is_family = etymon_data.is_family
term_node.is_uncertain = etymon_data.unc
term_node.ref = etymon_data.ref
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.pos = etymon_data.pos
term_node.ng = etymon_data.ng
term_node.lit = etymon_data.lit
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
else
-- Regular term: fetch arguments from page
local etymon_args, page_of, resolved_etymon_id, descendants_check = DataRetriever.get_etymon_args(etymon_data, is_toplevel)
if etymon_data.id and etymon_args == M.data.STATUS.MISSING and not etymon_data.ety then
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
local available_ids = __state.available_etymon_ids[base_key] or {}
if #available_ids > 0 then
__state.has_mismatched_id = true
end
end
-- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing
if etymon_data.ety then
if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then
__state.current_page_has_inline_etymology = true
if is_toplevel then
__state.toplevel_has_inline_etymology = true
end
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
-- Track inline ety keywords too
local inline_keyword = get_keyword(inline_args[2], true)
if inline_keyword and #inline_args >= 3 then
local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang)
if inline_etymon then
track_keyword_usage(inline_keyword, etymon_data.lang, inline_etymon.lang)
end
end
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
etymon_args = inline_args
term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent
else
-- Scraped arguments exist, <ety> is redundant and ignored
__state.current_page_has_redundant_etymology = true
if is_toplevel then
__state.toplevel_redundant_etymology = true
end
end
end
-- Ensure container exists before checking keyword info
ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = current_container.keyword_info
local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories))
term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion)
term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() ..
":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang)
term_node.id = etymon_data.id
term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.pos = etymon_data.pos
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.ng = etymon_data.ng
term_node.ref = etymon_data.ref
term_node.is_uncertain = etymon_data.unc
term_node.override = etymon_data.override
term_node.page_of = page_of
term_node.aftype = etymon_data.aftype
term_node.postype = etymon_data.postype
term_node.bor = etymon_data.bor
term_node.lbor = etymon_data.lbor
term_node.slbor = etymon_data.slbor
term_node.lit = etymon_data.lit
term_node.is_family = etymon_data.is_family
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
term_node.missing_descendants_header, term_node.missing_descendants_entry =
M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check)
end
table.insert(current_container.terms, term_node)
end
end
end
return node
end
-- Convert etymology tree to JSON-serializable table
local function tree_to_json(node)
local obj = {
term = node.title,
lang = node.lang:getCode(),
lang_name = node.lang:getCanonicalName(),
id = (node.id and node.id ~= "") and node.id or nil,
status = node.status,
is_uncertain = node.is_uncertain or nil,
is_duplicate = node.is_duplicate or nil,
gloss = node.t,
transliteration = node.tr,
transcription = node.ts,
alt = node.alt,
g = node.g,
pos = node.pos,
ng = node.ng,
children = {},
}
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if keyword_info then
local container_obj = {
keyword = container.keyword,
keyword_label = keyword_info.text,
keyword_abbrev = keyword_info.abbrev,
is_group = keyword_info.is_group or nil,
is_invisible = keyword_info.invisible or nil,
is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil,
terms = {},
}
for _, term in ipairs(container.terms or {}) do
table.insert(container_obj.terms, tree_to_json(term))
end
table.insert(obj.children, container_obj)
end
end
return obj
end
local function track_ranges(base_key, value, ranges, lang_code)
M.track("etymon/" .. base_key .. "/" .. value)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. value)
end
for _, range in ipairs(ranges) do
local matches = false
if range.min and range.max then
matches = value >= range.min and value <= range.max
elseif range.min then
matches = value >= range.min
elseif range.max then
matches = value <= range.max
elseif range.exact then
matches = value == range.exact
end
if matches then
M.track("etymon/" .. base_key .. "/" .. range.label)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. range.label)
end
break
end
end
end
local function track_title_pagename_mismatch_after_strip(lang)
local lang_code = lang:getCode()
M.track("etymon/title/pagename-mismatch-after-strip-diacritics")
M.track("etymon/lang/" .. lang_code .. "/title/pagename-mismatch-after-strip-diacritics")
end
-- Build and return the etymology data tree for a given term.
function export.get_tree(lang, title, args, options)
options = options or {}
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
if options.validate then
EtymonParser.validate(lang, args, options.id, title, options.pos, false)
end
local lang_code = lang:getCode()
local start_index = (args[1] == lang_code) and 2 or 1
local tree_args = { [1] = lang_code, id = options.id or args.id }
for i = start_index, #args do
table.insert(tree_args, args[i])
end
__state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args
local ety_data_tree = TreeBuilder.build(lang, title, tree_args)
parse_tree_references(ety_data_tree)
if options.json then
return M.JSON.toJSON(tree_to_json(ety_data_tree))
end
return ety_data_tree
end
-- Given a language code, page name and optionally the id= parameter,
-- render the tree and only the etymology tree for the relevant page.
-- Fetches and parses the corresponding {{etymon}} from the requested page,
-- and any further pages needed to render the tree.
-- Parameters can be passed either through the #invoke or as
-- template parameters *through* an #invoke.
function export.render_tree_for_etymon_on_page(frame)
local frame_args = frame.args
local parent_args = frame:getParent().args
local langcode = frame_args[1] or parent_args[1]
local pagename = frame_args[2] or parent_args[2]
local id = frame_args["id"] or parent_args["id"]
local display_title = frame_args["title"] or parent_args["title"]
local parsed_title = mw.title.new(pagename, 0)
local title
if parsed_title.namespace == 0 then
title = M.pages.safe_page_name(parsed_title)
elseif parsed_title.namespace == 118 then
title = "*" .. M.pages.safe_page_name(parsed_title)
else
error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace)
end
local lang = Util.get_lang(langcode)
-- Construct etymon_data for DataRetriever.get_args.
local etymon_data = {
lang = lang,
term = title,
id = id
}
local args, pagename = DataRetriever.get_etymon_args(etymon_data, true)
if args == M.data.STATUS.MISSING then
error("The etymon template was not found (language " ..
langcode ..
", title '" ..
title ..
"'" ..
(id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.")
end
local tree_title = display_title or title
if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then
track_title_pagename_mismatch_after_strip(lang)
end
local ety_data_tree = export.get_tree(lang, tree_title, args, {
validate = true,
id = id,
})
local output = {}
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
return table.concat(output)
end
function export.main(frame)
local parent_args = frame:getParent().args
local args = M.parameters.process(parent_args, M.parameters_data.etymon)
local lang = args[1]
local etymon_args = args[2]
local id = args.id
local title = args.title
local text = args.text
local tree = args.tree
local etydate = args.etydate
local rfe = args.rfe
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == "Reconstruction" then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == "Reconstruction" then
entry_pagename = "*" .. entry_pagename
end
if lang:stripDiacritics(M.links.remove_links(title)) ~= lang:stripDiacritics(M.links.remove_links(entry_pagename)) then
track_title_pagename_mismatch_after_strip(lang)
end
local current_L2 = M.pages.get_current_L2()
if current_L2 then
local norm_lang = "Bahasa " .. Util.get_norm_lang(lang)
local norm_name = "Bahasa " .. norm_lang:getCanonicalName()
if current_L2 ~= norm_name then
local lang_desc = lang:getCode() .. " (Bahasa " .. lang:getCanonicalName() .. ")"
if norm_lang:getCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", normalized to " .. norm_lang:getCode() .. " (Bahasa " .. norm_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
end
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
})
if args.json then
return ety_data_tree
end
local output = {}
local text_allowlist_mode = M.text_allowed.default_mode or "off"
if text and text_allowlist_mode ~= "off" and not Util.is_text_param_allowed_for_lang(lang) then
local msg = "Etymology texts (parameter <code>text=</code>) are not allowed for " .. lang:getFullName() ..
"; see [[Template:etymon#Text allowlist|Template:etymon § Text allowlist]] for the list of languages that may use the <code>text=</code> parameter."
if text_allowlist_mode == "error" then
error(msg)
else
Util.add_warning(msg, true)
end
end
local lang_exc = Util.get_lang_exception(lang)
if lang_exc and lang_exc.disallow then
local disallow = lang_exc.disallow
local error_text = " for " .. lang:getFullName()
if disallow.ref then
error_text = error_text .. "; see " .. disallow.ref
else
error_text = error_text .. "."
end
if tree and disallow.tree then
error("Etymology trees are not allowed" .. error_text)
end
if text and disallow.text then
error("Etymology texts are not allowed" .. error_text)
end
end
if etydate then
local etydate_param_mods = {
ref = { list = true, type = "references", allow_holes = true },
refn = { list = true, allow_holes = true },
nocap = { type = "boolean" },
}
local function generate_etydate_obj(etydate_text)
local etydate_specs = {}
for spec in etydate_text:gmatch("[^,]+") do
table.insert(etydate_specs, mw.text.trim(spec))
end
return { [1] = etydate_specs }
end
local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj })
local etydate_args = {
[1] = parsed_etydate[1],
nocap = parsed_etydate.nocap or false,
}
ety_data_tree.etydate = M.etydate.format_etydate(etydate_args, { omit_refs = true })
if parsed_etydate.ref and #parsed_etydate.ref > 0 then
ety_data_tree.etydate_refs = parsed_etydate.ref
end
end
if tree then
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
end
-- Check if there are any visible children in tree (invisible "all" or "tree" = hidden in tree)
local has_visible_children = false
for _, child in ipairs(ety_data_tree.children or {}) do
local child_keyword_info = child.keyword_info
local inv = child_keyword_info and child_keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
end
local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree
local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree))
local anchor = M.anchors.etymonid(lang, id, {
no_tree = args.notree,
title = title,
empty_tree = (not has_visible_children) or tree_disallowed,
ety_tree_json = ety_tree_json,
})
table.insert(output, anchor)
if text then
local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink
if text == "++" then
max_depth, stop_at_blue_link = false, false
elseif text == "+" then
max_depth, stop_at_blue_link = 1, false
elseif text == "*" then
max_depth, stop_at_blue_link = false, true
elseif text:match("^:[^*]+%*$") then
-- Stop at a specific language OR first bluelink after it, e.g., ":ota*"
-- If the target language is a redlink, continue to the first bluelink
local lang_code = text:match("^:([^*]+)%*$")
if lang_code and lang_code ~= "" then
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang_or_bluelink = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
elseif text:sub(1, 1) == ":" then
-- Stop at a specific language, e.g., ":ar" stops at first Arabic term
local lang_code = text:sub(2)
if lang_code ~= "" then
-- Validate the language code
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
local num = tonumber(text)
if num and num >= 1 then
max_depth, stop_at_blue_link = num, false
else
error('Invalid text value "' ..
text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)')
end
end
table.insert(output, M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
}))
end
if rfe then
local rfe_param_mods = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
}
local function generate_rfe_obj(rfe_text)
-- Check if it's a boolean true value
if M.yesno(rfe_text, false) then
return { is_boolean = true }
else
return { text = rfe_text }
end
end
local rfe_with_defaults = Util.add_boolean_defaults(rfe, rfe_param_mods)
local parsed_rfe = M.parse_utilities.parse_inline_modifiers(rfe_with_defaults, {
param_mods = rfe_param_mods,
generate_obj = generate_rfe_obj
})
local rfe_args = {
[1] = lang:getCode(),
nocat = parsed_rfe.nocat,
sort = parsed_rfe.sort,
y = parsed_rfe.y,
m = parsed_rfe.m,
fragment = parsed_rfe.fragment,
section = parsed_rfe.section,
box = parsed_rfe.box,
noes = parsed_rfe.noes,
}
if not parsed_rfe.is_boolean then
rfe_args[2] = parsed_rfe.text
end
table.insert(output, frame:expandTemplate({
title = "rfe",
args = rfe_args
}))
end
if Util.is_content_page() and __state.max_depth_reached > 0 then
local lang_code = lang:getCode()
local depth_ranges = {
{ min = 50, label = "extremely-deep" },
{ min = 20, label = "20+" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ max = 2, label = "1-2" }
}
local node_ranges = {
{ min = 100, label = "extremely-large" },
{ min = 50, label = "50+" },
{ min = 20, max = 49, label = "20-49" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ max = 4, label = "1-4" }
}
local language_ranges = {
{ min = 10, label = "10+" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ exact = 2, label = "2" },
{ exact = 1, label = "1" }
}
track_ranges("depth", __state.max_depth_reached, depth_ranges, lang_code)
track_ranges("nodes", __state.total_nodes, node_ranges, lang_code)
local unique_languages = 0
for _ in pairs(__state.language_count) do
unique_languages = unique_languages + 1
end
track_ranges("unique-languages", unique_languages, language_ranges, lang_code)
if __state.total_nodes == __state.max_depth_reached + 1 then
track_ranges("linear-depth", __state.max_depth_reached, depth_ranges, lang_code)
end
end
local categories = {}
if Util.is_content_page() then
local should_suppress_categories = lang_exc and lang_exc.suppress_categories
if not should_suppress_categories and not args.nocat then
categories = M.categories.render({
data_tree = ety_data_tree,
page_lang = lang,
available_etymon_ids = __state.available_etymon_ids,
senseid_parent_etymon = __state.senseid_parent_etymon,
get_norm_lang_func = Util.get_norm_lang,
lang_exc = lang_exc,
})
end
local target_lang_code = lang:getCode()
for keyword, keyword_data in pairs(__state.toplevel_keyword_stats) do
-- Track keyword globally
M.track("etymon/keyword/" .. keyword)
-- Track keyword per target language
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code)
-- Track keyword per source language
for source_code, count in pairs(keyword_data.source_langs) do
M.track("etymon/keyword/" .. keyword .. "/source/" .. source_code)
-- Track keyword per target+source combination
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code .. "/source/" .. source_code)
end
end
if tree then
table.insert(categories, "Pages with etymology trees")
table.insert(categories, lang:getCanonicalName() .. " entries with etymology trees")
end
if text then table.insert(categories, lang:getCanonicalName() .. " entries with etymology texts") end
if args.exnihilo then table.insert(categories, lang:getCanonicalName() .. " terms coined ex nihilo") end
if __state.toplevel_has_inline_etymology then
table.insert(categories, "Pages with inline etymon for redlinks")
end
if __state.toplevel_redundant_etymology then
table.insert(categories, "Pages with redundant inline etymon")
end
if __state.toplevel_idless_etymon then
table.insert(categories, "Pages using etymon with no ID")
end
if __state.has_mismatched_id then
table.insert(categories, lang:getCanonicalName() .. " entries referencing etymons with mismatched IDs")
end
if __state.linked_page_multiple_etymons_idless then
table.insert(categories,
lang:getCanonicalName() .. " entries referencing pages with multiple etymons missing IDs")
end
end
if #categories > 0 then
table.insert(output, M.categories.format(categories, lang))
end
if __state.warnings then
for i, warning in ipairs(__state.warnings) do
table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n")
end
end
return table.concat(output)
end
return export
i3qmyo14t6emnj9rtl3gahpnditiud4
342825
342824
2026-05-16T13:49:33Z
Hakimi97
2668
342825
Scribunto
text/plain
--[=[
This module implements the {{etymon}} template for structured etymology data on Wiktionary.
It enables the creation of etymology trees and text by parsing etymon chains,
scraping linked pages for their own {{etymon}} data, and recursively building a tree
of derivational relationships.
Authors:
- Original implementation: [[User:Ioaxxere]]
- Full refactor (September 2025): [[User:Fenakhay]] ([[Special:Diff/86717746]])
Modules:
- [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping
- [[Module:etymon/data]]: keyword definitions, configuration, and status constants
- [[Module:etymon/tree]]: etymology tree rendering
- [[Module:etymon/text]]: etymology text generation
- [[Module:etymon/categories]]: category generation logic
]=]
local export = {}
local etymon_data_module = "Module:etymon/data"
local etymon_text_module = "Module:etymon/text"
local etymon_tree_module = "Module:etymon/tree"
local etymon_categories_module = "Module:etymon/categories"
local etymon_descendants_module = "Module:etymon/descendants"
local __state = {
cached_etymon_args = {},
cached_etymon_pages = {},
cached_descendants_checks = {},
senseid_parent_etymon = {},
available_etymon_ids = {},
single_etymons = {},
entry_title = nil,
entry_lang_code = nil,
current_page_has_inline_etymology = false,
current_page_has_redundant_etymology = false,
used_idless_etymon = false,
toplevel_has_inline_etymology = false,
toplevel_redundant_etymology = false,
toplevel_idless_etymon = false,
has_mismatched_id = false,
linked_page_multiple_etymons_idless = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
warnings = {},
}
local loader = require("Module:module loader")
local M = loader.init({
require = {
data = etymon_data_module,
tree = etymon_tree_module,
text = etymon_text_module,
categories = etymon_categories_module,
descendants = etymon_descendants_module,
anchors = "Module:anchors",
etydate = "Module:etydate",
etymology = "Module:etymology",
families = "Module:families",
languages = "Module:languages",
languages_errorgetby = "Module:languages/errorGetBy",
links = "Module:links",
pages = "Module:pages",
parameters = "Module:parameters",
string_utilities = "Module:string utilities",
template_parser = "Module:template parser",
utilities = "Module:utilities",
debug = "Module:debug",
en_utilities = "Module:en-utilities",
parse_utilities = "Module:parse utilities",
references = "Module:references",
track = "Module:debug/track",
template_styles = "Module:TemplateStyles",
script_utilities = "Module:script utilities",
JSON = "Module:JSON",
yesno = "Module:yesno",
},
loadData = {
headword_data = "Module:headword/data",
parameters_data = "Module:parameters/data",
text_allowed = "Module:etymon/data/text_allowed",
},
})
local Util = {}
function Util.format_error(message, preview_only)
if preview_only and not M.pages.is_preview() then
return nil
end
return '<span class="error">' .. message .. '</span>'
end
function Util.add_warning(message, preview_only)
local formatted = Util.format_error(message, preview_only)
if formatted then
table.insert(__state.warnings, formatted)
end
end
function Util.is_text_param_allowed_for_lang(lang)
if not lang or type(lang) ~= "table" then
return false
end
local types = lang.getTypes and lang:getTypes()
if types and types.family then
local code = lang.getCode and lang:getCode()
return code and M.text_allowed.families[code] == true
end
local full_code = lang.getFullCode and lang:getFullCode()
if full_code and M.text_allowed.langs[full_code] then
return true
end
if lang.inFamily then
for family_code in pairs(M.text_allowed.families) do
if lang:inFamily(family_code) then
return true
end
end
end
return false
end
function Util.get_lang(code, no_error)
if no_error then
return M.languages.getByCode(code, nil, true)
end
return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true)
end
function Util.get_family(code)
return M.families.getByCode(code)
end
function Util.get_lang_exception(lang)
-- Families have no language-specific exceptions
if lang.getTypes and lang:getTypes().family then
return nil
end
local code = lang:getCode()
local lang_exceptions = M.data.config.lang_exceptions
if lang_exceptions[code] then
return lang_exceptions[code]
end
for norm_code, exc in pairs(lang_exceptions) do
if exc.normalize_to and code == exc.normalize_to then
return exc
end
if exc.normalize_from_families then
local should_normalize = false
for _, family in ipairs(exc.normalize_from_families) do
if lang:inFamily(family) then
should_normalize = true
break
end
end
if should_normalize and exc.normalize_exclude_families then
for _, family in ipairs(exc.normalize_exclude_families) do
if lang:inFamily(family) then
should_normalize = false
break
end
end
end
if should_normalize then
local ret = {}
for k, v in pairs(exc) do
ret[k] = v
end
ret.suppress_tr = nil
return ret
end
end
end
return nil
end
function Util.get_norm_lang(lang)
local exc = Util.get_lang_exception(lang)
if exc and exc.normalize_to then
return M.languages.getByCode(exc.normalize_to)
end
return lang
end
-- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>)
-- This is needed because Module:parse utilities expects boolean modifiers to have explicit values
function Util.add_boolean_defaults(str, param_mods)
local result = str
for name, spec in pairs(param_mods) do
if spec.type == "boolean" then
-- Replace <name> with <name:1> (but not <name:...> which already has a value)
result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>")
end
end
return result
end
-- Centralized term formatting: handles suppress_term, unknown_term, and regular terms
function Util.format_term(term, is_toplevel, opts)
opts = opts or {}
-- suppress_term (-) returns nil
if term.suppress_term then
return nil
end
local lang = term.lang
local exc = Util.get_lang_exception(lang)
if is_toplevel then
local display_text = term.alt or term.title or ""
local sc = term.sc or lang:findBestScript(display_text)
local bold_text = tostring(mw.html.create("strong")
:addClass("selflink")
:wikitext(display_text))
return M.script_utilities.tag_text(bold_text, lang, sc, "term")
end
local link_params = { lang = lang }
link_params.term = not term.unknown_term and term.title or nil
link_params.alt = term.alt
link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil
if not (exc and exc.suppress_tr) then
link_params.tr = term.tr
link_params.ts = term.ts
else
link_params.suppress_tr = true
end
link_params.lit = (opts.lit ~= "suppress") and term.lit or nil
if opts.gloss ~= "suppress" then
link_params.gloss = term.t
end
if term.g and term.g ~= "" then
local genders = M.string_utilities.split(term.g, ",")
for i = 1, #genders do
genders[i] = M.string_utilities.trim(genders[i])
end
link_params.genders = genders
end
if opts.pos ~= "suppress" then
link_params.pos = term.pos
link_params.ng = term.ng
end
if exc and exc.suppress_tr then
link_params.lit = nil
end
local show_qualifiers
if opts.tree_ql ~= "suppress" then
if term.q then
link_params.q = term.q
end
if term.qq then
link_params.qq = term.qq
end
if term.l then
link_params.l = term.l
end
if term.ll then
link_params.ll = term.ll
end
show_qualifiers = term.q or term.qq or term.l or term.ll
end
return M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
end
local __is_content_page_cached
function Util.is_content_page()
if __is_content_page_cached == nil then
__is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle())
end
return __is_content_page_cached
end
local __page_data_cached
function Util.get_page_data()
if not __page_data_cached then
__page_data_cached = M.headword_data.page
end
return __page_data_cached
end
-- Extract base keyword from param (without modifiers)
local function get_keyword_base(param)
if type(param) ~= "string" then return nil end
local base = param:match("^:?([^<]+)") or param:gsub("^:", "")
return base
end
local function is_keyword(param, allow_colon_less)
if type(param) ~= "string" then return false end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
if allow_colon_less then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
return false
end
local function get_keyword(param, allow_colon_less)
if type(param) ~= "string" then return nil end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
return get_keyword_base(param)
end
if allow_colon_less then
local base = get_keyword_base(param)
if keywords[base] then
return base
end
end
return nil
end
local function normalize_keyword(keyword)
if keyword:sub(1, 1) == ":" then
return keyword
end
return ":" .. keyword
end
-- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries
local function get_canonical_keyword(keyword)
if not keyword then return keyword end
return M.data.keyword_canonical[keyword] or keyword
end
-- Build text/phrase for nominalization with <g:code> (uses data module for codes only).
local function get_nominalization_label_for_g(code)
if not code or code == "" then return nil end
local codes = M.data.nominalization_g_codes
local adj = codes[code]
if not adj and #code == 2 then
local gender_adj = codes[code:sub(1, 1)]
local number_adj = codes[code:sub(2, 2)]
if gender_adj and number_adj then
adj = gender_adj .. " " .. number_adj
end
end
if not adj then return nil end
local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of"
local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false)
return { text = text, phrase = phrase }
end
local EtymonParser = {}
-- Keyword modifier definitions
EtymonParser.keyword_param_mods = {
unc = { type = "boolean" },
ref = {},
text = { restrict = { keywords = { "from", "derived" } } },
lit = { restrict = { keywords = { "affix", "surf", "univerbation" } } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
}
-- Term modifier definitions
EtymonParser.etymon_param_mods = {
id = {},
t = {},
tr = {},
ts = {},
q = {},
qq = {},
l = {},
ll = {},
pos = {},
ng = {},
alt = {},
g = {},
ety = {},
lit = {},
unc = { type = "boolean" },
ref = {},
aftype = { restrict = { keywords = { "affix", "surf", "afeq" } } },
postype = {},
bor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
slbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
lbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
}
local function get_clean_param_mods(param_mods)
local clean = {}
for mod_name, mod_def in pairs(param_mods) do
clean[mod_name] = {}
for key, value in pairs(mod_def) do
if key ~= "restrict" then
clean[mod_name][key] = value
end
end
end
return clean
end
function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods)
for mod_name, mod_value in pairs(modifiers) do
-- Only check restrictions if the modifier has a non-false/nil value
if mod_value then
local mod_def = param_mods[mod_name]
if mod_def and mod_def.restrict and mod_def.restrict.keywords then
local allowed_keywords = mod_def.restrict.keywords
local is_allowed = false
for _, allowed_keyword in ipairs(allowed_keywords) do
if current_keyword == allowed_keyword then
is_allowed = true
break
end
end
if not is_allowed then
local keyword_list = {}
for _, kw in ipairs(allowed_keywords) do
table.insert(keyword_list, ":" .. kw)
end
local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ")
if #keyword_list > 2 then
-- Replace last comma with "or"
keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1")
end
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".")
end
end
end
end
end
-- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>")
function EtymonParser.parse_keyword_modifiers(param)
if type(param) ~= "string" then return nil, {} end
local base_keyword = get_keyword_base(param)
if not base_keyword then return nil, {} end
local canonical_keyword = get_canonical_keyword(base_keyword)
-- Check if there are any modifiers
if not param:find("<", 1, true) then
return canonical_keyword, {}
end
-- Parse modifiers using the same mechanism as etymon parsing
local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods)
local function generate_obj(ignored)
return {}
end
local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""),
{ param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj })
local modifiers = {
unc = parsed.unc or false,
ref = parsed.ref,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
function EtymonParser.parse_balanced_segments(str)
local segments = {}
local current = ""
local depth = 0
local i = 1
while i <= #str do
local char = str:sub(i, i)
if char == "<" then
if depth == 0 and current ~= "" then
table.insert(segments, current)
current = ""
end
depth = depth + 1
current = current .. char
elseif char == ">" then
current = current .. char
depth = depth - 1
if depth == 0 then
table.insert(segments, current)
current = ""
elseif depth < 0 then
error("Unbalanced brackets in etymon: unexpected '>'")
end
else
current = current .. char
end
i = i + 1
end
if depth ~= 0 then
error("Unbalanced brackets in etymon: missing '>'")
end
if current ~= "" then
table.insert(segments, current)
end
return segments
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local segments = EtymonParser.parse_balanced_segments(ety_string)
if #segments == 0 then
error("Empty inline etymology")
end
local keyword = M.string_utilities.trim(segments[1])
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword(get_canonical_keyword(keyword)) }
for i = 2, #segments do
local segment = segments[i]
if segment:sub(1, 1) == "<" and segment:sub(-1) == ">" then
local inner = segment:sub(2, -2)
if inner ~= "" then
table.insert(args, inner)
end
elseif is_keyword(segment, true) then
-- Handle keywords that appear between bracketed segments
table.insert(args, normalize_keyword(get_canonical_keyword(get_keyword(segment, true))))
end
end
return args
end
function EtymonParser.parse_etymon(param, context_lang)
if is_keyword(param) then
return nil
end
if type(param) ~= "string" then
return nil
end
local lang, rest
local is_family = false
local before_bracket = param:match("^([^<]*)") or param
local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$")
if lang_code then
local potential_lang = Util.get_lang(lang_code, true)
if potential_lang then
lang = potential_lang
rest = param:sub(#lang_code + 2)
else
local potential_family = Util.get_family(lang_code)
if potential_family then
lang = potential_family
rest = param:sub(#lang_code + 2)
is_family = true
else
lang = context_lang
rest = param
end
end
else
lang = context_lang
rest = param
end
if rest == "" then
M.track("etymon/term/empty")
elseif rest == "?" then
M.track("etymon/term/question-mark")
elseif rest == "-" then
M.track("etymon/term/hyphen")
end
if rest == "" then
return {
lang = lang,
term = nil,
unknown_term = true,
is_family = is_family,
}
end
if rest == "-" then
return {
lang = lang,
term = nil,
suppress_term = true,
is_family = is_family,
}
end
if not rest:find("<", 1, true) then
return {
lang = lang,
term = M.string_utilities.trim(rest),
is_family = is_family,
}
end
local term_text = rest:match("^([^<]*)") or ""
local is_unknown = (term_text == "")
local is_suppress = (term_text == "-")
local function generate_obj(ignored_term)
return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) }
end
local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods)
local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults,
{ param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj })
if parsed_obj.id and parsed_obj.id:match("^!") then
parsed_obj.id = parsed_obj.id:sub(2)
parsed_obj.override = true
end
parsed_obj.lang = lang
parsed_obj.is_family = is_family
if is_unknown then
parsed_obj.unknown_term = true
elseif is_suppress then
parsed_obj.suppress_term = true
end
return parsed_obj
end
function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code)
-- id is now optional, so only validate if provided
if id then
if mw.ustring.len(id) < 2 then
error("The `id` parameter must have at least two characters.")
end
if id == title or id == Util.get_page_data().pagename then
error("The `id` parameter must not be the same as the page title.")
end
end
local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true }
if pos and not valid_pos[pos] then
error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".")
end
local current_keyword = "from"
local etymons_in_group = {}
local keywords = M.data.keywords
local function checkGroup()
if keywords[current_keyword] and keywords[current_keyword].is_group and current_keyword ~= "affix" and current_keyword ~= "surf" and current_keyword ~= "afeq" and current_keyword ~= "univerbation" and #etymons_in_group <= 1 then
error("Detected `:" .. current_keyword .. "` group with fewer than two etymons.")
end
etymons_in_group = {}
end
local start_index = starts_with_lang_code and 2 or 1
for i = start_index, #args do
local param = args[i]
if type(param) ~= "string" then
elseif param:sub(1, 1) == ":" and not is_keyword(param) then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkGroup()
current_keyword = get_canonical_keyword(get_keyword(param))
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(etymons_in_group, param)
local param_lang = etymon_data.lang
if etymon_data.is_family and current_keyword == "inherited" then
error("`:inh` does not support family codes; use a specific language.")
end
if etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
if current_keyword == "from" and param_lang:getFullCode() ~= lang:getFullCode() then
error("`:from` is for same-language derivation, but language does not match. " ..
"Expected '" .. lang:getFullCode() .. "', got '" .. param_lang:getFullCode() .. "'.")
elseif current_keyword == "inherited" then
M.etymology.check_ancestor(lang, param_lang)
end
-- Check modifier restrictions
EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods)
-- postype must be "root" or "word"
local VALID_POSTYPES = { root = true, word = true }
if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then
error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".")
end
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true)
end
else
table.insert(etymons_in_group, param)
end
end
end
checkGroup()
end
local DataRetriever = {}
-- Given an etymon data, scrape its page and cache the result in the global state object.
function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel)
local content = etymon_title:getContent()
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.REDLINK
return
end
-- Check if the linked page is a redirect. If it is, the template parsing
-- code below will be effectively skipped, and `scrape_page` will be called
-- again on the redirect target (see the bottom of this function)
local lang_section_for_descendants = nil
local redirect_target = etymon_title.redirect_target
if not redirect_target then
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
return
end
lang_section_for_descendants = content
end
local etymon_lang_code = etymon_lang:getFullCode()
local lang_page_key = etymon_lang_code .. ":" .. etymon_page
local found_templates_for_lang = {}
local found_ids = {}
local get_node_class = M.template_parser.class_else_type
-- Look for all {{etymon}} templates within the page content using the template parser
-- This way the same page is never parsed more than once
-- Build a map from senseids to their parent etymonids.
local active_etymon_args = nil
for node in M.template_parser.parse(content):iterate_nodes() do
local node_class = get_node_class(node)
if node_class == "heading" then
-- A new L2 or etymology section acts as a barrier: an {{etymon}} usage
-- used previously cannot be the parent of any subsequent senseids.
-- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above.
if node:get_name():find("^Etymology") then
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if template_name == "etymon" then
local template_args = node:get_arguments()
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
table.insert(found_templates_for_lang, template_args)
if template_args.id then
local etymon_key = lang_page_key .. ":" .. template_args.id
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, template_args.id)
active_etymon_args = template_args
else
-- Store idless etymon with default key
local etymon_key = lang_page_key .. ":*"
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, "*")
active_etymon_args = template_args
end
end
elseif active_etymon_args and template_name == "senseid" then
local template_args = node:get_arguments()
-- This should always be true for proper usages of {{senseid}}.
if template_args[1] == etymon_lang_code and template_args[2] then
local sense_id_key = lang_page_key .. ":" .. template_args[2]
__state.senseid_parent_etymon[sense_id_key] = active_etymon_args
__state.cached_etymon_pages[sense_id_key] = tostring(etymon_page)
end
end
end
end
if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then
M.descendants.cache_page_checks({
lang_section = lang_section_for_descendants,
etymon_lang_code = etymon_lang_code,
found_templates_for_lang = found_templates_for_lang,
entry_title = __state.entry_title,
entry_lang_code = __state.entry_lang_code,
entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil,
cached_descendants_checks = __state.cached_descendants_checks,
lang_page_key = lang_page_key,
redirected_from = redirected_from,
})
end
local id_data_list = {}
for _, args in ipairs(found_templates_for_lang) do
local id = args.id or "*"
table.insert(id_data_list, { id = id, pos = args.pos })
end
__state.available_etymon_ids[lang_page_key] = id_data_list
if #found_templates_for_lang == 1 then
__state.single_etymons[lang_page_key] = found_templates_for_lang[1]
end
if redirected_from and __state.available_etymon_ids[lang_page_key] then
__state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {}
for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do
table.insert(__state.available_etymon_ids[redirected_from], id_data)
end
end
if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then
-- All done!
return
elseif redirect_target and not redirected_from then
-- Try scraping the redirect.
etymon_page = redirect_target.prefixedText
DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel)
__state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id]
else
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
end
end
-- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name.
function DataRetriever.get_etymon_args(etymon_data, is_toplevel)
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
if etymon_data.id then
local key = base_key .. ":" .. etymon_data.id
local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key]
if cached_args == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel)
end
cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh
-- Get etymon_id from parent if this was resolved via senseid
local parent_etymon = __state.senseid_parent_etymon[key]
local resolved_etymon_id = parent_etymon and parent_etymon.id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
end
end
return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check
else
__state.used_idless_etymon = true
if is_toplevel then
__state.toplevel_idless_etymon = true
end
if __state.available_etymon_ids[base_key] == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel)
end
local ids = __state.available_etymon_ids[base_key] or {}
local count = #ids
-- Try to filter by postype if available and we have multiple candidates
if count > 1 and etymon_data.postype then
local matching_ids = {}
for _, id_data in ipairs(ids) do
if id_data.pos == etymon_data.postype then
table.insert(matching_ids, id_data)
end
end
if #matching_ids == 1 then
local matched_id = matching_ids[1].id
local matched_key = base_key .. ":" .. matched_id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id = matched_id },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id = matched_id },
})
end
end
return __state.cached_etymon_args[matched_key], __state.cached_etymon_pages[matched_key], nil, descendants_check
end
end
if count == 1 then
local only_id_data = ids[1]
local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*"
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id_data = only_id_data },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id_data = only_id_data },
})
end
end
return __state.single_etymons[base_key], __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
local page_link = M.links.full_link({
term = page,
lang = norm_lang,
no_generate_forms = true,
}, "term")
local function format_id_hint(id_data, idx)
local id = type(id_data) == "table" and id_data.id or id_data
local pos = type(id_data) == "table" and id_data.pos
if id and id ~= "" and id ~= "*" then
return '"' .. id .. '"'
end
if pos and pos ~= "" then
return "unnamed (|pos=" .. pos .. "|)"
end
return "etymon #" .. idx .. " (no |id= on page)"
end
local id_list = {}
local all_idless = true
local target_has_idless = false
for i, id_data in ipairs(ids) do
local id = type(id_data) == "table" and id_data.id or id_data
if id and id ~= "" and id ~= "*" then
all_idless = false
else
target_has_idless = true
end
table.insert(id_list, format_id_hint(id_data, i))
end
if is_toplevel and target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local any_pos = false
for _, id_data in ipairs(ids) do
local pos = type(id_data) == "table" and id_data.pos
if pos and pos ~= "" then
any_pos = true
break
end
end
local suggestion_text
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. count .. " {{etymon}} templates for "
.. norm_lang:getCanonicalName() .. ")."
if all_idless then
if any_pos then
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. mw.text.listToText(id_list) .. "."
else
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)."
end
else
suggestion_text = " Specify which one with `<id:identifier>` after the term. Options: " .. mw.text.listToText(id_list) .. "."
end
Util.add_warning(lead .. suggestion_text, true)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local TreeBuilder = {}
local function parse_etymon_references(refs_text)
if not refs_text or refs_text == "" then
return ""
end
return M.references.parse_references(refs_text)
end
local function parse_tree_references(node)
if node.ref then
node.parsed_ref = parse_etymon_references(node.ref)
end
if node.children then
for _, container in ipairs(node.children) do
if container.terms then
for _, term in ipairs(container.terms) do
parse_tree_references(term)
end
end
end
end
end
-- Build a unique key for deduplication in the seen table
function TreeBuilder.build_key(lang, title, args)
local norm_lang_code = Util.get_norm_lang(lang):getFullCode()
local is_table = type(args) == "table"
local id = (is_table and args.id) or ""
if title then
return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id
end
if is_table and args.status == M.data.STATUS.INLINE then
local content_parts = {}
for i = 1, #args do
content_parts[i] = tostring(args[i])
end
return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0")
end
return norm_lang_code .. ":*:" .. id
end
function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion)
seen = seen or {}
depth = depth or 0
local is_toplevel = (depth == 0)
if depth > __state.max_depth_reached then
__state.max_depth_reached = depth
end
__state.total_nodes = __state.total_nodes + 1
local lang_code = lang:getCode()
__state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1
local current_id = (type(args) == "table" and args.id) or ""
local key = TreeBuilder.build_key(lang, title, args)
local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK }
if type(args) ~= "table" or seen[key] then
node.status = args or M.data.STATUS.MISSING
-- Mark as duplicate if we've seen this node before
if seen[key] then
node.is_duplicate = true
node.duplicate_key = key
local original_node = seen[key]
if type(original_node) == "table" and original_node.children and #original_node.children > 0 then
node.original_has_children = true
end
end
return node
end
node.status = args.status or M.data.STATUS.OK
seen[key] = node
-- If stop_recursion is set, skip parsing children but check for visible children
if stop_recursion then
local keywords = M.data.keywords
local has_visible_children = false
for i = 2, #args do
local param = args[i]
if type(param) == "string" then
local keyword_base = get_keyword_base(param)
if keyword_base and keywords[keyword_base] then
-- It's a keyword, check if visible in tree (invisible "all" or "tree" = hidden in tree)
local keyword_info = keywords[keyword_base]
local inv = keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
elseif param:sub(1, 1) ~= ":" then
-- It's a term (not a keyword), so there are visible children
has_visible_children = true
break
end
end
end
node.has_visible_children = has_visible_children
return node
end
-- Parse args into keyword containers
local current_keyword = "from"
local current_keyword_modifiers = {}
local current_container = nil
-- Helper to track keyword usage at top level
local function track_keyword_usage(keyword, target_lang, source_lang)
if not is_toplevel then return end
if not __state.toplevel_keyword_stats[keyword] then
__state.toplevel_keyword_stats[keyword] = {
count = 0,
target_langs = {},
source_langs = {},
}
end
local keyword_data = __state.toplevel_keyword_stats[keyword]
keyword_data.count = keyword_data.count + 1
local target_code = target_lang:getCode()
keyword_data.target_langs[target_code] = (keyword_data.target_langs[target_code] or 0) + 1
if source_lang then
local source_code = source_lang:getCode()
keyword_data.source_langs[source_code] = (keyword_data.source_langs[source_code] or 0) + 1
end
end
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
current_container = {
keyword = current_keyword,
keyword_info = M.data.keywords[current_keyword],
keyword_modifiers = current_keyword_modifiers,
terms = {},
}
table.insert(node.children, current_container)
-- Override keyword text/phrase for nominalization with <g:code>
if current_keyword_modifiers.g and current_keyword == "nominalization" then
local labels = get_nominalization_label_for_g(current_keyword_modifiers.g)
if not labels then
local codes = {}
for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end
table.sort(codes)
error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", "))
end
current_container.keyword_info = {}
for k, v in pairs(M.data.keywords[current_keyword]) do current_container.keyword_info[k] = v end
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
end
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
current_keyword = keyword
current_keyword_modifiers = modifiers
current_container = nil -- Force new container for new keyword
elseif type(param) == "string" and param:sub(1, 1) == ":" then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif type(param) == "string" then
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
-- Track keyword usage at top level
track_keyword_usage(current_keyword, lang, etymon_data.lang)
local term_node = {}
-- Handle suppress_term (-) and unknown_term (empty) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
ensure_container()
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1)
else
term_node = {
lang = etymon_data.lang,
children = {},
status = M.data.STATUS.OK,
}
end
term_node.suppress_term = etymon_data.suppress_term
term_node.unknown_term = etymon_data.unknown_term
term_node.is_family = etymon_data.is_family
term_node.is_uncertain = etymon_data.unc
term_node.ref = etymon_data.ref
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.pos = etymon_data.pos
term_node.ng = etymon_data.ng
term_node.lit = etymon_data.lit
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
else
-- Regular term: fetch arguments from page
local etymon_args, page_of, resolved_etymon_id, descendants_check = DataRetriever.get_etymon_args(etymon_data, is_toplevel)
if etymon_data.id and etymon_args == M.data.STATUS.MISSING and not etymon_data.ety then
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
local available_ids = __state.available_etymon_ids[base_key] or {}
if #available_ids > 0 then
__state.has_mismatched_id = true
end
end
-- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing
if etymon_data.ety then
if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then
__state.current_page_has_inline_etymology = true
if is_toplevel then
__state.toplevel_has_inline_etymology = true
end
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
-- Track inline ety keywords too
local inline_keyword = get_keyword(inline_args[2], true)
if inline_keyword and #inline_args >= 3 then
local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang)
if inline_etymon then
track_keyword_usage(inline_keyword, etymon_data.lang, inline_etymon.lang)
end
end
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
etymon_args = inline_args
term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent
else
-- Scraped arguments exist, <ety> is redundant and ignored
__state.current_page_has_redundant_etymology = true
if is_toplevel then
__state.toplevel_redundant_etymology = true
end
end
end
-- Ensure container exists before checking keyword info
ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = current_container.keyword_info
local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories))
term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion)
term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() ..
":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang)
term_node.id = etymon_data.id
term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.pos = etymon_data.pos
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.ng = etymon_data.ng
term_node.ref = etymon_data.ref
term_node.is_uncertain = etymon_data.unc
term_node.override = etymon_data.override
term_node.page_of = page_of
term_node.aftype = etymon_data.aftype
term_node.postype = etymon_data.postype
term_node.bor = etymon_data.bor
term_node.lbor = etymon_data.lbor
term_node.slbor = etymon_data.slbor
term_node.lit = etymon_data.lit
term_node.is_family = etymon_data.is_family
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
term_node.missing_descendants_header, term_node.missing_descendants_entry =
M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check)
end
table.insert(current_container.terms, term_node)
end
end
end
return node
end
-- Convert etymology tree to JSON-serializable table
local function tree_to_json(node)
local obj = {
term = node.title,
lang = node.lang:getCode(),
lang_name = node.lang:getCanonicalName(),
id = (node.id and node.id ~= "") and node.id or nil,
status = node.status,
is_uncertain = node.is_uncertain or nil,
is_duplicate = node.is_duplicate or nil,
gloss = node.t,
transliteration = node.tr,
transcription = node.ts,
alt = node.alt,
g = node.g,
pos = node.pos,
ng = node.ng,
children = {},
}
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if keyword_info then
local container_obj = {
keyword = container.keyword,
keyword_label = keyword_info.text,
keyword_abbrev = keyword_info.abbrev,
is_group = keyword_info.is_group or nil,
is_invisible = keyword_info.invisible or nil,
is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil,
terms = {},
}
for _, term in ipairs(container.terms or {}) do
table.insert(container_obj.terms, tree_to_json(term))
end
table.insert(obj.children, container_obj)
end
end
return obj
end
local function track_ranges(base_key, value, ranges, lang_code)
M.track("etymon/" .. base_key .. "/" .. value)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. value)
end
for _, range in ipairs(ranges) do
local matches = false
if range.min and range.max then
matches = value >= range.min and value <= range.max
elseif range.min then
matches = value >= range.min
elseif range.max then
matches = value <= range.max
elseif range.exact then
matches = value == range.exact
end
if matches then
M.track("etymon/" .. base_key .. "/" .. range.label)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. range.label)
end
break
end
end
end
local function track_title_pagename_mismatch_after_strip(lang)
local lang_code = lang:getCode()
M.track("etymon/title/pagename-mismatch-after-strip-diacritics")
M.track("etymon/lang/" .. lang_code .. "/title/pagename-mismatch-after-strip-diacritics")
end
-- Build and return the etymology data tree for a given term.
function export.get_tree(lang, title, args, options)
options = options or {}
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
if options.validate then
EtymonParser.validate(lang, args, options.id, title, options.pos, false)
end
local lang_code = lang:getCode()
local start_index = (args[1] == lang_code) and 2 or 1
local tree_args = { [1] = lang_code, id = options.id or args.id }
for i = start_index, #args do
table.insert(tree_args, args[i])
end
__state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args
local ety_data_tree = TreeBuilder.build(lang, title, tree_args)
parse_tree_references(ety_data_tree)
if options.json then
return M.JSON.toJSON(tree_to_json(ety_data_tree))
end
return ety_data_tree
end
-- Given a language code, page name and optionally the id= parameter,
-- render the tree and only the etymology tree for the relevant page.
-- Fetches and parses the corresponding {{etymon}} from the requested page,
-- and any further pages needed to render the tree.
-- Parameters can be passed either through the #invoke or as
-- template parameters *through* an #invoke.
function export.render_tree_for_etymon_on_page(frame)
local frame_args = frame.args
local parent_args = frame:getParent().args
local langcode = frame_args[1] or parent_args[1]
local pagename = frame_args[2] or parent_args[2]
local id = frame_args["id"] or parent_args["id"]
local display_title = frame_args["title"] or parent_args["title"]
local parsed_title = mw.title.new(pagename, 0)
local title
if parsed_title.namespace == 0 then
title = M.pages.safe_page_name(parsed_title)
elseif parsed_title.namespace == 118 then
title = "*" .. M.pages.safe_page_name(parsed_title)
else
error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace)
end
local lang = Util.get_lang(langcode)
-- Construct etymon_data for DataRetriever.get_args.
local etymon_data = {
lang = lang,
term = title,
id = id
}
local args, pagename = DataRetriever.get_etymon_args(etymon_data, true)
if args == M.data.STATUS.MISSING then
error("The etymon template was not found (language " ..
langcode ..
", title '" ..
title ..
"'" ..
(id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.")
end
local tree_title = display_title or title
if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then
track_title_pagename_mismatch_after_strip(lang)
end
local ety_data_tree = export.get_tree(lang, tree_title, args, {
validate = true,
id = id,
})
local output = {}
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
return table.concat(output)
end
function export.main(frame)
local parent_args = frame:getParent().args
local args = M.parameters.process(parent_args, M.parameters_data.etymon)
local lang = args[1]
local etymon_args = args[2]
local id = args.id
local title = args.title
local text = args.text
local tree = args.tree
local etydate = args.etydate
local rfe = args.rfe
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == "Reconstruction" then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == "Reconstruction" then
entry_pagename = "*" .. entry_pagename
end
if lang:stripDiacritics(M.links.remove_links(title)) ~= lang:stripDiacritics(M.links.remove_links(entry_pagename)) then
track_title_pagename_mismatch_after_strip(lang)
end
local current_L2 = M.pages.get_current_L2()
if current_L2 then
local norm_lang = Util.get_norm_lang(lang)
local norm_name = norm_lang:getCanonicalName()
if current_L2 ~= norm_name then
local lang_desc = lang:getCode() .. " (" .. lang:getCanonicalName() .. ")"
if norm_lang:getCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", normalized to " .. norm_lang:getCode() .. " (" .. norm_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
end
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
})
if args.json then
return ety_data_tree
end
local output = {}
local text_allowlist_mode = M.text_allowed.default_mode or "off"
if text and text_allowlist_mode ~= "off" and not Util.is_text_param_allowed_for_lang(lang) then
local msg = "Etymology texts (parameter <code>text=</code>) are not allowed for " .. lang:getFullName() ..
"; see [[Template:etymon#Text allowlist|Template:etymon § Text allowlist]] for the list of languages that may use the <code>text=</code> parameter."
if text_allowlist_mode == "error" then
error(msg)
else
Util.add_warning(msg, true)
end
end
local lang_exc = Util.get_lang_exception(lang)
if lang_exc and lang_exc.disallow then
local disallow = lang_exc.disallow
local error_text = " for " .. lang:getFullName()
if disallow.ref then
error_text = error_text .. "; see " .. disallow.ref
else
error_text = error_text .. "."
end
if tree and disallow.tree then
error("Etymology trees are not allowed" .. error_text)
end
if text and disallow.text then
error("Etymology texts are not allowed" .. error_text)
end
end
if etydate then
local etydate_param_mods = {
ref = { list = true, type = "references", allow_holes = true },
refn = { list = true, allow_holes = true },
nocap = { type = "boolean" },
}
local function generate_etydate_obj(etydate_text)
local etydate_specs = {}
for spec in etydate_text:gmatch("[^,]+") do
table.insert(etydate_specs, mw.text.trim(spec))
end
return { [1] = etydate_specs }
end
local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj })
local etydate_args = {
[1] = parsed_etydate[1],
nocap = parsed_etydate.nocap or false,
}
ety_data_tree.etydate = M.etydate.format_etydate(etydate_args, { omit_refs = true })
if parsed_etydate.ref and #parsed_etydate.ref > 0 then
ety_data_tree.etydate_refs = parsed_etydate.ref
end
end
if tree then
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
end
-- Check if there are any visible children in tree (invisible "all" or "tree" = hidden in tree)
local has_visible_children = false
for _, child in ipairs(ety_data_tree.children or {}) do
local child_keyword_info = child.keyword_info
local inv = child_keyword_info and child_keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
end
local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree
local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree))
local anchor = M.anchors.etymonid(lang, id, {
no_tree = args.notree,
title = title,
empty_tree = (not has_visible_children) or tree_disallowed,
ety_tree_json = ety_tree_json,
})
table.insert(output, anchor)
if text then
local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink
if text == "++" then
max_depth, stop_at_blue_link = false, false
elseif text == "+" then
max_depth, stop_at_blue_link = 1, false
elseif text == "*" then
max_depth, stop_at_blue_link = false, true
elseif text:match("^:[^*]+%*$") then
-- Stop at a specific language OR first bluelink after it, e.g., ":ota*"
-- If the target language is a redlink, continue to the first bluelink
local lang_code = text:match("^:([^*]+)%*$")
if lang_code and lang_code ~= "" then
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang_or_bluelink = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
elseif text:sub(1, 1) == ":" then
-- Stop at a specific language, e.g., ":ar" stops at first Arabic term
local lang_code = text:sub(2)
if lang_code ~= "" then
-- Validate the language code
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
local num = tonumber(text)
if num and num >= 1 then
max_depth, stop_at_blue_link = num, false
else
error('Invalid text value "' ..
text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)')
end
end
table.insert(output, M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
}))
end
if rfe then
local rfe_param_mods = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
}
local function generate_rfe_obj(rfe_text)
-- Check if it's a boolean true value
if M.yesno(rfe_text, false) then
return { is_boolean = true }
else
return { text = rfe_text }
end
end
local rfe_with_defaults = Util.add_boolean_defaults(rfe, rfe_param_mods)
local parsed_rfe = M.parse_utilities.parse_inline_modifiers(rfe_with_defaults, {
param_mods = rfe_param_mods,
generate_obj = generate_rfe_obj
})
local rfe_args = {
[1] = lang:getCode(),
nocat = parsed_rfe.nocat,
sort = parsed_rfe.sort,
y = parsed_rfe.y,
m = parsed_rfe.m,
fragment = parsed_rfe.fragment,
section = parsed_rfe.section,
box = parsed_rfe.box,
noes = parsed_rfe.noes,
}
if not parsed_rfe.is_boolean then
rfe_args[2] = parsed_rfe.text
end
table.insert(output, frame:expandTemplate({
title = "rfe",
args = rfe_args
}))
end
if Util.is_content_page() and __state.max_depth_reached > 0 then
local lang_code = lang:getCode()
local depth_ranges = {
{ min = 50, label = "extremely-deep" },
{ min = 20, label = "20+" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ max = 2, label = "1-2" }
}
local node_ranges = {
{ min = 100, label = "extremely-large" },
{ min = 50, label = "50+" },
{ min = 20, max = 49, label = "20-49" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ max = 4, label = "1-4" }
}
local language_ranges = {
{ min = 10, label = "10+" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ exact = 2, label = "2" },
{ exact = 1, label = "1" }
}
track_ranges("depth", __state.max_depth_reached, depth_ranges, lang_code)
track_ranges("nodes", __state.total_nodes, node_ranges, lang_code)
local unique_languages = 0
for _ in pairs(__state.language_count) do
unique_languages = unique_languages + 1
end
track_ranges("unique-languages", unique_languages, language_ranges, lang_code)
if __state.total_nodes == __state.max_depth_reached + 1 then
track_ranges("linear-depth", __state.max_depth_reached, depth_ranges, lang_code)
end
end
local categories = {}
if Util.is_content_page() then
local should_suppress_categories = lang_exc and lang_exc.suppress_categories
if not should_suppress_categories and not args.nocat then
categories = M.categories.render({
data_tree = ety_data_tree,
page_lang = lang,
available_etymon_ids = __state.available_etymon_ids,
senseid_parent_etymon = __state.senseid_parent_etymon,
get_norm_lang_func = Util.get_norm_lang,
lang_exc = lang_exc,
})
end
local target_lang_code = lang:getCode()
for keyword, keyword_data in pairs(__state.toplevel_keyword_stats) do
-- Track keyword globally
M.track("etymon/keyword/" .. keyword)
-- Track keyword per target language
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code)
-- Track keyword per source language
for source_code, count in pairs(keyword_data.source_langs) do
M.track("etymon/keyword/" .. keyword .. "/source/" .. source_code)
-- Track keyword per target+source combination
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code .. "/source/" .. source_code)
end
end
if tree then
table.insert(categories, "Pages with etymology trees")
table.insert(categories, lang:getCanonicalName() .. " entries with etymology trees")
end
if text then table.insert(categories, lang:getCanonicalName() .. " entries with etymology texts") end
if args.exnihilo then table.insert(categories, lang:getCanonicalName() .. " terms coined ex nihilo") end
if __state.toplevel_has_inline_etymology then
table.insert(categories, "Pages with inline etymon for redlinks")
end
if __state.toplevel_redundant_etymology then
table.insert(categories, "Pages with redundant inline etymon")
end
if __state.toplevel_idless_etymon then
table.insert(categories, "Pages using etymon with no ID")
end
if __state.has_mismatched_id then
table.insert(categories, lang:getCanonicalName() .. " entries referencing etymons with mismatched IDs")
end
if __state.linked_page_multiple_etymons_idless then
table.insert(categories,
lang:getCanonicalName() .. " entries referencing pages with multiple etymons missing IDs")
end
end
if #categories > 0 then
table.insert(output, M.categories.format(categories, lang))
end
if __state.warnings then
for i, warning in ipairs(__state.warnings) do
table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n")
end
end
return table.concat(output)
end
return export
sd824iy93rvmo3oroit742p1i4oi4gt
342826
342825
2026-05-16T13:54:32Z
Hakimi97
2668
342826
Scribunto
text/plain
--[=[
This module implements the {{etymon}} template for structured etymology data on Wiktionary.
It enables the creation of etymology trees and text by parsing etymon chains,
scraping linked pages for their own {{etymon}} data, and recursively building a tree
of derivational relationships.
Authors:
- Original implementation: [[User:Ioaxxere]]
- Full refactor (September 2025): [[User:Fenakhay]] ([[Special:Diff/86717746]])
Modules:
- [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping
- [[Module:etymon/data]]: keyword definitions, configuration, and status constants
- [[Module:etymon/tree]]: etymology tree rendering
- [[Module:etymon/text]]: etymology text generation
- [[Module:etymon/categories]]: category generation logic
]=]
local export = {}
local etymon_data_module = "Module:etymon/data"
local etymon_text_module = "Module:etymon/text"
local etymon_tree_module = "Module:etymon/tree"
local etymon_categories_module = "Module:etymon/categories"
local etymon_descendants_module = "Module:etymon/descendants"
local __state = {
cached_etymon_args = {},
cached_etymon_pages = {},
cached_descendants_checks = {},
senseid_parent_etymon = {},
available_etymon_ids = {},
single_etymons = {},
entry_title = nil,
entry_lang_code = nil,
current_page_has_inline_etymology = false,
current_page_has_redundant_etymology = false,
used_idless_etymon = false,
toplevel_has_inline_etymology = false,
toplevel_redundant_etymology = false,
toplevel_idless_etymon = false,
has_mismatched_id = false,
linked_page_multiple_etymons_idless = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
warnings = {},
}
local loader = require("Module:module loader")
local M = loader.init({
require = {
data = etymon_data_module,
tree = etymon_tree_module,
text = etymon_text_module,
categories = etymon_categories_module,
descendants = etymon_descendants_module,
anchors = "Module:anchors",
etydate = "Module:etydate",
etymology = "Module:etymology",
families = "Module:families",
languages = "Module:languages",
languages_errorgetby = "Module:languages/errorGetBy",
links = "Module:links",
pages = "Module:pages",
parameters = "Module:parameters",
string_utilities = "Module:string utilities",
template_parser = "Module:template parser",
utilities = "Module:utilities",
debug = "Module:debug",
en_utilities = "Module:en-utilities",
parse_utilities = "Module:parse utilities",
references = "Module:references",
track = "Module:debug/track",
template_styles = "Module:TemplateStyles",
script_utilities = "Module:script utilities",
JSON = "Module:JSON",
yesno = "Module:yesno",
},
loadData = {
headword_data = "Module:headword/data",
parameters_data = "Module:parameters/data",
text_allowed = "Module:etymon/data/text_allowed",
},
})
local Util = {}
function Util.format_error(message, preview_only)
if preview_only and not M.pages.is_preview() then
return nil
end
return '<span class="error">' .. message .. '</span>'
end
function Util.add_warning(message, preview_only)
local formatted = Util.format_error(message, preview_only)
if formatted then
table.insert(__state.warnings, formatted)
end
end
function Util.is_text_param_allowed_for_lang(lang)
if not lang or type(lang) ~= "table" then
return false
end
local types = lang.getTypes and lang:getTypes()
if types and types.family then
local code = lang.getCode and lang:getCode()
return code and M.text_allowed.families[code] == true
end
local full_code = lang.getFullCode and lang:getFullCode()
if full_code and M.text_allowed.langs[full_code] then
return true
end
if lang.inFamily then
for family_code in pairs(M.text_allowed.families) do
if lang:inFamily(family_code) then
return true
end
end
end
return false
end
function Util.get_lang(code, no_error)
if no_error then
return M.languages.getByCode(code, nil, true)
end
return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true)
end
function Util.get_family(code)
return M.families.getByCode(code)
end
function Util.get_lang_exception(lang)
-- Families have no language-specific exceptions
if lang.getTypes and lang:getTypes().family then
return nil
end
local code = lang:getCode()
local lang_exceptions = M.data.config.lang_exceptions
if lang_exceptions[code] then
return lang_exceptions[code]
end
for norm_code, exc in pairs(lang_exceptions) do
if exc.normalize_to and code == exc.normalize_to then
return exc
end
if exc.normalize_from_families then
local should_normalize = false
for _, family in ipairs(exc.normalize_from_families) do
if lang:inFamily(family) then
should_normalize = true
break
end
end
if should_normalize and exc.normalize_exclude_families then
for _, family in ipairs(exc.normalize_exclude_families) do
if lang:inFamily(family) then
should_normalize = false
break
end
end
end
if should_normalize then
local ret = {}
for k, v in pairs(exc) do
ret[k] = v
end
ret.suppress_tr = nil
return ret
end
end
end
return nil
end
function Util.get_norm_lang(lang)
local exc = Util.get_lang_exception(lang)
if exc and exc.normalize_to then
return M.languages.getByCode(exc.normalize_to)
end
return lang
end
-- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>)
-- This is needed because Module:parse utilities expects boolean modifiers to have explicit values
function Util.add_boolean_defaults(str, param_mods)
local result = str
for name, spec in pairs(param_mods) do
if spec.type == "boolean" then
-- Replace <name> with <name:1> (but not <name:...> which already has a value)
result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>")
end
end
return result
end
-- Centralized term formatting: handles suppress_term, unknown_term, and regular terms
function Util.format_term(term, is_toplevel, opts)
opts = opts or {}
-- suppress_term (-) returns nil
if term.suppress_term then
return nil
end
local lang = term.lang
local exc = Util.get_lang_exception(lang)
if is_toplevel then
local display_text = term.alt or term.title or ""
local sc = term.sc or lang:findBestScript(display_text)
local bold_text = tostring(mw.html.create("strong")
:addClass("selflink")
:wikitext(display_text))
return M.script_utilities.tag_text(bold_text, lang, sc, "term")
end
local link_params = { lang = lang }
link_params.term = not term.unknown_term and term.title or nil
link_params.alt = term.alt
link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil
if not (exc and exc.suppress_tr) then
link_params.tr = term.tr
link_params.ts = term.ts
else
link_params.suppress_tr = true
end
link_params.lit = (opts.lit ~= "suppress") and term.lit or nil
if opts.gloss ~= "suppress" then
link_params.gloss = term.t
end
if term.g and term.g ~= "" then
local genders = M.string_utilities.split(term.g, ",")
for i = 1, #genders do
genders[i] = M.string_utilities.trim(genders[i])
end
link_params.genders = genders
end
if opts.pos ~= "suppress" then
link_params.pos = term.pos
link_params.ng = term.ng
end
if exc and exc.suppress_tr then
link_params.lit = nil
end
local show_qualifiers
if opts.tree_ql ~= "suppress" then
if term.q then
link_params.q = term.q
end
if term.qq then
link_params.qq = term.qq
end
if term.l then
link_params.l = term.l
end
if term.ll then
link_params.ll = term.ll
end
show_qualifiers = term.q or term.qq or term.l or term.ll
end
return M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
end
local __is_content_page_cached
function Util.is_content_page()
if __is_content_page_cached == nil then
__is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle())
end
return __is_content_page_cached
end
local __page_data_cached
function Util.get_page_data()
if not __page_data_cached then
__page_data_cached = M.headword_data.page
end
return __page_data_cached
end
-- Extract base keyword from param (without modifiers)
local function get_keyword_base(param)
if type(param) ~= "string" then return nil end
local base = param:match("^:?([^<]+)") or param:gsub("^:", "")
return base
end
local function is_keyword(param, allow_colon_less)
if type(param) ~= "string" then return false end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
if allow_colon_less then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
return false
end
local function get_keyword(param, allow_colon_less)
if type(param) ~= "string" then return nil end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
return get_keyword_base(param)
end
if allow_colon_less then
local base = get_keyword_base(param)
if keywords[base] then
return base
end
end
return nil
end
local function normalize_keyword(keyword)
if keyword:sub(1, 1) == ":" then
return keyword
end
return ":" .. keyword
end
-- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries
local function get_canonical_keyword(keyword)
if not keyword then return keyword end
return M.data.keyword_canonical[keyword] or keyword
end
-- Build text/phrase for nominalization with <g:code> (uses data module for codes only).
local function get_nominalization_label_for_g(code)
if not code or code == "" then return nil end
local codes = M.data.nominalization_g_codes
local adj = codes[code]
if not adj and #code == 2 then
local gender_adj = codes[code:sub(1, 1)]
local number_adj = codes[code:sub(2, 2)]
if gender_adj and number_adj then
adj = gender_adj .. " " .. number_adj
end
end
if not adj then return nil end
local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of"
local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false)
return { text = text, phrase = phrase }
end
local EtymonParser = {}
-- Keyword modifier definitions
EtymonParser.keyword_param_mods = {
unc = { type = "boolean" },
ref = {},
text = { restrict = { keywords = { "from", "derived" } } },
lit = { restrict = { keywords = { "affix", "surf", "univerbation" } } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
}
-- Term modifier definitions
EtymonParser.etymon_param_mods = {
id = {},
t = {},
tr = {},
ts = {},
q = {},
qq = {},
l = {},
ll = {},
pos = {},
ng = {},
alt = {},
g = {},
ety = {},
lit = {},
unc = { type = "boolean" },
ref = {},
aftype = { restrict = { keywords = { "affix", "surf", "afeq" } } },
postype = {},
bor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
slbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
lbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
}
local function get_clean_param_mods(param_mods)
local clean = {}
for mod_name, mod_def in pairs(param_mods) do
clean[mod_name] = {}
for key, value in pairs(mod_def) do
if key ~= "restrict" then
clean[mod_name][key] = value
end
end
end
return clean
end
function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods)
for mod_name, mod_value in pairs(modifiers) do
-- Only check restrictions if the modifier has a non-false/nil value
if mod_value then
local mod_def = param_mods[mod_name]
if mod_def and mod_def.restrict and mod_def.restrict.keywords then
local allowed_keywords = mod_def.restrict.keywords
local is_allowed = false
for _, allowed_keyword in ipairs(allowed_keywords) do
if current_keyword == allowed_keyword then
is_allowed = true
break
end
end
if not is_allowed then
local keyword_list = {}
for _, kw in ipairs(allowed_keywords) do
table.insert(keyword_list, ":" .. kw)
end
local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ")
if #keyword_list > 2 then
-- Replace last comma with "or"
keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1")
end
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".")
end
end
end
end
end
-- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>")
function EtymonParser.parse_keyword_modifiers(param)
if type(param) ~= "string" then return nil, {} end
local base_keyword = get_keyword_base(param)
if not base_keyword then return nil, {} end
local canonical_keyword = get_canonical_keyword(base_keyword)
-- Check if there are any modifiers
if not param:find("<", 1, true) then
return canonical_keyword, {}
end
-- Parse modifiers using the same mechanism as etymon parsing
local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods)
local function generate_obj(ignored)
return {}
end
local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""),
{ param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj })
local modifiers = {
unc = parsed.unc or false,
ref = parsed.ref,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
function EtymonParser.parse_balanced_segments(str)
local segments = {}
local current = ""
local depth = 0
local i = 1
while i <= #str do
local char = str:sub(i, i)
if char == "<" then
if depth == 0 and current ~= "" then
table.insert(segments, current)
current = ""
end
depth = depth + 1
current = current .. char
elseif char == ">" then
current = current .. char
depth = depth - 1
if depth == 0 then
table.insert(segments, current)
current = ""
elseif depth < 0 then
error("Unbalanced brackets in etymon: unexpected '>'")
end
else
current = current .. char
end
i = i + 1
end
if depth ~= 0 then
error("Unbalanced brackets in etymon: missing '>'")
end
if current ~= "" then
table.insert(segments, current)
end
return segments
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local segments = EtymonParser.parse_balanced_segments(ety_string)
if #segments == 0 then
error("Empty inline etymology")
end
local keyword = M.string_utilities.trim(segments[1])
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword(get_canonical_keyword(keyword)) }
for i = 2, #segments do
local segment = segments[i]
if segment:sub(1, 1) == "<" and segment:sub(-1) == ">" then
local inner = segment:sub(2, -2)
if inner ~= "" then
table.insert(args, inner)
end
elseif is_keyword(segment, true) then
-- Handle keywords that appear between bracketed segments
table.insert(args, normalize_keyword(get_canonical_keyword(get_keyword(segment, true))))
end
end
return args
end
function EtymonParser.parse_etymon(param, context_lang)
if is_keyword(param) then
return nil
end
if type(param) ~= "string" then
return nil
end
local lang, rest
local is_family = false
local before_bracket = param:match("^([^<]*)") or param
local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$")
if lang_code then
local potential_lang = Util.get_lang(lang_code, true)
if potential_lang then
lang = potential_lang
rest = param:sub(#lang_code + 2)
else
local potential_family = Util.get_family(lang_code)
if potential_family then
lang = potential_family
rest = param:sub(#lang_code + 2)
is_family = true
else
lang = context_lang
rest = param
end
end
else
lang = context_lang
rest = param
end
if rest == "" then
M.track("etymon/term/empty")
elseif rest == "?" then
M.track("etymon/term/question-mark")
elseif rest == "-" then
M.track("etymon/term/hyphen")
end
if rest == "" then
return {
lang = lang,
term = nil,
unknown_term = true,
is_family = is_family,
}
end
if rest == "-" then
return {
lang = lang,
term = nil,
suppress_term = true,
is_family = is_family,
}
end
if not rest:find("<", 1, true) then
return {
lang = lang,
term = M.string_utilities.trim(rest),
is_family = is_family,
}
end
local term_text = rest:match("^([^<]*)") or ""
local is_unknown = (term_text == "")
local is_suppress = (term_text == "-")
local function generate_obj(ignored_term)
return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) }
end
local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods)
local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults,
{ param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj })
if parsed_obj.id and parsed_obj.id:match("^!") then
parsed_obj.id = parsed_obj.id:sub(2)
parsed_obj.override = true
end
parsed_obj.lang = lang
parsed_obj.is_family = is_family
if is_unknown then
parsed_obj.unknown_term = true
elseif is_suppress then
parsed_obj.suppress_term = true
end
return parsed_obj
end
function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code)
-- id is now optional, so only validate if provided
if id then
if mw.ustring.len(id) < 2 then
error("The `id` parameter must have at least two characters.")
end
if id == title or id == Util.get_page_data().pagename then
error("The `id` parameter must not be the same as the page title.")
end
end
local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true }
if pos and not valid_pos[pos] then
error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".")
end
local current_keyword = "from"
local etymons_in_group = {}
local keywords = M.data.keywords
local function checkGroup()
if keywords[current_keyword] and keywords[current_keyword].is_group and current_keyword ~= "affix" and current_keyword ~= "surf" and current_keyword ~= "afeq" and current_keyword ~= "univerbation" and #etymons_in_group <= 1 then
error("Detected `:" .. current_keyword .. "` group with fewer than two etymons.")
end
etymons_in_group = {}
end
local start_index = starts_with_lang_code and 2 or 1
for i = start_index, #args do
local param = args[i]
if type(param) ~= "string" then
elseif param:sub(1, 1) == ":" and not is_keyword(param) then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkGroup()
current_keyword = get_canonical_keyword(get_keyword(param))
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(etymons_in_group, param)
local param_lang = etymon_data.lang
if etymon_data.is_family and current_keyword == "inherited" then
error("`:inh` does not support family codes; use a specific language.")
end
if etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
if current_keyword == "from" and param_lang:getFullCode() ~= lang:getFullCode() then
error("`:from` is for same-language derivation, but language does not match. " ..
"Expected '" .. lang:getFullCode() .. "', got '" .. param_lang:getFullCode() .. "'.")
elseif current_keyword == "inherited" then
M.etymology.check_ancestor(lang, param_lang)
end
-- Check modifier restrictions
EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods)
-- postype must be "root" or "word"
local VALID_POSTYPES = { root = true, word = true }
if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then
error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".")
end
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true)
end
else
table.insert(etymons_in_group, param)
end
end
end
checkGroup()
end
local DataRetriever = {}
-- Given an etymon data, scrape its page and cache the result in the global state object.
function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel)
local content = etymon_title:getContent()
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.REDLINK
return
end
-- Check if the linked page is a redirect. If it is, the template parsing
-- code below will be effectively skipped, and `scrape_page` will be called
-- again on the redirect target (see the bottom of this function)
local lang_section_for_descendants = nil
local redirect_target = etymon_title.redirect_target
if not redirect_target then
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
return
end
lang_section_for_descendants = content
end
local etymon_lang_code = etymon_lang:getFullCode()
local lang_page_key = etymon_lang_code .. ":" .. etymon_page
local found_templates_for_lang = {}
local found_ids = {}
local get_node_class = M.template_parser.class_else_type
-- Look for all {{etymon}} templates within the page content using the template parser
-- This way the same page is never parsed more than once
-- Build a map from senseids to their parent etymonids.
local active_etymon_args = nil
for node in M.template_parser.parse(content):iterate_nodes() do
local node_class = get_node_class(node)
if node_class == "heading" then
-- A new L2 or etymology section acts as a barrier: an {{etymon}} usage
-- used previously cannot be the parent of any subsequent senseids.
-- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above.
if node:get_name():find("^Etymology") then
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if template_name == "etymon" then
local template_args = node:get_arguments()
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
table.insert(found_templates_for_lang, template_args)
if template_args.id then
local etymon_key = lang_page_key .. ":" .. template_args.id
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, template_args.id)
active_etymon_args = template_args
else
-- Store idless etymon with default key
local etymon_key = lang_page_key .. ":*"
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, "*")
active_etymon_args = template_args
end
end
elseif active_etymon_args and template_name == "senseid" then
local template_args = node:get_arguments()
-- This should always be true for proper usages of {{senseid}}.
if template_args[1] == etymon_lang_code and template_args[2] then
local sense_id_key = lang_page_key .. ":" .. template_args[2]
__state.senseid_parent_etymon[sense_id_key] = active_etymon_args
__state.cached_etymon_pages[sense_id_key] = tostring(etymon_page)
end
end
end
end
if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then
M.descendants.cache_page_checks({
lang_section = lang_section_for_descendants,
etymon_lang_code = etymon_lang_code,
found_templates_for_lang = found_templates_for_lang,
entry_title = __state.entry_title,
entry_lang_code = __state.entry_lang_code,
entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil,
cached_descendants_checks = __state.cached_descendants_checks,
lang_page_key = lang_page_key,
redirected_from = redirected_from,
})
end
local id_data_list = {}
for _, args in ipairs(found_templates_for_lang) do
local id = args.id or "*"
table.insert(id_data_list, { id = id, pos = args.pos })
end
__state.available_etymon_ids[lang_page_key] = id_data_list
if #found_templates_for_lang == 1 then
__state.single_etymons[lang_page_key] = found_templates_for_lang[1]
end
if redirected_from and __state.available_etymon_ids[lang_page_key] then
__state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {}
for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do
table.insert(__state.available_etymon_ids[redirected_from], id_data)
end
end
if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then
-- All done!
return
elseif redirect_target and not redirected_from then
-- Try scraping the redirect.
etymon_page = redirect_target.prefixedText
DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel)
__state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id]
else
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
end
end
-- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name.
function DataRetriever.get_etymon_args(etymon_data, is_toplevel)
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
if etymon_data.id then
local key = base_key .. ":" .. etymon_data.id
local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key]
if cached_args == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel)
end
cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh
-- Get etymon_id from parent if this was resolved via senseid
local parent_etymon = __state.senseid_parent_etymon[key]
local resolved_etymon_id = parent_etymon and parent_etymon.id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
end
end
return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check
else
__state.used_idless_etymon = true
if is_toplevel then
__state.toplevel_idless_etymon = true
end
if __state.available_etymon_ids[base_key] == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel)
end
local ids = __state.available_etymon_ids[base_key] or {}
local count = #ids
-- Try to filter by postype if available and we have multiple candidates
if count > 1 and etymon_data.postype then
local matching_ids = {}
for _, id_data in ipairs(ids) do
if id_data.pos == etymon_data.postype then
table.insert(matching_ids, id_data)
end
end
if #matching_ids == 1 then
local matched_id = matching_ids[1].id
local matched_key = base_key .. ":" .. matched_id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id = matched_id },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id = matched_id },
})
end
end
return __state.cached_etymon_args[matched_key], __state.cached_etymon_pages[matched_key], nil, descendants_check
end
end
if count == 1 then
local only_id_data = ids[1]
local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*"
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id_data = only_id_data },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id_data = only_id_data },
})
end
end
return __state.single_etymons[base_key], __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
local page_link = M.links.full_link({
term = page,
lang = norm_lang,
no_generate_forms = true,
}, "term")
local function format_id_hint(id_data, idx)
local id = type(id_data) == "table" and id_data.id or id_data
local pos = type(id_data) == "table" and id_data.pos
if id and id ~= "" and id ~= "*" then
return '"' .. id .. '"'
end
if pos and pos ~= "" then
return "unnamed (|pos=" .. pos .. "|)"
end
return "etymon #" .. idx .. " (no |id= on page)"
end
local id_list = {}
local all_idless = true
local target_has_idless = false
for i, id_data in ipairs(ids) do
local id = type(id_data) == "table" and id_data.id or id_data
if id and id ~= "" and id ~= "*" then
all_idless = false
else
target_has_idless = true
end
table.insert(id_list, format_id_hint(id_data, i))
end
if is_toplevel and target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local any_pos = false
for _, id_data in ipairs(ids) do
local pos = type(id_data) == "table" and id_data.pos
if pos and pos ~= "" then
any_pos = true
break
end
end
local suggestion_text
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. count .. " {{etymon}} templates for "
.. norm_lang:getCanonicalName() .. ")."
if all_idless then
if any_pos then
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. mw.text.listToText(id_list) .. "."
else
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)."
end
else
suggestion_text = " Specify which one with `<id:identifier>` after the term. Options: " .. mw.text.listToText(id_list) .. "."
end
Util.add_warning(lead .. suggestion_text, true)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local TreeBuilder = {}
local function parse_etymon_references(refs_text)
if not refs_text or refs_text == "" then
return ""
end
return M.references.parse_references(refs_text)
end
local function parse_tree_references(node)
if node.ref then
node.parsed_ref = parse_etymon_references(node.ref)
end
if node.children then
for _, container in ipairs(node.children) do
if container.terms then
for _, term in ipairs(container.terms) do
parse_tree_references(term)
end
end
end
end
end
-- Build a unique key for deduplication in the seen table
function TreeBuilder.build_key(lang, title, args)
local norm_lang_code = Util.get_norm_lang(lang):getFullCode()
local is_table = type(args) == "table"
local id = (is_table and args.id) or ""
if title then
return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id
end
if is_table and args.status == M.data.STATUS.INLINE then
local content_parts = {}
for i = 1, #args do
content_parts[i] = tostring(args[i])
end
return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0")
end
return norm_lang_code .. ":*:" .. id
end
function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion)
seen = seen or {}
depth = depth or 0
local is_toplevel = (depth == 0)
if depth > __state.max_depth_reached then
__state.max_depth_reached = depth
end
__state.total_nodes = __state.total_nodes + 1
local lang_code = lang:getCode()
__state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1
local current_id = (type(args) == "table" and args.id) or ""
local key = TreeBuilder.build_key(lang, title, args)
local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK }
if type(args) ~= "table" or seen[key] then
node.status = args or M.data.STATUS.MISSING
-- Mark as duplicate if we've seen this node before
if seen[key] then
node.is_duplicate = true
node.duplicate_key = key
local original_node = seen[key]
if type(original_node) == "table" and original_node.children and #original_node.children > 0 then
node.original_has_children = true
end
end
return node
end
node.status = args.status or M.data.STATUS.OK
seen[key] = node
-- If stop_recursion is set, skip parsing children but check for visible children
if stop_recursion then
local keywords = M.data.keywords
local has_visible_children = false
for i = 2, #args do
local param = args[i]
if type(param) == "string" then
local keyword_base = get_keyword_base(param)
if keyword_base and keywords[keyword_base] then
-- It's a keyword, check if visible in tree (invisible "all" or "tree" = hidden in tree)
local keyword_info = keywords[keyword_base]
local inv = keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
elseif param:sub(1, 1) ~= ":" then
-- It's a term (not a keyword), so there are visible children
has_visible_children = true
break
end
end
end
node.has_visible_children = has_visible_children
return node
end
-- Parse args into keyword containers
local current_keyword = "from"
local current_keyword_modifiers = {}
local current_container = nil
-- Helper to track keyword usage at top level
local function track_keyword_usage(keyword, target_lang, source_lang)
if not is_toplevel then return end
if not __state.toplevel_keyword_stats[keyword] then
__state.toplevel_keyword_stats[keyword] = {
count = 0,
target_langs = {},
source_langs = {},
}
end
local keyword_data = __state.toplevel_keyword_stats[keyword]
keyword_data.count = keyword_data.count + 1
local target_code = target_lang:getCode()
keyword_data.target_langs[target_code] = (keyword_data.target_langs[target_code] or 0) + 1
if source_lang then
local source_code = source_lang:getCode()
keyword_data.source_langs[source_code] = (keyword_data.source_langs[source_code] or 0) + 1
end
end
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
current_container = {
keyword = current_keyword,
keyword_info = M.data.keywords[current_keyword],
keyword_modifiers = current_keyword_modifiers,
terms = {},
}
table.insert(node.children, current_container)
-- Override keyword text/phrase for nominalization with <g:code>
if current_keyword_modifiers.g and current_keyword == "nominalization" then
local labels = get_nominalization_label_for_g(current_keyword_modifiers.g)
if not labels then
local codes = {}
for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end
table.sort(codes)
error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", "))
end
current_container.keyword_info = {}
for k, v in pairs(M.data.keywords[current_keyword]) do current_container.keyword_info[k] = v end
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
end
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
current_keyword = keyword
current_keyword_modifiers = modifiers
current_container = nil -- Force new container for new keyword
elseif type(param) == "string" and param:sub(1, 1) == ":" then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif type(param) == "string" then
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
-- Track keyword usage at top level
track_keyword_usage(current_keyword, lang, etymon_data.lang)
local term_node = {}
-- Handle suppress_term (-) and unknown_term (empty) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
ensure_container()
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1)
else
term_node = {
lang = etymon_data.lang,
children = {},
status = M.data.STATUS.OK,
}
end
term_node.suppress_term = etymon_data.suppress_term
term_node.unknown_term = etymon_data.unknown_term
term_node.is_family = etymon_data.is_family
term_node.is_uncertain = etymon_data.unc
term_node.ref = etymon_data.ref
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.pos = etymon_data.pos
term_node.ng = etymon_data.ng
term_node.lit = etymon_data.lit
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
else
-- Regular term: fetch arguments from page
local etymon_args, page_of, resolved_etymon_id, descendants_check = DataRetriever.get_etymon_args(etymon_data, is_toplevel)
if etymon_data.id and etymon_args == M.data.STATUS.MISSING and not etymon_data.ety then
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
local available_ids = __state.available_etymon_ids[base_key] or {}
if #available_ids > 0 then
__state.has_mismatched_id = true
end
end
-- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing
if etymon_data.ety then
if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then
__state.current_page_has_inline_etymology = true
if is_toplevel then
__state.toplevel_has_inline_etymology = true
end
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
-- Track inline ety keywords too
local inline_keyword = get_keyword(inline_args[2], true)
if inline_keyword and #inline_args >= 3 then
local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang)
if inline_etymon then
track_keyword_usage(inline_keyword, etymon_data.lang, inline_etymon.lang)
end
end
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
etymon_args = inline_args
term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent
else
-- Scraped arguments exist, <ety> is redundant and ignored
__state.current_page_has_redundant_etymology = true
if is_toplevel then
__state.toplevel_redundant_etymology = true
end
end
end
-- Ensure container exists before checking keyword info
ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = current_container.keyword_info
local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories))
term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion)
term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() ..
":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang)
term_node.id = etymon_data.id
term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.pos = etymon_data.pos
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.ng = etymon_data.ng
term_node.ref = etymon_data.ref
term_node.is_uncertain = etymon_data.unc
term_node.override = etymon_data.override
term_node.page_of = page_of
term_node.aftype = etymon_data.aftype
term_node.postype = etymon_data.postype
term_node.bor = etymon_data.bor
term_node.lbor = etymon_data.lbor
term_node.slbor = etymon_data.slbor
term_node.lit = etymon_data.lit
term_node.is_family = etymon_data.is_family
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
term_node.missing_descendants_header, term_node.missing_descendants_entry =
M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check)
end
table.insert(current_container.terms, term_node)
end
end
end
return node
end
-- Convert etymology tree to JSON-serializable table
local function tree_to_json(node)
local obj = {
term = node.title,
lang = node.lang:getCode(),
lang_name = node.lang:getCanonicalName(),
id = (node.id and node.id ~= "") and node.id or nil,
status = node.status,
is_uncertain = node.is_uncertain or nil,
is_duplicate = node.is_duplicate or nil,
gloss = node.t,
transliteration = node.tr,
transcription = node.ts,
alt = node.alt,
g = node.g,
pos = node.pos,
ng = node.ng,
children = {},
}
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if keyword_info then
local container_obj = {
keyword = container.keyword,
keyword_label = keyword_info.text,
keyword_abbrev = keyword_info.abbrev,
is_group = keyword_info.is_group or nil,
is_invisible = keyword_info.invisible or nil,
is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil,
terms = {},
}
for _, term in ipairs(container.terms or {}) do
table.insert(container_obj.terms, tree_to_json(term))
end
table.insert(obj.children, container_obj)
end
end
return obj
end
local function track_ranges(base_key, value, ranges, lang_code)
M.track("etymon/" .. base_key .. "/" .. value)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. value)
end
for _, range in ipairs(ranges) do
local matches = false
if range.min and range.max then
matches = value >= range.min and value <= range.max
elseif range.min then
matches = value >= range.min
elseif range.max then
matches = value <= range.max
elseif range.exact then
matches = value == range.exact
end
if matches then
M.track("etymon/" .. base_key .. "/" .. range.label)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. range.label)
end
break
end
end
end
local function track_title_pagename_mismatch_after_strip(lang)
local lang_code = lang:getCode()
M.track("etymon/title/pagename-mismatch-after-strip-diacritics")
M.track("etymon/lang/" .. lang_code .. "/title/pagename-mismatch-after-strip-diacritics")
end
-- Build and return the etymology data tree for a given term.
function export.get_tree(lang, title, args, options)
options = options or {}
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
if options.validate then
EtymonParser.validate(lang, args, options.id, title, options.pos, false)
end
local lang_code = lang:getCode()
local start_index = (args[1] == lang_code) and 2 or 1
local tree_args = { [1] = lang_code, id = options.id or args.id }
for i = start_index, #args do
table.insert(tree_args, args[i])
end
__state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args
local ety_data_tree = TreeBuilder.build(lang, title, tree_args)
parse_tree_references(ety_data_tree)
if options.json then
return M.JSON.toJSON(tree_to_json(ety_data_tree))
end
return ety_data_tree
end
-- Given a language code, page name and optionally the id= parameter,
-- render the tree and only the etymology tree for the relevant page.
-- Fetches and parses the corresponding {{etymon}} from the requested page,
-- and any further pages needed to render the tree.
-- Parameters can be passed either through the #invoke or as
-- template parameters *through* an #invoke.
function export.render_tree_for_etymon_on_page(frame)
local frame_args = frame.args
local parent_args = frame:getParent().args
local langcode = frame_args[1] or parent_args[1]
local pagename = frame_args[2] or parent_args[2]
local id = frame_args["id"] or parent_args["id"]
local display_title = frame_args["title"] or parent_args["title"]
local parsed_title = mw.title.new(pagename, 0)
local title
if parsed_title.namespace == 0 then
title = M.pages.safe_page_name(parsed_title)
elseif parsed_title.namespace == 118 then
title = "*" .. M.pages.safe_page_name(parsed_title)
else
error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace)
end
local lang = Util.get_lang(langcode)
-- Construct etymon_data for DataRetriever.get_args.
local etymon_data = {
lang = lang,
term = title,
id = id
}
local args, pagename = DataRetriever.get_etymon_args(etymon_data, true)
if args == M.data.STATUS.MISSING then
error("The etymon template was not found (language " ..
langcode ..
", title '" ..
title ..
"'" ..
(id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.")
end
local tree_title = display_title or title
if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then
track_title_pagename_mismatch_after_strip(lang)
end
local ety_data_tree = export.get_tree(lang, tree_title, args, {
validate = true,
id = id,
})
local output = {}
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
return table.concat(output)
end
function export.main(frame)
local parent_args = frame:getParent().args
local args = M.parameters.process(parent_args, M.parameters_data.etymon)
local lang = args[1]
local etymon_args = args[2]
local id = args.id
local title = args.title
local text = args.text
local tree = args.tree
local etydate = args.etydate
local rfe = args.rfe
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == "Reconstruction" then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == "Reconstruction" then
entry_pagename = "*" .. entry_pagename
end
if lang:stripDiacritics(M.links.remove_links(title)) ~= lang:stripDiacritics(M.links.remove_links(entry_pagename)) then
track_title_pagename_mismatch_after_strip(lang)
end
local current_L2 = M.pages.get_current_L2()
if current_L2 then
local norm_lang = Util.get_norm_lang(lang)
local norm_name = "Bahasa " .. norm_lang:getCanonicalName()
if current_L2 ~= norm_name then
local lang_desc = lang:getCode() .. " (" .. lang:getCanonicalName() .. ")"
if norm_lang:getCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", normalized to " .. norm_lang:getCode() .. " (" .. norm_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
end
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
})
if args.json then
return ety_data_tree
end
local output = {}
local text_allowlist_mode = M.text_allowed.default_mode or "off"
if text and text_allowlist_mode ~= "off" and not Util.is_text_param_allowed_for_lang(lang) then
local msg = "Etymology texts (parameter <code>text=</code>) are not allowed for " .. lang:getFullName() ..
"; see [[Template:etymon#Text allowlist|Template:etymon § Text allowlist]] for the list of languages that may use the <code>text=</code> parameter."
if text_allowlist_mode == "error" then
error(msg)
else
Util.add_warning(msg, true)
end
end
local lang_exc = Util.get_lang_exception(lang)
if lang_exc and lang_exc.disallow then
local disallow = lang_exc.disallow
local error_text = " for " .. lang:getFullName()
if disallow.ref then
error_text = error_text .. "; see " .. disallow.ref
else
error_text = error_text .. "."
end
if tree and disallow.tree then
error("Etymology trees are not allowed" .. error_text)
end
if text and disallow.text then
error("Etymology texts are not allowed" .. error_text)
end
end
if etydate then
local etydate_param_mods = {
ref = { list = true, type = "references", allow_holes = true },
refn = { list = true, allow_holes = true },
nocap = { type = "boolean" },
}
local function generate_etydate_obj(etydate_text)
local etydate_specs = {}
for spec in etydate_text:gmatch("[^,]+") do
table.insert(etydate_specs, mw.text.trim(spec))
end
return { [1] = etydate_specs }
end
local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj })
local etydate_args = {
[1] = parsed_etydate[1],
nocap = parsed_etydate.nocap or false,
}
ety_data_tree.etydate = M.etydate.format_etydate(etydate_args, { omit_refs = true })
if parsed_etydate.ref and #parsed_etydate.ref > 0 then
ety_data_tree.etydate_refs = parsed_etydate.ref
end
end
if tree then
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
end
-- Check if there are any visible children in tree (invisible "all" or "tree" = hidden in tree)
local has_visible_children = false
for _, child in ipairs(ety_data_tree.children or {}) do
local child_keyword_info = child.keyword_info
local inv = child_keyword_info and child_keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
end
local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree
local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree))
local anchor = M.anchors.etymonid(lang, id, {
no_tree = args.notree,
title = title,
empty_tree = (not has_visible_children) or tree_disallowed,
ety_tree_json = ety_tree_json,
})
table.insert(output, anchor)
if text then
local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink
if text == "++" then
max_depth, stop_at_blue_link = false, false
elseif text == "+" then
max_depth, stop_at_blue_link = 1, false
elseif text == "*" then
max_depth, stop_at_blue_link = false, true
elseif text:match("^:[^*]+%*$") then
-- Stop at a specific language OR first bluelink after it, e.g., ":ota*"
-- If the target language is a redlink, continue to the first bluelink
local lang_code = text:match("^:([^*]+)%*$")
if lang_code and lang_code ~= "" then
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang_or_bluelink = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
elseif text:sub(1, 1) == ":" then
-- Stop at a specific language, e.g., ":ar" stops at first Arabic term
local lang_code = text:sub(2)
if lang_code ~= "" then
-- Validate the language code
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
local num = tonumber(text)
if num and num >= 1 then
max_depth, stop_at_blue_link = num, false
else
error('Invalid text value "' ..
text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)')
end
end
table.insert(output, M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
}))
end
if rfe then
local rfe_param_mods = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
}
local function generate_rfe_obj(rfe_text)
-- Check if it's a boolean true value
if M.yesno(rfe_text, false) then
return { is_boolean = true }
else
return { text = rfe_text }
end
end
local rfe_with_defaults = Util.add_boolean_defaults(rfe, rfe_param_mods)
local parsed_rfe = M.parse_utilities.parse_inline_modifiers(rfe_with_defaults, {
param_mods = rfe_param_mods,
generate_obj = generate_rfe_obj
})
local rfe_args = {
[1] = lang:getCode(),
nocat = parsed_rfe.nocat,
sort = parsed_rfe.sort,
y = parsed_rfe.y,
m = parsed_rfe.m,
fragment = parsed_rfe.fragment,
section = parsed_rfe.section,
box = parsed_rfe.box,
noes = parsed_rfe.noes,
}
if not parsed_rfe.is_boolean then
rfe_args[2] = parsed_rfe.text
end
table.insert(output, frame:expandTemplate({
title = "rfe",
args = rfe_args
}))
end
if Util.is_content_page() and __state.max_depth_reached > 0 then
local lang_code = lang:getCode()
local depth_ranges = {
{ min = 50, label = "extremely-deep" },
{ min = 20, label = "20+" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ max = 2, label = "1-2" }
}
local node_ranges = {
{ min = 100, label = "extremely-large" },
{ min = 50, label = "50+" },
{ min = 20, max = 49, label = "20-49" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ max = 4, label = "1-4" }
}
local language_ranges = {
{ min = 10, label = "10+" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ exact = 2, label = "2" },
{ exact = 1, label = "1" }
}
track_ranges("depth", __state.max_depth_reached, depth_ranges, lang_code)
track_ranges("nodes", __state.total_nodes, node_ranges, lang_code)
local unique_languages = 0
for _ in pairs(__state.language_count) do
unique_languages = unique_languages + 1
end
track_ranges("unique-languages", unique_languages, language_ranges, lang_code)
if __state.total_nodes == __state.max_depth_reached + 1 then
track_ranges("linear-depth", __state.max_depth_reached, depth_ranges, lang_code)
end
end
local categories = {}
if Util.is_content_page() then
local should_suppress_categories = lang_exc and lang_exc.suppress_categories
if not should_suppress_categories and not args.nocat then
categories = M.categories.render({
data_tree = ety_data_tree,
page_lang = lang,
available_etymon_ids = __state.available_etymon_ids,
senseid_parent_etymon = __state.senseid_parent_etymon,
get_norm_lang_func = Util.get_norm_lang,
lang_exc = lang_exc,
})
end
local target_lang_code = lang:getCode()
for keyword, keyword_data in pairs(__state.toplevel_keyword_stats) do
-- Track keyword globally
M.track("etymon/keyword/" .. keyword)
-- Track keyword per target language
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code)
-- Track keyword per source language
for source_code, count in pairs(keyword_data.source_langs) do
M.track("etymon/keyword/" .. keyword .. "/source/" .. source_code)
-- Track keyword per target+source combination
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code .. "/source/" .. source_code)
end
end
if tree then
table.insert(categories, "Pages with etymology trees")
table.insert(categories, lang:getCanonicalName() .. " entries with etymology trees")
end
if text then table.insert(categories, lang:getCanonicalName() .. " entries with etymology texts") end
if args.exnihilo then table.insert(categories, lang:getCanonicalName() .. " terms coined ex nihilo") end
if __state.toplevel_has_inline_etymology then
table.insert(categories, "Pages with inline etymon for redlinks")
end
if __state.toplevel_redundant_etymology then
table.insert(categories, "Pages with redundant inline etymon")
end
if __state.toplevel_idless_etymon then
table.insert(categories, "Pages using etymon with no ID")
end
if __state.has_mismatched_id then
table.insert(categories, lang:getCanonicalName() .. " entries referencing etymons with mismatched IDs")
end
if __state.linked_page_multiple_etymons_idless then
table.insert(categories,
lang:getCanonicalName() .. " entries referencing pages with multiple etymons missing IDs")
end
end
if #categories > 0 then
table.insert(output, M.categories.format(categories, lang))
end
if __state.warnings then
for i, warning in ipairs(__state.warnings) do
table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n")
end
end
return table.concat(output)
end
return export
ntgc8yk38onjlpngtccb4xjlsblhzh9
342829
342826
2026-05-16T13:59:42Z
Hakimi97
2668
342829
Scribunto
text/plain
--[=[
This module implements the {{etymon}} template for structured etymology data on Wiktionary.
It enables the creation of etymology trees and text by parsing etymon chains,
scraping linked pages for their own {{etymon}} data, and recursively building a tree
of derivational relationships.
Authors:
- Original implementation: [[User:Ioaxxere]]
- Full refactor (September 2025): [[User:Fenakhay]] ([[Special:Diff/86717746]])
Modules:
- [[Module:etymon]]: main module handling parsing, validation, tree building, and page scraping
- [[Module:etymon/data]]: keyword definitions, configuration, and status constants
- [[Module:etymon/tree]]: etymology tree rendering
- [[Module:etymon/text]]: etymology text generation
- [[Module:etymon/categories]]: category generation logic
]=]
local export = {}
local etymon_data_module = "Module:etymon/data"
local etymon_text_module = "Module:etymon/text"
local etymon_tree_module = "Module:etymon/tree"
local etymon_categories_module = "Module:etymon/categories"
local etymon_descendants_module = "Module:etymon/descendants"
local __state = {
cached_etymon_args = {},
cached_etymon_pages = {},
cached_descendants_checks = {},
senseid_parent_etymon = {},
available_etymon_ids = {},
single_etymons = {},
entry_title = nil,
entry_lang_code = nil,
current_page_has_inline_etymology = false,
current_page_has_redundant_etymology = false,
used_idless_etymon = false,
toplevel_has_inline_etymology = false,
toplevel_redundant_etymology = false,
toplevel_idless_etymon = false,
has_mismatched_id = false,
linked_page_multiple_etymons_idless = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
warnings = {},
}
local loader = require("Module:module loader")
local M = loader.init({
require = {
data = etymon_data_module,
tree = etymon_tree_module,
text = etymon_text_module,
categories = etymon_categories_module,
descendants = etymon_descendants_module,
anchors = "Module:anchors",
etydate = "Module:etydate",
etymology = "Module:etymology",
families = "Module:families",
languages = "Module:languages",
languages_errorgetby = "Module:languages/errorGetBy",
links = "Module:links",
pages = "Module:pages",
parameters = "Module:parameters",
string_utilities = "Module:string utilities",
template_parser = "Module:template parser",
utilities = "Module:utilities",
debug = "Module:debug",
en_utilities = "Module:en-utilities",
parse_utilities = "Module:parse utilities",
references = "Module:references",
track = "Module:debug/track",
template_styles = "Module:TemplateStyles",
script_utilities = "Module:script utilities",
JSON = "Module:JSON",
yesno = "Module:yesno",
},
loadData = {
headword_data = "Module:headword/data",
parameters_data = "Module:parameters/data",
text_allowed = "Module:etymon/data/text_allowed",
},
})
local Util = {}
function Util.format_error(message, preview_only)
if preview_only and not M.pages.is_preview() then
return nil
end
return '<span class="error">' .. message .. '</span>'
end
function Util.add_warning(message, preview_only)
local formatted = Util.format_error(message, preview_only)
if formatted then
table.insert(__state.warnings, formatted)
end
end
function Util.is_text_param_allowed_for_lang(lang)
if not lang or type(lang) ~= "table" then
return false
end
local types = lang.getTypes and lang:getTypes()
if types and types.family then
local code = lang.getCode and lang:getCode()
return code and M.text_allowed.families[code] == true
end
local full_code = lang.getFullCode and lang:getFullCode()
if full_code and M.text_allowed.langs[full_code] then
return true
end
if lang.inFamily then
for family_code in pairs(M.text_allowed.families) do
if lang:inFamily(family_code) then
return true
end
end
end
return false
end
function Util.get_lang(code, no_error)
if no_error then
return M.languages.getByCode(code, nil, true)
end
return M.languages.getByCode(code, nil, true) or M.languages_errorgetby.code(code, true, true)
end
function Util.get_family(code)
return M.families.getByCode(code)
end
function Util.get_lang_exception(lang)
-- Families have no language-specific exceptions
if lang.getTypes and lang:getTypes().family then
return nil
end
local code = lang:getCode()
local lang_exceptions = M.data.config.lang_exceptions
if lang_exceptions[code] then
return lang_exceptions[code]
end
for norm_code, exc in pairs(lang_exceptions) do
if exc.normalize_to and code == exc.normalize_to then
return exc
end
if exc.normalize_from_families then
local should_normalize = false
for _, family in ipairs(exc.normalize_from_families) do
if lang:inFamily(family) then
should_normalize = true
break
end
end
if should_normalize and exc.normalize_exclude_families then
for _, family in ipairs(exc.normalize_exclude_families) do
if lang:inFamily(family) then
should_normalize = false
break
end
end
end
if should_normalize then
local ret = {}
for k, v in pairs(exc) do
ret[k] = v
end
ret.suppress_tr = nil
return ret
end
end
end
return nil
end
function Util.get_norm_lang(lang)
local exc = Util.get_lang_exception(lang)
if exc and exc.normalize_to then
return M.languages.getByCode(exc.normalize_to)
end
return lang
end
-- Add default values for boolean modifiers (e.g., <unc> becomes <unc:1>)
-- This is needed because Module:parse utilities expects boolean modifiers to have explicit values
function Util.add_boolean_defaults(str, param_mods)
local result = str
for name, spec in pairs(param_mods) do
if spec.type == "boolean" then
-- Replace <name> with <name:1> (but not <name:...> which already has a value)
result = result:gsub("<" .. name .. ">", "<" .. name .. ":1>")
end
end
return result
end
-- Centralized term formatting: handles suppress_term, unknown_term, and regular terms
function Util.format_term(term, is_toplevel, opts)
opts = opts or {}
-- suppress_term (-) returns nil
if term.suppress_term then
return nil
end
local lang = term.lang
local exc = Util.get_lang_exception(lang)
if is_toplevel then
local display_text = term.alt or term.title or ""
local sc = term.sc or lang:findBestScript(display_text)
local bold_text = tostring(mw.html.create("strong")
:addClass("selflink")
:wikitext(display_text))
return M.script_utilities.tag_text(bold_text, lang, sc, "term")
end
local link_params = { lang = lang }
link_params.term = not term.unknown_term and term.title or nil
link_params.alt = term.alt
link_params.id = (not term.unknown_term and term.id and term.id ~= "") and term.id or nil
if not (exc and exc.suppress_tr) then
link_params.tr = term.tr
link_params.ts = term.ts
else
link_params.suppress_tr = true
end
link_params.lit = (opts.lit ~= "suppress") and term.lit or nil
if opts.gloss ~= "suppress" then
link_params.gloss = term.t
end
if term.g and term.g ~= "" then
local genders = M.string_utilities.split(term.g, ",")
for i = 1, #genders do
genders[i] = M.string_utilities.trim(genders[i])
end
link_params.genders = genders
end
if opts.pos ~= "suppress" then
link_params.pos = term.pos
link_params.ng = term.ng
end
if exc and exc.suppress_tr then
link_params.lit = nil
end
local show_qualifiers
if opts.tree_ql ~= "suppress" then
if term.q then
link_params.q = term.q
end
if term.qq then
link_params.qq = term.qq
end
if term.l then
link_params.l = term.l
end
if term.ll then
link_params.ll = term.ll
end
show_qualifiers = term.q or term.qq or term.l or term.ll
end
return M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
end
local __is_content_page_cached
function Util.is_content_page()
if __is_content_page_cached == nil then
__is_content_page_cached = M.pages.is_content_page(mw.title.getCurrentTitle())
end
return __is_content_page_cached
end
local __page_data_cached
function Util.get_page_data()
if not __page_data_cached then
__page_data_cached = M.headword_data.page
end
return __page_data_cached
end
-- Extract base keyword from param (without modifiers)
local function get_keyword_base(param)
if type(param) ~= "string" then return nil end
local base = param:match("^:?([^<]+)") or param:gsub("^:", "")
return base
end
local function is_keyword(param, allow_colon_less)
if type(param) ~= "string" then return false end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
if allow_colon_less then
local base = get_keyword_base(param)
return keywords[base] ~= nil
end
return false
end
local function get_keyword(param, allow_colon_less)
if type(param) ~= "string" then return nil end
local keywords = M.data.keywords
if param:sub(1, 1) == ":" then
return get_keyword_base(param)
end
if allow_colon_less then
local base = get_keyword_base(param)
if keywords[base] then
return base
end
end
return nil
end
local function normalize_keyword(keyword)
if keyword:sub(1, 1) == ":" then
return keyword
end
return ":" .. keyword
end
-- Resolve keyword (possibly an alias) to its canonical form. Used only at input boundaries
local function get_canonical_keyword(keyword)
if not keyword then return keyword end
return M.data.keyword_canonical[keyword] or keyword
end
-- Build text/phrase for nominalization with <g:code> (uses data module for codes only).
local function get_nominalization_label_for_g(code)
if not code or code == "" then return nil end
local codes = M.data.nominalization_g_codes
local adj = codes[code]
if not adj and #code == 2 then
local gender_adj = codes[code:sub(1, 1)]
local number_adj = codes[code:sub(2, 2)]
if gender_adj and number_adj then
adj = gender_adj .. " " .. number_adj
end
end
if not adj then return nil end
local text = adj:gsub("^%l", function(c) return string.upper(c) end) .. " [[Appendix:Glossary#nominalization|nominalization]] of"
local phrase = M.en_utilities.add_indefinite_article(adj .. " [[Appendix:Glossary#nominalization|nominalization]] of", false)
return { text = text, phrase = phrase }
end
local EtymonParser = {}
-- Keyword modifier definitions
EtymonParser.keyword_param_mods = {
unc = { type = "boolean" },
ref = {},
text = { restrict = { keywords = { "from", "derived" } } },
lit = { restrict = { keywords = { "affix", "surf", "univerbation" } } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
}
-- Term modifier definitions
EtymonParser.etymon_param_mods = {
id = {},
t = {},
tr = {},
ts = {},
q = {},
qq = {},
l = {},
ll = {},
pos = {},
ng = {},
alt = {},
g = {},
ety = {},
lit = {},
unc = { type = "boolean" },
ref = {},
aftype = { restrict = { keywords = { "affix", "surf", "afeq" } } },
postype = {},
bor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
slbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
lbor = { type = "boolean", restrict = { keywords = { "affix", "surf" } } },
}
local function get_clean_param_mods(param_mods)
local clean = {}
for mod_name, mod_def in pairs(param_mods) do
clean[mod_name] = {}
for key, value in pairs(mod_def) do
if key ~= "restrict" then
clean[mod_name][key] = value
end
end
end
return clean
end
function EtymonParser.check_modifier_restrictions(modifiers, current_keyword, param_mods)
for mod_name, mod_value in pairs(modifiers) do
-- Only check restrictions if the modifier has a non-false/nil value
if mod_value then
local mod_def = param_mods[mod_name]
if mod_def and mod_def.restrict and mod_def.restrict.keywords then
local allowed_keywords = mod_def.restrict.keywords
local is_allowed = false
for _, allowed_keyword in ipairs(allowed_keywords) do
if current_keyword == allowed_keyword then
is_allowed = true
break
end
end
if not is_allowed then
local keyword_list = {}
for _, kw in ipairs(allowed_keywords) do
table.insert(keyword_list, ":" .. kw)
end
local keyword_str = table.concat(keyword_list, #keyword_list == 2 and " or " or ", ")
if #keyword_list > 2 then
-- Replace last comma with "or"
keyword_str = keyword_str:gsub(", ([^,]+)$", " or %1")
end
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for the keyword" .. (#keyword_list > 1 and "s " or " ") .. keyword_str .. ".")
end
end
end
end
end
-- Parse keyword with modifiers (e.g., ":bor<unc>" or ":bor<ref:{{R:example}}>")
function EtymonParser.parse_keyword_modifiers(param)
if type(param) ~= "string" then return nil, {} end
local base_keyword = get_keyword_base(param)
if not base_keyword then return nil, {} end
local canonical_keyword = get_canonical_keyword(base_keyword)
-- Check if there are any modifiers
if not param:find("<", 1, true) then
return canonical_keyword, {}
end
-- Parse modifiers using the same mechanism as etymon parsing
local rest_with_defaults = Util.add_boolean_defaults(param, EtymonParser.keyword_param_mods)
local function generate_obj(ignored)
return {}
end
local parsed = M.parse_utilities.parse_inline_modifiers(rest_with_defaults:gsub("^:?[^<]+", ""),
{ param_mods = get_clean_param_mods(EtymonParser.keyword_param_mods), generate_obj = generate_obj })
local modifiers = {
unc = parsed.unc or false,
ref = parsed.ref,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
function EtymonParser.parse_balanced_segments(str)
local segments = {}
local current = ""
local depth = 0
local i = 1
while i <= #str do
local char = str:sub(i, i)
if char == "<" then
if depth == 0 and current ~= "" then
table.insert(segments, current)
current = ""
end
depth = depth + 1
current = current .. char
elseif char == ">" then
current = current .. char
depth = depth - 1
if depth == 0 then
table.insert(segments, current)
current = ""
elseif depth < 0 then
error("Unbalanced brackets in etymon: unexpected '>'")
end
else
current = current .. char
end
i = i + 1
end
if depth ~= 0 then
error("Unbalanced brackets in etymon: missing '>'")
end
if current ~= "" then
table.insert(segments, current)
end
return segments
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local segments = EtymonParser.parse_balanced_segments(ety_string)
if #segments == 0 then
error("Empty inline etymology")
end
local keyword = M.string_utilities.trim(segments[1])
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword(get_canonical_keyword(keyword)) }
for i = 2, #segments do
local segment = segments[i]
if segment:sub(1, 1) == "<" and segment:sub(-1) == ">" then
local inner = segment:sub(2, -2)
if inner ~= "" then
table.insert(args, inner)
end
elseif is_keyword(segment, true) then
-- Handle keywords that appear between bracketed segments
table.insert(args, normalize_keyword(get_canonical_keyword(get_keyword(segment, true))))
end
end
return args
end
function EtymonParser.parse_etymon(param, context_lang)
if is_keyword(param) then
return nil
end
if type(param) ~= "string" then
return nil
end
local lang, rest
local is_family = false
local before_bracket = param:match("^([^<]*)") or param
local lang_code, rest_match = before_bracket:match("^([a-zA-Z][a-zA-Z0-9._-]*):(.*)$")
if lang_code then
local potential_lang = Util.get_lang(lang_code, true)
if potential_lang then
lang = potential_lang
rest = param:sub(#lang_code + 2)
else
local potential_family = Util.get_family(lang_code)
if potential_family then
lang = potential_family
rest = param:sub(#lang_code + 2)
is_family = true
else
lang = context_lang
rest = param
end
end
else
lang = context_lang
rest = param
end
if rest == "" then
M.track("etymon/term/empty")
elseif rest == "?" then
M.track("etymon/term/question-mark")
elseif rest == "-" then
M.track("etymon/term/hyphen")
end
if rest == "" then
return {
lang = lang,
term = nil,
unknown_term = true,
is_family = is_family,
}
end
if rest == "-" then
return {
lang = lang,
term = nil,
suppress_term = true,
is_family = is_family,
}
end
if not rest:find("<", 1, true) then
return {
lang = lang,
term = M.string_utilities.trim(rest),
is_family = is_family,
}
end
local term_text = rest:match("^([^<]*)") or ""
local is_unknown = (term_text == "")
local is_suppress = (term_text == "-")
local function generate_obj(ignored_term)
return { term = (is_unknown or is_suppress) and nil or M.string_utilities.trim(term_text) }
end
local rest_with_defaults = Util.add_boolean_defaults(rest, EtymonParser.etymon_param_mods)
local parsed_obj = M.parse_utilities.parse_inline_modifiers(rest_with_defaults,
{ param_mods = get_clean_param_mods(EtymonParser.etymon_param_mods), generate_obj = generate_obj })
if parsed_obj.id and parsed_obj.id:match("^!") then
parsed_obj.id = parsed_obj.id:sub(2)
parsed_obj.override = true
end
parsed_obj.lang = lang
parsed_obj.is_family = is_family
if is_unknown then
parsed_obj.unknown_term = true
elseif is_suppress then
parsed_obj.suppress_term = true
end
return parsed_obj
end
function EtymonParser.validate(lang, args, id, title, pos, starts_with_lang_code)
-- id is now optional, so only validate if provided
if id then
if mw.ustring.len(id) < 2 then
error("The `id` parameter must have at least two characters.")
end
if id == title or id == Util.get_page_data().pagename then
error("The `id` parameter must not be the same as the page title.")
end
end
local valid_pos = { prefix = true, suffix = true, interfix = true, infix = true, root = true, word = true }
if pos and not valid_pos[pos] then
error("Unknown value provided for `pos`. Valid values: " .. table.concat(require("Module:table").keysToList(valid_pos), ", ") .. ".")
end
local current_keyword = "from"
local etymons_in_group = {}
local keywords = M.data.keywords
local function checkGroup()
if keywords[current_keyword] and keywords[current_keyword].is_group and current_keyword ~= "affix" and current_keyword ~= "surf" and current_keyword ~= "afeq" and current_keyword ~= "univerbation" and #etymons_in_group <= 1 then
error("Detected `:" .. current_keyword .. "` group with fewer than two etymons.")
end
etymons_in_group = {}
end
local start_index = starts_with_lang_code and 2 or 1
for i = start_index, #args do
local param = args[i]
if type(param) ~= "string" then
elseif param:sub(1, 1) == ":" and not is_keyword(param) then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkGroup()
current_keyword = get_canonical_keyword(get_keyword(param))
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(etymons_in_group, param)
local param_lang = etymon_data.lang
if etymon_data.is_family and current_keyword == "inherited" then
error("`:inh` does not support family codes; use a specific language.")
end
if etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
if current_keyword == "from" and param_lang:getFullCode() ~= lang:getFullCode() then
error("`:from` is for same-language derivation, but language does not match. " ..
"Expected '" .. lang:getFullCode() .. "', got '" .. param_lang:getFullCode() .. "'.")
elseif current_keyword == "inherited" then
M.etymology.check_ancestor(lang, param_lang)
end
-- Check modifier restrictions
EtymonParser.check_modifier_restrictions(etymon_data, current_keyword, EtymonParser.etymon_param_mods)
-- postype must be "root" or "word"
local VALID_POSTYPES = { root = true, word = true }
if etymon_data.postype and not VALID_POSTYPES[etymon_data.postype] then
error("Invalid <postype:" .. etymon_data.postype .. ">; must be \"root\" or \"word\".")
end
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
EtymonParser.validate(etymon_data.lang, inline_args, nil, nil, nil, true)
end
else
table.insert(etymons_in_group, param)
end
end
end
checkGroup()
end
local DataRetriever = {}
-- Given an etymon data, scrape its page and cache the result in the global state object.
function DataRetriever.cache_page_etymons(etymon_page, etymon_title, key, etymon_lang, etymon_id, redirected_from, descendants_is_toplevel)
local content = etymon_title:getContent()
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.REDLINK
return
end
-- Check if the linked page is a redirect. If it is, the template parsing
-- code below will be effectively skipped, and `scrape_page` will be called
-- again on the redirect target (see the bottom of this function)
local lang_section_for_descendants = nil
local redirect_target = etymon_title.redirect_target
if not redirect_target then
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
if not content then
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
return
end
lang_section_for_descendants = content
end
local etymon_lang_code = etymon_lang:getFullCode()
local lang_page_key = etymon_lang_code .. ":" .. etymon_page
local found_templates_for_lang = {}
local found_ids = {}
local get_node_class = M.template_parser.class_else_type
-- Look for all {{etymon}} templates within the page content using the template parser
-- This way the same page is never parsed more than once
-- Build a map from senseids to their parent etymonids.
local active_etymon_args = nil
for node in M.template_parser.parse(content):iterate_nodes() do
local node_class = get_node_class(node)
if node_class == "heading" then
-- A new L2 or etymology section acts as a barrier: an {{etymon}} usage
-- used previously cannot be the parent of any subsequent senseids.
-- Note that we don't have to check for L2s due to the usage of `M.pages.get_section` above.
if node:get_name():find("^Etymology") then
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if template_name == "etymon" then
local template_args = node:get_arguments()
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
table.insert(found_templates_for_lang, template_args)
if template_args.id then
local etymon_key = lang_page_key .. ":" .. template_args.id
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, template_args.id)
active_etymon_args = template_args
else
-- Store idless etymon with default key
local etymon_key = lang_page_key .. ":*"
__state.cached_etymon_args[etymon_key] = template_args
__state.cached_etymon_pages[etymon_key] = tostring(etymon_page)
table.insert(found_ids, "*")
active_etymon_args = template_args
end
end
elseif active_etymon_args and template_name == "senseid" then
local template_args = node:get_arguments()
-- This should always be true for proper usages of {{senseid}}.
if template_args[1] == etymon_lang_code and template_args[2] then
local sense_id_key = lang_page_key .. ":" .. template_args[2]
__state.senseid_parent_etymon[sense_id_key] = active_etymon_args
__state.cached_etymon_pages[sense_id_key] = tostring(etymon_page)
end
end
end
end
if descendants_is_toplevel and lang_section_for_descendants and #found_templates_for_lang > 0 then
M.descendants.cache_page_checks({
lang_section = lang_section_for_descendants,
etymon_lang_code = etymon_lang_code,
found_templates_for_lang = found_templates_for_lang,
entry_title = __state.entry_title,
entry_lang_code = __state.entry_lang_code,
entry_lang = __state.entry_lang_code and Util.get_lang(__state.entry_lang_code, true) or nil,
cached_descendants_checks = __state.cached_descendants_checks,
lang_page_key = lang_page_key,
redirected_from = redirected_from,
})
end
local id_data_list = {}
for _, args in ipairs(found_templates_for_lang) do
local id = args.id or "*"
table.insert(id_data_list, { id = id, pos = args.pos })
end
__state.available_etymon_ids[lang_page_key] = id_data_list
if #found_templates_for_lang == 1 then
__state.single_etymons[lang_page_key] = found_templates_for_lang[1]
end
if redirected_from and __state.available_etymon_ids[lang_page_key] then
__state.available_etymon_ids[redirected_from] = __state.available_etymon_ids[redirected_from] or {}
for _, id_data in ipairs(__state.available_etymon_ids[lang_page_key]) do
table.insert(__state.available_etymon_ids[redirected_from], id_data)
end
end
if __state.cached_etymon_args[key] ~= nil or __state.senseid_parent_etymon[key] ~= nil then
-- All done!
return
elseif redirect_target and not redirected_from then
-- Try scraping the redirect.
etymon_page = redirect_target.prefixedText
DataRetriever.cache_page_etymons(etymon_page, redirect_target, lang_page_key .. ":" .. etymon_id, etymon_lang, etymon_id, lang_page_key, descendants_is_toplevel)
__state.cached_etymon_args[key] = __state.cached_etymon_args[etymon_lang_code .. ":" .. etymon_page .. ":" .. etymon_id]
else
__state.cached_etymon_args[key] = M.data.STATUS.MISSING
end
end
-- Given an etymon object, scrape its page (if necessary) and return its own etymon arguments as well as the page name.
function DataRetriever.get_etymon_args(etymon_data, is_toplevel)
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
if etymon_data.id then
local key = base_key .. ":" .. etymon_data.id
local cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key]
if cached_args == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, is_toplevel)
end
cached_args = __state.cached_etymon_args[key] or __state.senseid_parent_etymon[key] -- refresh
-- Get etymon_id from parent if this was resolved via senseid
local parent_etymon = __state.senseid_parent_etymon[key]
local resolved_etymon_id = parent_etymon and parent_etymon.id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, key, norm_lang, etymon_data.id, nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = {
explicit_id = etymon_data.id,
parent_etymon = parent_etymon,
},
})
end
end
return cached_args, __state.cached_etymon_pages[key], resolved_etymon_id, descendants_check
else
__state.used_idless_etymon = true
if is_toplevel then
__state.toplevel_idless_etymon = true
end
if __state.available_etymon_ids[base_key] == nil then
local title = mw.title.new(page)
if not title then error('Invalid page title "' .. page .. '" encountered.') end
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, is_toplevel)
end
local ids = __state.available_etymon_ids[base_key] or {}
local count = #ids
-- Try to filter by postype if available and we have multiple candidates
if count > 1 and etymon_data.postype then
local matching_ids = {}
for _, id_data in ipairs(ids) do
if id_data.pos == etymon_data.postype then
table.insert(matching_ids, id_data)
end
end
if #matching_ids == 1 then
local matched_id = matching_ids[1].id
local matched_key = base_key .. ":" .. matched_id
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id = matched_id },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id = matched_id },
})
end
end
return __state.cached_etymon_args[matched_key], __state.cached_etymon_pages[matched_key], nil, descendants_check
end
end
if count == 1 then
local only_id_data = ids[1]
local only_id = (type(only_id_data) == "table" and only_id_data.id) or only_id_data or "*"
local descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = is_toplevel,
base_key = base_key,
lookup = { id_data = only_id_data },
})
if is_toplevel and descendants_check == nil then
local title = mw.title.new(page)
if title then
DataRetriever.cache_page_etymons(page, title, base_key .. ":*", norm_lang, "*", nil, true)
descendants_check = M.descendants.get_lookup_check({
cached_descendants_checks = __state.cached_descendants_checks,
is_toplevel = true,
base_key = base_key,
lookup = { id_data = only_id_data },
})
end
end
return __state.single_etymons[base_key], __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
local page_link = M.links.full_link({
term = page,
lang = norm_lang,
no_generate_forms = true,
}, "term")
local function format_id_hint(id_data, idx)
local id = type(id_data) == "table" and id_data.id or id_data
local pos = type(id_data) == "table" and id_data.pos
if id and id ~= "" and id ~= "*" then
return '"' .. id .. '"'
end
if pos and pos ~= "" then
return "unnamed (|pos=" .. pos .. "|)"
end
return "etymon #" .. idx .. " (no |id= on page)"
end
local id_list = {}
local all_idless = true
local target_has_idless = false
for i, id_data in ipairs(ids) do
local id = type(id_data) == "table" and id_data.id or id_data
if id and id ~= "" and id ~= "*" then
all_idless = false
else
target_has_idless = true
end
table.insert(id_list, format_id_hint(id_data, i))
end
if is_toplevel and target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local any_pos = false
for _, id_data in ipairs(ids) do
local pos = type(id_data) == "table" and id_data.pos
if pos and pos ~= "" then
any_pos = true
break
end
end
local suggestion_text
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. count .. " {{etymon}} templates for "
.. norm_lang:getCanonicalName() .. ")."
if all_idless then
if any_pos then
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. mw.text.listToText(id_list) .. "."
else
suggestion_text = " None set `|id=` yet; add a unique `|id=` to each {{etymon}} in that section from top to bottom, then `<id:identifier>` after the term here (same value as `|id=`)."
end
else
suggestion_text = " Specify which one with `<id:identifier>` after the term. Options: " .. mw.text.listToText(id_list) .. "."
end
Util.add_warning(lead .. suggestion_text, true)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local TreeBuilder = {}
local function parse_etymon_references(refs_text)
if not refs_text or refs_text == "" then
return ""
end
return M.references.parse_references(refs_text)
end
local function parse_tree_references(node)
if node.ref then
node.parsed_ref = parse_etymon_references(node.ref)
end
if node.children then
for _, container in ipairs(node.children) do
if container.terms then
for _, term in ipairs(container.terms) do
parse_tree_references(term)
end
end
end
end
end
-- Build a unique key for deduplication in the seen table
function TreeBuilder.build_key(lang, title, args)
local norm_lang_code = Util.get_norm_lang(lang):getFullCode()
local is_table = type(args) == "table"
local id = (is_table and args.id) or ""
if title then
return norm_lang_code .. ":" .. M.links.get_link_page(title, lang) .. ":" .. id
end
if is_table and args.status == M.data.STATUS.INLINE then
local content_parts = {}
for i = 1, #args do
content_parts[i] = tostring(args[i])
end
return norm_lang_code .. ":*:" .. id .. "\0" .. table.concat(content_parts, "\0")
end
return norm_lang_code .. ":*:" .. id
end
function TreeBuilder.build(lang, title, args, seen, depth, stop_recursion)
seen = seen or {}
depth = depth or 0
local is_toplevel = (depth == 0)
if depth > __state.max_depth_reached then
__state.max_depth_reached = depth
end
__state.total_nodes = __state.total_nodes + 1
local lang_code = lang:getCode()
__state.language_count[lang_code] = (__state.language_count[lang_code] or 0) + 1
local current_id = (type(args) == "table" and args.id) or ""
local key = TreeBuilder.build_key(lang, title, args)
local node = { lang = lang, title = title, id = current_id, args = args, children = {}, status = M.data.STATUS.OK }
if type(args) ~= "table" or seen[key] then
node.status = args or M.data.STATUS.MISSING
-- Mark as duplicate if we've seen this node before
if seen[key] then
node.is_duplicate = true
node.duplicate_key = key
local original_node = seen[key]
if type(original_node) == "table" and original_node.children and #original_node.children > 0 then
node.original_has_children = true
end
end
return node
end
node.status = args.status or M.data.STATUS.OK
seen[key] = node
-- If stop_recursion is set, skip parsing children but check for visible children
if stop_recursion then
local keywords = M.data.keywords
local has_visible_children = false
for i = 2, #args do
local param = args[i]
if type(param) == "string" then
local keyword_base = get_keyword_base(param)
if keyword_base and keywords[keyword_base] then
-- It's a keyword, check if visible in tree (invisible "all" or "tree" = hidden in tree)
local keyword_info = keywords[keyword_base]
local inv = keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
elseif param:sub(1, 1) ~= ":" then
-- It's a term (not a keyword), so there are visible children
has_visible_children = true
break
end
end
end
node.has_visible_children = has_visible_children
return node
end
-- Parse args into keyword containers
local current_keyword = "from"
local current_keyword_modifiers = {}
local current_container = nil
-- Helper to track keyword usage at top level
local function track_keyword_usage(keyword, target_lang, source_lang)
if not is_toplevel then return end
if not __state.toplevel_keyword_stats[keyword] then
__state.toplevel_keyword_stats[keyword] = {
count = 0,
target_langs = {},
source_langs = {},
}
end
local keyword_data = __state.toplevel_keyword_stats[keyword]
keyword_data.count = keyword_data.count + 1
local target_code = target_lang:getCode()
keyword_data.target_langs[target_code] = (keyword_data.target_langs[target_code] or 0) + 1
if source_lang then
local source_code = source_lang:getCode()
keyword_data.source_langs[source_code] = (keyword_data.source_langs[source_code] or 0) + 1
end
end
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
current_container = {
keyword = current_keyword,
keyword_info = M.data.keywords[current_keyword],
keyword_modifiers = current_keyword_modifiers,
terms = {},
}
table.insert(node.children, current_container)
-- Override keyword text/phrase for nominalization with <g:code>
if current_keyword_modifiers.g and current_keyword == "nominalization" then
local labels = get_nominalization_label_for_g(current_keyword_modifiers.g)
if not labels then
local codes = {}
for c in pairs(M.data.nominalization_g_codes) do table.insert(codes, c) end
table.sort(codes)
error("Invalid <g:" .. tostring(current_keyword_modifiers.g) .. ">. Supported codes for nominalization: " .. table.concat(codes, ", "))
end
current_container.keyword_info = {}
for k, v in pairs(M.data.keywords[current_keyword]) do current_container.keyword_info[k] = v end
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
end
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
current_keyword = keyword
current_keyword_modifiers = modifiers
current_container = nil -- Force new container for new keyword
elseif type(param) == "string" and param:sub(1, 1) == ":" then
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif type(param) == "string" then
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
-- Track keyword usage at top level
track_keyword_usage(current_keyword, lang, etymon_data.lang)
local term_node = {}
-- Handle suppress_term (-) and unknown_term (empty) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
ensure_container()
if etymon_data.ety then
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
term_node = TreeBuilder.build(etymon_data.lang, nil, inline_args, seen, depth + 1)
else
term_node = {
lang = etymon_data.lang,
children = {},
status = M.data.STATUS.OK,
}
end
term_node.suppress_term = etymon_data.suppress_term
term_node.unknown_term = etymon_data.unknown_term
term_node.is_family = etymon_data.is_family
term_node.is_uncertain = etymon_data.unc
term_node.ref = etymon_data.ref
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.pos = etymon_data.pos
term_node.ng = etymon_data.ng
term_node.lit = etymon_data.lit
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
else
-- Regular term: fetch arguments from page
local etymon_args, page_of, resolved_etymon_id, descendants_check = DataRetriever.get_etymon_args(etymon_data, is_toplevel)
if etymon_data.id and etymon_args == M.data.STATUS.MISSING and not etymon_data.ety then
local page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
local norm_lang = Util.get_norm_lang(etymon_data.lang)
local base_key = norm_lang:getFullCode() .. ":" .. page
local available_ids = __state.available_etymon_ids[base_key] or {}
if #available_ids > 0 then
__state.has_mismatched_id = true
end
end
-- Check for <ety> inline parameter doesn't override the scraped arguments, unless the latter are missing
if etymon_data.ety then
if etymon_args == M.data.STATUS.REDLINK or etymon_args == M.data.STATUS.MISSING then
__state.current_page_has_inline_etymology = true
if is_toplevel then
__state.toplevel_has_inline_etymology = true
end
local inline_args = EtymonParser.parse_inline_ety(etymon_data.ety, etymon_data.lang)
-- Track inline ety keywords too
local inline_keyword = get_keyword(inline_args[2], true)
if inline_keyword and #inline_args >= 3 then
local inline_etymon = EtymonParser.parse_etymon(inline_args[3], etymon_data.lang)
if inline_etymon then
track_keyword_usage(inline_keyword, etymon_data.lang, inline_etymon.lang)
end
end
inline_args.id = etymon_data.id
inline_args.status = M.data.STATUS.INLINE
etymon_args = inline_args
term_node.page_of = __state.cached_etymon_pages[key] -- term node is on the same page as the parent
else
-- Scraped arguments exist, <ety> is redundant and ignored
__state.current_page_has_redundant_etymology = true
if is_toplevel then
__state.toplevel_redundant_etymology = true
end
end
end
-- Ensure container exists before checking keyword info
ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = current_container.keyword_info
local should_stop_recursion = (stop_recursion or (keyword_info and keyword_info.no_child_categories))
term_node = TreeBuilder.build(etymon_data.lang, etymon_data.term, etymon_args, seen, depth + 1, should_stop_recursion)
term_node.target_key = Util.get_norm_lang(etymon_data.lang):getFullCode() ..
":" .. M.links.get_link_page(etymon_data.term, etymon_data.lang)
term_node.id = etymon_data.id
term_node.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.t = etymon_data.t
term_node.tr = etymon_data.tr
term_node.ts = etymon_data.ts
term_node.pos = etymon_data.pos
term_node.alt = etymon_data.alt
term_node.g = etymon_data.g
term_node.ng = etymon_data.ng
term_node.ref = etymon_data.ref
term_node.is_uncertain = etymon_data.unc
term_node.override = etymon_data.override
term_node.page_of = page_of
term_node.aftype = etymon_data.aftype
term_node.postype = etymon_data.postype
term_node.bor = etymon_data.bor
term_node.lbor = etymon_data.lbor
term_node.slbor = etymon_data.slbor
term_node.lit = etymon_data.lit
term_node.is_family = etymon_data.is_family
term_node.q = etymon_data.q
term_node.qq = etymon_data.qq
term_node.l = etymon_data.l
term_node.ll = etymon_data.ll
term_node.missing_descendants_header, term_node.missing_descendants_entry =
M.descendants.get_term_sync_flags(current_keyword, term_node.status, descendants_check)
end
table.insert(current_container.terms, term_node)
end
end
end
return node
end
-- Convert etymology tree to JSON-serializable table
local function tree_to_json(node)
local obj = {
term = node.title,
lang = node.lang:getCode(),
lang_name = node.lang:getCanonicalName(),
id = (node.id and node.id ~= "") and node.id or nil,
status = node.status,
is_uncertain = node.is_uncertain or nil,
is_duplicate = node.is_duplicate or nil,
gloss = node.t,
transliteration = node.tr,
transcription = node.ts,
alt = node.alt,
g = node.g,
pos = node.pos,
ng = node.ng,
children = {},
}
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if keyword_info then
local container_obj = {
keyword = container.keyword,
keyword_label = keyword_info.text,
keyword_abbrev = keyword_info.abbrev,
is_group = keyword_info.is_group or nil,
is_invisible = keyword_info.invisible or nil,
is_uncertain = (container.keyword_modifiers and container.keyword_modifiers.unc) or nil,
terms = {},
}
for _, term in ipairs(container.terms or {}) do
table.insert(container_obj.terms, tree_to_json(term))
end
table.insert(obj.children, container_obj)
end
end
return obj
end
local function track_ranges(base_key, value, ranges, lang_code)
M.track("etymon/" .. base_key .. "/" .. value)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. value)
end
for _, range in ipairs(ranges) do
local matches = false
if range.min and range.max then
matches = value >= range.min and value <= range.max
elseif range.min then
matches = value >= range.min
elseif range.max then
matches = value <= range.max
elseif range.exact then
matches = value == range.exact
end
if matches then
M.track("etymon/" .. base_key .. "/" .. range.label)
if lang_code then
M.track("etymon/lang/" .. lang_code .. "/" .. base_key .. "/" .. range.label)
end
break
end
end
end
local function track_title_pagename_mismatch_after_strip(lang)
local lang_code = lang:getCode()
M.track("etymon/title/pagename-mismatch-after-strip-diacritics")
M.track("etymon/lang/" .. lang_code .. "/title/pagename-mismatch-after-strip-diacritics")
end
-- Build and return the etymology data tree for a given term.
function export.get_tree(lang, title, args, options)
options = options or {}
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
if options.validate then
EtymonParser.validate(lang, args, options.id, title, options.pos, false)
end
local lang_code = lang:getCode()
local start_index = (args[1] == lang_code) and 2 or 1
local tree_args = { [1] = lang_code, id = options.id or args.id }
for i = start_index, #args do
table.insert(tree_args, args[i])
end
__state.cached_etymon_args[lang_code .. ":" .. title .. ":" .. (tree_args.id or "")] = tree_args
local ety_data_tree = TreeBuilder.build(lang, title, tree_args)
parse_tree_references(ety_data_tree)
if options.json then
return M.JSON.toJSON(tree_to_json(ety_data_tree))
end
return ety_data_tree
end
-- Given a language code, page name and optionally the id= parameter,
-- render the tree and only the etymology tree for the relevant page.
-- Fetches and parses the corresponding {{etymon}} from the requested page,
-- and any further pages needed to render the tree.
-- Parameters can be passed either through the #invoke or as
-- template parameters *through* an #invoke.
function export.render_tree_for_etymon_on_page(frame)
local frame_args = frame.args
local parent_args = frame:getParent().args
local langcode = frame_args[1] or parent_args[1]
local pagename = frame_args[2] or parent_args[2]
local id = frame_args["id"] or parent_args["id"]
local display_title = frame_args["title"] or parent_args["title"]
local parsed_title = mw.title.new(pagename, 0)
local title
if parsed_title.namespace == 0 then
title = M.pages.safe_page_name(parsed_title)
elseif parsed_title.namespace == 118 then
title = "*" .. M.pages.safe_page_name(parsed_title)
else
error("Unsupported namespace for render_tree_for_etymon_on_page: " .. parsed_title.namespace)
end
local lang = Util.get_lang(langcode)
-- Construct etymon_data for DataRetriever.get_args.
local etymon_data = {
lang = lang,
term = title,
id = id
}
local args, pagename = DataRetriever.get_etymon_args(etymon_data, true)
if args == M.data.STATUS.MISSING then
error("The etymon template was not found (language " ..
langcode ..
", title '" ..
title ..
"'" ..
(id and ", ID '" .. id .. "'" or ", no ID given") .. "). Page contents may have changed in the interim.")
end
local tree_title = display_title or title
if lang:stripDiacritics(M.links.remove_links(tree_title)) ~= lang:stripDiacritics(M.links.remove_links(title)) then
track_title_pagename_mismatch_after_strip(lang)
end
local ety_data_tree = export.get_tree(lang, tree_title, args, {
validate = true,
id = id,
})
local output = {}
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
return table.concat(output)
end
function export.main(frame)
local parent_args = frame:getParent().args
local args = M.parameters.process(parent_args, M.parameters_data.etymon)
local lang = args[1]
local etymon_args = args[2]
local id = args.id
local title = args.title
local text = args.text
local tree = args.tree
local etydate = args.etydate
local rfe = args.rfe
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == "Reconstruction" then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == "Reconstruction" then
entry_pagename = "*" .. entry_pagename
end
if lang:stripDiacritics(M.links.remove_links(title)) ~= lang:stripDiacritics(M.links.remove_links(entry_pagename)) then
track_title_pagename_mismatch_after_strip(lang)
end
local current_L2 = M.pages.get_current_L2()
if current_L2 then
local norm_lang = Util.get_norm_lang(lang)
local norm_name = "Bahasa " .. norm_lang:getCanonicalName()
if current_L2 ~= norm_name then
local lang_desc = lang:getCode() .. " (" .. lang:getCanonicalName() .. ")"
if norm_lang:getCode() ~= lang:getCode() then
lang_desc = lang_desc .. ", normalized to " .. norm_lang:getCode() .. " (" .. norm_name .. ")"
end
error("Language '" .. lang_desc .. "' does not match the L2 header (" .. current_L2 .. ").")
end
end
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
})
if args.json then
return ety_data_tree
end
local output = {}
local text_allowlist_mode = M.text_allowed.default_mode or "off"
if text and text_allowlist_mode ~= "off" and not Util.is_text_param_allowed_for_lang(lang) then
local msg = "Etymology texts (parameter <code>text=</code>) are not allowed for " .. lang:getFullName() ..
"; see [[Template:etymon#Text allowlist|Template:etymon § Text allowlist]] for the list of languages that may use the <code>text=</code> parameter."
if text_allowlist_mode == "error" then
error(msg)
else
Util.add_warning(msg, true)
end
end
local lang_exc = Util.get_lang_exception(lang)
if lang_exc and lang_exc.disallow then
local disallow = lang_exc.disallow
local error_text = " for " .. lang:getFullName()
if disallow.ref then
error_text = error_text .. "; see " .. disallow.ref
else
error_text = error_text .. "."
end
if tree and disallow.tree then
error("Etymology trees are not allowed" .. error_text)
end
if text and disallow.text then
error("Etymology texts are not allowed" .. error_text)
end
end
if etydate then
local etydate_param_mods = {
ref = { list = true, type = "references", allow_holes = true },
refn = { list = true, allow_holes = true },
nocap = { type = "boolean" },
}
local function generate_etydate_obj(etydate_text)
local etydate_specs = {}
for spec in etydate_text:gmatch("[^,]+") do
table.insert(etydate_specs, mw.text.trim(spec))
end
return { [1] = etydate_specs }
end
local parsed_etydate = M.parse_utilities.parse_inline_modifiers(etydate, { param_mods = etydate_param_mods, generate_obj = generate_etydate_obj })
local etydate_args = {
[1] = parsed_etydate[1],
nocap = parsed_etydate.nocap or false,
}
ety_data_tree.etydate = M.etydate.format_etydate(etydate_args, { omit_refs = true })
if parsed_etydate.ref and #parsed_etydate.ref > 0 then
ety_data_tree.etydate_refs = parsed_etydate.ref
end
end
if tree then
table.insert(output, M.template_styles("Module:etymon/styles.css"))
table.insert(output, M.tree.render({
data_tree = ety_data_tree,
format_term_func = function(term, is_toplevel)
return Util.format_term(term, is_toplevel, {
gloss = "suppress",
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
})
end,
}))
end
-- Check if there are any visible children in tree (invisible "all" or "tree" = hidden in tree)
local has_visible_children = false
for _, child in ipairs(ety_data_tree.children or {}) do
local child_keyword_info = child.keyword_info
local inv = child_keyword_info and child_keyword_info.invisible
if not (inv == "all" or inv == true or inv == "tree") then
has_visible_children = true
break
end
end
local tree_disallowed = lang_exc and lang_exc.disallow and lang_exc.disallow.tree
local ety_tree_json = M.JSON.toJSON(tree_to_json(ety_data_tree))
local anchor = M.anchors.etymonid(lang, id, {
no_tree = args.notree,
title = title,
empty_tree = (not has_visible_children) or tree_disallowed,
ety_tree_json = ety_tree_json,
})
table.insert(output, anchor)
if text then
local max_depth, stop_at_blue_link, stop_at_lang, stop_at_lang_or_bluelink
if text == "++" then
max_depth, stop_at_blue_link = false, false
elseif text == "+" then
max_depth, stop_at_blue_link = 1, false
elseif text == "*" then
max_depth, stop_at_blue_link = false, true
elseif text:match("^:[^*]+%*$") then
-- Stop at a specific language OR first bluelink after it, e.g., ":ota*"
-- If the target language is a redlink, continue to the first bluelink
local lang_code = text:match("^:([^*]+)%*$")
if lang_code and lang_code ~= "" then
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang_or_bluelink = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false
end
elseif text:sub(1, 1) == ":" then
-- Stop at a specific language, e.g., ":ar" stops at first Arabic term
local lang_code = text:sub(2)
if lang_code ~= "" then
-- Validate the language code
local lang_obj = Util.get_lang(lang_code, true)
if lang_obj then
stop_at_lang = lang_code
else
Util.add_warning('Invalid language code "' .. lang_code .. '" in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
Util.add_warning('Empty language code in text parameter. Showing full chain instead.')
max_depth, stop_at_blue_link = false, false -- default to ++
end
else
local num = tonumber(text)
if num and num >= 1 then
max_depth, stop_at_blue_link = num, false
else
error('Invalid text value "' ..
text .. '". Valid values are: "++" (full chain), "+" (first step only), "*" (until first blue link), a number (max steps), ":lang" (stop at language), or ":lang*" (stop at language or first bluelink if redlink)')
end
end
table.insert(output, M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
}))
end
if rfe then
local rfe_param_mods = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
}
local function generate_rfe_obj(rfe_text)
-- Check if it's a boolean true value
if M.yesno(rfe_text, false) then
return { is_boolean = true }
else
return { text = rfe_text }
end
end
local rfe_with_defaults = Util.add_boolean_defaults(rfe, rfe_param_mods)
local parsed_rfe = M.parse_utilities.parse_inline_modifiers(rfe_with_defaults, {
param_mods = rfe_param_mods,
generate_obj = generate_rfe_obj
})
local rfe_args = {
[1] = lang:getCode(),
nocat = parsed_rfe.nocat,
sort = parsed_rfe.sort,
y = parsed_rfe.y,
m = parsed_rfe.m,
fragment = parsed_rfe.fragment,
section = parsed_rfe.section,
box = parsed_rfe.box,
noes = parsed_rfe.noes,
}
if not parsed_rfe.is_boolean then
rfe_args[2] = parsed_rfe.text
end
table.insert(output, frame:expandTemplate({
title = "rfe",
args = rfe_args
}))
end
if Util.is_content_page() and __state.max_depth_reached > 0 then
local lang_code = lang:getCode()
local depth_ranges = {
{ min = 50, label = "extremely-deep" },
{ min = 20, label = "20+" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ max = 2, label = "1-2" }
}
local node_ranges = {
{ min = 100, label = "extremely-large" },
{ min = 50, label = "50+" },
{ min = 20, max = 49, label = "20-49" },
{ min = 10, max = 19, label = "10-19" },
{ min = 5, max = 9, label = "5-9" },
{ max = 4, label = "1-4" }
}
local language_ranges = {
{ min = 10, label = "10+" },
{ min = 5, max = 9, label = "5-9" },
{ min = 3, max = 4, label = "3-4" },
{ exact = 2, label = "2" },
{ exact = 1, label = "1" }
}
track_ranges("depth", __state.max_depth_reached, depth_ranges, lang_code)
track_ranges("nodes", __state.total_nodes, node_ranges, lang_code)
local unique_languages = 0
for _ in pairs(__state.language_count) do
unique_languages = unique_languages + 1
end
track_ranges("unique-languages", unique_languages, language_ranges, lang_code)
if __state.total_nodes == __state.max_depth_reached + 1 then
track_ranges("linear-depth", __state.max_depth_reached, depth_ranges, lang_code)
end
end
local categories = {}
if Util.is_content_page() then
local should_suppress_categories = lang_exc and lang_exc.suppress_categories
if not should_suppress_categories and not args.nocat then
categories = M.categories.render({
data_tree = ety_data_tree,
page_lang = lang,
available_etymon_ids = __state.available_etymon_ids,
senseid_parent_etymon = __state.senseid_parent_etymon,
get_norm_lang_func = Util.get_norm_lang,
lang_exc = lang_exc,
})
end
local target_lang_code = lang:getCode()
for keyword, keyword_data in pairs(__state.toplevel_keyword_stats) do
-- Track keyword globally
M.track("etymon/keyword/" .. keyword)
-- Track keyword per target language
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code)
-- Track keyword per source language
for source_code, count in pairs(keyword_data.source_langs) do
M.track("etymon/keyword/" .. keyword .. "/source/" .. source_code)
-- Track keyword per target+source combination
M.track("etymon/keyword/" .. keyword .. "/target/" .. target_lang_code .. "/source/" .. source_code)
end
end
if tree then
table.insert(categories, "Laman dengan pokok etimologi")
table.insert(categories, lang:getCanonicalName() .. " entries with etymology trees")
end
if text then table.insert(categories, lang:getCanonicalName() .. " entries with etymology texts") end
if args.exnihilo then table.insert(categories, lang:getCanonicalName() .. " terms coined ex nihilo") end
if __state.toplevel_has_inline_etymology then
table.insert(categories, "Laman dengan etimon dalam baris untuk pautan merah")
end
if __state.toplevel_redundant_etymology then
table.insert(categories, "Laman dengan etimon dalam baris lewah")
end
if __state.toplevel_idless_etymon then
table.insert(categories, "Laman menggunakan etimon tanpa ID")
end
if __state.has_mismatched_id then
table.insert(categories, lang:getCanonicalName() .. " entries referencing etymons with mismatched IDs")
end
if __state.linked_page_multiple_etymons_idless then
table.insert(categories,
lang:getCanonicalName() .. " entries referencing pages with multiple etymons missing IDs")
end
end
if #categories > 0 then
table.insert(output, M.categories.format(categories, lang))
end
if __state.warnings then
for i, warning in ipairs(__state.warnings) do
table.insert(output, (i == 1 and "\n" or "") .. warning .. "\n")
end
end
return table.concat(output)
end
return export
tlojdwh1m67a5w9qz5fatk486sr5xh5
Modul:category tree/topic/Music
828
74318
342797
223226
2026-05-16T12:13:25Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/86803496|86803496]])
342797
Scribunto
text/plain
local labels = {}
------------------------------------ music base labels ------------------------------------
labels["muzik"] = {
type = "berkenaan",
description = "default",
parents = {"seni", "bunyi"},
}
labels["menyanyi"] = {
type = "berkenaan",
description = "default",
parents = {"muzik", "bercakap"},
}
------------------------------------ music theory ------------------------------------
labels["suara dan daftar muzik"] = {
type = "jenis",
description = "default",
parents = {"muzik", "menyanyi"},
}
labels["meter muzik"] = {
type = "jenis",
description = "default",
parents = {"muzik"},
}
labels["not muzik"] = {
type = "jenis",
description = "default",
parents = {"muzik"},
}
------------------------------------ musicians ------------------------------------
labels["pemuzik"] = {
type = "jenis",
description = "default",
parents = {"pekerjaan", "muzik"},
}
labels["Justin Bieber"] = {
type = "berkenaan",
description = "=Penyanyi Kanada {{w|Justin Bieber}}",
parents = {"individu", "muzik"},
}
labels["Taylor Swift"] = {
type = "berkenaan",
description = "=Penyanyi-penulis lagu Amerika {{w|Taylor Swift}}",
parents = {"individu", "muzik"},
}
------------------------------------ musical genres ------------------------------------
labels["genre muzik"] = {
type = "jenis",
description = "default",
parents = {"muzik", "genre"},
}
labels["muzik blues"] = {
type = "berkenaan",
wikidata = 9759,
description = "default",
parents = {"genre muzik"},
}
labels["jazz"] = {
type = "berkenaan",
description = "default",
parents = {"genre muzik"},
}
labels["opera"] = {
type = "berkenaan",
description = "default",
parents = {"teater", "genre muzik"},
}
------------------------------------ musical instruments ------------------------------------
labels["alat muzik"] = {
type = "set",
description = "default",
parents = {"muzik"},
}
labels["alat tiup logam"] = {
type = "jenis",
description = "default",
parents = {"alat tiup"},
}
labels["gong"] = {
type = "berkenaan",
description = "default",
parents = {"alat perkusi"},
}
labels["alat organ"] = {
type = "jenis",
description = "default",
parents = {"alat tiup"},
}
labels["alat perkusi"] = {
type = "set",
description = "default",
parents = {"alat muzik"},
}
labels["sintesis suara nyanyian"] = {
type = "berkenaan",
description = "default",
parents = {"alat muzik", "menyanyi"},
}
labels["alat muzik bertali"] = {
type = "set",
description = "Nama-nama [[alat muzik bertali]] dalam bahasa {{{langname}}}.",
parents = {"alat muzik"},
}
labels["alat tiup"] = {
type = "jenis",
description = "default",
parents = {"alat muzik"},
}
labels["alat tiup kayu"] = {
type = "jenis",
description = "default",
parents = {"alat tiup"},
}
------------------------------------ music misc ------------------------------------
labels["pembuatan alat muzik bertali"] = {
type = "berkenaan",
description = "default",
parents = {"muzik", "kraf"},
}
labels["industri muzik"] = {
type = "berkenaan",
description = "default with the",
parents = {"industri", "muzik"},
}
labels["lagu kebangsaan"] = {
type = "nama",
description = "default",
parents = {"karya seni", "muzik"},
}
return labels
g227nyhq2uje33iwqtvx2kdq3ko1ggr
Modul:etymon/tree
828
82357
342818
234441
2026-05-16T13:39:35Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/89866649|89866649]]), terjemahan belum selesai
342818
Scribunto
text/plain
local export = {}
local html_create = mw.html.create
local max = math.max
local function create_vertical_connector()
return html_create('span'):addClass('etytree-connector-vertical')
end
local function create_abbr(text, title, glossary)
local abbr = html_create('abbr')
:attr('title', title)
:wikitext(text)
if glossary then
abbr = '[[Appendix:Glossary#' .. glossary .. '|' .. tostring(abbr) .. ']]'
end
return html_create('span'):addClass('etytree-label'):node(abbr)
end
local function create_uncertainty_marker()
return html_create('abbr')
:addClass('etytree-unc')
:attr('title', 'uncertain')
:wikitext('?')
end
local function create_label_container()
return html_create('span'):addClass('etytree-label-container')
end
local function invisible_in_tree(inv)
return inv == "all" or inv == true or inv == "tree"
end
local function render_label(term_block, keyword_info, keyword_modifiers, is_uncertain, is_group_child, term_labels)
-- Skip label when invisible in tree
local has_label = keyword_info and keyword_info.abbrev and not is_group_child and not invisible_in_tree(keyword_info.invisible)
-- For group children, keyword uncertainty is shown on the group label, not on individual terms
local keyword_uncertain = keyword_modifiers and keyword_modifiers.unc and not is_group_child
local show_term_uncertainty = is_uncertain
-- Check if we have term-specific labels
local has_term_labels = term_labels and #term_labels > 0
if not has_label and not show_term_uncertainty and not keyword_uncertain and not has_term_labels then
return
end
local label_span = create_label_container()
if has_label then
local glossary_title = keyword_info.glossary
and keyword_info.glossary:gsub("_", " ")
or keyword_info.abbrev
label_span:node(create_abbr(
keyword_info.abbrev,
glossary_title,
keyword_info.glossary
))
-- Show uncertainty marker if term or keyword is uncertain
if show_term_uncertainty or keyword_uncertain then
label_span:node(create_uncertainty_marker())
end
else
-- No label, but term or keyword is uncertain
if show_term_uncertainty or keyword_uncertain then
label_span:node(create_uncertainty_marker())
end
end
-- Add term-specific labels
if has_term_labels then
for _, label_info in ipairs(term_labels) do
label_span:node(create_abbr(label_info.abbrev, label_info.title, label_info.glossary))
end
end
term_block:node(label_span)
end
local function render_term_block(node_data, format_term_func, is_toplevel)
local link_content = html_create()
link_content
:tag('span')
:addClass('etyl')
:wikitext(node_data.lang:getCanonicalName())
:done()
local term_text = format_term_func(node_data, is_toplevel)
if term_text then
link_content
:wikitext(' ')
:tag('span')
:addClass('etytree-term')
:wikitext(term_text)
:done()
end
local block = html_create('div'):addClass('etytree-block'):node(link_content)
-- Add duplicate styling if this is a duplicate node
if node_data.is_duplicate then
block:addClass('etytree-duplicate')
end
return block
end
local function create_dotted_connector()
return html_create('span'):addClass('etytree-connector-dotted')
end
-- Create an L-shaped connector for nodes with hidden ancestry (duplicate or no_child_categories)
local function create_duplicate_connector()
local container = html_create('div'):addClass('etytree-duplicate-connector')
local inner_wrapper = container:tag('div')
inner_wrapper:tag('span'):addClass('etytree-dup-right')
inner_wrapper:tag('span'):addClass('etytree-dup-horiz')
inner_wrapper:tag('span'):addClass('etytree-dup-left')
inner_wrapper:tag('span'):addClass('etytree-dup-arrow'):wikitext('▲')
return container
end
local function render_group_label(connecting_line, keyword_info, keyword_modifiers)
local has_abbrev = keyword_info and keyword_info.abbrev
local keyword_uncertain = keyword_modifiers and keyword_modifiers.unc
-- Nothing to show if no abbrev and no uncertainty
if not has_abbrev and not keyword_uncertain then
return
end
local label_span = connecting_line:tag('span'):addClass('etytree-group-label')
if has_abbrev then
local glossary_title = keyword_info.glossary
and keyword_info.glossary:gsub("_", " ")
or keyword_info.abbrev
label_span:node(create_abbr(keyword_info.abbrev, glossary_title, nil))
end
-- Add uncertainty marker if keyword has <unc> modifier
if keyword_uncertain then
label_span:node(create_uncertainty_marker())
end
end
local function add_branch_connector(column, index, total)
if index == 1 then
column:tag('span'):addClass('etytree-branch-left')
elseif index == total then
column:tag('span'):addClass('etytree-branch-right')
else
column:tag('span'):addClass('etytree-connector-vertical-short')
column:tag('span'):addClass('etytree-branch-mid')
end
end
function export.render(opts)
opts = opts or {}
local data_tree = opts.data_tree
local format_term_func = opts.format_term_func
-- Forward declaration
local render_term
-- Render a container (keyword + its terms)
local function render_container(container, is_toplevel)
local keyword_info = container.keyword_info
local keyword_modifiers = container.keyword_modifiers or {}
local is_group = keyword_info and keyword_info.is_group
local terms = container.terms or {}
-- Skip container entirely only when invisible = "all" (or true)
if keyword_info and invisible_in_tree(keyword_info.invisible) then
return nil, 0, 0
end
if #terms == 0 then
return nil, 0, 0
end
-- For no_child_categories keywords (calque, semantic loan, etc.), don't render term's children
local skip_child_rendering = keyword_info and keyword_info.no_child_categories
-- Render each term in the container
local rendered_terms = {}
local container_height = 0
local container_width = 0
for _, term in ipairs(terms) do
-- Collect term-specific labels
local term_labels = {}
if term.bor then
table.insert(term_labels, { abbrev = "bor.", title = "borrowed", glossary = "loanword" })
end
if term.slbor then
table.insert(term_labels, { abbrev = "slbor.", title = "semi-learned borrowing", glossary = "semi-learned_borrowing" })
end
if term.lbor then
table.insert(term_labels, { abbrev = "lbor.", title = "learned borrowing", glossary = "learned_borrowing" })
end
local term_tree, term_height, term_width = render_term(term, keyword_info, keyword_modifiers, is_group, false, skip_child_rendering, term_labels)
table.insert(rendered_terms, {
tree = term_tree,
height = term_height,
width = term_width,
is_uncertain = term.is_uncertain,
})
container_height = max(container_height, term_height)
container_width = container_width + term_width
end
local rendered_html
local has_connector = false
if #rendered_terms == 1 then
-- Single term: just return it directly
rendered_html = rendered_terms[1].tree
container_height = rendered_terms[1].height
container_width = rendered_terms[1].width
else
-- Multiple terms: group them together
local subtree_container = html_create('div'):addClass('etytree-branch-group')
for i, term_data in ipairs(rendered_terms) do
local column = html_create('div'):addClass('etytree-branch')
column:node(term_data.tree)
add_branch_connector(column, i, #rendered_terms)
subtree_container:node(column)
end
local connecting_line = create_vertical_connector()
-- Add group label for group keywords
if is_group and not invisible_in_tree(keyword_info.invisible) then
render_group_label(connecting_line, keyword_info, keyword_modifiers)
end
rendered_html = html_create()
:node(subtree_container)
:node(connecting_line)
has_connector = true
end
return rendered_html, container_height, container_width, has_connector
end
-- Render a term node
render_term = function(term_node, keyword_info, keyword_modifiers, is_group_child, is_toplevel_term, skip_child_rendering, term_labels)
local tree_width, tree_height = 0, 0
local subtrees = {}
-- Process term's children (which are containers)
local has_hidden_children = false
if not term_node.is_duplicate and not skip_child_rendering then
for _, container in ipairs(term_node.children or {}) do
local subtree, sub_height, sub_width, subtree_has_connector = render_container(container, is_toplevel_term)
if subtree then
table.insert(subtrees, {
tree = subtree,
height = sub_height,
width = sub_width,
has_connector = subtree_has_connector,
})
tree_height = max(tree_height, sub_height)
tree_width = tree_width + sub_width
end
end
elseif skip_child_rendering then
-- Check if there are any visible children
-- When stop_recursion is true, children aren't parsed, but has_visible_children flag is set
if term_node.has_visible_children then
has_hidden_children = true
elseif term_node.children and #term_node.children > 0 then
-- Fallback: check parsed children for visibility
for _, container in ipairs(term_node.children) do
local child_keyword_info = container.keyword_info
if not (child_keyword_info and (child_keyword_info.invisible == "all" or child_keyword_info.invisible == true)) then
has_hidden_children = true
break
end
end
end
end
local is_toplevel_node = (keyword_info == nil)
local term_block = render_term_block(term_node, format_term_func, is_toplevel_node)
render_label(term_block, keyword_info, keyword_modifiers, term_node.is_uncertain, is_group_child, term_labels or {})
local term_html = html_create()
if #subtrees == 0 then
local show_connector = (term_node.is_duplicate and term_node.original_has_children) or has_hidden_children
if show_connector then
term_html:node(create_duplicate_connector())
end
term_html:node(term_block)
tree_width = tree_width + 1
elseif #subtrees == 1 then
term_html:node(subtrees[1].tree)
if not subtrees[1].has_connector then
term_html:node(create_vertical_connector())
end
term_html:node(term_block)
else
-- Multiple containers: need to merge them
local subtree_container = html_create('div'):addClass('etytree-branch-group')
for i, subtree_data in ipairs(subtrees) do
local column = html_create('div'):addClass('etytree-branch')
column:node(subtree_data.tree)
add_branch_connector(column, i, #subtrees)
subtree_container:node(column)
end
local connecting_line = create_vertical_connector()
term_html
:node(subtree_container)
:node(connecting_line)
:node(term_block)
end
return term_html, tree_height + 1, tree_width
end
local final_tree, final_height, final_width = render_term(data_tree, nil, nil, false, true)
local container = html_create('div')
:addClass('etytree-body')
:node(final_tree)
return tostring(html_create('div')
:addClass('etytree NavFrame')
:attr('data-etytree-height', final_height)
:attr('data-etytree-width', final_width)
:tag('div')
:addClass('NavHead')
:tag('div')
:wikitext('Etymology tree')
:done()
:done()
:tag('div')
:addClass('NavContent')
:node(container)
:done())
end
return export
ok2w6x96bozaxhyj2cy4qex9anfbtx5
Modul:etymon/categories
828
82358
342820
258374
2026-05-16T13:43:11Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikisumber bahasa Inggeris (semakan [[en:Special:Diff/90755775|90755775]]), terjemahan belum selesai
342820
Scribunto
text/plain
local export = {}
local M = require("Module:module loader").init({
require = {
etymology = "Module:etymology",
affix = "Module:affix",
etymology_specialized = "Module:etymology/specialized",
utilities = "Module:utilities",
},
loadData = {
data = "Module:etymon/data",
},
})
-- Evaluate whether a keyword is transitive for a given term
local function is_transitive(transitive_mode, page_lang, term_lang)
if transitive_mode == M.data.TRANSITIVE.ALWAYS then
return true
elseif transitive_mode == M.data.TRANSITIVE.NEVER then
return false
elseif transitive_mode == M.data.TRANSITIVE.CROSS_LANG then
return page_lang:getCode() ~= term_lang:getCode()
elseif transitive_mode == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
return page_lang:getCode() ~= term_lang:getCode()
end
error("Unknown transitive mode: " .. tostring(transitive_mode))
end
-- Get keyword config with language-specific overrides
local function get_keyword_config(keyword, lang_exc)
local base_config = M.data.keywords[keyword]
if not base_config then
return nil -- Invalid keyword
end
local overrides = lang_exc and lang_exc.keyword_overrides and lang_exc.keyword_overrides[keyword]
if not overrides then
return base_config
end
-- Merge overrides into base config
local merged = {}
for k, v in pairs(base_config) do
merged[k] = v
end
for k, v in pairs(overrides) do
merged[k] = v
end
return merged
end
function export.get_cat_name(source)
local _, cat_name = M.etymology.get_display_and_cat_name(source, true)
return cat_name
end
-- Normalize affix type aliases
local aftype_aliases = {
["pre"] = "prefix",
["suf"] = "suffix",
["in"] = "infix",
["inter"] = "interfix",
["circum"] = "circumfix",
["naf"] = "non-affix",
["root"] = "non-affix",
}
local function add_category(categories, cat_name, sort_key, sort_base)
if categories[cat_name] == nil then
categories[cat_name] = {
sort_key = sort_key,
sort_base = sort_base,
}
return
end
local existing = categories[cat_name]
if existing.sort_key == nil and sort_key ~= nil then
existing.sort_key = sort_key
end
if existing.sort_base == nil and sort_base ~= nil then
existing.sort_base = sort_base
end
end
-- Collect affix categories from top-level group containers
local function collect_affix_categories(node, page_lang, available_etymon_ids, senseid_parent_etymon, lang_exc)
local parts = {}
local part_index = 1
for _, container in ipairs(node.children or {}) do
local config = container.keyword_info
if config and config.affix_categories then
for _, term in ipairs(container.terms or {}) do
if not term.unknown_term then
local part_data = {
term = term.title,
tr = term.tr,
ts = term.ts,
alt = term.alt,
itemno = part_index,
orig_index = part_index
}
-- Determine affix type: explicit aftype > pos=root > auto-detect
local aftype = term.aftype
if aftype then
aftype = aftype_aliases[aftype] or aftype
part_data.type = aftype
elseif term.args and term.args.pos and term.args.pos == "root" then
part_data.type = "non-affix"
end
if term.lang:getCode() ~= page_lang:getCode() then
part_data.lang = term.lang
end
local target_ids = available_etymon_ids[term.target_key]
local has_multiple_ids = target_ids and #target_ids > 1
local id_exists_in_disambiguation = false
local matched_id = nil
-- Count available senseids for the target page
local senseid_count = 0
local target_prefix = term.target_key .. ":"
if senseid_parent_etymon then
for key, _ in pairs(senseid_parent_etymon) do
if key:sub(1, #target_prefix) == target_prefix then
senseid_count = senseid_count + 1
end
end
end
local has_multiple_senseids = senseid_count > 1
if term.id then
-- Check if user provided a valid senseid
local senseid_key = term.target_key .. ":" .. term.id
if senseid_parent_etymon and senseid_parent_etymon[senseid_key] then
if has_multiple_senseids then
-- Ambiguous senseid: use senseid
matched_id = term.id
id_exists_in_disambiguation = true
elseif has_multiple_ids then
-- Unique senseid but ambiguous etymon: use etymon ID
matched_id = term.etymon_id or term.id
id_exists_in_disambiguation = true
end
else
-- Check if user provided a valid etymon ID
if has_multiple_ids and target_ids then
for _, id_data in ipairs(target_ids) do
local stored_id = type(id_data) == "table" and id_data.id or id_data
if stored_id == term.id then
-- Ambiguous etymon: use etymon ID
id_exists_in_disambiguation = true
matched_id = term.id
break
end
end
end
-- Fallback: check resolved etymon_id (e.g. from previous steps)
if not id_exists_in_disambiguation and has_multiple_ids and term.etymon_id and target_ids then
for _, id_data in ipairs(target_ids) do
local stored_id = type(id_data) == "table" and id_data.id or id_data
if stored_id == term.etymon_id then
id_exists_in_disambiguation = true
matched_id = term.etymon_id
break
end
end
end
end
end
-- Use the matched ID if found
if term.override or id_exists_in_disambiguation then
part_data.id = matched_id or term.id
end
table.insert(parts, part_data)
part_index = part_index + 1
end
end
end
end
if #parts == 0 then return {} end
local affix_data = {
lang = page_lang,
parts = parts,
pos = "term",
sort_key = nil,
}
if #parts == 1 then
affix_data.allow_no_affixes_or_compounds = true
end
local affix_categories = M.affix.get_affix_categories_only(affix_data)
local result = {}
for _, cat in ipairs(affix_categories) do
if type(cat) == "table" then
table.insert(result, { cat = cat.cat, sort_key = cat.sort_key, sort_base = cat.sort_base })
else
table.insert(result, { cat = cat })
end
end
return result
end
-- Add borrowing-related categories (top-level only)
local function collect_borrowing_categories(categories, page_lang, term, config)
if config.borrowing_type == "borrowed" then
local temp_categories = {}
M.etymology.insert_borrowed_cat(temp_categories, page_lang, term.lang)
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
if config.specialized_borrowing then
local result = M.etymology_specialized.specialized_borrowing {
bortype = config.specialized_borrowing,
lang = page_lang,
sources = { term.lang },
terms = { { lang = term.lang, term = "-" } },
notext = true,
nocat = false,
}
for cat_name in result:gmatch("%[%[Category:([^%]]+)%]%]") do
add_category(categories, cat_name)
end
end
end
-- Add source-based derivation categories (top-level only)
local function collect_source_derivation_categories(categories, page_lang, term, config)
if not config.source_category_type then
return
end
local temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
borrowing_type = config.source_category_type,
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
-- Add source language categories
local function collect_source_categories(categories, page_lang, term, chain, get_norm_lang_func)
if page_lang:getCode() == get_norm_lang_func(term.lang):getCode() then
return
end
local temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
if chain.inherited then
temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
borrowing_type = "terms inherited",
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
end
-- Add root/word categories
local function collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, chain,
get_norm_lang_func, lang_exc, keyword)
local pos_types = { root = "root", word = "word" }
-- Determine pos: from term's postype, keyword's pos_override, or args.pos
local pos
local config = get_keyword_config(keyword, lang_exc)
if term.postype then
-- Term-level postype modifier takes highest priority
pos = term.postype
elseif config and config.pos_override then
pos = config.pos_override
elseif type(term.args) == "table" and term.args.pos then
pos = term.args.pos
end
local pos_type = pos_types[pos]
if not pos_type or term.unknown_term then
return
end
-- Skip root/word categories for descendants of affix groups
-- if pos_type then
-- return
-- end
local same_language = get_norm_lang_func(page_lang):getFullCode() == get_norm_lang_func(term.lang):getFullCode()
-- Skip self-references
if same_language and root_title == term.title then
return
end
-- Use makeEntryName to strip diacritics for category names
local entry_name = term.lang:makeEntryName(term.title)
local lang_name = page_lang:getCanonicalName()
local cat_name
if chain.passed_through then
local etymon_lang_name = export.get_cat_name(term.lang)
cat_name = lang_name .. " terms derived from the " .. etymon_lang_name .. " " .. pos_type .. " " .. entry_name
else
cat_name = lang_name .. " terms belonging to the " .. pos_type .. " " .. entry_name
end
-- Add ID disambiguation if needed (for roots/words: use etymon_id if resolved via senseid, otherwise use id)
local target_ids = available_etymon_ids[term.target_key]
local effective_id = term.etymon_id or term.id -- etymon_id if senseid, otherwise id is already an etymon id
if target_ids and effective_id then
local same_pos_count = 0
for _, id_data in ipairs(target_ids) do
if type(id_data) == "table" and id_data.pos == pos then
same_pos_count = same_pos_count + 1
end
end
if same_pos_count > 1 then
cat_name = cat_name .. " (" .. effective_id .. ")"
end
end
add_category(categories, cat_name)
end
-- Compute chain state for a term based on parent chain and keyword config
-- Hyphen patterns for affix detection (regular hyphen + script-specific)
local AFFIX_HYPHEN_PATTERN = "[%-%־ـ᠊]" -- regular hyphen, Hebrew maqqef, Arabic tatweel, Mongolian hyphen
-- Check if a term is an actual affix (not a non-affix member of an affix group)
local function is_actual_affix(term)
-- Check explicit aftype modifier
if term.aftype then
local normalized = aftype_aliases[term.aftype] or term.aftype
return normalized ~= "non-affix"
end
-- Check if pos=root (treated as non-affix)
if term.args and term.args.pos and term.args.pos == "root" then
return false
end
-- Auto-detect by hyphen: prefix ends with -, suffix starts with -, etc.
if term.title then
local title = term.title
-- Strip leading * for reconstructed terms before checking hyphens
title = title:gsub("^%*", "")
-- Check for hyphens at start or end (handles script-specific hyphens too)
if title:match("^" .. AFFIX_HYPHEN_PATTERN) or title:match(AFFIX_HYPHEN_PATTERN .. "$") then
return true
end
end
-- Default: not an affix
return false
end
local function compute_category_chain(parent_chain, config, page_lang, term_lang, get_norm_lang_func, parent_term_lang, term)
-- Track if we're inside an actual affix (for suppressing root categories on descendants)
-- Only set if the term is an actual affix (prefix, suffix, etc.), not a non-affix member
local inside_affix = parent_chain.inside_affix
if config.affix_categories and term and is_actual_affix(term) then
inside_affix = true
end
-- If no_child_categories is set, disable everything
if config.no_child_categories then
return {
passed_through = parent_chain.passed_through or page_lang:getCode() ~= get_norm_lang_func(term_lang):getCode(),
inherited = false,
source = false,
pos = false,
recurse = false,
inside_affix = inside_affix,
}
end
local term_is_transitive = is_transitive(config.transitive, page_lang, term_lang)
local new_source = parent_chain.source and term_is_transitive
-- For CROSS_LANG_NO_INTERNAL_SOURCE: track internal derivation language context
-- Check if this term is internal relative to parent term's language (if parent_term_lang provided)
-- or relative to page language (if no parent_term_lang)
local internal_lang = parent_chain.internal_lang
local is_internal_in_context = false
if config.transitive == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
local check_lang = parent_term_lang or page_lang
local term_lang_code = get_norm_lang_func(term_lang):getCode()
local check_lang_code = get_norm_lang_func(check_lang):getCode()
if internal_lang then
-- Already in an internal derivation context: check if this term is also internal
is_internal_in_context = term_lang_code == internal_lang
else
-- Check if this term is internal relative to parent term (or page if no parent)
is_internal_in_context = term_lang_code == check_lang_code
end
end
-- Source chain behavior for CROSS_LANG_NO_INTERNAL_SOURCE
if config.transitive == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
if is_internal_in_context then
-- Internal derivation
new_source = false
internal_lang = get_norm_lang_func(term_lang):getCode()
else
-- Cross-language
new_source = parent_chain.source and term_is_transitive
internal_lang = nil
end
end
local new_pos = parent_chain.pos
return {
passed_through = parent_chain.passed_through or page_lang:getCode() ~= get_norm_lang_func(term_lang):getCode(),
inherited = parent_chain.inherited and config.inherited_chain,
source = new_source,
pos = new_pos,
internal_lang = internal_lang,
recurse = new_source or new_pos,
inside_affix = inside_affix,
}
end
function export.render(opts)
opts = opts or {}
local data_tree = opts.data_tree
local page_lang = opts.page_lang
local available_etymon_ids = opts.available_etymon_ids
local senseid_parent_etymon = opts.senseid_parent_etymon
local get_norm_lang_func = opts.get_norm_lang_func
local lang_exc = opts.lang_exc
local categories = {}
local seen = {}
local lang_name = page_lang:getCanonicalName()
local root_title = data_tree.title
-- Collect the tree recursively
local function collect(node, parent_chain, is_toplevel)
-- Avoid processing same node twice
if not node.unknown_term and node.title then
local key = node.lang:getFullCode() .. ":" .. (node.title or "") .. ":" .. (node.id or "")
if seen[key] then return end
seen[key] = true
end
-- Collect affix categories at top level only
if is_toplevel then
local affix_cats = collect_affix_categories(node, page_lang, available_etymon_ids, senseid_parent_etymon, lang_exc)
for _, cat in ipairs(affix_cats) do
add_category(categories, lang_name .. " " .. cat.cat, cat.sort_key, cat.sort_base)
end
end
-- Process each container
for _, container in ipairs(node.children or {}) do
local keyword = container.keyword
local config = get_keyword_config(keyword, lang_exc)
-- Skip invalid keywords
if config then
-- Process each term in the container
for _, term in ipairs(container.terms or {}) do
local term_chain = compute_category_chain(parent_chain, config, page_lang, term.lang, get_norm_lang_func, node.lang, term)
local no_child_categories = config.no_child_categories == true
local term_is_transitive = is_transitive(config.transitive, page_lang, term.lang)
-- Top-level only processing
if is_toplevel then
-- Missing/ambiguous etymon tracking
if not term.unknown_term and (term.status == M.data.STATUS.MISSING or term.status == M.data.STATUS.REDLINK) then
add_category(categories, lang_name .. " entries referencing missing etymons")
end
if not term.unknown_term and term.status == M.data.STATUS.AMBIGUOUS then
add_category(categories, lang_name .. " entries referencing ambiguous etymons")
end
if term.missing_descendants_header then
add_category(categories, lang_name .. " entries referencing etymons without Descendants sections")
end
if term.missing_descendants_entry then
add_category(categories, lang_name .. " entries referencing etymons without this term in Descendants sections")
end
-- Top-level category (e.g., "undefined derivations")
if config.toplevel_category then
add_category(categories, lang_name .. " " .. config.toplevel_category)
end
-- Borrowing categories (bor, lbor, slbor, ubor, obor)
if config.borrowing_type or config.specialized_borrowing then
collect_borrowing_categories(categories, page_lang, term, config)
end
-- Borrowing categories from <bor>, <lbor>, or <slbor> modifiers on :af/:surf terms
if keyword == "affix" or keyword == "surf" then
if term.bor then
local bor_config = { borrowing_type = "borrowed" }
collect_borrowing_categories(categories, page_lang, term, bor_config)
elseif term.lbor then
local bor_config = { specialized_borrowing = "learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config)
elseif term.slbor then
local bor_config = { specialized_borrowing = "semi-learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config)
end
end
-- Source-based derivation categories (sl, calque, pcal)
if config.source_category_type then
collect_source_derivation_categories(categories, page_lang, term, config)
end
-- Skip all child categorisation if no_child_categories is set
if not no_child_categories then
-- Source categories only if transitive
if term_is_transitive then
collect_source_categories(categories, page_lang, term, term_chain, get_norm_lang_func)
end
-- Pos categories always (unless no_child_categories)
collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, term_chain,
get_norm_lang_func, lang_exc, keyword)
end
else
-- Below top level, respect the parent chain
if parent_chain.source then
collect_source_categories(categories, page_lang, term, term_chain, get_norm_lang_func)
end
if parent_chain.pos then
collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, term_chain,
get_norm_lang_func, lang_exc, keyword)
end
end
-- Recurse into term's children if needed and status allows
if term_chain.recurse and (term.status == M.data.STATUS.OK or term.status == M.data.STATUS.INLINE) then
collect(term, term_chain, false)
end
end
end
end
end
-- Initial chain state
local initial_chain = {
passed_through = false,
inherited = true,
source = true,
pos = true,
internal_lang = nil,
recurse = true,
inside_affix = false,
}
collect(data_tree, initial_chain, true)
local cat_list = {}
for cat_name, sort_data in pairs(categories) do
if sort_data.sort_key ~= nil or sort_data.sort_base ~= nil then
table.insert(cat_list, {
name = cat_name,
sort_key = sort_data.sort_key,
sort_base = sort_data.sort_base,
})
else
table.insert(cat_list, cat_name)
end
end
return cat_list
end
function export.format(entries, lang)
if type(entries) ~= "table" or #entries == 0 then
return ""
end
local parts = {}
for _, category in ipairs(entries) do
if type(category) == "table" and type(category.name) == "string" then
table.insert(parts, M.utilities.format_categories({ category.name }, lang, category.sort_key, category.sort_base))
elseif type(category) == "string" then
table.insert(parts, M.utilities.format_categories({ category }, lang))
end
end
return table.concat(parts)
end
return export
qaazt3r5h8qyyj6gn4rl4zbmoaycwgt
342830
342820
2026-05-16T14:01:15Z
Hakimi97
2668
342830
Scribunto
text/plain
local export = {}
local M = require("Module:module loader").init({
require = {
etymology = "Module:etymology",
affix = "Module:affix",
etymology_specialized = "Module:etymology/specialized",
utilities = "Module:utilities",
},
loadData = {
data = "Module:etymon/data",
},
})
-- Evaluate whether a keyword is transitive for a given term
local function is_transitive(transitive_mode, page_lang, term_lang)
if transitive_mode == M.data.TRANSITIVE.ALWAYS then
return true
elseif transitive_mode == M.data.TRANSITIVE.NEVER then
return false
elseif transitive_mode == M.data.TRANSITIVE.CROSS_LANG then
return page_lang:getCode() ~= term_lang:getCode()
elseif transitive_mode == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
return page_lang:getCode() ~= term_lang:getCode()
end
error("Unknown transitive mode: " .. tostring(transitive_mode))
end
-- Get keyword config with language-specific overrides
local function get_keyword_config(keyword, lang_exc)
local base_config = M.data.keywords[keyword]
if not base_config then
return nil -- Invalid keyword
end
local overrides = lang_exc and lang_exc.keyword_overrides and lang_exc.keyword_overrides[keyword]
if not overrides then
return base_config
end
-- Merge overrides into base config
local merged = {}
for k, v in pairs(base_config) do
merged[k] = v
end
for k, v in pairs(overrides) do
merged[k] = v
end
return merged
end
function export.get_cat_name(source)
local _, cat_name = M.etymology.get_display_and_cat_name(source, true)
return cat_name
end
-- Normalize affix type aliases
local aftype_aliases = {
["pre"] = "prefix",
["suf"] = "suffix",
["in"] = "infix",
["inter"] = "interfix",
["circum"] = "circumfix",
["naf"] = "non-affix",
["root"] = "non-affix",
}
local function add_category(categories, cat_name, sort_key, sort_base)
if categories[cat_name] == nil then
categories[cat_name] = {
sort_key = sort_key,
sort_base = sort_base,
}
return
end
local existing = categories[cat_name]
if existing.sort_key == nil and sort_key ~= nil then
existing.sort_key = sort_key
end
if existing.sort_base == nil and sort_base ~= nil then
existing.sort_base = sort_base
end
end
-- Collect affix categories from top-level group containers
local function collect_affix_categories(node, page_lang, available_etymon_ids, senseid_parent_etymon, lang_exc)
local parts = {}
local part_index = 1
for _, container in ipairs(node.children or {}) do
local config = container.keyword_info
if config and config.affix_categories then
for _, term in ipairs(container.terms or {}) do
if not term.unknown_term then
local part_data = {
term = term.title,
tr = term.tr,
ts = term.ts,
alt = term.alt,
itemno = part_index,
orig_index = part_index
}
-- Determine affix type: explicit aftype > pos=root > auto-detect
local aftype = term.aftype
if aftype then
aftype = aftype_aliases[aftype] or aftype
part_data.type = aftype
elseif term.args and term.args.pos and term.args.pos == "root" then
part_data.type = "non-affix"
end
if term.lang:getCode() ~= page_lang:getCode() then
part_data.lang = term.lang
end
local target_ids = available_etymon_ids[term.target_key]
local has_multiple_ids = target_ids and #target_ids > 1
local id_exists_in_disambiguation = false
local matched_id = nil
-- Count available senseids for the target page
local senseid_count = 0
local target_prefix = term.target_key .. ":"
if senseid_parent_etymon then
for key, _ in pairs(senseid_parent_etymon) do
if key:sub(1, #target_prefix) == target_prefix then
senseid_count = senseid_count + 1
end
end
end
local has_multiple_senseids = senseid_count > 1
if term.id then
-- Check if user provided a valid senseid
local senseid_key = term.target_key .. ":" .. term.id
if senseid_parent_etymon and senseid_parent_etymon[senseid_key] then
if has_multiple_senseids then
-- Ambiguous senseid: use senseid
matched_id = term.id
id_exists_in_disambiguation = true
elseif has_multiple_ids then
-- Unique senseid but ambiguous etymon: use etymon ID
matched_id = term.etymon_id or term.id
id_exists_in_disambiguation = true
end
else
-- Check if user provided a valid etymon ID
if has_multiple_ids and target_ids then
for _, id_data in ipairs(target_ids) do
local stored_id = type(id_data) == "table" and id_data.id or id_data
if stored_id == term.id then
-- Ambiguous etymon: use etymon ID
id_exists_in_disambiguation = true
matched_id = term.id
break
end
end
end
-- Fallback: check resolved etymon_id (e.g. from previous steps)
if not id_exists_in_disambiguation and has_multiple_ids and term.etymon_id and target_ids then
for _, id_data in ipairs(target_ids) do
local stored_id = type(id_data) == "table" and id_data.id or id_data
if stored_id == term.etymon_id then
id_exists_in_disambiguation = true
matched_id = term.etymon_id
break
end
end
end
end
end
-- Use the matched ID if found
if term.override or id_exists_in_disambiguation then
part_data.id = matched_id or term.id
end
table.insert(parts, part_data)
part_index = part_index + 1
end
end
end
end
if #parts == 0 then return {} end
local affix_data = {
lang = page_lang,
parts = parts,
pos = "term",
sort_key = nil,
}
if #parts == 1 then
affix_data.allow_no_affixes_or_compounds = true
end
local affix_categories = M.affix.get_affix_categories_only(affix_data)
local result = {}
for _, cat in ipairs(affix_categories) do
if type(cat) == "table" then
table.insert(result, { cat = cat.cat, sort_key = cat.sort_key, sort_base = cat.sort_base })
else
table.insert(result, { cat = cat })
end
end
return result
end
-- Add borrowing-related categories (top-level only)
local function collect_borrowing_categories(categories, page_lang, term, config)
if config.borrowing_type == "borrowed" then
local temp_categories = {}
M.etymology.insert_borrowed_cat(temp_categories, page_lang, term.lang)
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
if config.specialized_borrowing then
local result = M.etymology_specialized.specialized_borrowing {
bortype = config.specialized_borrowing,
lang = page_lang,
sources = { term.lang },
terms = { { lang = term.lang, term = "-" } },
notext = true,
nocat = false,
}
for cat_name in result:gmatch("%[%[Category:([^%]]+)%]%]") do
add_category(categories, cat_name)
end
end
end
-- Add source-based derivation categories (top-level only)
local function collect_source_derivation_categories(categories, page_lang, term, config)
if not config.source_category_type then
return
end
local temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
borrowing_type = config.source_category_type,
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
-- Add source language categories
local function collect_source_categories(categories, page_lang, term, chain, get_norm_lang_func)
if page_lang:getCode() == get_norm_lang_func(term.lang):getCode() then
return
end
local temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
if chain.inherited then
temp_categories = {}
M.etymology.insert_source_cat_get_display {
lang = page_lang,
source = term.lang,
categories = temp_categories,
borrowing_type = "dipinjam",
nocat = false,
}
for _, cat in ipairs(temp_categories) do
add_category(categories, cat)
end
end
end
-- Add root/word categories
local function collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, chain,
get_norm_lang_func, lang_exc, keyword)
local pos_types = { root = "root", word = "word" }
-- Determine pos: from term's postype, keyword's pos_override, or args.pos
local pos
local config = get_keyword_config(keyword, lang_exc)
if term.postype then
-- Term-level postype modifier takes highest priority
pos = term.postype
elseif config and config.pos_override then
pos = config.pos_override
elseif type(term.args) == "table" and term.args.pos then
pos = term.args.pos
end
local pos_type = pos_types[pos]
if not pos_type or term.unknown_term then
return
end
-- Skip root/word categories for descendants of affix groups
-- if pos_type then
-- return
-- end
local same_language = get_norm_lang_func(page_lang):getFullCode() == get_norm_lang_func(term.lang):getFullCode()
-- Skip self-references
if same_language and root_title == term.title then
return
end
-- Use makeEntryName to strip diacritics for category names
local entry_name = term.lang:makeEntryName(term.title)
local lang_name = page_lang:getCanonicalName()
local cat_name
if chain.passed_through then
local etymon_lang_name = export.get_cat_name(term.lang)
cat_name = "Perkataan " .. lang_name .. " diterbitkan daripada " .. etymon_lang_name .. " " .. pos_type .. " " .. entry_name
else
cat_name = "Perkataan " .. lang_name .. " milik " .. pos_type .. " " .. entry_name
end
-- Add ID disambiguation if needed (for roots/words: use etymon_id if resolved via senseid, otherwise use id)
local target_ids = available_etymon_ids[term.target_key]
local effective_id = term.etymon_id or term.id -- etymon_id if senseid, otherwise id is already an etymon id
if target_ids and effective_id then
local same_pos_count = 0
for _, id_data in ipairs(target_ids) do
if type(id_data) == "table" and id_data.pos == pos then
same_pos_count = same_pos_count + 1
end
end
if same_pos_count > 1 then
cat_name = cat_name .. " (" .. effective_id .. ")"
end
end
add_category(categories, cat_name)
end
-- Compute chain state for a term based on parent chain and keyword config
-- Hyphen patterns for affix detection (regular hyphen + script-specific)
local AFFIX_HYPHEN_PATTERN = "[%-%־ـ᠊]" -- regular hyphen, Hebrew maqqef, Arabic tatweel, Mongolian hyphen
-- Check if a term is an actual affix (not a non-affix member of an affix group)
local function is_actual_affix(term)
-- Check explicit aftype modifier
if term.aftype then
local normalized = aftype_aliases[term.aftype] or term.aftype
return normalized ~= "non-affix"
end
-- Check if pos=root (treated as non-affix)
if term.args and term.args.pos and term.args.pos == "root" then
return false
end
-- Auto-detect by hyphen: prefix ends with -, suffix starts with -, etc.
if term.title then
local title = term.title
-- Strip leading * for reconstructed terms before checking hyphens
title = title:gsub("^%*", "")
-- Check for hyphens at start or end (handles script-specific hyphens too)
if title:match("^" .. AFFIX_HYPHEN_PATTERN) or title:match(AFFIX_HYPHEN_PATTERN .. "$") then
return true
end
end
-- Default: not an affix
return false
end
local function compute_category_chain(parent_chain, config, page_lang, term_lang, get_norm_lang_func, parent_term_lang, term)
-- Track if we're inside an actual affix (for suppressing root categories on descendants)
-- Only set if the term is an actual affix (prefix, suffix, etc.), not a non-affix member
local inside_affix = parent_chain.inside_affix
if config.affix_categories and term and is_actual_affix(term) then
inside_affix = true
end
-- If no_child_categories is set, disable everything
if config.no_child_categories then
return {
passed_through = parent_chain.passed_through or page_lang:getCode() ~= get_norm_lang_func(term_lang):getCode(),
inherited = false,
source = false,
pos = false,
recurse = false,
inside_affix = inside_affix,
}
end
local term_is_transitive = is_transitive(config.transitive, page_lang, term_lang)
local new_source = parent_chain.source and term_is_transitive
-- For CROSS_LANG_NO_INTERNAL_SOURCE: track internal derivation language context
-- Check if this term is internal relative to parent term's language (if parent_term_lang provided)
-- or relative to page language (if no parent_term_lang)
local internal_lang = parent_chain.internal_lang
local is_internal_in_context = false
if config.transitive == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
local check_lang = parent_term_lang or page_lang
local term_lang_code = get_norm_lang_func(term_lang):getCode()
local check_lang_code = get_norm_lang_func(check_lang):getCode()
if internal_lang then
-- Already in an internal derivation context: check if this term is also internal
is_internal_in_context = term_lang_code == internal_lang
else
-- Check if this term is internal relative to parent term (or page if no parent)
is_internal_in_context = term_lang_code == check_lang_code
end
end
-- Source chain behavior for CROSS_LANG_NO_INTERNAL_SOURCE
if config.transitive == M.data.TRANSITIVE.CROSS_LANG_NO_INTERNAL_SOURCE then
if is_internal_in_context then
-- Internal derivation
new_source = false
internal_lang = get_norm_lang_func(term_lang):getCode()
else
-- Cross-language
new_source = parent_chain.source and term_is_transitive
internal_lang = nil
end
end
local new_pos = parent_chain.pos
return {
passed_through = parent_chain.passed_through or page_lang:getCode() ~= get_norm_lang_func(term_lang):getCode(),
inherited = parent_chain.inherited and config.inherited_chain,
source = new_source,
pos = new_pos,
internal_lang = internal_lang,
recurse = new_source or new_pos,
inside_affix = inside_affix,
}
end
function export.render(opts)
opts = opts or {}
local data_tree = opts.data_tree
local page_lang = opts.page_lang
local available_etymon_ids = opts.available_etymon_ids
local senseid_parent_etymon = opts.senseid_parent_etymon
local get_norm_lang_func = opts.get_norm_lang_func
local lang_exc = opts.lang_exc
local categories = {}
local seen = {}
local lang_name = page_lang:getCanonicalName()
local root_title = data_tree.title
-- Collect the tree recursively
local function collect(node, parent_chain, is_toplevel)
-- Avoid processing same node twice
if not node.unknown_term and node.title then
local key = node.lang:getFullCode() .. ":" .. (node.title or "") .. ":" .. (node.id or "")
if seen[key] then return end
seen[key] = true
end
-- Collect affix categories at top level only
if is_toplevel then
local affix_cats = collect_affix_categories(node, page_lang, available_etymon_ids, senseid_parent_etymon, lang_exc)
for _, cat in ipairs(affix_cats) do
add_category(categories, lang_name .. " " .. cat.cat, cat.sort_key, cat.sort_base)
end
end
-- Process each container
for _, container in ipairs(node.children or {}) do
local keyword = container.keyword
local config = get_keyword_config(keyword, lang_exc)
-- Skip invalid keywords
if config then
-- Process each term in the container
for _, term in ipairs(container.terms or {}) do
local term_chain = compute_category_chain(parent_chain, config, page_lang, term.lang, get_norm_lang_func, node.lang, term)
local no_child_categories = config.no_child_categories == true
local term_is_transitive = is_transitive(config.transitive, page_lang, term.lang)
-- Top-level only processing
if is_toplevel then
-- Missing/ambiguous etymon tracking
if not term.unknown_term and (term.status == M.data.STATUS.MISSING or term.status == M.data.STATUS.REDLINK) then
add_category(categories, lang_name .. " entries referencing missing etymons")
end
if not term.unknown_term and term.status == M.data.STATUS.AMBIGUOUS then
add_category(categories, lang_name .. " entries referencing ambiguous etymons")
end
if term.missing_descendants_header then
add_category(categories, lang_name .. " entries referencing etymons without Descendants sections")
end
if term.missing_descendants_entry then
add_category(categories, lang_name .. " entries referencing etymons without this term in Descendants sections")
end
-- Top-level category (e.g., "undefined derivations")
if config.toplevel_category then
add_category(categories, lang_name .. " " .. config.toplevel_category)
end
-- Borrowing categories (bor, lbor, slbor, ubor, obor)
if config.borrowing_type or config.specialized_borrowing then
collect_borrowing_categories(categories, page_lang, term, config)
end
-- Borrowing categories from <bor>, <lbor>, or <slbor> modifiers on :af/:surf terms
if keyword == "affix" or keyword == "surf" then
if term.bor then
local bor_config = { borrowing_type = "borrowed" }
collect_borrowing_categories(categories, page_lang, term, bor_config)
elseif term.lbor then
local bor_config = { specialized_borrowing = "learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config)
elseif term.slbor then
local bor_config = { specialized_borrowing = "semi-learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config)
end
end
-- Source-based derivation categories (sl, calque, pcal)
if config.source_category_type then
collect_source_derivation_categories(categories, page_lang, term, config)
end
-- Skip all child categorisation if no_child_categories is set
if not no_child_categories then
-- Source categories only if transitive
if term_is_transitive then
collect_source_categories(categories, page_lang, term, term_chain, get_norm_lang_func)
end
-- Pos categories always (unless no_child_categories)
collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, term_chain,
get_norm_lang_func, lang_exc, keyword)
end
else
-- Below top level, respect the parent chain
if parent_chain.source then
collect_source_categories(categories, page_lang, term, term_chain, get_norm_lang_func)
end
if parent_chain.pos then
collect_pos_categories(categories, page_lang, root_title, term, available_etymon_ids, term_chain,
get_norm_lang_func, lang_exc, keyword)
end
end
-- Recurse into term's children if needed and status allows
if term_chain.recurse and (term.status == M.data.STATUS.OK or term.status == M.data.STATUS.INLINE) then
collect(term, term_chain, false)
end
end
end
end
end
-- Initial chain state
local initial_chain = {
passed_through = false,
inherited = true,
source = true,
pos = true,
internal_lang = nil,
recurse = true,
inside_affix = false,
}
collect(data_tree, initial_chain, true)
local cat_list = {}
for cat_name, sort_data in pairs(categories) do
if sort_data.sort_key ~= nil or sort_data.sort_base ~= nil then
table.insert(cat_list, {
name = cat_name,
sort_key = sort_data.sort_key,
sort_base = sort_data.sort_base,
})
else
table.insert(cat_list, cat_name)
end
end
return cat_list
end
function export.format(entries, lang)
if type(entries) ~= "table" or #entries == 0 then
return ""
end
local parts = {}
for _, category in ipairs(entries) do
if type(category) == "table" and type(category.name) == "string" then
table.insert(parts, M.utilities.format_categories({ category.name }, lang, category.sort_key, category.sort_base))
elseif type(category) == "string" then
table.insert(parts, M.utilities.format_categories({ category }, lang))
end
end
return table.concat(parts)
end
return export
g2ljrv19nstuu4cg2oqpsykek0ecpsc
Modul:etymon/text
828
82359
342819
234443
2026-05-16T13:40:41Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/90369741|90369741]]), terjemahan belum selesai
342819
Scribunto
text/plain
local export = {}
local loader = require("Module:module loader")
local M = loader.init({
require = {
en_utilities = "Module:en-utilities",
references = "Module:references",
},
loadData = {
data = "Module:etymon/data",
},
})
function export.render(opts)
opts = opts or {}
local data_tree = opts.data_tree
local format_term_func = opts.format_term_func
local max_depth = opts.max_depth
local stop_at_blue_link = opts.stop_at_blue_link
local curr_page = opts.curr_page
local nodot = opts.nodot
local stop_at_lang = opts.stop_at_lang
local stop_at_lang_or_bluelink = opts.stop_at_lang_or_bluelink
local children = data_tree.children
if not children or #children == 0 then
return ""
end
local top_l2 = data_tree.lang:getFullCode() .. ":" .. curr_page
-- Get refs for a term
local function get_term_refs(term, term_lang, depth)
local term_l2 = term_lang:getFullCode() .. ":" .. curr_page
if term.parsed_ref and (depth == 1 or term_l2 == top_l2) then
return M.references.format_references(term.parsed_ref)
end
return ""
end
-- Build a text part for a single term
local function build_term_part(term, current_lang, depth)
local text = ""
local new_lang = current_lang
local lang_changed = term.lang:getCanonicalName() ~= current_lang:getCanonicalName()
-- Use centralized format_term (handles suppress_term, unknown_term, and regular terms)
local term_text = format_term_func(term)
if lang_changed then
new_lang = term.lang
if term_text then
text = term.lang:makeWikipediaLink() .. " " .. term_text
elseif term.is_family then
text = M.en_utilities.add_indefinite_article(term.lang:makeWikipediaLink() .. " language", false)
else
-- suppress_term with language change: show only language
text = term.lang:makeWikipediaLink()
end
else
text = term_text or ""
end
return {
type = "term",
text = text,
refs = get_term_refs(term, new_lang, depth),
lang = new_lang,
is_uncertain = term.is_uncertain or false,
}
end
-- Build text parts for a container
local function build_container_part(container, node, depth, allow_continuation, fallback_to_bluelink)
local keyword_info = container.keyword_info
local keyword_modifiers = container.keyword_modifiers or {}
local terms = container.terms or {}
if not keyword_info or #terms == 0 then
return nil
end
-- Skip building text part when invisible in text ("all", "text", or true)
local inv = keyword_info.invisible
if inv == "all" or inv == true or inv == "text" then
return nil
end
local is_group = keyword_info.is_group
local keyword_uncertain = keyword_modifiers.unc or false
-- Determine text and phrase (allowing for overrides)
local intro_text = keyword_info.text
local phrase = keyword_info.phrase
if keyword_modifiers.text then
-- User-provided override: assumed to be lowercase
phrase = keyword_modifiers.text
-- Auto-capitalize for intro text (e.g., "derived from" -> "Derived from")
intro_text = mw.ustring.upper(phrase:sub(1, 1)) .. phrase:sub(2)
end
-- Get keyword references
local keyword_refs = ""
if keyword_modifiers.ref then
local parsed_keyword_refs = M.references.parse_references(keyword_modifiers.ref)
if parsed_keyword_refs and parsed_keyword_refs ~= "" then
keyword_refs = M.references.format_references(parsed_keyword_refs)
end
end
-- Build term parts
local term_parts = {}
local current_lang = node.lang
for _, term in ipairs(terms) do
local term_part = build_term_part(term, current_lang, depth)
if term_part.text ~= "" then
table.insert(term_parts, term_part)
current_lang = term_part.lang
end
end
-- Check uncertainty distribution
local uncertain_count = 0
for _, term_part in ipairs(term_parts) do
if term_part.is_uncertain then
uncertain_count = uncertain_count + 1
end
end
-- If keyword itself is uncertain, treat all terms as uncertain
local all_uncertain = keyword_uncertain or (uncertain_count == #term_parts and #term_parts > 0)
local has_mixed_uncertainty = not keyword_uncertain and uncertain_count > 0 and uncertain_count < #term_parts
-- Check if there are more steps (only if continuation is allowed)
local has_more_steps = false
local next_node = nil
local first_term = terms[1]
-- Check if we should stop at this language
local reached_stop_lang = false
if stop_at_lang then
for _, term in ipairs(terms) do
if term.lang and term.lang:getCode() == stop_at_lang then
reached_stop_lang = true
break
end
end
elseif stop_at_lang_or_bluelink then
-- Check if we should stop at this language, or at the first bluelink if it's a redlink
for _, term in ipairs(terms) do
if term.lang and term.lang:getCode() == stop_at_lang_or_bluelink then
if first_term.status == M.data.STATUS.OK then
reached_stop_lang = true
else
fallback_to_bluelink = true
end
break
end
end
if fallback_to_bluelink and first_term.status == M.data.STATUS.OK then
reached_stop_lang = true
end
end
if allow_continuation and not is_group and #terms == 1 and not reached_stop_lang then
local first_term_children = first_term.children
if first_term_children and #first_term_children > 0 and (not max_depth or depth < max_depth) then
local next_container = first_term_children[1]
local next_keyword_info = next_container and next_container.keyword_info
if not (next_keyword_info and next_keyword_info.invisible) then
if stop_at_blue_link then
if first_term.status ~= M.data.STATUS.OK then
has_more_steps = true
next_node = first_term
end
else
has_more_steps = true
next_node = first_term
end
end
end
end
return {
type = "container",
intro_text = intro_text,
phrase = phrase,
is_uncertain = all_uncertain,
has_mixed_uncertainty = has_mixed_uncertainty,
term_parts = term_parts,
is_group = is_group,
has_more_steps = has_more_steps,
next_node = next_node,
new_sentence = keyword_info.new_sentence or false,
separate_clause = keyword_info.separate_clause or false,
conj = keyword_modifiers.conj, -- custom conjunction: "and", "or", "and/or", etc.
lit = keyword_modifiers.lit,
keyword_refs = keyword_refs,
fallback_to_bluelink = fallback_to_bluelink,
}
end
-- Build the full tree of text parts
local function build_text_tree(node, depth, allow_continuation, fallback_to_bluelink)
local containers = node.children
if not containers or #containers == 0 then
return nil
end
local container_parts = {}
-- Count containers that get a text part (invisible in text = "all", "text", or true)
local visible_container_count = 0
for _, container in ipairs(containers) do
local keyword_info = container.keyword_info
local inv = keyword_info and keyword_info.invisible
if not (inv == "all" or inv == true or inv == "text") then
visible_container_count = visible_container_count + 1
end
end
-- If there are multiple visible containers at this level, don't allow continuation for any
local has_multiple_containers = visible_container_count > 1
local should_allow_continuation = allow_continuation and not has_multiple_containers
for _, container in ipairs(containers) do
local part = build_container_part(container, node, depth, should_allow_continuation, fallback_to_bluelink)
if part then
-- Recursively build children if there are more steps
if part.has_more_steps and part.next_node then
part.continuation = build_text_tree(part.next_node, depth + 1, true, part.fallback_to_bluelink)
end
table.insert(container_parts, part)
end
end
if #container_parts == 0 then
return nil
end
return {
type = "tree",
container_parts = container_parts,
depth = depth,
}
end
-- Check if tree has mixed joining types
local function check_complexity(tree)
if not tree then return nil end
local parts = tree.container_parts
if #parts <= 1 then
-- Single container
if parts[1] and parts[1].continuation then
return check_complexity(parts[1].continuation)
end
return nil
end
-- Multiple containers
local has_or_join = false
local has_new_sentence = false
local has_separate_clause = false
for i = 2, #parts do
local part = parts[i]
-- Ignore etydate parts for complexity checks
if part.type ~= "etydate" then
if part.new_sentence then
has_new_sentence = true
elseif part.separate_clause then
has_separate_clause = true
else
has_or_join = true
end
end
end
local join_type_count = 0
if has_or_join then join_type_count = join_type_count + 1 end
if has_new_sentence then join_type_count = join_type_count + 1 end
if has_separate_clause then join_type_count = join_type_count + 1 end
if join_type_count > 1 then
error(
"Cannot generate etymology text: mixed joining styles (e.g., alternatives joined with 'or' cannot be combined with calques or influences in the same list).")
end
return nil
end
-- Analyze tree and assign punctuation
local function analyze_punctuation(tree, is_toplevel)
if not tree then return end
local parts = tree.container_parts
local num_parts = #parts
for i, part in ipairs(parts) do
local is_first = (i == 1)
local is_last = (i == num_parts)
local next_part = parts[i + 1]
-- Analyze term punctuation within container
-- Terms use Oxford comma style: "A, B, or C"
-- Custom conjunction can be specified via conj modifier (e.g., "and/or", "and")
local num_terms = #part.term_parts
local term_conj = part.conj or "or" -- default to "or"
for j, term_part in ipairs(part.term_parts) do
local is_last_term = (j == num_terms)
if part.is_group then
-- Group: terms joined with " + "
term_part.joiner = is_last_term and "" or " + "
elseif num_terms > 1 then
-- Multiple terms not in a group: Oxford comma style
if is_last_term then
term_part.joiner = ""
elseif j == num_terms - 1 then
-- Second to last term
if num_terms == 2 then
term_part.joiner = " " .. term_conj .. " "
else
term_part.joiner = ", " .. term_conj .. " "
end
else
term_part.joiner = ", "
end
else
-- Single term
term_part.joiner = ""
end
end
-- Determine container punctuation based on what comes next
if part.continuation then
-- Has continuation
part.punctuation = ","
-- Recursively analyze continuation
analyze_punctuation(part.continuation, false)
elseif is_last then
-- Last container
part.punctuation = (is_toplevel and nodot) and "" or "."
elseif next_part and next_part.new_sentence then
-- Next container starts a new sentence
part.punctuation = "."
elseif next_part and next_part.separate_clause then
-- Next container is a separate clause
part.punctuation = ","
else
-- Not last, next is joined with "or"
-- Containers use repeated "or" style: "A, or B, or C"
part.punctuation = ","
end
-- Determine joiner to next part
-- Containers use repeated "or" style: ", or" between each
-- Custom conjunction can be specified via conj modifier
local container_conj = part.conj or "or" -- default to "or"
if not is_last then
if next_part and next_part.new_sentence then
-- New sentence
part.joiner = " "
elseif next_part and next_part.separate_clause then
-- Separate clause
part.joiner = " "
else
-- Same sentence: use custom conjunction or default "or"
part.joiner = " " .. container_conj .. " "
end
else
part.joiner = ""
end
-- Determine intro formatting
-- Capitalize if first at top level, OR if this container starts a new sentence
if (is_first and is_toplevel) or part.new_sentence then
part.intro_capitalized = true
part.use_full_intro = true
else
part.intro_capitalized = false
part.use_full_intro = false
end
end
end
-- Assemble text from analyzed tree
local function assemble_text(tree)
if not tree then return "" end
local result = ""
for i, part in ipairs(tree.container_parts) do
if part.type == "etydate" then
result = result .. part.etydate_text
if part.punctuation and part.punctuation ~= "" then
result = result .. part.punctuation
end
if part.etydate_refs and next(part.etydate_refs) then
result = result .. M.references.format_references(part.etydate_refs)
end
if part.joiner and part.joiner ~= "" then
result = result .. part.joiner
end
else
-- Build intro
local intro
if part.use_full_intro then
if part.is_uncertain then
intro = "Possibly " .. part.phrase
else
intro = part.intro_text
end
else
if part.is_uncertain then
intro = "possibly " .. part.phrase
else
intro = part.phrase
end
end
result = result .. intro
-- Build terms
if #part.term_parts > 0 then
result = result .. " "
for j, term_part in ipairs(part.term_parts) do
-- Add "possibly" prefix for uncertain terms when there's mixed uncertainty
if part.has_mixed_uncertainty and term_part.is_uncertain then
result = result .. "possibly "
end
result = result .. term_part.text
-- Add joiner between terms
if term_part.joiner ~= "" then
-- Check if joiner contains comma (punctuation)
local comma_pos = term_part.joiner:find(",")
if comma_pos then
-- Add up to and including comma
result = result .. term_part.joiner:sub(1, comma_pos)
-- Add refs after comma
if term_part.refs ~= "" then
result = result .. term_part.refs
end
-- Add rest of joiner
result = result .. term_part.joiner:sub(comma_pos + 1)
else
-- No comma, add refs before joiner
if term_part.refs ~= "" then
result = result .. term_part.refs
end
result = result .. term_part.joiner
end
end
end
-- For the last term, add punctuation then refs
local last_term = part.term_parts[#part.term_parts]
if last_term and last_term.joiner == "" then
if part.punctuation ~= "" then
-- If we have literal text, punctuation goes AFTER it
if part.lit then
-- Add refs first (attached to term)
if last_term.refs ~= "" then
result = result .. last_term.refs
end
-- Add keyword refs
if part.keyword_refs and part.keyword_refs ~= "" then
result = result .. part.keyword_refs
end
-- Add literal text
result = result .. ", literally “" .. part.lit .. "”"
-- Add punctuation
result = result .. part.punctuation
else
-- Normal behavior: punctuation then refs
result = result .. part.punctuation
if last_term.refs ~= "" then
result = result .. last_term.refs
end
-- Add keyword refs after term refs
if part.keyword_refs and part.keyword_refs ~= "" then
result = result .. part.keyword_refs
end
end
else
-- No punctuation
if last_term.refs ~= "" then
result = result .. last_term.refs
end
-- Add keyword refs
if part.keyword_refs and part.keyword_refs ~= "" then
result = result .. part.keyword_refs
end
-- Add literal text if present (even without punctuation)
if part.lit then
result = result .. ", literally “" .. part.lit .. "”"
end
end
end
else
-- No terms, just add punctuation and keyword refs
if part.punctuation ~= "" then
result = result .. part.punctuation
end
-- Add keyword refs even when there are no terms
if part.keyword_refs and part.keyword_refs ~= "" then
result = result .. part.keyword_refs
end
end
-- Add continuation
if part.continuation then
result = result .. " " .. assemble_text(part.continuation)
end
-- Add joiner to next container
if part.joiner ~= "" then
result = result .. part.joiner
end
end
end
return result
end
local text_tree = build_text_tree(data_tree, 1, true, false)
if not text_tree then
return ""
end
-- Add etydate container
if data_tree.etydate and data_tree.etydate ~= "" then
table.insert(text_tree.container_parts, {
type = "etydate",
etydate_text = data_tree.etydate,
etydate_refs = data_tree.etydate_refs,
term_parts = {},
new_sentence = true,
})
end
check_complexity(text_tree)
analyze_punctuation(text_tree, true)
return assemble_text(text_tree)
end
return export
apkzd2ykoxaur8xi7e7q6ckn0p4heg1
Templat:table:gregorian calendar
10
85110
342836
316921
2026-05-16T23:58:50Z
Hakimi97
2668
342836
wikitext
text/x-wiki
{| class="wikitable" style="text-align: center; white-space: nowrap; width 100%"
! colspan="4" | Bulan takwim Masihi {{table mul test|lang={{{lang|und}}}}} {{#ifeq:{{{gregorian calendar months}}}|-|| · {{{gregorian calendar months}}}}} {{table edit|gregorian calendar|{{{lang|}}}}}
|-
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Januari'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Februari'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Mac'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''April'''
|-
| {{{jan}}}
| {{{feb}}}
| {{{mar}}}
| {{{apr}}}
|-
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Mei'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Jun'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Julai'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Ogos'''
|-
| {{{may}}}
| {{{jun}}}
| {{{jul}}}
| {{{aug}}}
|-
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''September'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Oktober'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''November'''
| style="background: var(--wikt-palette-lightergrey,#f2f2f2);" | '''Disember'''
|-
| {{{sep}}}
| {{{oct}}}
| {{{nov}}}
| {{{dec}}}
|}<includeonly>[[Category:{{{lang|}}}:Bulan takwim Masihi]]</includeonly><noinclude>{{table doc}}</noinclude>
kj3vmpi66k5ie4aaa9ggxbpcx5pjukd
Kategori:Parapsikologi
14
87718
342853
247941
2026-05-17T08:41:30Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:Forteana]] ke [[Kategori:Parapsikologi]]: Tajuk salah eja
247941
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Kategori:ar:Bulan takwim Masihi
14
112657
342800
278863
2026-05-16T12:28:39Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:ar:Bulan kalendar Masihi]] ke [[Kategori:ar:Bulan takwim Masihi]]: Tajuk salah eja
278863
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Kategori:ar:Kelengkapan dandanan diri
14
112663
342860
278869
2026-05-17T08:47:37Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:ar:Kelengkapan tandas]] ke [[Kategori:ar:Kelengkapan dandanan diri]]: Tajuk salah eja
278869
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Kategori:ar:Parapsikologi
14
112695
342850
278901
2026-05-17T08:40:15Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:ar:Forteana]] ke [[Kategori:ar:Parapsikologi]]: Tajuk salah eja
278901
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Kategori:my:Paranormal
14
113436
342858
279700
2026-05-17T08:43:58Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:my:Forteana]] ke [[Kategori:my:Paranormal]]: Tajuk salah eja
279700
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Kategori:zh:Kelengkapan dandanan diri
14
115316
342856
281887
2026-05-17T08:42:38Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:zh:Kelengkapan tandas]] ke [[Kategori:zh:Kelengkapan dandanan diri]]: Tajuk salah eja
281887
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Kategori:en:Bulan takwim Masihi
14
117622
342810
327410
2026-05-16T12:36:12Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:en:Bulan kalendar Masihi]] ke [[Kategori:en:Bulan takwim Masihi]]: Tajuk salah eja
327410
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Kategori:en:Bulan takwim Gregory
14
117623
342837
327414
2026-05-17T00:01:01Z
Hakimi97
2668
342837
wikitext
text/x-wiki
{{delete|sudah ada [[:Kategori:en:Bulan takwim Masihi]]}}
epacimr4fah52boyx2b06bcasp21gox
Kategori:en:Kelengkapan dandanan diri
14
117668
342864
327633
2026-05-17T08:49:00Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:en:Kelengkapan tandas]] ke [[Kategori:en:Kelengkapan dandanan diri]]: Tajuk salah eja
327633
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Kategori:en:Forteana
14
117788
342852
328246
2026-05-17T08:41:07Z
Hakimi97
2668
342852
wikitext
text/x-wiki
{{delete|tidak diperlukan}}
44ksjr1xemtnyzfprzw0v5sq74e9sbx
Kategori:af:Bulan takwim Masihi
14
117929
342813
332640
2026-05-16T12:37:41Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:af:bulan kalendar Masihi]] ke [[Kategori:af:Bulan takwim Masihi]]
332640
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Modul:template cat
828
118621
342833
342775
2026-05-16T14:18:43Z
Hakimi97
2668
342833
Scribunto
text/plain
-- Author: Benwing
local export = {}
local require_when_needed = require("Module:utilities/require when needed")
local is_callable = require_when_needed("Module:fun", "is_callable")
local format_categories = require_when_needed("Module:utilities", "format_categories")
local parse_interface_module = "Module:parse interface"
local m_string_utilities = require("Module:string utilities")
local und_lang = require("Module:languages").getByCode("und", true)
local ugsub = m_string_utilities.gsub
local ufind = m_string_utilities.find
local split = m_string_utilities.split
local insert = table.insert
local concat = table.concat
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
-- This table detects the category type of the template given its name. When this is invoked, the language code has
-- already been removed from the beginning, and anything starting with a slash has been truncated. The entries are
-- processed in order and are two-element lists of Lua patterns (anchored on both sides; beware of hyphens, which need
-- to be %-escaped) and the category type to use. The category types themselves are mapped to categories in
-- category_type_to_category.
local detect_category_type_list = {
-- order is important here
-- nouns, proper nouns, pronouns
-- (1) unambiguous decl/infl templates
{"decl%-.*proper.*", "noun inflection-table"},
{"infl%-.*proper.*", "noun inflection-table"},
{"decl%-.*pron.*", "pronoun inflection-table"},
{"infl%-.*pron.*", "pronoun inflection-table"},
{"decl%-noun.*", "noun inflection-table"},
{"infl%-noun.*", "noun inflection-table"},
-- (2) nouns
{"noun", "headword-line"},
{"noun[ -]form", "headword-line"},
{"noun[ -]pl", "headword-line"},
{"noun[ -]plonly", "headword-line"},
-- Some languages, e.g. Urdu, have inflection templates called e.g. [[Template:ur-noun-f-ī]]. They should be called
-- [[Template:ur-decl-noun-f-ī]] but we can autodetect them if we exclude the likely cases that are not declension
-- templates.
{"noun%-.*", "noun inflection-table"},
{"ndecl", "noun inflection-table"},
{"ndecl%-.*", "noun inflection-table"},
-- (3) proper nouns
{"proper[ -]?noun", "headword-line"},
{"proper[ -]?noun[ -]form", "headword-line"},
{"proper[ -]?noun[ -]pl", "headword-line"},
{"proper[ -]?noun[ -]plonly", "headword-line"},
{"pnoun", "headword-line"},
{"pnoun[ -]form", "headword-line"},
{"pnoun[ -]pl", "headword-line"},
{"pnoun[ -]plonly", "headword-line"},
{"propn", "headword-line"},
{"propn[ -]form", "headword-line"},
{"propn[ -]pl", "headword-line"},
{"propn[ -]plonly", "headword-line"},
-- See above for inflection templates without 'decl' or 'infl' in them.
{"proper[ -]?noun%-.*", "noun inflection-table"},
{"pnoun%-.*", "noun inflection-table"},
{"propn%-.*", "noun inflection-table"},
-- (4) pronouns
{"pron", "headword-line"},
{"pronoun", "headword-line"},
{"pron[ -]form", "headword-line"},
{"pronoun[ -]form", "headword-line"},
{"prondecl", "pronoun inflection-table"},
{"prondecl%-.*", "pronoun inflection-table"},
-- adjectives
{"decl%-adj.*", "adjective inflection-table"},
{"infl%-adj.*", "adjective inflection-table"},
{"adj", "headword-line"},
{"adjective", "headword-line"},
{"adj[ -]form", "headword-line"},
{"adjective[ -]form", "headword-line"},
{"adj[ -]comp", "headword-line"},
{"adjective[ -]comp", "headword-line"},
{"adj[ -]sup", "headword-line"},
{"adjective[ -]sup", "headword-line"},
-- Some languages, e.g. Urdu and Lithuanian, have inflection templates called e.g. [[Template:ur-adj-1]] and
-- [[Template:lt-adj-is]]. They should be called [[Template:ur-decl-adj-1]] and [[Template:lt-decl-adj-is]] but we
-- can autodetect them if we exclude the likely cases that are not declension templates.
{"adj%-.*", "adjective inflection-table"},
{"adecl", "adjective inflection-table"},
{"adecl%-.*", "adjective inflection-table"},
-- verbs; need to avoid including conjunctions
{"verb", "headword-line"},
{"conj", "verb inflection-table"},
{"conj[0-9 -].*", "verb inflection-table"},
{"conjug.*", "verb inflection-table"},
{"infl%-verb.*", "verb inflection-table"},
-- pronunciation
{".*IPA.*", "pronunciation"},
{"pronunciation", "pronunciation"},
{"pr", "pronunciation"},
{"p", "pronunciation"},
-- form-of
{".*form of", "form-of"},
-- pronominal boxes
{".*personal pronouns", "personal pronoun"},
{".*demonstrative.*", "demonstrative"},
{".*interrogative.*", "interrogative"},
{".*possessives", "possessive"},
{".*possessive .*", "possessive"},
{".*reflexives", "reflexive"},
{".*reflexive .*", "reflexive"},
-- these next two should precede 'relative'
{".*correlatives", "pro-form"},
{".*correlative .*", "pro-form"},
{".*relative .*", "relative"},
{".*articles", "article"},
{".*prefixes", "affix"},
{".*suffixes", "affix"},
-- TOC tables
{".*TOC", "TOC"},
-- numbers, numerals
{".*numbers", "number"},
{".*numerals", "number"},
{".*ordinals", "ordinal"},
{".*cardinals", "cardinal"},
{".*digits", "digit"},
-- transliteration
{".*xlit", "orthographic conversion"},
{".*translit", "orthographic conversion"},
-- orthographic and regional variants
{".*variant", "orthographic variant"},
{".*regional", "regional variant"},
-- sign production templates
-- FIXME: We should limit this to sign languages. As it is, we put it near the bottom of the
-- pattern list so it doesn't accidentally override other patterns for non-sign languages.
{"prod .*", "sign production"},
}
-- This table indicates how to convert template category types to categories. It consists of a list of pairs, where the
-- first element is the category type and the second element is a key-value table containing the following keys:
-- * `aliases`: Optional list of aliases for the category type, which can be used when explicitly specifying the type,
-- e.g. {{tcat|hw}} instead of {{tcat|headword-line}}.
-- * `cats`: List of categories to add the template to. Each entry either specifies a ''raw'' category (if the category
-- name begins with "Category:"), a ''full table'' category (if the category name begins with "label:", where what
-- follows specifies the full label without the prefixed language name) or a ''regular label'' category (for other
-- strings, where e.g. if the label is "noun inflection-table", the category name is
-- "LANG noun inflection-table templates"). An entry is either a string, directly specifying the category name, or a
-- key-value table with keys `name` (the category name) and `sort` (how to generate the sort base). By default, the
-- sort base for raw categories is a comma-separated list of the language names (not codes) associated with the
-- template, or the full template name if there are no languages, and the sort base for label categories is the
-- template name minus the initial language code. If this isn't correct, the `sort` field specifies how to compute the
-- sort base. It is either a function of two arguments, the template name and language object, which should return the
-- sort base; or a table of specs telling how to compute the sort base. In the case of a function, the template name
-- passed in is the full name for raw categories, but the name minus any language code prefix in the case of label
-- categories; and for raw categories, a list of all associated language objects is passed in, or {nil} if none, while
-- for label categories, a single language object is passed in. (Label categories can only exist if there are
-- associated languages.) In the case where `sort` is a table of specs, it is a list where each element is a
-- two-element list of a Lua pattern anchored on both sides and the corresponding pattern replacement string. The
-- specs are processed in order.
local category_type_to_category = {
-- Inflection-table templates
{"noun inflection-table", {
aliases = {"nouninfl", "noundecl", "ndecl"},
cats = {{name = "noun inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"noun%-(.*)", "%1"},
-- put this twice to catch noun-decl-* and decl-noun-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"noun", "*"},
{"ndecl%-base%-(.*)", " %1"},
{"ndecl%-base", " "},
{"ndecl%-(.*)", "%1"},
{"ndecl", "*"},
{"propndecl%-base%-(.*)", " %1"},
{"propndecl%-base", " "},
{"propndecl%-(.*)", "%1"},
{"propndecl", "*"},
{"proper[ -]?noun%-(.*)", "%1"},
{"propn%-(.*)", "%1"},
{"pnoun%-(.*)", "%1"},
}}},
}},
{"pronoun inflection-table", {
aliases = {"proninfl", "prondecl"},
cats = {{name = "pronoun inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"pronoun%-(.*)", "%1"},
{"pron%-(.*)", "%1"},
-- put this twice to catch pron-decl-* and decl-pron-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"pronoun", "*"},
{"pron", "*"},
{"prondecl%-base%-(.*)", " %1"},
{"prondecl%-base", " "},
{"prondecl%-(.*)", "%1"},
{"prondecl", "*"},
}}},
}},
{"article inflection-table", {
aliases = {"artinfl", "artdecl"},
cats = {{name = "article inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"article%-(.*)", "%1"},
{"art%-(.*)", "%1"},
-- put this twice to catch art-decl-* and decl-art-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"article", "*"},
{"art", "*"},
{"artdecl%-base%-(.*)", " %1"},
{"artdecl%-base", " "},
{"artdecl%-(.*)", "%1"},
{"artdecl", "*"},
}}},
}},
{"determiner inflection-table", {
aliases = {"detinfl", "detdecl"},
cats = {{name = "determiner inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"determiner%-(.*)", "%1"},
{"det%-(.*)", "%1"},
-- put this twice to catch det-decl-* and decl-det-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"determiner", "*"},
{"det", "*"},
{"detdecl%-base%-(.*)", " %1"},
{"detdecl%-base", " "},
{"detdecl%-(.*)", "%1"},
{"detdecl", "*"},
}}},
}},
{"preposition inflection-table", {
aliases = {"prepinfl", "prepdecl"},
cats = {{name = "preposition inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"preposition%-(.*)", "%1"},
{"prep%-(.*)", "%1"},
-- put this twice to catch prep-decl-* and decl-prep-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"preposition", "*"},
{"prep", "*"},
{"prepdecl%-base%-(.*)", " %1"},
{"prepdecl%-base", " "},
{"prepdecl%-(.*)", "%1"},
{"prepdecl", "*"},
{"prepinfl%-base%-(.*)", " %1"},
{"prepinfl%-base", " "},
{"prepinfl%-(.*)", "%1"},
{"prepinfl", "*"},
}}},
}},
{"postposition inflection-table", {
aliases = {"postpinfl", "postpdecl"},
cats = {{name = "postposition inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"postposition%-(.*)", "%1"},
{"postp%-(.*)", "%1"},
{"post%-(.*)", "%1"},
-- put this twice to catch postp-decl-* and decl-postp-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"postposition", "*"},
{"postp", "*"},
{"post", "*"},
{"postpdecl%-base%-(.*)", " %1"},
{"postpdecl%-base", " "},
{"postpdecl%-(.*)", "%1"},
{"postpdecl", "*"},
{"postpinfl%-base%-(.*)", " %1"},
{"postpinfl%-base", " "},
{"postpinfl%-(.*)", "%1"},
{"postpinfl", "*"},
}}},
}},
{"adjective inflection-table", {
aliases = {"adjinfl", "adjdecl", "adecl"},
cats = {{name = "adjective inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"adj%-(.*)", "%1"},
{"adjective%-(.*)", "%1"},
-- put this twice to catch adj-decl-* and decl-adj-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"adj", "*"},
{"adjective", "*"},
{"adecl%-base%-(.*)", " %1"},
{"adecl%-base", " "},
{"adecl%-(.*)", "%1"},
{"adecl", "*"},
}}},
}},
{"numeral inflection-table", {
aliases = {"numinfl", "numdecl"},
cats = {{name = "numeral inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"numeral%-(.*)", "%1"},
{"number%-(.*)", "%1"},
{"num%-(.*)", "%1"},
-- put this twice to catch num-decl-* and decl-num-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"numeral", "*"},
{"number", "*"},
{"num", "*"},
{"numdecl%-base%-(.*)", " %1"},
{"numdecl%-base", " "},
{"numdecl%-(.*)", "%1"},
{"numdecl", "*"},
}}},
}},
{"nominal inflection-table", {
aliases = {"nominfl", "nomdecl"},
cats = {{name = "nominal inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"nominal%-(.*)", "%1"},
{"nom%-(.*)", "%1"},
-- put this twice to catch nom-decl-* and decl-nom-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"nominal", "*"},
{"nom", "*"},
{"nomdecl%-base%-(.*)", " %1"},
{"nomdecl%-base", " "},
{"nomdecl%-(.*)", "%1"},
{"nomdecl", "*"},
}}},
}},
{"verb inflection-table", {
aliases = {"verbinfl", "conj"},
cats = {{name = "verb inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"verb%-(.*)", "%1"},
-- put this twice to catch verb-infl-* and infl-verb-*
{"infl%-(.*)", "%1"},
{"conj%-base%-(.*)", " %1"},
{"conj%-base", " "},
-- handle conj2, conj1-c, etc.
{"conj%-?(.*)", "%1"},
{"conj", "*"},
}}},
}},
{"adverb inflection-table", {
aliases = {"advinfl"},
cats = {{name = "adverb inflection-table", sort = {
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"adverb%-(.*)", "%1"},
{"adv%-(.*)", "%1"},
-- put this twice to catch adv-decl-* and decl-adv-*
{"infl%-(.*)", "%1"},
{"decl%-(.*)", "%1"},
{"adverb", "*"},
{"adv", "*"},
{"advinfl%-base%-(.*)", " %1"},
{"advinfl%-base", " "},
{"advinfl%-(.*)", "%1"},
{"advinfl", "*"},
}}},
}},
-- Inflection-table subtemplates
{"inflection-table sub", {
aliases = {"inflsub"},
cats = {"label:inflection-table subtemplates"},
}},
-- Definition templates
-- Headword-line templates
{"headword-line", {
aliases = {"hw", "headword"},
cats = {"baris pengepala kata"},
}},
-- Definition templates
{"definition", {
aliases = {"def", "defn"},
cats = {"takrifan"},
}},
{"form-of", {
aliases = {"form of"},
cats = {"bentuk bagi"},
}},
-- Etymology and pronunciation templates
{"etymology", {
aliases = {"etym"},
cats = {"etimologi"},
}},
{"morphology", {
aliases = {"morph"},
cats = {"etimologi", "Kategori:Templat morfologi khusus bahasa"},
}},
{"pronunciation", {
aliases = {"pron"},
cats = {"sebutan"},
}},
{"sign production", {
aliases = {"signprod"},
cats = {{name = "sign production", sort = {
{"prod (.*)", "%1"},
}}},
}},
-- Pseudo-namespace templates
{"reference", {
aliases = {"ref"},
cats = {{name = "rujukan", allow_etym = true}},
}},
{"quotation", {
aliases = {"quote"},
cats = {"petikan"},
}},
{"usage", {
cats = {"penggunaan"},
}},
{"list", {
cats = {"senarai"},
}},
{"auto-table", {
aliases = {"table"},
cats = {"jadual automatik"},
}},
-- Navigation templates
-- Pro-form box templates
{"pro-form", {
aliases = {"pro-forms"},
cats = {"navigasi", "Kategori:Templat kotak pro-bentuk"},
}},
{"adposition", {
aliases = {"adpositions", "preposition", "prepositions", "postposition", "postpositions"},
cats = {"navigasi", "Kategori:Templat kotak adposisi"},
}},
{"affix", {
aliases = {"affixes", "prefix", "prefixes", "suffix", "suffixes"},
cats = {"navigasi", "Kategori:Templat kotak imbuhan pro-bentuk"},
}},
{"article", {
aliases = {"articles"},
cats = {"navigasi", "Category:Grammatical article box templates"},
}},
{"demonstrative", {
aliases = {"demonstratives"},
cats = {"navigasi", "Category:Demonstrative box templates"},
}},
{"interrogative", {
aliases = {"interrogatives"},
cats = {"navigasi", "Category:Interrogative box templates"},
}},
{"personal pronoun", {
aliases = {"perspron", "personal pronouns"},
cats = {"navigasi", "Category:Personal pronoun box templates"},
}},
{"possessive", {
aliases = {"possessives"},
cats = {"navigasi", "Category:Possessive pronoun and determiner box templates"},
}},
{"reflexive", {
aliases = {"reflexives"},
cats = {"navigasi", "Category:Reflexive pronoun and determiner box templates"},
}},
{"relative", {
aliases = {"relatives"},
cats = {"navigasi", "Category:Relative pronoun and determiner box templates"},
}},
{"navigation", {
-- miscellaneous navigation box templates like {{eu-aux verbs}}, {{pt-forms of address}}
aliases = {"nav"},
cats = {"navigasi"},
}},
{"TOC", {
cats = {{name = "navigation", sort = {
{"categoryTOC", "TOC"},
}}, "Kategori:Templat TOC"},
}},
{"number", {
aliases = {"numbers"},
cats = {"navigasi", "Kategori:Templat nombor khusus bahasa"},
}},
{"cardinal", {
aliases = {"cardinals"},
cats = {"navigasi", "Kategori:Templat nombor khusus bahasa"},
}},
{"ordinal", {
aliases = {"ordinals"},
cats = {"navigasi", "Kategori:Templat nombor khusus bahasa"},
}},
{"digit", {
aliases = {"digits"},
cats = {"senarai", "Kategori:Templat nombor khusus bahasa"},
}},
-- Entry templates
{"entry", {
cats = {"kata masukan"},
}},
-- Orthographic conversion templates (e.g. for converting between scripts)
{"orthographic conversion", {
aliases = {"transliteration", "translit", "xlit", "orthconv", "scriptconv"},
cats = {"pertukaran", "Kategori:Templat pertukaran ortografi"},
}},
-- Orthographic and regional variant templates (for displaying orthographic, script and/or regional variants)
{"orthographic variant", {
aliases = {"orthvar", "scriptvar"},
-- Currently we categorize orthographic and regional variants the same but we could split them if needed
cats = {"navigasi", "Kategori:Templat kelainan ortografi dan setempat"},
}},
{"regional variant", {
aliases = {"regvar"},
cats = {"navigasi", "Kategori:Templat kelainan ortografi dan setempat"},
}},
-- Internal link templates
{"link", {
cats = {"pautan", "Kategori:Templat pautan dalaman khusus bahasa"},
}},
}
local category_type_to_category_map = {}
for _, category_type_to_category_spec in ipairs(category_type_to_category) do
local category_type, props = unpack(category_type_to_category_spec)
category_type_to_category_map[category_type] = props
if props.aliases then
for _, alias in ipairs(props.aliases) do
category_type_to_category_map[alias] = props
end
end
end
-- Split an argument on comma, but not comma followed by whitespace; split off sort base after a colon.
local function split_on_comma_and_split_off_sort_base(val)
local cattypes
if val:find(",") then
-- Don't optimize more than this because there can be commas backslashed, inside of links or followed by
-- whitespace that don't cause splitting.
cattypes = require(parse_interface_module).split_on_comma(val)
else
cattypes = {val}
end
for i, cattype_spec in ipairs(cattypes) do
if cattype_spec:find(":") then
local cattype, sort_base = cattype_spec:match("^(.-):(.*)$")
sort_base = sort_base:gsub("_", " ")
cattypes[i] = {name = cattype, sort_base = sort_base}
end
end
return cattypes
end
local function get_lang_or_script(code)
return code == "-" and code or
require("Module:languages").getByCode(code, nil, "allow etym") or
require("Module:languages").getByCode(code .. "-pro", nil, "allow etym") or
require("Module:scripts").getByCode(code)
end
local function obj_code(obj)
if obj == "-" then
return obj
end
return obj:getCode()
end
local function get_prefixed_obj(after_prefix)
return after_prefix:match("^(%a[%a-]*%a):(.+)$")
end
local function get_suffixed_obj(after_prefix)
local rest, objcode = after_prefix:match("^(.+)/(%a[%a-]*%a)$")
return objcode, rest
end
local pseudo_namespace_templates = {
{"R:", {
category_type = "rujukan",
get_obj_and_rest = get_prefixed_obj,
}},
{"RQ:", {
category_type = "petikan",
get_obj_and_rest = get_prefixed_obj,
}},
{"U:", {
category_type = "penggunaan",
get_obj_and_rest = get_prefixed_obj,
}},
{"list:", {
category_type = "senarai",
get_obj_and_rest = get_suffixed_obj,
}},
{"table:", {
category_type = "jadual automatik",
get_obj_and_rest = get_suffixed_obj,
}},
}
local function infer_lang_or_script_code_and_category_type(name)
if name:find(":") then -- only check for pseudo-namespace prefix when a colon is present
for _, pseudo_namespace_spec in ipairs(pseudo_namespace_templates) do
local prefix, props = unpack(pseudo_namespace_spec)
local after_prefix = name:match("^" .. prefix .. "(.+)$")
if after_prefix then
local objcode, rest = props.get_obj_and_rest(after_prefix)
local obj
if objcode then
obj = get_lang_or_script(objcode) -- may return nil
if not obj then
rest = after_prefix
end
else
rest = after_prefix
end
return obj, rest, props.category_type
end
end
end
local hyphen_parts = split(name, "%-")
for i = #hyphen_parts - 1, 1, -1 do
local code = concat(hyphen_parts, "-", 1, i)
local obj = get_lang_or_script(code)
if obj then
local rest = concat(hyphen_parts, "-", i + 1)
return obj, rest, nil
end
end
return nil, name, nil
end
local function process_sortbase_specs(sortbase, specs)
for _, spec in ipairs(specs) do
local from, to = unpack(spec)
sortbase = ugsub(sortbase, "^" .. from .. "$", to)
end
return sortbase
end
local function template_name_minus_langcode_to_category_type(name)
for _, type_spec in ipairs(detect_category_type_list) do
local pattern, intended_type = unpack(type_spec)
if ufind(name, "^" .. pattern .. "$") then
return intended_type
end
end
return nil
end
local function compute_categories_for_template(full_template_name, template_name_minus_langcode, category_type,
langs_or_scripts)
local overriding_sort_base
if type(category_type) == "table" then
overriding_sort_base = category_type.sort_base
category_type = category_type.name
end
if not category_type_to_category_map[category_type] then
error("Unrecognized template category type: " .. category_type)
end
local props = category_type_to_category_map[category_type]
if not props.cats then
error("Internal error: No categories given for category type: " .. category_type)
end
local categories = {}
for _, catspec in ipairs(props.cats) do
if type(catspec) == "string" then
catspec = {name = catspec}
end
local rawcat = catspec.name:match("^Kategori:(.*)")
if rawcat then
local sortbase
-- User-specified sort base does not apply to raw categories, which have a different sort key format
-- than language-specific categories.
if not catspec.sort then
if langs_or_scripts then
local langnames = {}
for _, lang_or_sc in ipairs(langs_or_scripts) do
insert(langnames, lang_or_sc:getCanonicalName()) -- FIXME: or lang:getFullName()?
end
sortbase = concat(langnames, ",")
else
sortbase = full_template_name
end
elseif is_callable(catspec.sort) then
sortbase = catspec.sort(full_template_name, langs_or_scripts)
else
sortbase = process_sortbase_specs(full_template_name, catspec.sort)
end
insert(categories, {cat = rawcat, lang = und_lang, sort_base = sortbase})
elseif langs_or_scripts then
for _, lang_or_sc in ipairs(langs_or_scripts) do
local sortbase
if overriding_sort_base then
sortbase = overriding_sort_base
elseif not catspec.sort then
sortbase = template_name_minus_langcode
elseif is_callable(catspec.sort) then
sortbase = catspec.sort(template_name_minus_langcode, lang_or_sc)
else
sortbase = process_sortbase_specs(template_name_minus_langcode, catspec.sort)
end
if lang_or_sc:hasType("script") then
insert(categories, {
cat = ("Templat %s"):format(lang_or_sc:getCategoryName()), lang = und_lang, sc = lang_or_sc,
sort_base = sortbase,
})
else
local cat
local full_label = catspec.name:match("^label:(.*)$")
local lang_name = catspec.allow_etym and lang_or_sc:getCanonicalName() or lang_or_sc:getFullName()
if full_label then
cat = ("%s bahasa %s"):format(full_label, lang_name)
else
cat = ("Templat %s bahasa %s"):format(catspec.name, lang_name)
end
insert(categories, {
cat = cat, lang = lang_or_sc:getFull(), sort_base = sortbase,
})
end
end
end
end
if not categories[1] then
error(("No categories generated for template [[Template:%s]] with category type '%s'"):format(
full_template_name, category_type))
end
return categories
end
--[==[
Main entry point.
]==]
function export.categorize(frame)
local params = {
[1] = {}, -- comma-separated list of category types; by default, inferred from template name
lang = {}, -- comma-separated list of languages; by default, inferred from template name
["pagename"] = {}, -- for testing
["json"] = {type = "boolean"}, -- for testing
}
local parent_args = frame:getParent().args
args = require("Module:parameters").process(parent_args, params)
local category_specs = {}
local function insert_cat(cat, sort_key)
for _, existing_cat in ipairs(category_specs) do
if existing_cat.cat == cat then
return
end
end
insert(category_specs, {cat = cat, sort_key = sort_key})
end
local pagename = args.pagename
if not pagename then
title = mw.title.getCurrentTitle()
pagename = title.fullText
end
if pagename:find("/doc$") or pagename:find("/doc/") then
return ""
end
if pagename:find("^Templat:Pengguna:") then
insert_cat("Templat kotak pasir pengguna", (pagename:gsub("^Templat:Pengguna:", "")))
elseif pagename:find("^Pengguna:") then
insert_cat("Templat kotak pasir pengguna", (pagename:gsub("^Pengguna:", "")))
else
if not pagename:find("^Templat:") then
error(("This template should only be used in the Template namespace, not on page '%s'"):format(pagename))
end
local full_template_name = pagename:gsub("^Templat:", "")
local rootpage = full_template_name:gsub("/.*", "")
if full_template_name:find("/sandbox") then
insert_cat("Templat kotak pasir", full_template_name)
elseif full_template_name:find("^sandbox/") then
insert_cat("Templat kotak pasir", full_template_name:gsub("^sandbox/", ""))
else
local template_objs
if args.lang == "-" then
template_objs = false
elseif args.lang then
template_objs = {}
for _, code in ipairs(split(args.lang, ",")) do
-- We need to have an indicator of families because we allow bare family codes to stand for proto-languages.
if code:find("^fam:") then
code = code:gsub("^fam:", "")
local family = require("Module:families").getByCode(code) or
error(("Unrecognized family code '%s' in [[Module:template cat]]"):format(code))
local descendants = family:getDescendantCodes()
for _, desc in ipairs(descendants) do
local obj = get_lang_or_script(desc)
if obj then
-- make sure we skip families without proto-languages
insert(template_objs, obj)
end
end
else
local obj = get_lang_or_script(code)
if not obj then
error(("Unrecognized language or script code '%s'"):format(code))
end
insert(template_objs, obj)
end
end
end
local cattypes
if args[1] then
cattypes = split_on_comma_and_split_off_sort_base(args[1])
end
local inferred_obj, inferred_rest, inferred_cattype =
infer_lang_or_script_code_and_category_type(rootpage)
if template_objs == nil or not cattypes then
if template_objs == nil then
if not inferred_obj then
if not inferred_cattype then
error(("Unable to infer language or script from template root page '%s' for template '%s'; specify lang/script and type explicitly"):format(
rootpage, pagename))
else
error(("Unable to infer language or script from template root page '%s' for template '%s', inferred category type '%s'; specify lang/script explicitly"):format(
rootpage, pagename, inferred_cattype))
end
else
template_objs = {inferred_obj}
end
end
if not cattypes then
inferred_cattype = inferred_cattype or
template_name_minus_langcode_to_category_type(inferred_rest)
if not inferred_cattype then
error(("Unable to infer template category type from template remainder (after stripping langcode) '%s' for template '%s'; specify type explicitly"):format(
inferred_rest, pagename))
end
cattypes = {inferred_cattype}
end
end
for _, cattype in ipairs(cattypes) do
local cats = compute_categories_for_template(full_template_name, inferred_rest, cattype, template_objs)
for _, cat in ipairs(cats) do
insert(category_specs, cat)
end
end
end
end
-- We are returning categories for templates or user-space pages, so we need to force the output.
local retval = format_categories(category_specs, nil, nil, nil, "force_output")
if args.json then
return require("Module:JSON").toJSON {
category_specs = category_specs,
retval = mw.text.nowiki(retval),
}
else
return retval
end
end
--[==[Table used in the documentation to {{tl|template cat}}.]==]
function export.pattern_to_category_type_table()
local parts = {}
local function ins(text)
insert(parts, text)
end
ins('{|class="wikitable"')
ins("! Corak !! Jenis kategori terinferensi")
for _, detect_spec in ipairs(detect_category_type_list) do
local pattern, category_type = unpack(detect_spec)
ins("|-")
ins(("| <code>%s</code> || <code>%s</code>"):format(pattern, category_type))
end
ins("|}")
return concat(parts, "\n")
end
--[==[Table used in the documentation to {{tl|template cat}}.]==]
function export.category_type_to_category_table()
local parts = {}
local function ins(text)
insert(parts, text)
end
local category_types = {}
local category_type_to_aliases = {}
for _, category_type_to_category_spec in ipairs(category_type_to_category) do
local category_type, props = unpack(category_type_to_category_spec)
insert(category_types, category_type)
category_type_to_aliases[category_type] = {}
if props.aliases then
for _, alias in ipairs(props.aliases) do
insert(category_type_to_aliases[category_type], alias)
end
table.sort(category_type_to_aliases[category_type])
end
end
table.sort(category_types)
local function get_category_type_categories(category_type)
local cats = {}
for _, catspec in ipairs(category_type_to_category_map[category_type].cats) do
if type(catspec) == "string" then
catspec = {name = catspec}
end
local cat = catspec.name
if cat:find("^Kategori:") then
insert(cats, ("<code>%s</code>"):format((cat:gsub("^Kategori:", ""))))
elseif cat:find("^label:") then
insert(cats, ("<code><var>LANG</var> %s</code>"):format((cat:gsub("^label:", ""))))
else
insert(cats, ("<code><var>LANG</var> %s templates</code>"):format(cat))
end
end
return concat(cats, ", ")
end
ins('{|class="wikitable"')
ins("! Jenis kategori !! Jenis kategori berkanun !! Kategori")
for _, category_type in ipairs(category_types) do
ins("|-")
ins(("| <code>'''%s'''</code> || ''(same)'' || <code>%s</code>"):format(
category_type, get_category_type_categories(category_type)))
for _, alias in ipairs(category_type_to_aliases[category_type]) do
ins("|-")
ins(("| <code>%s</code> || <code>'''%s'''</code> || <code>%s</code>"):format(
alias, category_type, get_category_type_categories(category_type)))
end
end
ins("|}")
return concat(parts, "\n")
end
return export
0ff5hjaygssgnrmwqbpo1g4fnx1ce30
Kategori:ar:Bulan kalendar Masihi
14
118624
342801
2026-05-16T12:28:39Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:ar:Bulan kalendar Masihi]] ke [[Kategori:ar:Bulan takwim Masihi]]: Tajuk salah eja
342801
wikitext
text/x-wiki
#LENCONG [[:Kategori:ar:Bulan takwim Masihi]]
6mynv8thv7j6wd18unqctgih53zjwzz
Kategori:en:Bulan kalendar Masihi
14
118625
342811
2026-05-16T12:36:13Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:en:Bulan kalendar Masihi]] ke [[Kategori:en:Bulan takwim Masihi]]: Tajuk salah eja
342811
wikitext
text/x-wiki
#LENCONG [[:Kategori:en:Bulan takwim Masihi]]
nap3daw8rypybqp7h9hr34fl0dod9v2
Kategori:af:bulan kalendar Masihi
14
118626
342814
2026-05-16T12:37:42Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:af:bulan kalendar Masihi]] ke [[Kategori:af:Bulan takwim Masihi]]
342814
wikitext
text/x-wiki
#LENCONG [[:Kategori:af:Bulan takwim Masihi]]
aql0dtizg44gdobr01lbzuiycwmzm02
Modul:etymon/data
828
118627
342817
2026-05-16T13:38:28Z
Hakimi97
2668
Mencipta laman baru dengan kandungan 'local export = {} export.STATUS = { OK = "ok", INLINE = "inline", MISSING = "missing", REDLINK = "redlink", AMBIGUOUS = "ambiguous", } export.TRANSITIVE = { ALWAYS = "always", -- always recurse into children NEVER = "never", -- never recurse into children CROSS_LANG = "cross_lang", -- only recurse when source lang differs...'
342817
Scribunto
text/plain
local export = {}
export.STATUS = {
OK = "ok",
INLINE = "inline",
MISSING = "missing",
REDLINK = "redlink",
AMBIGUOUS = "ambiguous",
}
export.TRANSITIVE = {
ALWAYS = "always", -- always recurse into children
NEVER = "never", -- never recurse into children
CROSS_LANG = "cross_lang", -- only recurse when source lang differs from target lang (but pos chain continues)
CROSS_LANG_NO_INTERNAL_SOURCE = "cross_lang_no_internal_source", -- like CROSS_LANG, but source breaks for internal derivations in the same language context
}
-- Deep merge tables (nested tables are merged recursively, later values override earlier)
local function deep_merge(...)
local result = {}
for _, t in ipairs({ ... }) do
for k, v in pairs(t) do
if type(v) == "table" and type(result[k]) == "table" then
result[k] = deep_merge(result[k], v)
else
result[k] = v
end
end
end
return result
end
local function make_glossary_link(term, display_text)
if not term then return display_text end
return "[[Appendix:Glossary#" .. term:gsub(" ", "_") .. "|" .. display_text .. "]]"
end
-- Extract base word and connector from text like "Borrowed from" or "calque of"
local function split_glossary_text(text)
for _, pattern in ipairs({ "^(.-)(%s+[Oo][Ff])$", "^(.-)(%s+[Ff][Rr][Oo][Mm])$" }) do
local base, rest = text:match(pattern)
if base then return base, rest end
end
return text, ""
end
local TRANSITIVE = export.TRANSITIVE
local function create_keyword(opts)
local entry = {
is_group = opts.is_group or false,
abbrev = opts.abbrev,
glossary = opts.glossary,
transitive = opts.transitive or TRANSITIVE.ALWAYS, -- default "always"
inherited_chain = opts.inherited_chain or false,
affix_categories = opts.affix_categories or false,
borrowing_type = opts.borrowing_type,
specialized_borrowing = opts.specialized_borrowing,
toplevel_category = opts.toplevel_category,
no_child_categories = opts.no_child_categories or false,
source_category_type = opts.source_category_type,
invisible = (opts.invisible == true and "all") or opts.invisible or false,
pos_override = opts.pos_override,
new_sentence = opts.new_sentence or false,
separate_clause = opts.separate_clause or false,
aliases = opts.aliases,
}
-- Only set text/phrase when visible in text (invisible ~= "all" and ~= "text")
local inv = entry.invisible
if inv ~= "all" and inv ~= "text" then
entry.phrase = opts.phrase
if opts.text then
if opts.glossary then
local base_word, rest = split_glossary_text(opts.text)
entry.text = make_glossary_link(opts.glossary, base_word) .. rest
else
entry.text = opts.text
end
end
end
return entry
end
-- Shared defaults for keyword groups
local DEFAULTS = {
-- Keywords that pass through inheritance chain
inheritance = {
transitive = TRANSITIVE.ALWAYS,
inherited_chain = true,
},
-- Standard transitive derivation
transitive = {
transitive = TRANSITIVE.ALWAYS,
},
-- Standard for internal derivations: transitive across languages, but not within them
internal_derivation = {
transitive = TRANSITIVE.CROSS_LANG,
},
-- Borrowing keywords
borrowing = {
transitive = TRANSITIVE.ALWAYS,
},
-- Affix group keywords (compound words, blends, etc.)
affix_group = {
is_group = true,
transitive = TRANSITIVE.CROSS_LANG,
affix_categories = true,
},
-- Calque-like keywords (calque, partial calque, semantic loan)
calque_like = {
transitive = TRANSITIVE.NEVER,
no_child_categories = true,
new_sentence = true,
},
-- Non-transitive influence
influence_like = {
transitive = TRANSITIVE.NEVER,
no_child_categories = true,
},
}
export.keywords = {
--
-- Inheritance keywords
--
["from"] = create_keyword(deep_merge(DEFAULTS.inheritance, {
text = "From", phrase = "from",
})),
["inherited"] = create_keyword(deep_merge(DEFAULTS.inheritance, {
text = "Inherited from",
phrase = "from",
glossary = "inherited",
aliases = { "inh" },
})),
--
-- Basic derivation keywords
--
["uder"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "From",
phrase = "from",
toplevel_category = "undefined derivations",
})),
["derived"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Derived from",
phrase = "from",
abbrev = "der.",
glossary = "derived terms",
aliases = { "der" },
})),
--
-- Affix/compound group keywords
--
["affix"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "From",
phrase = "from",
aliases = { "af" },
})),
["surf"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "By surface analysis,",
phrase = "by surface analysis,",
abbrev = "surf.",
glossary = "surface analysis",
new_sentence = true,
invisible = "tree",
})),
["blend"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "Blend of",
phrase = "a blend of",
abbrev = "blend",
glossary = "blend",
toplevel_category = "blends",
})),
["univerbation"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "Univerbation of",
phrase = "univerbation of",
abbrev = "univ.",
glossary = "univerbation",
toplevel_category = "univerbations",
aliases = { "univ" },
})),
--
-- Borrowing keywords
--
["bor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Borrowed from",
phrase = "borrowed from",
abbrev = "bor.",
glossary = "loanword",
borrowing_type = "borrowed",
aliases = { "borrowed" },
})),
["lbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Learned borrowing from",
phrase = "a learned borrowing from",
abbrev = "lbor.",
glossary = "learned borrowing",
specialized_borrowing = "learned",
})),
["obor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Orthographic borrowing from",
phrase = "an orthographic borrowing from",
abbrev = "obor.",
glossary = "orthographic borrowing",
specialized_borrowing = "orthographic",
})),
["slbor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Semi-learned borrowing from",
phrase = "a semi-learned borrowing from",
abbrev = "slbor.",
glossary = "semi-learned borrowing",
specialized_borrowing = "semi-learned",
})),
["ubor"] = create_keyword(deep_merge(DEFAULTS.borrowing, {
text = "Unadapted borrowing from",
phrase = "an unadapted borrowing from",
abbrev = "ubor.",
glossary = "unadapted borrowing",
specialized_borrowing = "unadapted",
})),
--
-- Calque-like keywords (non-transitive, start new sentence)
--
["calque"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Calque of",
phrase = "a calque of",
abbrev = "calq.",
glossary = "calque",
specialized_borrowing = "calque",
aliases = { "cal", "clq" },
})),
["partial calque"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Partial calque of",
phrase = "a partial calque of",
abbrev = "pcalq.",
glossary = "partial calque",
specialized_borrowing = "partial-calque",
aliases = { "pcal" },
})),
["semantic loan"] = create_keyword(deep_merge(DEFAULTS.calque_like, {
text = "Semantic loan of",
phrase = "a semantic loan of",
abbrev = "sl.",
glossary = "semantic loan",
specialized_borrowing = "semantic-loan",
aliases = { "sl" },
})),
--
-- Influence keywords (non-transitive, separate clause)
--
["influence"] = create_keyword(deep_merge(DEFAULTS.influence_like, {
text = "Influenced by",
phrase = "influenced by",
abbrev = "influ.",
glossary = "contamination",
separate_clause = true,
})),
--
-- Morphological derivation keywords
--
["clipping"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Clipping of",
phrase = "clipping of",
abbrev = "clip.",
glossary = "clipping",
toplevel_category = "clippings",
aliases = { "clip" },
})),
["ellipsis"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Ellipsis of",
phrase = "ellipsis of",
abbrev = "ellip.",
glossary = "ellipsis",
toplevel_category = "ellipses",
aliases = { "ellip" },
})),
["back-formation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Back-formation from",
phrase = "a back-formation from",
abbrev = "bf.",
glossary = "back-formation",
toplevel_category = "back-formations",
aliases = { "bf" },
})),
["nominalization"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Nominalization of",
phrase = "a nominalization of",
abbrev = "nom.",
glossary = "nominalization",
toplevel_category = "nominalizations",
aliases = { "nom" },
})),
["transliteration"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Transliteration of",
phrase = "borrowed from",
abbrev = "translit.",
glossary = "transliteration",
aliases = { "translit" },
})),
["vrd"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Vṛddhi derivative of",
phrase = "a vṛddhi derivative of",
abbrev = "vṛd.",
glossary = "vṛddhi derivative",
})),
["apheretic"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Apheretic form of",
phrase = "an apheretic form of",
abbrev = "aph.",
glossary = "apheresis",
aliases = { "apheresis", "aphetic" },
})),
["denominal"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Denominal verb from",
phrase = "denominal verb from",
abbrev = "denom.",
glossary = "denominal",
toplevel_category = "denominal verbs",
aliases = { "denom" },
})),
["deverbal"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Deverbal from",
phrase = "deverbal from",
abbrev = "deverb.",
glossary = "deverbal",
toplevel_category = "deverbals",
})),
["reduplication"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Reduplication of",
phrase = "reduplication of",
abbrev = "redup.",
glossary = "reduplication",
toplevel_category = "reduplications",
aliases = { "redup" },
})),
["abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Abbreviation of",
phrase = "abbreviation of",
abbrev = "abbr.",
glossary = "abbreviation",
aliases = { "abbr", "abbrev" },
})),
["syllabic abbreviation"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Syllabic abbreviation of",
phrase = "syllabic abbreviation of",
abbrev = "syl. abbr.",
glossary = "syllabic abbreviation",
aliases = { "sylabbr", "sylabbrev" },
})),
["acronym"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Acronym of",
phrase = "acronym of",
abbrev = "acronym",
glossary = "acronym",
aliases = { "acro" },
})),
["initialism"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Initialism of",
phrase = "initialism of",
abbrev = "init.",
glossary = "initialism",
aliases = { "init" },
})),
["metathesis"] = create_keyword(deep_merge(DEFAULTS.transitive, {
text = "Metathesis of",
phrase = "metathesis of",
abbrev = "meta.",
glossary = "metathesis",
toplevel_category = "words derived through metathesis",
aliases = { "meta" },
})),
--
-- Invisible keywords (no text output)
--
["root"] = create_keyword {
transitive = TRANSITIVE.ALWAYS,
invisible = "all",
pos_override = "root",
},
["afeq"] = create_keyword(deep_merge(DEFAULTS.affix_group, {
text = "From",
phrase = "from",
transitive = TRANSITIVE.NEVER,
invisible = "all",
})),
}
local aliases_to_register = {}
local canonical_aliases = {}
-- Map every keyword (canonical or alias) to its canonical form for consistent checks and tracking.
export.keyword_canonical = {}
for name, keyword_data in pairs(export.keywords) do
export.keyword_canonical[name] = name
if keyword_data.aliases then
canonical_aliases[name] = keyword_data.aliases
for _, alias in ipairs(keyword_data.aliases) do
if export.keywords[alias] then
error("Alias '" ..
alias .. "' defined in keyword '" .. name .. "' collides with existing keyword '" .. alias .. "'.")
end
if aliases_to_register[alias] then
error("Alias '" ..
alias .. "' defined in keyword '" .. name .. "' is already claimed by another keyword.")
end
aliases_to_register[alias] = keyword_data
export.keyword_canonical[alias] = name
end
keyword_data.aliases = nil
end
end
for alias, data in pairs(aliases_to_register) do
export.keywords[alias] = data
end
--
-- Language exception presets
--
local EXCEPTION_PRESETS = {
-- Fully disallowed: no tree, no text, no categories
disallowed = {
disallow = { tree = true, text = true },
suppress_categories = true,
},
-- Suppress transliteration only
no_translit = {
suppress_tr = true,
},
-- Suppress all categories only
no_categories = {
suppress_categories = true,
},
}
--[=[
Available exception options:
disallow = { Related options for disallowing output:
tree Disallow etymology trees for this language
text Disallow etymology text generation for this language
ref Reference link shown when tree/text is disallowed
}
suppress_tr Suppress transliteration in links
suppress_categories Suppress all category generation
normalize_to Normalize language code to a different code
normalize_from_families Apply normalization to languages in these families
normalize_exclude_families Exclude these families from normalization
keyword_overrides Per-keyword categorisation overrides (e.g. { ["af"] = { transitive = TRANSITIVE.NEVER } })
]=]
local function create_exception(preset, overrides)
local base = preset and EXCEPTION_PRESETS[preset] or {}
return deep_merge(base, overrides or {})
end
export.config = {
lang_exceptions = {
["zh"] = create_exception("disallowed", {
disallow = { ref = "[[Wiktionary:Beer parlour/2025/May#Template:etymon for Chinese]]" },
suppress_tr = true,
normalize_to = "zh",
normalize_from_families = { "zhx" },
normalize_exclude_families = { "qfa-cnt" },
}),
},
}
-- Supported codes for the nominalization <g:code> modifier (subset of common gender/number-style codes)
export.nominalization_g_codes = {
["m"] = "masculine",
["f"] = "feminine",
["n"] = "neuter",
["c"] = "common",
["gneut"] = "gender-neutral",
["s"] = "singular",
["p"] = "plural",
["d"] = "dual",
["pauc"] = "paucal",
["mf"] = "masculine or feminine",
["fm"] = "masculine or feminine",
["mfn"] = "masculine, feminine or neuter",
["mnf"] = "masculine, feminine or neuter",
["fmn"] = "masculine, feminine or neuter",
["fnm"] = "masculine, feminine or neuter",
["nmf"] = "masculine, feminine or neuter",
["nfm"] = "masculine, feminine or neuter",
}
--
-- Propagate keyword overrides to aliases
--
if export.config.lang_exceptions then
for lang_code, exception in pairs(export.config.lang_exceptions) do
if exception.keyword_overrides then
for canonical, aliases in pairs(canonical_aliases) do
if exception.keyword_overrides[canonical] then
local override_data = exception.keyword_overrides[canonical]
for _, alias in ipairs(aliases) do
if not exception.keyword_overrides[alias] then
exception.keyword_overrides[alias] = override_data
end
end
end
end
end
end
end
return export
en6z2me7ggveh5mllhfh8xeo89dolmh
Modul:module loader
828
118628
342821
2026-05-16T13:44:42Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/89545220|89545220]])
342821
Scribunto
text/plain
--[==[ intro:
A generic lazy-loading module loader with function-level caching. Provides a unified interface
for loading modules via `require()` or `mw.loadData()`.
== Features ==
* Lazy loading: modules are only loaded when first accessed
* Function caching: individual functions are cached after first call
* Duplicate detection: errors on duplicate names or paths
== Usage ==
```
local loader = require("Module:module loader")
local M = loader.init({
require = {
utilities = "Module:utilities",
languages = "Module:languages",
},
loadData = {
headword_data = "Module:headword/data",
parameters_data = "Module:parameters/data",
},
})
-- Modules are loaded lazily on first access:
M.utilities.format_categories(...) -- loads Module:utilities, caches function
M.headword_data.page -- loads via mw.loadData
```
== Parameters ==
; ``opts``.require : __table__
: Maps short names to module paths. Modules are loaded via {require()} on first access.
Functions within the module are cached individually.
; ``opts``.loadData : __table__
: Maps short names to module paths. Modules are loaded via {mw.loadData()} on first access.
The entire table is cached (no function-level proxying).
; ``opts``.loadDataOptional : __table__
: Maps short names to module paths. Same as {loadData} but load is wrapped in pcall; missing or failing modules yield nil.
; ``opts``.requireOptional : __table__
: Maps short names to module paths. Same as {require} but load is wrapped in pcall; missing or failing modules yield nil.
; ``opts``.lazy : __table__
: Maps short names to lazy getters. Each entry is either a function () → value (cached on first access)
or a table of name → function () → value for a nested lazy object (e.g. ``mw`` with getCurrentTitle, getCurrentFrame, getContentLanguage).
]==]
local export = {}
local NIL = {} -- sentinel for optional loadData/require that failed (so we don't retry)
-- Validate module configuration for duplicates.
-- Throws an error if duplicate names or paths are found.
local function validate(modules, data_modules, optional_data_modules, optional_require_modules, lazy_modules)
local seen_names = {}
local seen_paths = {}
-- Check require modules
for name, path in pairs(modules) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "'")
end
seen_names[name] = "require"
if seen_paths[path] then
error("Module loader: module '" .. path .. "' loaded twice (as '" .. seen_paths[path] .. "' and '" .. name .. "')")
end
seen_paths[path] = name
end
-- Check loadData modules
for name, path in pairs(data_modules) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "' (in both require and loadData)")
end
seen_names[name] = "loadData"
if seen_paths[path] then
error("Module loader: module '" .. path .. "' loaded twice (as '" .. seen_paths[path] .. "' and '" .. name .. "')")
end
seen_paths[path] = name
end
-- Check loadDataOptional (names must not clash; paths can duplicate optional)
for name, path in pairs(optional_data_modules or {}) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "'")
end
seen_names[name] = "loadDataOptional"
end
-- Check requireOptional (names must not clash)
for name, path in pairs(optional_require_modules or {}) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "'")
end
seen_names[name] = "requireOptional"
end
-- Check lazy (names must not clash)
for name in pairs(lazy_modules or {}) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "'")
end
seen_names[name] = "lazy"
end
end
--[==[
Create a lazy-loading module proxy.
The returned proxy uses metatables to intercept access:
* For `require` modules: creates a sub-proxy that caches individual functions
* For `loadData` modules: returns the data table directly (read-only, no proxying)
* For function modules: caches and returns the function directly
Arguments:
* ``opts`` (__table__): Configuration options.
** ``opts``.require (__table__): Maps short names to module paths (loaded via {require()}).
** ``opts``.loadData (__table__): Maps short names to module paths (loaded via {mw.loadData()}).
** ``opts``.loadDataOptional (__table__): Same as loadData but load is wrapped in pcall; missing modules yield nil.
** ``opts``.requireOptional (__table__): Same as require but load is wrapped in pcall; missing or failing modules yield nil.
** ``opts``.lazy (__table__): Maps names to lazy getters. Value is a function () → value, or a table of name → function () → value for a nested lazy object.
Returns a __table__ proxy that loads modules/functions on demand.
]==]
-- Build a proxy for a lazy object: table of name → function () → value. Each key is evaluated once and cached.
local function make_lazy_object_proxy(getters)
return setmetatable({}, {
__index = function(proxy, key)
local getter = getters[key]
if type(getter) ~= "function" then return nil end
local value = getter()
rawset(proxy, key, value)
return value
end
})
end
function export.init(opts)
local modules = opts.require or {}
local data_modules = opts.loadData or {}
local optional_data_modules = opts.loadDataOptional or {}
local optional_require_modules = opts.requireOptional or {}
local lazy_modules = opts.lazy or {}
validate(modules, data_modules, optional_data_modules, optional_require_modules, lazy_modules)
local loaded_modules = {}
local function get_module(module_name)
if loaded_modules[module_name] ~= nil then
local m = loaded_modules[module_name]
return m == NIL and nil or m
end
if data_modules[module_name] then
loaded_modules[module_name] = mw.loadData(data_modules[module_name])
elseif optional_data_modules[module_name] then
local ok, data = pcall(mw.loadData, optional_data_modules[module_name])
loaded_modules[module_name] = (ok and data) and data or NIL
elseif optional_require_modules[module_name] then
local ok, mod = pcall(require, optional_require_modules[module_name])
loaded_modules[module_name] = (ok and mod and type(mod) == "table") and mod or NIL
elseif modules[module_name] then
loaded_modules[module_name] = require(modules[module_name])
end
local m = loaded_modules[module_name]
return m == NIL and nil or m
end
local proxy_metatable = {
__index = function(proxy, module_name)
-- Lazy: single getter function or table of getters (nested lazy object)
if lazy_modules[module_name] then
local lazy_def = lazy_modules[module_name]
if type(lazy_def) == "function" then
local value = lazy_def()
rawset(proxy, module_name, value)
return value
end
if type(lazy_def) == "table" then
local sub = make_lazy_object_proxy(lazy_def)
rawset(proxy, module_name, sub)
return sub
end
end
local module = get_module(module_name)
if not module then return nil end
-- If the module itself is a function, cache and return it directly
if type(module) == "function" then
rawset(proxy, module_name, module)
return module
end
-- For loadData / loadDataOptional modules, return the table directly (no function proxying)
if data_modules[module_name] or optional_data_modules[module_name] then
rawset(proxy, module_name, module)
return module
end
-- requireOptional modules get the same function-level proxy as require
-- (optional_require_modules is not listed above so they fall through to the proxy)
-- Otherwise create a proxy that lazily caches individual functions
local function_proxy = setmetatable({}, {
__index = function(function_cache, function_name)
local cached_function = module[function_name]
rawset(function_cache, function_name, cached_function)
return cached_function
end
})
rawset(proxy, module_name, function_proxy)
return function_proxy
end
}
return setmetatable({}, proxy_metatable)
end
return export
iwyqw4o7jo9m9e6uy2lpdgswy00nml1
Modul:etymon/descendants
828
118629
342822
2026-05-16T13:46:24Z
Hakimi97
2668
Mengemas kini mengikut padanan Wikikamus bahasa Inggeris (semakan [[en:Special:Diff/90119833|90119833]])
342822
Scribunto
text/plain
local export = {}
local M = require("Module:module loader").init({
require = {
template_parser = "Module:template parser",
links = "Module:links",
languages = "Module:languages",
},
})
local TRACKABLE_KEYWORDS = {
inherited = true,
bor = true,
lbor = true,
slbor = true,
derived = true,
uder = true,
}
local DESCENDANT_TEMPLATES = {
["desc"] = true,
["descendant"] = true,
["desctree"] = true,
["descendants tree"] = true,
}
local SKIPPED_TERMS = {
[""] = true,
["-"] = true,
}
local CHECK_MISSING_HEADER = { missing_header = true, missing_entry = false }
local STRIPPED_REF_TEMPLATES = {
ref = true,
refn = true,
}
local ENABLE_DESCENDANTS_TRACKING = false
local function _resolve_explicit_cache_id(explicit_id, parent_etymon)
if parent_etymon then
return parent_etymon.id or "*"
end
return explicit_id
end
local function _resolve_single_cache_id(id_data)
return (type(id_data) == "table" and id_data.id) or id_data or "*"
end
local function _resolve_cache_id(lookup)
if not lookup then
return nil
end
if lookup.id then
return lookup.id
end
if lookup.explicit_id then
return _resolve_explicit_cache_id(lookup.explicit_id, lookup.parent_etymon)
end
if lookup.id_data ~= nil then
return _resolve_single_cache_id(lookup.id_data)
end
return nil
end
local function _get_cached_check(cache_table, is_toplevel, base_key, cache_id)
if not is_toplevel or not cache_table or not base_key or not cache_id then
return nil
end
return cache_table[base_key .. ":" .. cache_id]
end
local function _store_checks(cache_table, lang_page_key, checks_by_id, redirected_from)
if not cache_table or not lang_page_key or not checks_by_id then
return
end
for id, check in pairs(checks_by_id) do
cache_table[lang_page_key .. ":" .. id] = check
end
if redirected_from then
for id, check in pairs(checks_by_id) do
cache_table[redirected_from .. ":" .. id] = check
end
end
end
local function _strip_refs(value)
if type(value) ~= "string" or value == "" then
return value
end
local get_node_class = M.template_parser.class_else_type
local function trim_local(text)
return (type(text) == "string" and text:match("^%s*(.-)%s*$")) or ""
end
local function lower_local(text)
return text and mw.ustring.lower(text) or text
end
local function template_name_unexpanded(template_node)
return lower_local(trim_local(tostring(template_node[1]) or ""))
end
local function stringify_without_ref_templates(node)
if type(node) ~= "table" then
return node == nil and "" or tostring(node)
end
local node_class = get_node_class(node)
if node_class == "template" and STRIPPED_REF_TEMPLATES[template_name_unexpanded(node)] then
return ""
end
if node_class == "template" then
return tostring(node)
end
if node_class == "heading" then
return tostring(node)
end
if node_class and node_class ~= "wikitext" then
return tostring(node)
end
local pieces = {}
for i = 1, #node do
pieces[i] = stringify_without_ref_templates(node[i])
end
return table.concat(pieces)
end
local parsed = M.template_parser.parse(value)
local cleaned = stringify_without_ref_templates(parsed)
local changed = true
while changed do
local next_cleaned = mw.ustring.gsub(cleaned, "<%s*[Rr][Ee][Ff][^>]-/>", "")
next_cleaned = mw.ustring.gsub(next_cleaned, "<%s*[Rr][Ee][Ff][^>]*>.-<%s*/%s*[Rr][Ee][Ff]%s*>", "")
next_cleaned = mw.ustring.gsub(next_cleaned, "{{%s*[Rr][Ee][Ff]%s*}}", "")
next_cleaned = mw.ustring.gsub(next_cleaned, "{{%s*[Rr][Ee][Ff]%s*|[^{}]-}}", "")
changed = next_cleaned ~= cleaned
cleaned = next_cleaned
end
return cleaned
end
local function _trim(value)
if type(value) ~= "string" then
return nil
end
return value:match("^%s*(.-)%s*$")
end
local function _lower(value)
return value and mw.ustring.lower(value) or value
end
local function _is_index_in_range(index, start_index, end_index)
return index and index >= start_index and index <= end_index
end
local function _build_parse_index(lang_section)
local parsed = M.template_parser.parse(lang_section)
local headings = {}
local templates = {}
for heading in parsed:iterate_nodes("heading") do
local heading_name = heading:get_name()
table.insert(headings, {
index = heading.index or 1,
level = heading.level or 0,
name = heading_name and _lower(_trim(heading_name) or "") or nil,
length = #tostring(heading),
})
end
for template in parsed:iterate_nodes("template") do
local template_name = template:get_name()
local template_args = template:get_arguments()
table.insert(templates, {
index = template.index or 1,
name = template_name and _lower(_trim(template_name) or "") or "",
args = template_args or {},
})
end
return {
headings = headings,
templates = templates,
}
end
local function _build_regions(lang_section, headings)
local content_length = #lang_section
local etymology_headings = {}
for _, heading in ipairs(headings) do
if heading.level == 3 and heading.name and mw.ustring.match(heading.name, "^etymology") then
table.insert(etymology_headings, heading)
end
end
if #etymology_headings == 0 then
return {
{ start_index = 1, end_index = content_length }
}
end
local regions = {}
for i = 1, #etymology_headings do
local heading = etymology_headings[i]
local next_heading = etymology_headings[i + 1]
local start_index = heading.index
local end_index = next_heading and (next_heading.index - 1) or content_length
if i == 1 and heading.index > 1 then
local preamble = lang_section:sub(1, heading.index - 1)
if preamble:match("%S") then
start_index = 1
end
end
table.insert(regions, {
start_index = start_index,
end_index = end_index,
})
end
return regions
end
local function _count_region_etymons(region, templates, etymon_lang_code)
local count = 0
for _, template in ipairs(templates) do
if _is_index_in_range(template.index, region.start_index, region.end_index)
and template.name == "etymon"
and template.args[1] == etymon_lang_code then
count = count + 1
end
end
return count
end
local function _get_descendants_sections(region, headings)
local sections = {}
for i = 1, #headings do
local heading = headings[i]
if _is_index_in_range(heading.index, region.start_index, region.end_index)
and heading.name == "descendants" then
local body_start = heading.index + heading.length
local body_end = region.end_index
for j = i + 1, #headings do
local next_heading = headings[j]
if next_heading.index > region.end_index then
break
end
if next_heading.level <= heading.level then
body_end = next_heading.index - 1
break
end
end
table.insert(sections, {
start_index = body_start,
end_index = body_end,
})
end
end
return sections
end
local function _template_lang_matches_entry(template_lang_code, entry_full_code, cache)
local normalized_code = _trim(template_lang_code)
if not normalized_code or normalized_code == "" then
return false
end
local cached = cache[normalized_code]
if cached ~= nil then
return cached
end
local template_lang = M.languages.getByCode(normalized_code, nil, true)
local is_match = template_lang and template_lang:getFullCode() == entry_full_code or false
cache[normalized_code] = is_match
return is_match
end
local function _template_lists_target(template_args, first_term_index, target_page_lower, entry_lang)
for arg_index = first_term_index, #template_args do
local template_term = _trim(template_args[arg_index])
if template_term and not SKIPPED_TERMS[template_term] then
if _lower(template_term) == target_page_lower then
return true
end
local template_page = M.links.get_link_page(template_term, entry_lang)
if _lower(template_page) == target_page_lower then
return true
end
end
end
return false
end
local function _is_target_listed_in_descendants(descendants_sections, templates, entry_title, entry_lang)
if #descendants_sections == 0 then
return false
end
local target_page = M.links.get_link_page(entry_title, entry_lang)
local target_page_lower = _lower(target_page)
local entry_full_code = entry_lang:getFullCode()
local lang_match_cache = {}
for _, section in ipairs(descendants_sections) do
for _, template in ipairs(templates) do
if _is_index_in_range(template.index, section.start_index, section.end_index)
and DESCENDANT_TEMPLATES[template.name] then
local lang_arg_index = nil
for arg_index = 1, #template.args do
if _template_lang_matches_entry(template.args[arg_index], entry_full_code, lang_match_cache) then
lang_arg_index = arg_index
break
end
end
if lang_arg_index and _template_lists_target(template.args, lang_arg_index + 1, target_page_lower, entry_lang) then
return true
end
end
end
end
return false
end
local function _region_has_descendant_template(region, templates)
for _, template in ipairs(templates) do
if _is_index_in_range(template.index, region.start_index, region.end_index)
and DESCENDANT_TEMPLATES[template.name] then
return true
end
end
return false
end
local function _get_region_check(region, parsed_index, entry_title, entry_lang)
local descendants_sections = _get_descendants_sections(region, parsed_index.headings)
if #descendants_sections == 0 then
if _region_has_descendant_template(region, parsed_index.templates) then
local fallback_sections = { {
start_index = region.start_index,
end_index = region.end_index,
} }
return {
missing_header = false,
missing_entry = not _is_target_listed_in_descendants(fallback_sections, parsed_index.templates, entry_title, entry_lang),
}
end
return CHECK_MISSING_HEADER
end
return {
missing_header = false,
missing_entry = not _is_target_listed_in_descendants(descendants_sections, parsed_index.templates, entry_title, entry_lang),
}
end
local function _build_checks_by_id(lang_section, etymon_lang_code, found_templates_for_lang, entry_title, entry_lang)
local checks_by_id = {}
local parsed_index = _build_parse_index(lang_section)
local regions = _build_regions(lang_section, parsed_index.headings)
local template_list_index = 1
local mapping_failed = false
for _, region in ipairs(regions) do
local region_etymon_count = _count_region_etymons(region, parsed_index.templates, etymon_lang_code)
local region_check = nil
if region_etymon_count > 0 then
region_check = _get_region_check(region, parsed_index, entry_title, entry_lang)
end
for _ = 1, region_etymon_count do
local found_template_args = found_templates_for_lang[template_list_index]
if not found_template_args then
mapping_failed = true
break
end
checks_by_id[found_template_args.id or "*"] = region_check
template_list_index = template_list_index + 1
end
if mapping_failed then
break
end
end
if mapping_failed or template_list_index ~= (#found_templates_for_lang + 1) then
local global_region = { start_index = 1, end_index = #lang_section }
local fallback_check = _get_region_check(global_region, parsed_index, entry_title, entry_lang)
checks_by_id = {}
for _, template_args in ipairs(found_templates_for_lang) do
checks_by_id[template_args.id or "*"] = fallback_check
end
end
return checks_by_id
end
local function _compute_checks_for_page(opts)
opts = opts or {}
local found_templates_for_lang = opts.found_templates_for_lang or {}
if #found_templates_for_lang == 0 then
return {}
end
local entry_title = opts.entry_title
local entry_lang = opts.entry_lang
local etymon_lang_code = opts.etymon_lang_code
if not entry_title or not entry_lang or not etymon_lang_code then
return {}
end
local sanitized_lang_section = _strip_refs(opts.lang_section or "")
return _build_checks_by_id(
sanitized_lang_section,
etymon_lang_code,
found_templates_for_lang,
entry_title,
entry_lang
)
end
function export.cache_page_checks(opts)
if not ENABLE_DESCENDANTS_TRACKING then
return {}
end
opts = opts or {}
local cache_table = opts.cached_descendants_checks
local lang_page_key = opts.lang_page_key
if not cache_table or not lang_page_key then
return {}
end
local checks_by_id = _compute_checks_for_page(opts)
_store_checks(cache_table, lang_page_key, checks_by_id, opts.redirected_from)
return checks_by_id
end
function export.get_lookup_check(opts)
if not ENABLE_DESCENDANTS_TRACKING then
return nil
end
opts = opts or {}
return _get_cached_check(
opts.cached_descendants_checks,
opts.is_toplevel,
opts.base_key,
_resolve_cache_id(opts.lookup)
)
end
function export.get_term_sync_flags(keyword, term_status, descendants_check)
if not ENABLE_DESCENDANTS_TRACKING then
return false, false
end
local should_track = term_status == "ok"
and TRACKABLE_KEYWORDS[keyword] == true
and descendants_check ~= nil
if not should_track then
return false, false
end
return descendants_check.missing_header or false, descendants_check.missing_entry or false
end
return export
njoa75ldlbxvifpj024ykipfx44kfcd
Modul:etymon/data/text allowed
828
118630
342827
2026-05-16T13:55:21Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '--[=[ Languages and families that may use the {{etymon}} `text=` parameter (language-community consensus). ]=] return { -- Mode: "off" = disabled, "warn" = warn only, "error" = enforce. default_mode = "warn", langs = { ["bg"] = true, ["bnt-sab-pro"] = true, ["cs"] = true, ["en"] = true, ["es"] = true, ["fa"] = true, ["hrx"] = true, ["hsb"] = true, ["iir-pro"] = true, ["jbo"] = true, ["jdt"] = true, ["la"] = true, ["mul"] =...'
342827
Scribunto
text/plain
--[=[
Languages and families that may use the {{etymon}} `text=` parameter (language-community consensus).
]=]
return {
-- Mode: "off" = disabled, "warn" = warn only, "error" = enforce.
default_mode = "warn",
langs = {
["bg"] = true,
["bnt-sab-pro"] = true,
["cs"] = true,
["en"] = true,
["es"] = true,
["fa"] = true,
["hrx"] = true,
["hsb"] = true,
["iir-pro"] = true,
["jbo"] = true,
["jdt"] = true,
["la"] = true,
["mul"] = true,
["ota"] = true,
["ps"] = true,
["ro"] = true,
["sk"] = true,
["sw"] = true,
["tg"] = true,
["tl"] = true,
["tr"] = true,
["uk"] = true,
["uz"] = true,
["zlw-ocs"] = true,
["zlw-osk"] = true,
},
families = {
["ber"] = true, -- Berber
["dra"] = true, -- Dravidian
["inc"] = true, -- Indo-Aryan
["iir-nur"] = true, -- Nuristani
["roa-gap"] = true, -- Galician-Portuguese
["sem-ara"] = true, -- Aramaic
["sem-arb"] = true, -- Arabic
["tup"] = true, -- Tupian
["zlw-lch"] = true, -- Lechitic
},
}
tc1am4wig5hhrcueh4azua0q8gj7j8o
Kategori:Templat Tulisan Hiragana
14
118631
342834
2026-05-16T14:19:07Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '{{auto cat}}'
342834
wikitext
text/x-wiki
{{auto cat}}
eomzlm5v4j7ond1phrju7cnue91g5qx
Templat:Hira-categoryTOC/full
10
118632
342835
2026-05-16T14:20:15Z
Hakimi97
2668
Mencipta laman baru dengan kandungan '<small>{{#invoke:table of contents|full|lang=ja|あ|い|う|え|お|か|き|く|け|こ|さ|し|す|せ|そ|た|ち|つ|て|と|な|に|ぬ|ね|の|は|ひ|ふ|へ|ほ|ま|み|む|め|も|や|ゆ|よ|ら|り|る|れ|ろ|わ|ん}}</small><noinclude>{{tcat}}</noinclude>'
342835
wikitext
text/x-wiki
<small>{{#invoke:table of contents|full|lang=ja|あ|い|う|え|お|か|き|く|け|こ|さ|し|す|せ|そ|た|ち|つ|て|と|な|に|ぬ|ね|の|は|ひ|ふ|へ|ほ|ま|み|む|め|も|や|ゆ|よ|ら|り|る|れ|ろ|わ|ん}}</small><noinclude>{{tcat}}</noinclude>
iuh2ty7ezs4s2r60sncbiig1ordb61d
Wikikamus:Bahasa Isyarat Malaysia
4
118633
342838
2026-05-17T02:57:56Z
PeaceSeekers
3334
Mencipta laman baru dengan kandungan '{{gb|bahasa=Isyarat Malaysia|kod=zml|keterangan=Bahasa Isyarat Malaysia (BIM) ialah bahasa isyarat rasmi yang digunakan di Malaysia, berdasarkan Akta Orang Kurang Upaya 2008. Ia terbit sebagai keturunan kepada [[wt:Bahasa Isyarat Amerika|Bahasa Isyarat Amerika]].}}'
342838
wikitext
text/x-wiki
{{gb|bahasa=Isyarat Malaysia|kod=zml|keterangan=Bahasa Isyarat Malaysia (BIM) ialah bahasa isyarat rasmi yang digunakan di Malaysia, berdasarkan Akta Orang Kurang Upaya 2008. Ia terbit sebagai keturunan kepada [[wt:Bahasa Isyarat Amerika|Bahasa Isyarat Amerika]].}}
m8lt9bpj8chhtwm813dcmbswlqd3swg
342840
342838
2026-05-17T03:09:04Z
PeaceSeekers
3334
342840
wikitext
text/x-wiki
{{gb|bahasa=Isyarat Malaysia|kod=xml|keterangan=Bahasa Isyarat Malaysia (BIM) ialah bahasa isyarat rasmi yang digunakan di Malaysia, berdasarkan Akta Orang Kurang Upaya 2008. Ia terbit sebagai keturunan kepada [[wt:Bahasa Isyarat Amerika|Bahasa Isyarat Amerika]].}}
jmoo27qih07n0s4ocfua8h6errxs2uf
Wikikamus:xml
4
118634
342839
2026-05-17T03:02:02Z
PeaceSeekers
3334
Melencong ke [[Wikikamus:Bahasa Isyarat Malaysia]]
342839
wikitext
text/x-wiki
#LENCONG [[Wikikamus:Bahasa Isyarat Malaysia]]
bovh100onrsqw6scqsfwj1y34n0dtho
342841
342839
2026-05-17T03:10:19Z
PeaceSeekers
3334
PeaceSeekers telah memindahkan laman [[Wikikamus:zml]] ke [[Wikikamus:xml]] tanpa meninggalkan lencongan: Tajuk salah eja
342839
wikitext
text/x-wiki
#LENCONG [[Wikikamus:Bahasa Isyarat Malaysia]]
bovh100onrsqw6scqsfwj1y34n0dtho
Wikikamus:xml/tahun
4
118635
342842
2026-05-17T03:19:10Z
PeaceSeekers
3334
Mencipta laman baru dengan kandungan '=={{bahasa|{{safesubst:ROOTPAGENAME}}}}== [[Fail:BIM tahun.webm|thumb|"tahun" dalam Bahasa Isyarat Malaysia.]] ===Kata nama=== {{inti|{{safesubst:ROOTPAGENAME}}|kata nama}} #Kedua-dua tangan digenggam lalu satu genggamam mengelilingi satu lagi genggaman ibarat Bumi mengelilingi Matahari.'
342842
wikitext
text/x-wiki
=={{bahasa|xml}}==
[[Fail:BIM tahun.webm|thumb|"tahun" dalam Bahasa Isyarat Malaysia.]]
===Kata nama===
{{inti|xml|kata nama}}
#Kedua-dua tangan digenggam lalu satu genggamam mengelilingi satu lagi genggaman ibarat Bumi mengelilingi Matahari.
rw357gbxaz6v5it3i8x97ls31q3d5rq
Wikikamus:Bahasa Isyarat Jepun
4
118636
342843
2026-05-17T03:26:06Z
PeaceSeekers
3334
Mencipta laman baru dengan kandungan '{{gb|bahasa=Isyarat Jepun|kod=jsl|keterangan=Bahasa Isyarat Jepun (日本手話; ''Nihon Shuwa'') ialah bahasa isyarat utama Jepun.}}'
342843
wikitext
text/x-wiki
{{gb|bahasa=Isyarat Jepun|kod=jsl|keterangan=Bahasa Isyarat Jepun (日本手話; ''Nihon Shuwa'') ialah bahasa isyarat utama Jepun.}}
0waj1vbaj7t01k5flpiza5dboz3l7el
Wikikamus:jsl/gunung
4
118637
342844
2026-05-17T03:29:00Z
PeaceSeekers
3334
Mencipta laman baru dengan kandungan '=={{bahasa|{{safesubst:ROOTPAGENAME}}}}== [[Fail:JSLyama.gif|thumb|172x172px]] ===Kata nama=== {{inti|{{safesubst:ROOTPAGENAME}}|kata nama}} # Jari melakar rupa [[gunung]] secara kasar dengan melakar bentuk segi tiga.'
342844
wikitext
text/x-wiki
=={{bahasa|jsl}}==
[[Fail:JSLyama.gif|thumb|172x172px]]
===Kata nama===
{{inti|jsl|kata nama}}
# Jari melakar rupa [[gunung]] secara kasar dengan melakar bentuk segi tiga.
9v68a6bfv5pm2t7kekft670k2kacued
Wikikamus:jsl/aku
4
118638
342845
2026-05-17T03:48:24Z
PeaceSeekers
3334
Mencipta laman baru dengan kandungan '=={{bahasa|jsl}}== [[File:JSLwatshi06041102.gif|thumb|172x172px|"aku" dalam Bahasa Isyarat Jepun (1).]] [[File:JSLwatashi060411.gif|thumb|172x172px|"aku" dalam Bahasa Isyarat Jepun (2).]] ===Kata ganti nama=== {{inti|jsl|kata ganti nama}} # Jari menunjuk diri di kepala. # Jari menunjuk diri di kepala sambil menundukkan diri.'
342845
wikitext
text/x-wiki
=={{bahasa|jsl}}==
[[File:JSLwatshi06041102.gif|thumb|172x172px|"aku" dalam Bahasa Isyarat Jepun (1).]]
[[File:JSLwatashi060411.gif|thumb|172x172px|"aku" dalam Bahasa Isyarat Jepun (2).]]
===Kata ganti nama===
{{inti|jsl|kata ganti nama}}
# Jari menunjuk diri di kepala.
# Jari menunjuk diri di kepala sambil menundukkan diri.
nei3bzlf4w51dlh4uojlhgsmesfzko6
Kategori:ar:Forteana
14
118639
342851
2026-05-17T08:40:15Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:ar:Forteana]] ke [[Kategori:ar:Parapsikologi]]: Tajuk salah eja
342851
wikitext
text/x-wiki
#LENCONG [[:Kategori:ar:Parapsikologi]]
2hf60xel4nfz5zdogyw6rutjhrl57ot
Kategori:Forteana
14
118640
342854
2026-05-17T08:41:31Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:Forteana]] ke [[Kategori:Parapsikologi]]: Tajuk salah eja
342854
wikitext
text/x-wiki
#LENCONG [[:Kategori:Parapsikologi]]
igm1ygx6f11ra2gfd999wsx6n2m8ivk
Kategori:zh:Kelengkapan tandas
14
118641
342857
2026-05-17T08:42:39Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:zh:Kelengkapan tandas]] ke [[Kategori:zh:Kelengkapan dandanan diri]]: Tajuk salah eja
342857
wikitext
text/x-wiki
#LENCONG [[:Kategori:zh:Kelengkapan dandanan diri]]
bmp4zz5798f78hlxksczh0wgs5b0wvc
Kategori:my:Forteana
14
118642
342859
2026-05-17T08:43:58Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:my:Forteana]] ke [[Kategori:my:Paranormal]]: Tajuk salah eja
342859
wikitext
text/x-wiki
#LENCONG [[:Kategori:my:Paranormal]]
n8r642q339rg2hv83o6kgq5yafpjpow
Kategori:ar:Kelengkapan tandas
14
118643
342861
2026-05-17T08:47:37Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:ar:Kelengkapan tandas]] ke [[Kategori:ar:Kelengkapan dandanan diri]]: Tajuk salah eja
342861
wikitext
text/x-wiki
#LENCONG [[:Kategori:ar:Kelengkapan dandanan diri]]
61cwfqtnk3dpxc8t3z70r4j9izynzwh
Kategori:en:Kelengkapan tandas
14
118644
342865
2026-05-17T08:49:00Z
Hakimi97
2668
Hakimi97 telah memindahkan laman [[Kategori:en:Kelengkapan tandas]] ke [[Kategori:en:Kelengkapan dandanan diri]]: Tajuk salah eja
342865
wikitext
text/x-wiki
#LENCONG [[:Kategori:en:Kelengkapan dandanan diri]]
bpha8no9yle0v4h58xjjgup47ag0zw8