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