Wîkîferheng
kuwiktionary
https://ku.wiktionary.org/wiki/Destp%C3%AAk
MediaWiki 1.47.0-wmf.20
case-sensitive
Medya
Taybet
Gotûbêj
Bikarhêner
Gotûbêja bikarhêner
Wîkîferheng
Gotûbêja Wîkîferhengê
Wêne
Gotûbêja wêneyî
MediaWiki
Gotûbêja MediaWiki
Şablon
Gotûbêja şablonê
Alîkarî
Gotûbêja alîkariyê
Kategorî
Gotûbêja kategoriyê
Pêvek
Gotûbêja pêvekê
Nimînok
Gotûbêja nimînokê
Portal
Gotûbêja portalê
Tewandin
Gotûbêja tewandinê
Jinûvesazî
Gotûbêja jinûvesaziyê
TimedText
TimedText talk
Modul
Gotûbêja modulê
Event
Event talk
bira
0
3697
6531737
6393155
2026-09-15T20:35:03Z
Herald2039
15339
/* Bilêvkirin */
6531737
wikitext
text/x-wiki
{{h-bnr|bîra}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/ˈbɾɑ:/, /bɪˈɾɑ:/}}
* {{kîte|ku|bi|ra}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-bira.wav|Deng|dever=Bidlîs}}
=== Navdêr 1 ===
[[Wêne:Twin_boys.JPG|thumb|Du '''birayên''' [[cêwî]].]]
{{navdêr|ku|z=n}}
# {{f|ku|malbat}} [[kur|Kurê]] [[dêbab]]ên kesekê/î, [[law]]ê [[deybab]]ên kesekê/î.
#: {{mk|ku|Ew '''birayê''' min e.|[[dêbav|Dêbavên]] min û wî yek in.|rêz=1}}
#: {{mk|ku|'''Bira''' bira ye, bazar cuda ye}}
#* {{J:Serdar Roşan:Çîroka Hezar û Şevekê|jêgirtin=Şahriyarî bi tirs ji Şahzemanî re got: "Ax '''bira''', tu ewil vî karî bike." Lê '''birayê''' wî bersîv da: "Na, tu '''birayê''' mezin î, divê ez her di riya te de bim."}}
# [[kur|Kurê]] ji [[dayik]]a yan ji [[bab]]ê kesekê/î.
#* {{J:Serdar Roşan:Çîroka Hezar û Şevekê|jêgirtin=Ey mîrê cinan yê li welatê efrîtan, ev herdu kûçik '''birayên''' min in.}}
# {{f|ku|wek gotineke şîrîn ya [[gazî]]kirinê}} [[heval|Heval]], [[dost]], [[kek]].
#* {{J:Serdar Roşan:Çîroka Hezar û Şevekê|jêgirtin=Bi Xwedê, '''birawo''', her ez jî wisa fikirîm.}}
# {{f|ku|dîn}} [[hemû|Hemû]] [[kur]] û [[mêr]]ên ji [[eynî]] [[dîn]]î yan [[mezheb]]ê.
==== Ji wêjeya klasîk ====
* {{J:Mem û Zîn:1692|jêgirtin=Roja şerî ew hezare mêr bû<br>Tacdîn du '''bira''' hebûn di qellaş<br>Manendê du şahbazê cemmaş}}
==== Tewandin ====
{{ku-tewîn-nav|nêr}}
==== Bi alfabeyên din ====
* {{ku-ar|برا}}
* {{ku-kr|бьра}}
==== Formên alternatîv ====
{{kol3|ku
|bra
}}
==== Nêzîk ====
{{kol3|ku
|kak
|kek
|birader
|birak
}}
==== Jê ====
{{kol3|ku|cure=Jê
|birabab
|birabav
|birader
|birakuj
|birakujî
|biratî
|birayane
|birayetî
|biraza
|birazî
|destebira
|destebiratî
|zirbira
|zirbiratî
|birak<q:forma biçûkkirî>
|biracan<w:ı- peyva ji bo xweşgotina bira, ıı- peyva ji bo bang/gazî kirina kesekî>
|birader<w:kesê wek bira; kesê ji der hatiye û wek bira nêzik e>
|birako<q:ji bo denglêkirina kesekî>
|birakuj<w:kesê birayê xwe dikuje>
|birakujî<w:kiryara birakuştinê>
|birakuştî<w:kesê ku birayê wî hatiye kuştin>
|biramak<w:kesê ji heman dayikê lê ne ji heman bavî>
|biramarî<w:birayê ji zewaca dê yan jî bavê pêkhatî>
|biramarîtî<w:biratiya ji zewaca dê yan jî bavê pêkhatî>
|birang<q:forma biçûkkirî/şîrînkirî>
|biraşîr<w:kesê ku dema zarotiya xwe de ji diya kesekî şîr mêtî ye>
|biratî<w:1) hevpişka di dê û bavî de, 2) rewşa wek bira>
|biraxwê<w:xizmên mêr û ji dûr ve>
|birayetî<w:dostaniya wek bira>
|biraza<w:zarokên birayê kesekî>
|birazava<w:kesê di daweta wî de alîkariya zavayî dike>
|birazê<w:keça bira>
|birazî<w:kurê/lawê bira>
|destebirak<w:hevalê wek bira nêzîk>
|jinbira<w:jina birayê kesekî>
}}
==== Etîmolojî ====
{{etîmon|ku|:mîr|ine-pro:*bʰréh₂tēr|metn=++|dar=1|id=kurên ji eynî dêûbavan}}
Hevreha soranî/kelhûrî/lekî/hewramî/zazakî ''bira'', mazenderanî {{g|mzn|برار}} ''(birar)'', hexamenişî [[𐎲𐎼𐎠𐎫𐎠]] ''(brātā)'', pertî û pehlewî [[𐭡𐭥𐭠𐭣𐭥]] ''(brad, brader)'', avestayî [[𐬠𐬭𐬁𐬙𐬀𐬭]] ''(brater)'', belûçî {{g|bal|برات}} ''(berat)'', farisî {{g|fa|برادر}} ''(birader, berader)'', tacikî [[бародар]] ''(barodar)'', {{hv|sa|भ्रातृ}} ''(bhrā́tṛ)'', hindî [[भ्राता]] ''(bhrātā)'' û [[भ्रातृ]] ''(bhrātŕ)'', ûrdûyî {{g|ur|بھراتا}} ''(bhrātā)'' û {{g|ur|بھراتر}} ''(bhrātŕ)'', rusî/belarusî/ukraynî/bulgarî/makedonî [[брат]] ''(brat)'', sirbî [[бра̏т]] ''(brȁt)'', polonî/slovakî ''[[brat]]'', çekî ''[[bratr]]'', latviyayî ''[[brātarītis]]'', lîtwanî ''[[broterėlis]]/[[brólis]]/[[brotis]]'', {{hv|grc|φρᾱ́τηρ}} ''(phrā́tēr)'', îngilîzî ''[[brother]]'', almanî ''[[Bruder]]'', holendî ''[[broeder]]/[[broer]]'', danmarkî/norwecî/swêdî ''[[broder]]/[[bror]]'', latînî ''[[frāter]]'', îtalî ''[[frate]]/[[fratello]]'', fransî ''[[frère]]'', {{hv|hy|եղբայր}} ''(ełbayr)''...
;{{biçûk|Çavkanî:
* {{Horn|r=45}}
* {{Watkins|r=12}}
* {{MacKenzie|r=118}}
* {{Etymonline|Etymonline}}
* {{Pokorny|r=163}}
}}
==== Bide ber ====
{{kol3|ku
|xwişk
}}
==== Bi zaravayên din ====
* {{Z|ckb}}: bira
* {{Z|sdh}}: bira
*: Şêxbizinî: bira, [[birang]]
* {{Z|lki}}: bira
* {{Z|hac}}: bira
* {{Z|zza}}: bira, {{W-|zza|birar}}
==== Werger ====
{{werger-ser}}
* {{Z|aiw}}: {{W-|aiw|isimanna}}
* {{Z|ab}}: {{W-|ab|аиашьа}}
* {{Z|ace}}: {{W-|ace|aduen}}
* {{Z|ady}}: {{W-|ady|шы}}
* {{Z|aa}}: {{W-|aa|saqal}}
* {{Z|af}}: {{W+|af|broer}}, {{W-|af|boetie}}
* {{Z|xag}}: {{W-|xag|𐕛𐔼𐕖𐔼}}
* {{Z|akk}}: {{W-|akk|𒋀|tr=aḫu}}
* {{Z|sq}}: {{W+|sq|vëlla}}
* {{Z|pdc}}: {{W-|pdc|Bruder|n}}
* {{Z|de}}: {{W+|de|Bruder|n}}
*: {{Z|gsw}}: {{W-|gsw|Brüeder|n}}
* {{Z|am}}: {{W+|am|ወንድም}}
* {{Z|ani}}: {{W-|ani|воцци}}
* {{Z|agg}}: {{W-|agg|ranihı}}
* Apaçî:
*: {{Z|apw}}: {{W-|apw|bikʼisn}}
* {{Z|an}}: {{W+|an|chirmán|n}}
* {{Z|arc}}:
*: {{Z|aii}}: {{W-|aii|ܐܲܚܵܐ|n|tr=āḳa}}, {{W-|aii|ܐܲܚܘܿܢܵܐ|n|tr=āḳona}}
*: {{Z|syc}}: {{W-|syc|ܐܚܐ|n|tr=ʾăḥā}}
*: Aramiya cihû: {{W-|arc|אַחָא|n|tr=ʾaḥā}}
* {{Z|aqc}}: {{W-|aqc|у́шду}}
* {{Z|agj}}: {{W-|agj|እህ|tr=ǝh}}
* {{Z|rup}}: {{W+|rup|frati|n}}, {{W+|rup|frate|n}}
* {{Z|as}}: {{W-|as|ভাই}}
* {{Z|ast}}: {{W+|ast|hermanu|n}}
* {{Z|av}}: {{W-|av|вац}}
* {{Z|ay}}: {{W-|ay|jilata}}, {{W+|ay|jila}}
* {{Z|ain}}: {{W-|ain|ユポ|tr=yupo}}
* {{Z|az}}: {{W+|az|qardaş}}
* {{Z|ban}}: {{W-|ban|nyama muani}}
* {{Z|eu}}: {{W+|eu|anaia}}, {{W+|eu|neba}}
* {{Z|ba}}: {{W-|ba|ағай}} ; {{W-|ba|ҡусты}}, {{W-|ba|эне}}, {{W-|ba|мырҙа}}
* {{Z|bar}}: {{W-|bar|bruada}}
* {{Z|be}}: {{W-|be|брат|n}}
* {{Z|bal}}: {{W-|bal|برات|tr=barát}}
* {{Z|bn}}: {{W+|bn|ভাই}}, {{W-|bn|ভ্রাতা}}, {{W+|bn|বেরাদর}}
* {{Z|bqi}}: {{W-|bqi|گگه|tr=gage}}
* {{Z|bi}}: {{W-|bi|brata}}
* {{Z|br}}: {{W+|br|breur|n}}, {{W+|br|breudeur|p}}
* {{Z|bg}}: {{W+|bg|брат|n}}
* {{Z|my}}: {{W-|my|အကို}}, {{W+|my|ညီ}}, {{W+|my|မောင်}}
* {{Z|bua}}: {{W-|bua|аха}}, {{W-|bua|дуу}}
* {{Z|dz}}: {{W-|dz|བུ་སྤུན}}, {{W-|dz|ཕོ་རྒནམ}}, {{W-|dz|ནུ་གཅུང}}
* {{Z|ce}}: {{W-|ce|ваша}}
* {{Z|cs}}: {{W+|cs|bratr|n}}
* {{Z|chr}}: {{W-|chr|ᎤᏙ}}, {{W-|chr|ᏗᎾᏓᏅᏟ}}
* {{Z|chn}}: {{W-|chn|ow}}, {{W-|chn|kahpo}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|哥哥|tr=go1-4 go1}}, {{W-|yue|弟弟|tr=dai6-4 dai6-2}}, {{W-|yue|兄弟|tr=hing1 dai6}}, {{W-|yue|大佬|tr=daai6 lou2}}, {{W-|yue|細佬|tr=sai3 lou2}}, {{W-|yue|阿哥|tr=aa3 go1}}
*: {{Z|cmn}}: {{W+|cmn|哥哥|tr=gēge}}, {{W+|cmn|弟弟|tr=dìdi}}, {{W+|cmn|兄弟|tr=xiōngdì}}, {{W+|cmn|兄弟|tr=xiōngdi}}
*: {{Z|nan}}: {{W+|nan|阿兄|tr=[[a-hiaⁿ]]}}, {{W+|nan|小弟|tr=[[sió-tī]]}}, {{W+|nan|兄弟|tr=hiaⁿ-tī}}, {{W+|nan|哥哥|tr=ko--koh, ko-ko}}, {{W+|nan|兄哥|tr=hiaⁿ-ko}}, {{W+|nan|大兄|tr=tōa-hiaⁿ}}, {{W-|nan|大哥|tr=tōa-ko}}
*:: Teochew: {{W-|nan-tws|阿兄|tr=a1 hian1}}, {{W-|nan-tws|阿弟|tr=a1 di6}}
* {{Z|cv}}: {{W-|cv|ар тӑван}}
* {{Z|dlm}}: {{W-|dlm|frutro|n}}
* {{Z|da}}: {{W+|da|broder|g}}, {{W+|da|bror|g}}
* {{Z|dv}}: {{W-|dv|ކޮއްކޮ}}, {{W-|dv|ބޭބެ}}
* {{Z|dlg}}: {{W-|dlg|убай}}, {{W-|dlg|бий}}
* {{Z|ovd}}: {{W-|ovd|bruor|n}}
* {{Z|id}}: {{W+|id|saudara|nt}}, {{W-|id|[[kakak laki-laki]]|n}} {{W+|id|abang|n}}, {{W-|id|[[adik laki-laki]]|n}}
* {{Z|ar}}: {{W+|ar|أَخ|n}}, {{W+|ar|شَقِيق|n}}
*: {{Z|arz}}: {{W-|arz|أخ|n|tr=ʔaḵḵ}} (plural: {{W-|arz|إخوات|tr=ʔixwāt}})
*: {{Z|apc}}: {{W-|apc|أخ|n|tr=ʔaḵḵ}}, {{W-|apc|خي|n|tr=ḵayy}} ()
*: {{Z|ajp}}: {{W-|ajp|أخو|n|tr=ʔaḵu}}, {{W-|ajp|أخ|n|tr=ʔaḵḵ}}
* {{Z|hy}}: {{W+|hy|եղբայր}}, {{W+|hy|ախպեր}}
*: {{Z|xcl}}: {{W-|xcl|եղբայր}}
* {{Z|myv}}: {{W-|myv|леля}}, {{W-|myv|ялакс}}
* {{Z|eo}}: {{W+|eo|frato}}
* {{Z|et}}: {{W+|et|vend}}, {{W+|et|veli}}
* {{Z|gez}}: {{W-|gez|እኅው}}
* {{Z|eve}}: {{W-|eve|акан}}, {{W-|eve|нө}}
* {{Z|evn}}: {{W-|evn|акин}}, {{W-|evn|нэкун}}
* {{Z|ee}}: {{W-|ee|nɔviŋutsu}}, {{W-|ee|fo}}, {{W-|ee|tsɛ}}
* {{Z|fa}}: {{W+|fa|برادر|tr=barâdar}}, {{W+|fa|داداش|tr=dâdâš}}, {{W+|fa|اخ|tr=ax}}
* {{Z|ddg}}: {{W-|ddg|kaka}}, {{W-|ddg|noko}}
* {{Z|phn}}: {{W-|phn|𐤀𐤇}}
* {{Z|fo}}: {{W+|fo|bróðir|n}}, {{W+|fo|beiggi|n}}
* {{Z|fi}}: {{W+|fi|veli}}
* {{Z|fr}}: {{W+|fr|frère|n}}
* {{Z|fur}}: {{W-|fur|fradi|n}}
* {{Z|frr}}: {{W-|frr|brouder|n}}, {{W-|frr|bruler|n}}, {{W-|frr|Bröđer|g}}
* {{Z|fy}}: {{W+|fy|broer}}
* {{Z|gd}}: {{W+|gd|bràthair|n}}
* {{Z|gl}}: {{W+|gl|irmán|n}}
* {{Z|gni}}: {{W-|gni|marna}}
* {{Z|gu}}: {{W+|gu|ભાઈ|n}}
* {{Z|ka}}: {{W-|ka|ძმა}}
* {{Z|ht}}: {{W-|ht|frè}}
* {{Z|haw}}: {{W-|haw|kaikuaʻana}}
* {{Z|ha}}: {{W-|ha|ɗanuwa}}
* {{Z|mba}}: {{W-|mba|sulud}} {{W-|mba|ma-ama}}
* {{Z|hi}}: {{W+|hi|भाई|n}}, {{W+|hi|भ्राता|n}}, {{W+|hi|सहोदर|n}}, {{W+|hi|बिरादर|n}}, {{W+|hi|दादा|n}}, {{W+|hi|भाऊ|n}}, {{W+|hi|वीर|n}}, {{W+|hi|बंधु|n}}, {{W+|hi|भैया|n}}, {{W-|hi|भ्रातृ|n}}
* {{Z|mww}}: {{W-|mww|kwv}}, {{W-|mww|tij}}, {{W-|mww|kwv tij}}, {{W-|mww|tij laug}}, {{W-|mww|nus}}
* {{Z|nl}}: {{W+|nl|broer|n}}, {{W+|nl|broeder|n}}
* {{Z|he}}: {{W+|he|אָח|n|tr=akh}}
* {{Z|io}}: {{W+|io|fratulo}}
* {{Z|ig}}: {{W-|ig|nwanne nwoke}}
* {{Z|ilo}}: {{W-|ilo|kabsat a lalaki}}, {{W-|ilo|kabagis}}, {{W-|ilo|manong}}, {{W-|ilo|ading a lalaki}}
* {{Z|ang}}: {{W-|ang|broþor|cuda=brōþor|n}}
* {{Z|en}}: {{W+|en|brother}}
* {{Z|izh}}: {{W-|izh|veljä}}, {{W-|izh|veli}}, {{W-|izh|vello}}, {{W-|izh|velvyt}}, {{W-|izh|veikko}}
* {{Z|inh}}: {{W-|inh|воша}}
* {{Z|ia}}: {{W+|ia|fratre}}
* {{Z|ga}}: {{W-|ga|deartháir|n}}
* {{Z|ist}}: {{W-|ist|fra|n}}
* {{Z|ruo}}: {{W-|ruo|fråte|n}}
* {{Z|it}}: {{W+|it|fratello|n}}
* {{Z|is}}: {{W+|is|bróðir}}, {{W-|is|brói|n}}
* {{Z|ja}}: {{W+|ja|兄弟|tr=きょうだい, kyōdai}}, {{W+|ja|あに|cuda=兄|tr=あに, ani}}, {{W+|ja|お兄さん|tr=おにいさん, oniisan}}, {{W-|ja|兄ちゃん|tr=にいちゃん, nīchan}}, {{W+|ja|おとうと|cuda=弟|tr=おとうと, otōto}}, {{W-|ja|ブラザー|tr=burazā}}
* {{Z|kaw}}: {{W-|kaw|ari}}
* {{Z|jv}}: {{W-|jv|kangmas|n}}, {{W-|jv|dimas|n}}
* {{Z|kl}}: {{W-|kl|qatanngut angut|n}}, {{W-|kl|angaju}}, {{W-|kl|nuka}}, {{W-|kl|ani}}, {{W-|kl|aqqaluk}}
* {{Z|xal}}: {{W-|xal|дү}}
* {{Z|kea}}: {{W-|kea|irmon}}, {{W-|kea|armun}}
* {{Z|krc}}: {{W-|krc|къарнаш|tr=qarnaş}}, {{W-|krc|къарындаш|tr=qarındaş}}
* {{Z|krl}}: {{W-|krl|velli}}
* {{Z|csb}}: {{W-|csb|brat|n}}
* {{Z|qu}}: {{W-|qu|turi}}, {{W-|qu|wawqi}}
* {{Z|ks}}: {{W+|ks|بوے}}
* {{Z|ky}}: {{W+|ky|байке}}, {{W+|ky|ага}}, {{W+|ky|ини}}
* {{Z|si}}: {{W-|si|අය්යා}}, {{W-|si|මල්ලි}}
* {{Z|com}}: {{W-|com|tami}}, {{W-|com|pabi}}
* {{Z|koi}}: {{W-|koi|вон}}
* {{Z|ko}}: {{W+|ko|형제}}, {{W+|ko|형님}}, {{W+|ko|오빠}}, {{W+|ko|남동생}}, {{W+|ko|동생}}, {{W+|ko|아우}}, {{W-|ko|브라더}}, {{W-|ko|오라버님}}
* {{Z|co}}: {{W+|co|fratellu|n}}, {{W+|co|frateddu|n}}
* {{Z|jam}}: {{W-|jam|broda}}, {{W-|jam|breda}}
* {{Z|mfe}}: {{W-|mfe|frer}}
* {{Z|crs}}: {{W-|crs|frer}}
* {{Z|kum}}: {{W-|kum|къардаш}}, {{W-|kum|эр къардаш}}
* {{Z|lad}}: {{W-|lad|ermano}}
* {{Z|lbe}}: {{W-|lbe|уссу}}
* {{Z|ltg}}: {{W-|ltg|bruoļs}}
* {{Z|la}}: {{W+|la|frater|n}}, {{W+|la|germanus}}
* {{Z|lv}}: {{W+|lv|brālis|n}}
* {{Z|lo}}: {{W-|lo|ອ້າຍ}}, {{W-|lo|ນ້ອງຊາຍ}}
* {{Z|lij}}: {{W-|lij|fræ}}
* {{Z|lt}}: {{W+|lt|brolis|n}}
* {{Z|liv}}: {{W-|liv|veļ}}
* {{Z|lmo}}: {{W-|lmo|fradell}}
* {{Z|lg}}: {{W-|lg|ow'oluganda}}
* {{Z|luy}}: {{W-|luy|wandaywe©}}
* {{Z|lb}}: {{W+|lb|Brudder|n}}
* {{Z|mhn}}: {{W-|mhn|pruader|n}}
* {{Z|mdh}}: {{W-|mdh|pagali}}, {{W-|mdh|lusud}} {{W-|mdh|sa}} {{W-|mdh|tian}}
* {{Z|mkz}}: {{W-|mkz|kaka}}, {{W-|mkz|noko}}
* {{Z|mk}}: {{W+|mk|брат|n}}
* {{Z|mg}}: {{W+|mg|anadahy}}
* Malay:
*: Rûmî: {{W+|ms|abang}}, {{W-|ms|adik lelaki}}
*: {{Z|ms}}: {{W+|ms|ابڠ}}, {{W-|ms|اديق للاکي}}
* {{Z|ml}}: {{W+|ml|സഹോദരന്}}, {{W+|ml|ചേട്ടൻ}}, {{W+|ml|അനിയന്}}
* {{Z|mt}}: {{W+|mt|ħu}}
* {{Z|mnc}}: {{W-|mnc|ᠠᡥᡡᠨ}}, {{W-|mnc|ᡩᡝᠣ}}
* {{Z|gv}}: {{W-|gv|braar|n}}
* {{Z|mi}}: {{W-|mi|tuakana}}, {{W-|mi|teina}}, {{W-|mi|tungāne}}, {{W-|mi|taina}}, {{W-|mi|tuakana}}, {{W-|mi|hāmua}}
* {{Z|mrw}}: {{W-|mrw|pagari}}
* {{Z|mr}}: {{W-|mr|भाऊ|n}}
* Marî:
*: {{Z|mhr}}: {{W-|mhr|иза}}, {{W-|mhr|шольо}}
* {{Z|yua}}: {{W-|yua|iitsʼin}}, {{W-|yua|sukuʼun}}
* {{Z|mzn}}: {{W-|mzn|برار|tr=berâr}}
* {{Z|hu}}: {{W+|hu|fivér}}, {{W+|hu|fiútestvér}}, {{W+|hu|bátya}}, {{W+|hu|öcs}}
* {{Z|egy}}: {{W-egy|sn|h=sn-n:A1}}
* {{Z|mic}}: {{W-|mic|nidap}}
* {{Z|min}}: {{W-|min|uda}}, {{W-|min|udo}}, {{W-|min|ajo}}, {{W-|min|ombak}}, {{W-|min|uwan}}, {{W-|min|uwen}}, {{W-|min|adiak}}
* {{Z|mwl}}: {{W-|mwl|armano|n}}
* {{Z|mdf}}: {{W-|mdf|альняка}}, {{W-|mdf|пяльне}}
* {{Z|mn}}: {{W+|mn|ах}}, {{W+|mn|дүү}}, {{W+|mn|ах дүү}}
* {{Z|naq}}: {{W-|naq|ǃgâsab}}
* {{Z|gld}}: {{W-|gld|ага}}, {{W-|gld|нэил}}
* {{Z|nap}}: {{W-|nap|frate|n}}
* {{Z|nv}}: {{W-|nv|atsilí}}, {{W-|nv|ánaaí}}
* {{Z|nds}}: {{W+|nds|Broder|n}}, {{W-|nds|Brauder|n}}, {{W-|nds|Broer|n}}, {{W-|nds|Bruer|n}}
* {{Z|yrk}}: {{W-|yrk|ня}}
* {{Z|ne}}: {{W+|ne|दाजु}}
* {{Z|niv}}: {{W-|niv|асӄ}}, {{W-|niv|асӄа}}, {{W-|niv|ыкын}}, {{W-|niv|ыкына}}, {{W-|niv|атик}}
* {{Z|non}}: {{W-|non|bróðir|n}}, {{W-|non|barmi|n}}
* {{Z|no}}: {{W+|no|bror|n}}
* {{Z|nup}}: {{W-|nup|yégi bagi}}
* {{Z|cst}}: {{W-|cst|ták̄a}}, {{W-|cst|táusíkís}}, {{W-|cst|ká̄nak tausík̄is}}
* {{Z|oj}}: {{W-|oj|nishiime}}, {{W-|oj|nisayenh}}, {{W-|oj|niijikiwenh}}
* {{Z|ryu}}: {{W-|ryu|ゐきー|tr=wikī}}
* {{Z|pro}}: {{W-|pro|frayre}}
* {{Z|oc}}: {{W+|oc|fraire|n}}
* {{Z|or}}: {{W+|or|ଭାଇ}}
* {{Z|om}}: {{W-|om|obboleessa}}
, {{W-|os|ӕфсымӕр}}, {{W-|os|ӕрвад}}
* {{Z|ug}}: {{W+|ug|ئاكا}}, {{W-|ug|ئىنى}}
* {{Z|uz}}: {{W+|uz|aka}}, {{W+|uz|uka}}
* {{Z|pi}}: {{W-|pi|bhātar|n}}
* {{Z|ps}}: {{W+|ps|ورور|n|tr=wror}}
* {{Z|pms}}: {{W-|pms|frèl|n}}, {{W-|pms|fradel|n}}
* {{Z|pdt}}: {{W-|pdt|Brooda|n}}
* {{Z|pox}}: {{W-|pox|brot|n}}
* {{Z|pl}}: {{W+|pl|brat|m-pr}}
* {{Z|pt}}: {{W+|pt|irmão|n}}
* {{Z|prg}}: {{W-|prg|brāti|n}}
* {{Z|pa}}: {{W-|pa|ਵੀਰ}}
* {{Z|kek}}: {{W-|kek|as}}
* {{Z|kk}}: {{W+|kk|аға}}, {{W+|kk|іні}}
* {{Z|cop}}: {{W-|cop|ⲥⲟⲛ}}
* {{Z|rhg}}: {{W-|rhg|bai}}
* {{Z|rgn}}: {{W-|rgn|fradël|n}}
* {{Z|rm}}: {{W-|rm|frar|n}}
* {{Z|rom}}: {{W-|rom|phral|n}}
*: {{Z|rmf}}: {{W-|rmf|phaal|n}}
* {{Z|ro}}: {{W+|ro|frate|n}}
* {{Z|ru}}: {{W+|ru|брат|n}}, {{W+|ru|бра́тец|n}}, {{W+|ru|брати́шка|n}}, {{W+|ru|брата́н|n}}
* {{Z|rue}}: {{W-|rue|брат|n}}
* {{Z|ssy}}: {{W-|ssy|sacal}}
* {{Z|osx}}: {{W-|osx|brōthar|n}}
* {{Z|se}}: {{W-|se|viellja}}
* {{Z|sm}}: {{W-|sm|uso}}
* {{Z|sa}}: {{W+|sa|भ्रातृ|n}}
* {{Z|sc}}: {{W-|sc|fràde|n}}
* {{Z|sei}}: {{W-|sei|aacaz}}
* {{Z|scn}}: {{W+|scn|frati|n}}
* {{Z|sid}}: {{W-|sid|rodo}}
* {{Z|szl}}: {{W-|szl|brat|n}}
* {{Z|sd}}: {{W-|sd|ڀاء|n|cuda=ڀاءُ}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|бра̏т|n}}
*: Latînî: {{W+|sh|brȁt|n}}
* {{Z|sco}}: {{W-|sco|brither}}, {{W-|sco|brar}}
* {{Z|cu}}:
*: Kirîlî: {{W-|cu|братръ|n}}
*: Glagolîtîk: {{W-|cu|ⰱⱃⰰⱅⱃⱏ|n}}
* {{Z|orv}}: {{W-|orv|братъ|n}}
* {{Z|sk}}: {{W+|sk|brat|n}}
* {{Z|sl}}: {{W+|sl|brat|n}}
* {{Z|zlw-slv}}: {{W-|zlw-slv|brãt}}
* {{Z|so}}: {{W-|so|walaal}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|bratš|n}}
*: {{Z|hsb}}: {{W+|hsb|bratr|n}}
* {{Z|es}}: {{W+|es|hermano|n}}
* {{Z|su}}: {{W+|su|akang}}, {{W+|su|aa}}, {{W-|su|raka|n}}, {{W-|su|rayi}}, {{W-|su|dédé|n}}
* {{Z|sux}}: {{W-|sux|𒋀|tr=ŠEŠ}}
* {{Z|sva}}: {{W-|sva|მუხვბე}}, {{W-|sva|ჯჷმილ}}
* {{Z|sw}}: {{W+|sw|ndugu}}, {{W+|sw|kaka}}
* {{Z|sv}}: {{W+|sv|bror|g}}, {{W+|sv|broder|g}}, {{W+|sv|brorsa|g}}
* {{Z|tg}}: {{W+|tg|бародар}}, {{W+|tg|ах}}
* {{Z|tl}}: {{W-|tl|kapatid na lalaki}}, {{W+|tl|kuya}}, {{W-|tl|totoy}}, {{W-|tl|diko}}, {{W-|tl|sangko}}, {{W-|tl|siko}}
* {{Z|ta}}: {{W+|ta|சகோதரன்}}, {{W+|ta|அண்ணன்}}, {{W+|ta|தம்பி}}
* {{Z|twf}}: {{W-|twf|p’ǫ́yna}} (younger), {{W-|twf|pòpóna}} (elder)
* {{Z|rif}}: {{W-|rif|uma|n}}
* {{Z|tsg}}: {{W-|tsg|magolang}}, {{W-|tsg|manghūd}}
* {{Z|th}}: {{W+|th|พี่}}, {{W+|th|พี่ชาย}}, {{W+|th|น้อง}}, {{W+|th|น้องชาย}}, {{W+|th|ภราดร}}, {{W+|th|ภราดา}}
* {{Z|te}}: {{W-|te|ಅಣ್ಣ}}, {{W-|te|ತಮ್ಮ}}, {{W+|kn|ಸಹೋದರ}}
* {{Z|te}}: {{W+|te|అన్న}}, {{W+|te|తమ్ముడు}}, {{W+|te|సోదరుడు}}
* {{Z|crh}}: {{W-|crh|ağa}} }}, {{W-|crh|qardaş}}, {{W-|crh|kadâ}} }}
* {{Z|tt}}: {{W+|tt|абый}}, {{W+|tt|абзый}}, {{W+|tt|эне}}
* {{Z|tet}}: {{W-|tet|maun}}
* {{Z|ti}}: {{W-|ti|ሓወ}}
* {{Z|tr}}: {{W+|tr|erkek kardeş}}, {{W+|tr|kardeş}}, {{W+|tr|birader}}, {{W+|tr|ağabey}}, {{W+|tr|abi}}
* {{Z|tk}}: {{W-|tk|dogan}}
* {{Z|bo}}: {{W-|bo|ཇོ་ཇོ}}, {{W-|bo|ཅོ་ཅོག}}, {{W-|bo|ཇོ་ལགས}}, {{W-|bo|འོག་མ}}, {{W-|bo|ནུ་བོ}}, {{W-|bo|ཨོ་ལགས}}
* {{Z|to}}: {{W-|to|tokoua}}, {{W-|to|tuonga'ane}}
* {{Z|xto}}: {{W-|xto|pracar}}
* {{Z|txb}}: {{W-|txb|procer}}
* {{Z|tru}}: {{W-|tru|ܐܰܚܘܢܳܐ}}
* {{Z|tyv}}: {{W-|tyv|акы}}, {{W-|tyv|дуңма}}
* {{Z|udi}}: {{W-|udi|вичи}}
* {{Z|udm}}: {{W-|udm|брат}}, {{W-|udm|агай}}, {{W-|udm|нюня}}, {{W-|udm|вын}}
* {{Z|uga}}: {{W-|uga|𐎀𐎃}}
* {{Z|ur}}: {{W+|ur|بھائی|n|tr=bhāī}}, {{W-|ur|بھراتا|n|tr=bhrātā}}
* {{Z|uk}}: {{W+|uk|брат|n}}
* {{Z|vec}}: {{W-|vec|fradèlo}}, {{W-|vec|fradelo|n}}, {{W-|vec|fradel|n}}, {{W-|vec|german|n}}
* {{Z|vep}}: {{W-|vep|veik}}
* {{Z|vi}}: {{W+|vi|anh}} or {{W+|vi|anh trai}}, {{W+|vi|em trai}}, {{W+|vi|em}}, {{W+|vi|anh em}}
* {{Z|vo}}: {{W+|vo|blod}}, {{W+|vo|higem}}
* {{Z|vro}}: {{W-|vro|veli}}
* {{Z|vot}}: {{W-|vot|velli}}
* {{Z|cy}}: {{W+|cy|brawd}}, {{W+|cy|brodyr|n-p}}
* {{Z|wo}}: {{W-|wo|mag ju góor}}, {{W-|wo|ràkk ju góor}}, {{W-|wo|càmmiñ}}
* {{Z|km}}: {{W-|km|បងប្រុស}}, {{W-|km|ប្អូនប្រុស}}, {{W-|km|ភាតរ}}, {{W+|km|ភាតា}}
* {{Z|xh}}: {{W-|xh|ubhuti}}, {{W-|xh|umntase}}, {{W-|xh|umnakwe}}
* {{Z|yai}}: {{W-|yai|буродар}}, {{W-|yai|вирот}}
* {{Z|sah}}: {{W-|sah|убай}}, {{W-|sah|бий}}, {{W-|sah|быраат}}, {{W-|sah|ини}}
* {{Z|yag}}: {{W-|yag|waym}}
* {{Z|yi}}: {{W-|yi|ברודער|n}}
* {{Z|yo}}: {{W-|yo|arakunrin}}
* {{Z|esu}}: {{W-|esu|anngaq}}
* {{Z|el}}: {{W+|el|αδελφός|n}}, {{W+|el|αδερφός|n}}
*: {{Z|grc}}: {{W-|grc|ἀδελφός|n}}, {{W-|grc|κάσις|n}}
* {{Z|za}}: {{W-|za|beix}}, {{W-|za|nuengx}}, {{W-|za|beixneungx}}
* {{Z|zu}}: {{W+|zu|ubhuti}}
{{werger-bin}}
:{{werger çavkanî|en|brother|oldid=75626421|beş=Ev qismê Wergerê}}
=== Navdêr 2 ===
{{navdêr|ku|z=}}
# {{mj|bira, bi rastî, rasteqîn}} ''Binêrin'': '''[[birra]]''' û '''[[berra]]'''
=== Hoker ===
{{hoker|ku}}
# {{f|ku|Sirûc|Anatolia/Axsere|Wêranşar|Bismil|Riha|Deşta Mêrdînê|Îdir|Agirî|Wan/Bazîdaxa|Qers/Qaxizman|Mûş|Semsûr/Kolik|Farqîn|Sêwreg|Meletî|Çewlik/Gêxî|Êlih}} {{guharto|ku|bila|w=dibe ku; divê ku}}
=== Formeke navdêrê ===
{{ku-tew-nav|bir|mê|îzafe|binavkirî}}
{{navdêr|ku|form=erê}}
# ''Rewşa [[îzafeyî]] ya yekjimar a binavkirî ya'' '''{{g|ku|bir}}''' ([[birr]]).
==== Anagram ====
* {{anagram|ku|a=abir|biar|riba}}
== {{ziman|zza}} ==
=== Navdêr ===
{{navdêr|zza|z=n}}
# {{f|zza|malbat}} [[bira#Kurmancî|bira]], <br>([[kur]]ê ji eynî [[dêbab]]an)
== {{ziman|ckb}} ==
=== Navdêr ===
{{navdêr|ckb|sc=Latn|z=}}
# {{f|ckb|malbat}} [[bira#Kurmancî|bira]], <br>([[kur]]ê ji eynî [[dêbab]]an)
== {{ziman|juc}} ==
=== Navdêr ===
{{navdêr|juc}}
# {{f|juc|erdnîgarî}} {{g|ku|çem}}, {{g|ku|ro}}, {{g|ku|robar}}
=== Çavkanî ===
{{rûpela wergerê|juc|çem}}
== {{ziman|tr}} ==
=== Bilêvkirin ===
* {{deng|tr|LL-Q256 (tur)-ToprakM-bira.wav|Deng|dever=}}
=== Navdêr ===
{{navdêr|tr}}
# [[bîre]], [[bîra]], [[avceh]]
==== Anagram ====
* {{anagram|tr|a=abir|bari|ibra}}
ll92ddouccg4g1ij8831orfhlj3fu9z
şemirandin
0
4461
6531783
6428037
2026-09-16T07:14:10Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531783
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Lêker ===
{{lêker|ku|c=}}
# [[têk vedan]], [[tevlihev]] kirin
# [[bi rê ve birin]]
#: {{nm|ku|Min xwendina xwe '''şemirand''' daku karekî bikim û malbata xwe pê xwedî bikim.}}
#: {{nm|ku|Herdu welatan hemî têkiliyên xwe dîplomasî yên bi hev re welatan '''şemirandine'''.}}
# {{mane?|ku}}
#* {{J:Mem û Zîn:2022|jêgirtin=We zû şemirand ji bo çi sersal<br>Hûn rast bibên ku bûye ehwal?}}
#* {{J:Mem û Zîn:2022|jêgirtin=Safî şemirand vexwerî durdî<br>Manendê durrê, lîsanê kurdî}}
==== Tewandin ====
: -şemirîn-
==== Hevmane ====
{{kol3|ku
|qut kirin
|rawestandin
|sekinandin
|xetimandin
|vebirrîn
|hêlan
|hiştin
|tebitandin
|bi rê ve birin
}}
==== Etîmolojî ====
Ji {{ji|ar|ku|شمر}} (şemere) + kurdî [[-andin]]
==== Werger ====
{{werger-ser}}
* {{Z|en}}: {{W+|en|stir up}}
{{werger-bin}}
9pkwuiti23cj24q4uwlx2g6s9lr9djy
6531784
6531783
2026-09-16T07:18:25Z
Wikihez
7344
/* Etîmolojî */
6531784
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Lêker ===
{{lêker|ku|c=}}
# [[têk vedan]], [[tevlihev]] kirin
# [[bi rê ve birin]]
#: {{nm|ku|Min xwendina xwe '''şemirand''' daku karekî bikim û malbata xwe pê xwedî bikim.}}
#: {{nm|ku|Herdu welatan hemî têkiliyên xwe dîplomasî yên bi hev re welatan '''şemirandine'''.}}
# {{mane?|ku}}
#* {{J:Mem û Zîn:2022|jêgirtin=We zû şemirand ji bo çi sersal<br>Hûn rast bibên ku bûye ehwal?}}
#* {{J:Mem û Zîn:2022|jêgirtin=Safî şemirand vexwerî durdî<br>Manendê durrê, lîsanê kurdî}}
==== Tewandin ====
: -şemirîn-
==== Hevmane ====
{{kol3|ku
|qut kirin
|rawestandin
|sekinandin
|xetimandin
|vebirrîn
|hêlan
|hiştin
|tebitandin
|bi rê ve birin
}}
==== Etîmolojî ====
{{etîmon|ku|:qf|ar:شمر<tr:şemere>|-andin|metn=++|dar=1}}
==== Werger ====
{{werger-ser}}
* {{Z|en}}: {{W+|en|stir up}}
{{werger-bin}}
lkseb89lj0s0k26j836agafddm92ra9
romanvanî
0
5063
6531761
6428895
2026-09-15T21:43:52Z
Wikihez
7344
/* Navdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531761
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[wêje]]ya [[roman]]î
# Nivîsîna [[roman]]an, karê [[romannivîs]]an.
#: {{hevmane|ku|romannivîsî}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
Ji {{qertaf|ku|romanvan|-î|cuda2=î}}.
==== Bi soranî ====
* [[romanewanî]]
4h16i2f1yxs1qoh1prs61n99ut0s35j
6531762
6531761
2026-09-15T21:44:13Z
Wikihez
7344
/* Navdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531762
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[wêje]]ya [[roman]]î{{p-mane}}
# Nivîsîna [[roman]]an, karê [[romannivîs]]an.
#: {{hevmane|ku|romannivîsî}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
Ji {{qertaf|ku|romanvan|-î|cuda2=î}}.
==== Bi soranî ====
* [[romanewanî]]
bchdq8qocwe4rdfgkxiux25czqmvdz2
civan
0
6141
6531704
6430127
2026-09-15T18:30:27Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531704
wikitext
text/x-wiki
{{h-bnr|jivan}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/d͡ʒɪˈvɑːn/}}
* {{kîte|ku|ci|van}}
=== Navdêr ===
{{navdêr|ku|z=?}}
# [[sehî]], [[saxî]], [[komkirin]], [[gelejmar]], [[pirejimar]].
# Cihê civînê ku meriv diraweste heta kesê meriv li bendê ye tê.
# {{mane?|ku}}
#* {{jêder|ku|jêgirtin=Delal û rind û şêrîn,<br>'''Civan''' da bû, ku bê dînê<br>Me dî naka şeva înê<br>Sivik hatî civanê min.|Cegerxwîn:Helbest ''HAT CIVANÊ MIN BI ŞEV''}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|en}}: {{W+|en|venue}}
{{werger-bin}}
== {{ziman|tr}} ==
=== Rengdêr ===
{{rengdêr|tr}}
# [[ciwan]], [[bedew]]
# [[timal]], [[tevnal]]
o6np2otq0qofm99tevoiaafy85bln8w
qepat kirin
0
12935
6531639
5886835
2026-09-15T16:45:16Z
Wikihez
7344
6531639
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Lêker ===
{{lêker|ku|c=h-gh}}
# [[girtin]]
==== Etîmolojî ====
Ji {{heve|ku|qepat|kirin}}.
==== Werger ====
{{werger-ser}}
* {{Z|en}}: {{W+|en|secure}}, {{W+|en|close}}
{{werger-bin}}
junlhw30fpo0bwnxbrepsrbln1f4ek6
îdeolojî
0
13673
6531697
6348563
2026-09-15T18:18:00Z
Wikihez
7344
Kurmancî/Navdêr: ±nimûneya bikaranînê bi amûra « [[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]] » (v2.0) (DefSideBoxes)
6531697
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[doktrîn|Doktrîn]], [[felsefe]], komek [[bawerî]] an [[prensîb]]ên ku aîdê kesek an komekê ne.
#: {{nm|ku|Sosyalîzm, komunîzm, kapîtalîzm, lîberalîzm û hwd. '''îdeolojî''' ne.}}
#* {{J:Firat Cewerî:Payiza Dereng|weşanxane={{w|Avesta}}|sal=2014|r=27|jêgirtin=Wê xweşiyê pir bala min kişand û ne tavilê be jî, hisa şer li ba min kêmtir kir, dilê min yê ji ber berberî û '''îdeolojiyan''' hişk bûbû nerm kir {{...}}}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|îdeolocî
|îdêolocî
|îdêolojî
|îdiyolocî
|îdiyolojî
|îdyolocî
|îdyolojî
|aydiyolocî
|aydiyolojî
}}
==== Hevmane ====
{{kol3|ku
|bîrdozî
}}
==== Têkildar ====
{{kol3|ku
|îdeolog
|îdeolojîst
|îdeolojîzm
}}
==== Etîmolojî ====
Ji {{deyn|fr|ku|ideologie}}. Wekheva {{paş|ku|îdea|-lojî}}.
==== Werger ====
{{werger-ser}}
* {{Z|sq}}: {{W+|sq|ideologji|m}}
* {{Z|de}}: {{W+|de|Ideologie|m}}
* {{Z|ast}}: {{W-|ast|ideoloxía|m}}
* {{Z|az}}: {{W+|az|görüş}}, {{W-|az|ideologiya}}, {{W-|az|məfkurə}}
* {{Z|be}}: {{W-|be|ідэало́гія|m}}, {{W-|be|ідэалёгія|m}}
* {{Z|bn}}: {{W-|bn|ভাবাদর্শ}}
* {{Z|bg}}: {{W+|bg|идеоло́гия|m}}
* {{Z|my}}: {{W+|my|အယူဝါဒ}}
* {{Z|cs}}: {{W+|cs|ideologie|m}}
* {{Z|zh}}:
*: {{Z|cmn}}: {{W+|cmn|意識形態|tr=yìshi xíngtài}}, {{W+|cmn|思想意識|tr=sīxiǎng yìshi}}, {{W+|cmn|思想體系|tr=sīxiǎng tǐxì}}, {{W-|cmn|意締牢結|tr=yìdìláojié}}, {{W+|cmn|理念}}
* {{Z|da}}: {{W-|da|ideologi|g}}
* {{Z|id}}: {{W+|id|ideologi}}
* {{Z|ar}}: {{W-|ar|إِيدِيُولُوجِيَّة|m|tr=ʔīdiyulūjiyya}}
* {{Z|hy}}: {{W+|hy|գաղափարախոսություն}}
* {{Z|eo}}: {{W-|eo|idearo}}
* {{Z|et}}: {{W+|et|ideoloogia}}
* {{Z|fa}}: {{W+|fa|ایدِئولوژی|tr=ide'oloži}}
* {{Z|fi}}: {{W+|fi|ideologia}}, {{W-|fi|aatemaailma}}, {{W+|fi|maailmankatsomus}}
* {{Z|fr}}: {{W+|fr|idéologie|m}}
* {{Z|gl}}: {{W+|gl|ideoloxía|m}}
* {{Z|ka}}: {{W-|ka|იდეოლოგია}}
* {{Z|hi}}: {{W-|hi|विचारधारा|m}}, {{W-|hi|नज़रिया|n|tr=nazariyā}}
* {{Z|nl}}: {{W+|nl|ideologie|m}}
* {{Z|he}}: {{W+|he|אִידֵאוֹלוֹגִיָה|m|tr=ideológya}}
* {{Z|en}}: {{W|en|ideology}}
* {{Z|ga}}: {{W-|ga|idé-eolaíocht|m}}
* {{Z|it}}: {{W+|it|ideologia|m}}
* {{Z|is}}: {{W-|is|hugmyndafræði|m}}
* {{Z|ja}}: {{W-|ja|[[観念]][[形態]]|tr=かんねんけいたい, kannen keitai}}, {{W+|ja|イデオロギー|tr=ideorogī}}, {{W+|ja|理念|tr=りねん, rinen}}
* {{Z|ca}}: {{W+|ca|ideologia}}
* {{Z|ky}}: {{W+|ky|идеология}}
* {{Z|si}}: {{W-|si|දෘෂ්ටිවාදය}}
* {{Z|ko}}: {{W+|ko|이데올로기}}, {{W+|ko|이념}}, {{W+|ko|리념}}
* {{Z|la}}: {{W-|la|ideologia|m}}
* {{Z|lv}}: {{W-|lv|ideoloģija|m}}
* {{Z|lo}}: {{W-|lo|ອຸດົມການ}}
* {{Z|lt}}: {{W-|lt|ideologija|m}}
* {{Z|mk}}: {{W-|mk|идеологија|m}}
* {{Z|ml}}: {{W-|ml|പ്രത്യയശാസ്ത്രം}}
* {{Z|ms}}: {{W-|ms|ideologi}}, {{W+|ms|adicita}}
* {{Z|mt}}: {{W-|mt|ideologija|m}}
* {{Z|mi}}: {{W-|mi|kaupapa whakaaro}}
* {{Z|hu}}: {{W+|hu|ideológia}}
* {{Z|mn}}:
*: Kirîlî: {{W-|mn|үзэл суртал}}, {{W-|mn|идеологи}}
* {{Z|no}}:
*: {{Z|nb}}: {{W-|nb|ideologi|n}}
*: {{Z|nn}}: {{W-|nn|ideologi|n}}
* {{Z|ug}}: {{W-|ug|ئىدېئولوگىيە}}
* {{Z|uz}}: {{W+|uz|ideologiya}}, {{W+|uz|mafkura}}
* {{Z|ps}}: {{W-|ps|ايديولوژي|m|tr=idioloži}}, {{W-|ps|ايډيالوجي|m|tr=iḍyāloji}}
* {{Z|pl}}: {{W+|pl|ideologia|m}}
* {{Z|pt}}: {{W+|pt|ideologia|m}}
* {{Z|pa}}: {{W-|pa|ਵਿਚਾਰਧਾਰਾ|m}}
* {{Z|kk}}: {{W-|kk|идеология}}
* {{Z|ro}}: {{W+|ro|ideologie|m}}
* {{Z|ru}}: {{W+|ru|идеоло́гия|m}}, {{W+|ru|мировоззре́ние|nt}}
* {{Z|rue}}: {{W-|rue|ідеоло́ґія|m}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|идеоло̀гија|m}}
*: Latînî: {{W+|sh|ideològija|m}}
* {{Z|sk}}: {{W-|sk|ideológia|m}}
* {{Z|sl}}: {{W-|sl|ideologija|m}}
* {{Z|es}}: {{W+|es|ideología|m}}
* {{Z|sv}}: {{W+|sv|ideologi|g}}
* {{Z|tg}}: {{W+|tg|идеология}}, {{W-|tg|мафкура}}
* {{Z|tl}}: {{W-|tl|sangkuruan}}
* {{Z|th}}: {{W+|th|อุดมการณ์}}
* {{Z|tt}}: {{W-|tt|идеология}}
* {{Z|tr}}: {{W+|tr|ideoloji}}
* {{Z|tk}}: {{W-|tk|ideologiýa}}
* {{Z|ur}}: {{W-|ur|نَظَرِیَہ|n|tr=nazariyā}}
* {{Z|uk}}: {{W-|uk|ідеоло́гія|m}}
* {{Z|km}}: {{W-|km|មនោគមវិជ្ជា}}
* {{Z|yi}}: {{W-|yi|אידעאָלאָגיע|m}}
* {{Z|el}}: {{W+|el|ιδεολογία|m}}
{{werger-bin}}
:{{werger çavkanî|en|ideology|beş=Ev qismê Wergerê|oldid=76005082}}
jv3evoanp4io6fsej0ee24bnkf0fom3
dom
0
19043
6531649
6439128
2026-09-15T17:07:38Z
~2026-49765-60
15374
6531649
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[dewam]], [[berdewamî]], ne[[sekinîn]], ne[[rawestîn]], ne[[qedîn]], ne[[xilas]]bûn
# [[xew]], [[giranî]],[[tevz]],[[xemş]], [[xelmaşî]], [[xayisî]],''doma xewê da ser min''
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Jê ====
{{kol3|ku|cure=Jê
|domandin
|domandî
|domiyayî
|domî
|domîn
|domîner
|dom bûn<pos:lêker>
|dom kirin<pos:lêker>
|dombûn<pos:navdêr>
|domkirin<pos:navdêr>
|dombûyî<pos:rengdêr>
|domkirî<pos:rengdêr>
}}
==== Etîmolojî ====
Ji ''[[dewam]]'' ji {{deyn|ar|ku|دَوَام}}. Bo guherîna -EW- bi O bidin ber ''[[dor]], [[xox]]''.
==== Bide ber ====
{{kol3|ku
|com
|rom
|tom
|zom
}}
==== Herwiha binêre ====
{{kol3|ku
|doh
|dol
|dor
|dot
|doz
}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Andauern}}, {{W+|de|Fortdauern}}
* {{Z|fa}}: {{W+|fa|تداوم}}, {{W+|fa|استمرار}}, {{W+|fa|تاکید}}
* {{Z|fi}}: {{W+|fi|jatko}}, {{W+|fi|jatkuvuus}}
* {{Z|en}}: {{W+|en|continuity}}, {{W+|en|pinfold}}
* {{Z|tr}}: {{W+|tr|devam}}, {{W+|tr|süre}}, {{W+|tr|idame}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=dmo|mod}}
== {{ziman|da}} ==
=== Navdêr ===
{{navdêr|da}}
# {{g|ku|hukim}}
==== Anagram ====
* {{anagram|da|a=dmo|mod}}
=== Çavkanî ===
{{rûpela wergerê|da|hukim}}
== {{ziman|ang}} ==
=== Navdêr ===
{{navdêr|ang|dōm}}
# {{g|ku|qanûn}} ([[yasa]], [[zagon]])
==== Anagram ====
* {{anagram|ang|a=dmo|mod}}
== {{ziman|jv}} ==
=== Navdêr ===
{{navdêr|jv}}
# {{g|ku|derzî}}
=== Çavkanî ===
{{rûpela wergerê|jv|derzî}}
== {{ziman|nb}} ==
=== Navdêr ===
{{navdêr|nb|z=n}}
# {{g|ku|hukim}}
=== Çavkanî ===
{{rûpela wergerê|nb|hukim}}
== {{ziman|pl}} ==
=== Bilêvkirin ===
* {{deng|pl|Pl-{{PAGENAME}}.ogg|Deng}}
* {{deng|pl|LL-Q809 (pol)-KaMan-dom.wav|Deng|dever=Polonya}}
* {{deng|pl|LL-Q809 (pol)-Gower-dom.wav|Deng|dever=Bytom, Polonya}}
=== Navdêr ===
{{navdêr|pl|z=}}
# [[mal]], [[xanî]]
== {{ziman|sh}} ==
=== Navdêr ===
{{navdêr|sh|dȍm|z=n}}
# {{f|sh|avahî}} {{g|ku|avahî}}, {{g|ku|xanî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|sh|avahî}}
== {{ziman|sk}} ==
=== Navdêr ===
{{navdêr|sk|z=n}}
# {{f|sk|avahî}} {{g|ku|avahî}}, {{g|ku|xanî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|sk|avahî}}
== {{ziman|dsb}} ==
=== Navdêr ===
{{navdêr|dsb|z=n}}
# {{f|dsb|avahî}} {{g|ku|xanî}}, {{g|ku|avahî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|dsb|xanî}}
== {{ziman|hsb}} ==
=== Navdêr ===
{{navdêr|hsb|z=n}}
# {{f|hsb|avahî}} {{g|ku|xanî}}, {{g|ku|avahî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|hsb|xanî}}
== {{ziman|sv}} ==
=== Cînav ===
{{cînav|sv}}
# [[ew#ku|ew]] (pirjimar), [[wan#ku|wan]]
== {{ziman|vo}} ==
=== Navdêr ===
{{navdêr|vo}}
# {{f|vo|avahî}} {{g|ku|xanî}}, {{g|ku|avahî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|vo|xanî}}
inhgns8wvm156vk0c4mtdjhbwmq7tjq
6531665
6531649
2026-09-15T17:29:07Z
Balyozbot
7867
[[Bikarhêner:Wikihez/skrîpt/py/dengbot.py|Bot]]: Formata parametreya "dever" hat guhartin
6531665
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[dewam]], [[berdewamî]], ne[[sekinîn]], ne[[rawestîn]], ne[[qedîn]], ne[[xilas]]bûn
# [[xew]], [[giranî]],[[tevz]],[[xemş]], [[xelmaşî]], [[xayisî]],''doma xewê da ser min''
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Jê ====
{{kol3|ku|cure=Jê
|domandin
|domandî
|domiyayî
|domî
|domîn
|domîner
|dom bûn<pos:lêker>
|dom kirin<pos:lêker>
|dombûn<pos:navdêr>
|domkirin<pos:navdêr>
|dombûyî<pos:rengdêr>
|domkirî<pos:rengdêr>
}}
==== Etîmolojî ====
Ji ''[[dewam]]'' ji {{deyn|ar|ku|دَوَام}}. Bo guherîna -EW- bi O bidin ber ''[[dor]], [[xox]]''.
==== Bide ber ====
{{kol3|ku
|com
|rom
|tom
|zom
}}
==== Herwiha binêre ====
{{kol3|ku
|doh
|dol
|dor
|dot
|doz
}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Andauern}}, {{W+|de|Fortdauern}}
* {{Z|fa}}: {{W+|fa|تداوم}}, {{W+|fa|استمرار}}, {{W+|fa|تاکید}}
* {{Z|fi}}: {{W+|fi|jatko}}, {{W+|fi|jatkuvuus}}
* {{Z|en}}: {{W+|en|continuity}}, {{W+|en|pinfold}}
* {{Z|tr}}: {{W+|tr|devam}}, {{W+|tr|süre}}, {{W+|tr|idame}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=dmo|mod}}
== {{ziman|da}} ==
=== Navdêr ===
{{navdêr|da}}
# {{g|ku|hukim}}
==== Anagram ====
* {{anagram|da|a=dmo|mod}}
=== Çavkanî ===
{{rûpela wergerê|da|hukim}}
== {{ziman|ang}} ==
=== Navdêr ===
{{navdêr|ang|dōm}}
# {{g|ku|qanûn}} ([[yasa]], [[zagon]])
==== Anagram ====
* {{anagram|ang|a=dmo|mod}}
== {{ziman|jv}} ==
=== Navdêr ===
{{navdêr|jv}}
# {{g|ku|derzî}}
=== Çavkanî ===
{{rûpela wergerê|jv|derzî}}
== {{ziman|nb}} ==
=== Navdêr ===
{{navdêr|nb|z=n}}
# {{g|ku|hukim}}
=== Çavkanî ===
{{rûpela wergerê|nb|hukim}}
== {{ziman|pl}} ==
=== Bilêvkirin ===
* {{deng|pl|Pl-{{PAGENAME}}.ogg}}
* {{deng|pl|LL-Q809 (pol)-KaMan-dom.wav|Deng|dever=Polonya}}
* {{deng|pl|LL-Q809 (pol)-Gower-dom.wav|Deng|dever=Bytom, Polonya}}
=== Navdêr ===
{{navdêr|pl|z=}}
# [[mal]], [[xanî]]
== {{ziman|sh}} ==
=== Navdêr ===
{{navdêr|sh|dȍm|z=n}}
# {{f|sh|avahî}} {{g|ku|avahî}}, {{g|ku|xanî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|sh|avahî}}
== {{ziman|sk}} ==
=== Navdêr ===
{{navdêr|sk|z=n}}
# {{f|sk|avahî}} {{g|ku|avahî}}, {{g|ku|xanî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|sk|avahî}}
== {{ziman|dsb}} ==
=== Navdêr ===
{{navdêr|dsb|z=n}}
# {{f|dsb|avahî}} {{g|ku|xanî}}, {{g|ku|avahî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|dsb|xanî}}
== {{ziman|hsb}} ==
=== Navdêr ===
{{navdêr|hsb|z=n}}
# {{f|hsb|avahî}} {{g|ku|xanî}}, {{g|ku|avahî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|hsb|xanî}}
== {{ziman|sv}} ==
=== Cînav ===
{{cînav|sv}}
# [[ew#ku|ew]] (pirjimar), [[wan#ku|wan]]
== {{ziman|vo}} ==
=== Navdêr ===
{{navdêr|vo}}
# {{f|vo|avahî}} {{g|ku|xanî}}, {{g|ku|avahî}}, {{g|ku|bîna}}
=== Çavkanî ===
{{rûpela wergerê|vo|xanî}}
kn3yzh23csb5r2g4v0hw3j3u5pvpmdh
teşebis
0
21436
6531748
6440593
2026-09-15T20:42:45Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531748
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[peyan]], [[destavêtin]] (tiştekî), [[dest pê kirin|destkirina bi tiştekî]]
#* {{jêder-websîte|ku|jêgirtin=[B]i dizî ji xwe re telefon kiriye. Xwişka wê ya mezin pê dihese û ji bavê xwe re dibêje. Bavê wê jî êrîşa kuştinê lê dike. Xelas dibe lê ji tirsa bavê xwe re '''teşebusê''' întîxarê dike.|nivîskar=Armanc Celîl|sernav=Bikaranîn a [[mobîl]]ê serê keçan dixwe!|websîte=Netkurd.com|sal=7/2006}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|teşebus
}}
==== Jê ====
{{kol3|ku
|teşebisî
|teşebisker
}}
==== Etîmolojî ====
Ji {{ji|ar|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Initiative|f}}, {{W+|de|Versuch|m|guman=erê}}
* {{Z|fi}}: {{W+|fi|yritys}}
* {{Z|en}}: {{W+|en|attempt}}, {{W+|en|enterprise}}, {{W+|en|undertaking}}, {{W+|en|initiative}}, {{W+|en|effort}}
* {{Z|tr}}: {{W+|tr|teşebbüs}}, {{W+|tr|girişim}}, {{W+|tr|kıyam|guman=erê}}
{{werger-bin}}
0uxod89uk0f0qmkkrcl8fkqp32k7vho
Wîkîferheng:ceribandin
4
23016
6531719
6430801
2026-09-15T20:10:54Z
Wikihez
7344
/* Rengdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531719
wikitext
text/x-wiki
{{h-bnr|* {{ku-kîte}}
{{/vê rêzê neguhêre}}
== {{ziman|ku}} ==
=== Rengdêr ===
{{ku-tewîn-rd|nû}}
[[Wêne:12-10-03-autostadt-wob-by-RalfR-12.jpg|thumb|Ttirimpêleke '''nû'''.]]
[[Wêne:Mobile Phone Evolution 1992 - 2014.jpg|thumb|Çend telefonên destan ji '''nûtirîn''' (li pêş) heta [[kevn]]tirîn (li paş).]]
{{rengdêr|ku|form=-}}
# Tiştê ku ne [[ji mêj ve]] çêbûye yan hatiye çêkirin.
#: {{hevmane|ku|teze|cedîd}}
#: {{dijmane|ku|kevn}}
## Tiştê ku [[hê]] nehatiye [[bikaranîn]] yan tenê [[kêm]] hatiye bikaranîn.
##: {{nimûne|ku|kitêba '''ceribandin'''}}
##: {{nimûne|ku|erebeya '''ceribandin'''}}
##: {{mk|ku|cilên '''nû''', pêlavên '''nû'''}}
## Tiştê ku cihê tiştekî [[berî]] xwe digire.
##: {{mk|ku|Fermanên '''nû''' ji generalê wan hatin.}}
## [[destpêk]], li destpêka tiştekî.
##: {{mk|ku|Ez ê serê sala '''nû''' bêm te bibînim.}}
##: {{nimûne|ku|test}}
## Tiştê ku ne ji mêj ve hatiye dîtin yan hayjêbûn.
##: {{mk|ku|Parîs ji min re '''nû''' bû.}}
##* {{jêder|ku|test, 2015|jêgirtin=test}}
## Yê ne ji mêj ve bûye tiştek.
##: {{mk|ku|Du xwendekarên '''nû''' li zanîngehê.}}
## [[guherî]], Yê wek xwe nemaye
##: {{mk|ku|Ew bûye mirovek '''nû'''.}}
==== Ji wêjeya klasîk ====
* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Lê xettê reyhanî li dêm<br />Katib ji '''nû''' i'rab-i da<br />Nazikqeda '''nû'''rînbeşer}}
==== Bi alfabeyên din ====
* {{ku-ar|نوو}}
* {{ku-kr|ну}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
|new-
|nev-
|nuh
|nûh
|nwî<ee:behdînanî>
|nî<ee:behdînanî>
|nûv
}}
==== Hevmane ====
* [[hecer]]
* [[modern]]
* [[taze]], [[teze]]
==== Dijmane ====
* [[kevn]]
==== Jê ====
{{kol3|ku|cure=Jê
|nû bûn<q:lêker>
|nû kirin<q:lêker>
|nûbûn<q:navdêr>
|nûkirin<q:navdêr>
|nûbûyî<q:rengdêr>
|nûkirî<q:rengdêr>
|nipînû
|nûçe
|nûjen
|nûjenî
|nûjenxwaz
|nûjenxwazî
|nûxwaz
|nûxwazî
|nûyandin
|nûyandî
|nûyîn
|nûyîtî
|venûyandin
|venûyîn
}}
==== Etîmolojî ====
Hevreha soranî {{g|ckb|نوێ|tr=nwê}}, [[kurdiya başûrî]] ''[[nü]]'', hewramî ''[[no]]'', zazakî ''[[newe]]'', farisî {{g|fa|نو|tr=now}}, belûçî {{g|bal|نوک|tr=nok}}, pehlewî [[𐭭𐭥𐭪𐭩]] / [[𐫗𐫇𐫃]] (nwk/nwg /nōg/), peştûyî {{g|ps|نوی|tr=nevey, nivey}}, osetî [[нӕуӕг]] (næwæg) û [[ног]] (nog), bextiyariya kevn [[νωγο]] (nōgo), avestayî [[𐬥𐬀𐬎𐬎𐬀]] (nauua, /newe/)... sanskrîtî [[नव]] (náva), hindî [[नवा]] (navā)... hîtîtî [[𒉋𒀸]] (nēwas), luwiyayî [[𒈾𒀀𒌑𒉿𒋾]] (nāúwati)... bulgarî [[нов]] (nov), çekî ''[[nový]]'', polonî ''[[nowy]]'', rusî [[но́вый]] (nóviy)... latînî ''[[novus]]'', îtalî ''[[novo]], [[nuovo]]'', portugalî ''[[novo]]'', spanî ''[[nuevo]]'', fransî ''[[neuf]], [[nouveau]]''... yûnaniya kevn û nû [[νέος]] (néos)... ermeniya kevn û nû [[նոր]] (nor)... lîtwanî ''[[naũjas]]''... bretonî ''[[nevez]]'', îrlendî ''[[nua]]''... almanî ''[[neu]]'', danmarkî/norwecî/swêdî ''[[ny]]'', holendî ''[[nieuw]]'', îngilîzî ''[[new]]''... hemû ji {{ji|ine-pro|ku|-}} {{recons|ine-pro|néwos}}, {{recons|ine-pro|néwyos}} (nû).
;{{biçûk|Çavkanî:
* {{Chyet}}
* {{Horn}}
* {{MacKenzie-2}}
* {{Pokorny}}
* {{The Tower of Babel}}
* {{Tsabolov}}
* {{Watkins}}
}}
==== Bi zaravayên din ====
* {{Z|ckb}}: {{W-|ckb|نوێ|tr=[[nwê]]}}
* {{Z|sdh}}: {{W-|sdh|sc=Latn|nü}}
* {{Z|hac}}: {{W-|hac|sc=Latn|no}}
* {{Z|zza}}: {{W+|zza|newe}}
===Navdêr ===
{{navdêr|ku}}
# test
95lcpxh7iozquuxr9imyjhf64a3w5vv
6531720
6531719
2026-09-15T20:11:09Z
Wikihez
7344
Kurmancî: beşa «Navdêr» bû «Rengdêr» (DefSideBoxes)
6531720
wikitext
text/x-wiki
{{h-bnr|* {{ku-kîte}}
{{/vê rêzê neguhêre}}
== {{ziman|ku}} ==
=== Rengdêr 1 ===
{{ku-tewîn-rd|nû}}
[[Wêne:12-10-03-autostadt-wob-by-RalfR-12.jpg|thumb|Ttirimpêleke '''nû'''.]]
[[Wêne:Mobile Phone Evolution 1992 - 2014.jpg|thumb|Çend telefonên destan ji '''nûtirîn''' (li pêş) heta [[kevn]]tirîn (li paş).]]
{{rengdêr|ku|form=-}}
# Tiştê ku ne [[ji mêj ve]] çêbûye yan hatiye çêkirin.
#: {{hevmane|ku|teze|cedîd}}
#: {{dijmane|ku|kevn}}
## Tiştê ku [[hê]] nehatiye [[bikaranîn]] yan tenê [[kêm]] hatiye bikaranîn.
##: {{nimûne|ku|kitêba '''ceribandin'''}}
##: {{nimûne|ku|erebeya '''ceribandin'''}}
##: {{mk|ku|cilên '''nû''', pêlavên '''nû'''}}
## Tiştê ku cihê tiştekî [[berî]] xwe digire.
##: {{mk|ku|Fermanên '''nû''' ji generalê wan hatin.}}
## [[destpêk]], li destpêka tiştekî.
##: {{mk|ku|Ez ê serê sala '''nû''' bêm te bibînim.}}
##: {{nimûne|ku|test}}
## Tiştê ku ne ji mêj ve hatiye dîtin yan hayjêbûn.
##: {{mk|ku|Parîs ji min re '''nû''' bû.}}
##* {{jêder|ku|test, 2015|jêgirtin=test}}
## Yê ne ji mêj ve bûye tiştek.
##: {{mk|ku|Du xwendekarên '''nû''' li zanîngehê.}}
## [[guherî]], Yê wek xwe nemaye
##: {{mk|ku|Ew bûye mirovek '''nû'''.}}
==== Ji wêjeya klasîk ====
* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Lê xettê reyhanî li dêm<br />Katib ji '''nû''' i'rab-i da<br />Nazikqeda '''nû'''rînbeşer}}
==== Bi alfabeyên din ====
* {{ku-ar|نوو}}
* {{ku-kr|ну}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
|new-
|nev-
|nuh
|nûh
|nwî<ee:behdînanî>
|nî<ee:behdînanî>
|nûv
}}
==== Hevmane ====
* [[hecer]]
* [[modern]]
* [[taze]], [[teze]]
==== Dijmane ====
* [[kevn]]
==== Jê ====
{{kol3|ku|cure=Jê
|nû bûn<q:lêker>
|nû kirin<q:lêker>
|nûbûn<q:navdêr>
|nûkirin<q:navdêr>
|nûbûyî<q:rengdêr>
|nûkirî<q:rengdêr>
|nipînû
|nûçe
|nûjen
|nûjenî
|nûjenxwaz
|nûjenxwazî
|nûxwaz
|nûxwazî
|nûyandin
|nûyandî
|nûyîn
|nûyîtî
|venûyandin
|venûyîn
}}
==== Etîmolojî ====
Hevreha soranî {{g|ckb|نوێ|tr=nwê}}, [[kurdiya başûrî]] ''[[nü]]'', hewramî ''[[no]]'', zazakî ''[[newe]]'', farisî {{g|fa|نو|tr=now}}, belûçî {{g|bal|نوک|tr=nok}}, pehlewî [[𐭭𐭥𐭪𐭩]] / [[𐫗𐫇𐫃]] (nwk/nwg /nōg/), peştûyî {{g|ps|نوی|tr=nevey, nivey}}, osetî [[нӕуӕг]] (næwæg) û [[ног]] (nog), bextiyariya kevn [[νωγο]] (nōgo), avestayî [[𐬥𐬀𐬎𐬎𐬀]] (nauua, /newe/)... sanskrîtî [[नव]] (náva), hindî [[नवा]] (navā)... hîtîtî [[𒉋𒀸]] (nēwas), luwiyayî [[𒈾𒀀𒌑𒉿𒋾]] (nāúwati)... bulgarî [[нов]] (nov), çekî ''[[nový]]'', polonî ''[[nowy]]'', rusî [[но́вый]] (nóviy)... latînî ''[[novus]]'', îtalî ''[[novo]], [[nuovo]]'', portugalî ''[[novo]]'', spanî ''[[nuevo]]'', fransî ''[[neuf]], [[nouveau]]''... yûnaniya kevn û nû [[νέος]] (néos)... ermeniya kevn û nû [[նոր]] (nor)... lîtwanî ''[[naũjas]]''... bretonî ''[[nevez]]'', îrlendî ''[[nua]]''... almanî ''[[neu]]'', danmarkî/norwecî/swêdî ''[[ny]]'', holendî ''[[nieuw]]'', îngilîzî ''[[new]]''... hemû ji {{ji|ine-pro|ku|-}} {{recons|ine-pro|néwos}}, {{recons|ine-pro|néwyos}} (nû).
;{{biçûk|Çavkanî:
* {{Chyet}}
* {{Horn}}
* {{MacKenzie-2}}
* {{Pokorny}}
* {{The Tower of Babel}}
* {{Tsabolov}}
* {{Watkins}}
}}
==== Bi zaravayên din ====
* {{Z|ckb}}: {{W-|ckb|نوێ|tr=[[nwê]]}}
* {{Z|sdh}}: {{W-|sdh|sc=Latn|nü}}
* {{Z|hac}}: {{W-|hac|sc=Latn|no}}
* {{Z|zza}}: {{W+|zza|newe}}
=== Rengdêr 2 ===
{{rengdêr|ku}}
# test
qo9euhl4th3quzfve0g2xotsh0wziy7
6531721
6531720
2026-09-15T20:11:40Z
Wikihez
7344
Kurmancî: beşa «Rengdêr 1» bû «Navdêr» (DefSideBoxes)
6531721
wikitext
text/x-wiki
{{h-bnr|* {{ku-kîte}}
{{/vê rêzê neguhêre}}
== {{ziman|ku}} ==
=== Navdêr ===
{{ku-tewîn-rd|nû}}
[[Wêne:12-10-03-autostadt-wob-by-RalfR-12.jpg|thumb|Ttirimpêleke '''nû'''.]]
[[Wêne:Mobile Phone Evolution 1992 - 2014.jpg|thumb|Çend telefonên destan ji '''nûtirîn''' (li pêş) heta [[kevn]]tirîn (li paş).]]
{{navdêr|ku|c=?}}
# Tiştê ku ne [[ji mêj ve]] çêbûye yan hatiye çêkirin.
#: {{hevmane|ku|teze|cedîd}}
#: {{dijmane|ku|kevn}}
## Tiştê ku [[hê]] nehatiye [[bikaranîn]] yan tenê [[kêm]] hatiye bikaranîn.
##: {{nimûne|ku|kitêba '''ceribandin'''}}
##: {{nimûne|ku|erebeya '''ceribandin'''}}
##: {{mk|ku|cilên '''nû''', pêlavên '''nû'''}}
## Tiştê ku cihê tiştekî [[berî]] xwe digire.
##: {{mk|ku|Fermanên '''nû''' ji generalê wan hatin.}}
## [[destpêk]], li destpêka tiştekî.
##: {{mk|ku|Ez ê serê sala '''nû''' bêm te bibînim.}}
##: {{nimûne|ku|test}}
## Tiştê ku ne ji mêj ve hatiye dîtin yan hayjêbûn.
##: {{mk|ku|Parîs ji min re '''nû''' bû.}}
##* {{jêder|ku|test, 2015|jêgirtin=test}}
## Yê ne ji mêj ve bûye tiştek.
##: {{mk|ku|Du xwendekarên '''nû''' li zanîngehê.}}
## [[guherî]], Yê wek xwe nemaye
##: {{mk|ku|Ew bûye mirovek '''nû'''.}}
==== Ji wêjeya klasîk ====
* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Lê xettê reyhanî li dêm<br />Katib ji '''nû''' i'rab-i da<br />Nazikqeda '''nû'''rînbeşer}}
==== Bi alfabeyên din ====
* {{ku-ar|نوو}}
* {{ku-kr|ну}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
|new-
|nev-
|nuh
|nûh
|nwî<ee:behdînanî>
|nî<ee:behdînanî>
|nûv
}}
==== Hevmane ====
* [[hecer]]
* [[modern]]
* [[taze]], [[teze]]
==== Dijmane ====
* [[kevn]]
==== Jê ====
{{kol3|ku|cure=Jê
|nû bûn<q:lêker>
|nû kirin<q:lêker>
|nûbûn<q:navdêr>
|nûkirin<q:navdêr>
|nûbûyî<q:rengdêr>
|nûkirî<q:rengdêr>
|nipînû
|nûçe
|nûjen
|nûjenî
|nûjenxwaz
|nûjenxwazî
|nûxwaz
|nûxwazî
|nûyandin
|nûyandî
|nûyîn
|nûyîtî
|venûyandin
|venûyîn
}}
==== Etîmolojî ====
Hevreha soranî {{g|ckb|نوێ|tr=nwê}}, [[kurdiya başûrî]] ''[[nü]]'', hewramî ''[[no]]'', zazakî ''[[newe]]'', farisî {{g|fa|نو|tr=now}}, belûçî {{g|bal|نوک|tr=nok}}, pehlewî [[𐭭𐭥𐭪𐭩]] / [[𐫗𐫇𐫃]] (nwk/nwg /nōg/), peştûyî {{g|ps|نوی|tr=nevey, nivey}}, osetî [[нӕуӕг]] (næwæg) û [[ног]] (nog), bextiyariya kevn [[νωγο]] (nōgo), avestayî [[𐬥𐬀𐬎𐬎𐬀]] (nauua, /newe/)... sanskrîtî [[नव]] (náva), hindî [[नवा]] (navā)... hîtîtî [[𒉋𒀸]] (nēwas), luwiyayî [[𒈾𒀀𒌑𒉿𒋾]] (nāúwati)... bulgarî [[нов]] (nov), çekî ''[[nový]]'', polonî ''[[nowy]]'', rusî [[но́вый]] (nóviy)... latînî ''[[novus]]'', îtalî ''[[novo]], [[nuovo]]'', portugalî ''[[novo]]'', spanî ''[[nuevo]]'', fransî ''[[neuf]], [[nouveau]]''... yûnaniya kevn û nû [[νέος]] (néos)... ermeniya kevn û nû [[նոր]] (nor)... lîtwanî ''[[naũjas]]''... bretonî ''[[nevez]]'', îrlendî ''[[nua]]''... almanî ''[[neu]]'', danmarkî/norwecî/swêdî ''[[ny]]'', holendî ''[[nieuw]]'', îngilîzî ''[[new]]''... hemû ji {{ji|ine-pro|ku|-}} {{recons|ine-pro|néwos}}, {{recons|ine-pro|néwyos}} (nû).
;{{biçûk|Çavkanî:
* {{Chyet}}
* {{Horn}}
* {{MacKenzie-2}}
* {{Pokorny}}
* {{The Tower of Babel}}
* {{Tsabolov}}
* {{Watkins}}
}}
==== Bi zaravayên din ====
* {{Z|ckb}}: {{W-|ckb|نوێ|tr=[[nwê]]}}
* {{Z|sdh}}: {{W-|sdh|sc=Latn|nü}}
* {{Z|hac}}: {{W-|hac|sc=Latn|no}}
* {{Z|zza}}: {{W+|zza|newe}}
=== Rengdêr ===
{{rengdêr|ku}}
# test
c66rfo4f77beixa3dbniy2889juwku2
6531778
6531721
2026-09-16T07:05:09Z
Wikihez
7344
Kurmancî/Navdêr: +nimûneya bikaranînê bi amûra « [[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]] » (v2.0) (DefSideBoxes)
6531778
wikitext
text/x-wiki
{{h-bnr|* {{ku-kîte}}
{{/vê rêzê neguhêre}}
== {{ziman|ku}} ==
=== Navdêr ===
{{ku-tewîn-rd|nû}}
[[Wêne:12-10-03-autostadt-wob-by-RalfR-12.jpg|thumb|Ttirimpêleke '''nû'''.]]
[[Wêne:Mobile Phone Evolution 1992 - 2014.jpg|thumb|Çend telefonên destan ji '''nûtirîn''' (li pêş) heta [[kevn]]tirîn (li paş).]]
{{navdêr|ku|c=?}}
# Tiştê ku ne [[ji mêj ve]] çêbûye yan hatiye çêkirin.
#: {{hevmane|ku|teze|cedîd}}
#: {{dijmane|ku|kevn}}
## Tiştê ku [[hê]] nehatiye [[bikaranîn]] yan tenê [[kêm]] hatiye bikaranîn.
##: {{nimûne|ku|kitêba '''ceribandin'''}}
##: {{nimûne|ku|erebeya '''ceribandin'''}}
##: {{mk|ku|cilên '''nû''', pêlavên '''nû'''}}
##* {{jêder-vîdeo|ku|hunermend=Hawar Shaban|platform=w:Youtube|sernav=Erdawan Zaxoyi - Çoxe Minoارده وان زاخۆی - چووخکێ منو|tarîx=2015-02-19|url=https://www.youtube.com/watch?v=dspX2PBd_aU|tarîxa-gihiştinê=2026-09-16|jêgirtin=https://www.youtube.com/watch?v=dspX2PBd_aU}}
## Tiştê ku cihê tiştekî [[berî]] xwe digire.
##: {{mk|ku|Fermanên '''nû''' ji generalê wan hatin.}}
## [[destpêk]], li destpêka tiştekî.
##: {{mk|ku|Ez ê serê sala '''nû''' bêm te bibînim.}}
##: {{nimûne|ku|test}}
## Tiştê ku ne ji mêj ve hatiye dîtin yan hayjêbûn.
##: {{mk|ku|Parîs ji min re '''nû''' bû.}}
##* {{jêder|ku|test, 2015|jêgirtin=test}}
## Yê ne ji mêj ve bûye tiştek.
##: {{mk|ku|Du xwendekarên '''nû''' li zanîngehê.}}
## [[guherî]], Yê wek xwe nemaye
##: {{mk|ku|Ew bûye mirovek '''nû'''.}}
==== Ji wêjeya klasîk ====
* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Lê xettê reyhanî li dêm<br />Katib ji '''nû''' i'rab-i da<br />Nazikqeda '''nû'''rînbeşer}}
==== Bi alfabeyên din ====
* {{ku-ar|نوو}}
* {{ku-kr|ну}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
|new-
|nev-
|nuh
|nûh
|nwî<ee:behdînanî>
|nî<ee:behdînanî>
|nûv
}}
==== Hevmane ====
* [[hecer]]
* [[modern]]
* [[taze]], [[teze]]
==== Dijmane ====
* [[kevn]]
==== Jê ====
{{kol3|ku|cure=Jê
|nû bûn<q:lêker>
|nû kirin<q:lêker>
|nûbûn<q:navdêr>
|nûkirin<q:navdêr>
|nûbûyî<q:rengdêr>
|nûkirî<q:rengdêr>
|nipînû
|nûçe
|nûjen
|nûjenî
|nûjenxwaz
|nûjenxwazî
|nûxwaz
|nûxwazî
|nûyandin
|nûyandî
|nûyîn
|nûyîtî
|venûyandin
|venûyîn
}}
==== Etîmolojî ====
Hevreha soranî {{g|ckb|نوێ|tr=nwê}}, [[kurdiya başûrî]] ''[[nü]]'', hewramî ''[[no]]'', zazakî ''[[newe]]'', farisî {{g|fa|نو|tr=now}}, belûçî {{g|bal|نوک|tr=nok}}, pehlewî [[𐭭𐭥𐭪𐭩]] / [[𐫗𐫇𐫃]] (nwk/nwg /nōg/), peştûyî {{g|ps|نوی|tr=nevey, nivey}}, osetî [[нӕуӕг]] (næwæg) û [[ног]] (nog), bextiyariya kevn [[νωγο]] (nōgo), avestayî [[𐬥𐬀𐬎𐬎𐬀]] (nauua, /newe/)... sanskrîtî [[नव]] (náva), hindî [[नवा]] (navā)... hîtîtî [[𒉋𒀸]] (nēwas), luwiyayî [[𒈾𒀀𒌑𒉿𒋾]] (nāúwati)... bulgarî [[нов]] (nov), çekî ''[[nový]]'', polonî ''[[nowy]]'', rusî [[но́вый]] (nóviy)... latînî ''[[novus]]'', îtalî ''[[novo]], [[nuovo]]'', portugalî ''[[novo]]'', spanî ''[[nuevo]]'', fransî ''[[neuf]], [[nouveau]]''... yûnaniya kevn û nû [[νέος]] (néos)... ermeniya kevn û nû [[նոր]] (nor)... lîtwanî ''[[naũjas]]''... bretonî ''[[nevez]]'', îrlendî ''[[nua]]''... almanî ''[[neu]]'', danmarkî/norwecî/swêdî ''[[ny]]'', holendî ''[[nieuw]]'', îngilîzî ''[[new]]''... hemû ji {{ji|ine-pro|ku|-}} {{recons|ine-pro|néwos}}, {{recons|ine-pro|néwyos}} (nû).
;{{biçûk|Çavkanî:
* {{Chyet}}
* {{Horn}}
* {{MacKenzie-2}}
* {{Pokorny}}
* {{The Tower of Babel}}
* {{Tsabolov}}
* {{Watkins}}
}}
==== Bi zaravayên din ====
* {{Z|ckb}}: {{W-|ckb|نوێ|tr=[[nwê]]}}
* {{Z|sdh}}: {{W-|sdh|sc=Latn|nü}}
* {{Z|hac}}: {{W-|hac|sc=Latn|no}}
* {{Z|zza}}: {{W+|zza|newe}}
=== Rengdêr ===
{{rengdêr|ku}}
# test
k9scm927t17r9qxrb38idqfg054d36p
mertek
0
23323
6531788
6441640
2026-09-16T10:14:13Z
Joseph Hemê
2610
/* Formên alternatîv */
6531788
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Castle beam01.jpg|thumb|gelender]]
{{navdêr|ku|z=n}}
# [[karîte]], [[max]], [[kêran]], [[beşt]], [[ribanek]], [[nîre]], [[şeqle]], [[girş]], [[onî]], [[gelender]], [[mirdiyaq]], [[garîte]], <br>[[dar]]ê yan [[asin]]ê [[dirêj]] û [[qayîm]] (bi taybetî yê ku [[ban]] xwe li ser [[ragirtin|radigire]])
==== Tewandin ====
{{ku-tewîn-nav|nêr}}
==== Formên alternatîv ====
* [[martag]]
* [[martak]]
* [[merteg]]
* [[mircaq]]
==== Jê ====
* [[mertekî]]
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W+|af|balk}}
* {{Z|de}}: {{W|de|Balken|m}}
* {{Z|da}}: {{W+|da|bjælke}}
* {{Z|eo}}: {{W+|eo|trabo}}
* {{Z|fo}}: {{W+|fo|bjálki}}
* {{Z|fi}}: {{W+|fi|poikkipuu}}
* {{Z|fy}}: {{W+|fy|balke}}
* {{Z|nl}}: {{W+|nl|balk}}, {{W+|nl|onderlegger}}, {{W+|nl|ribbe}}
* {{Z|en}}: (thick wooden) [[beam]] (used in ceiling), {{W+|en|beam}}, {{W+|en|lotus}}
* {{Z|it}}: {{W+|it|trave}}
* {{Z|pap}}: {{W+|pap|balki}}, {{W+|pap|biga}}
* {{Z|pt}}: {{W+|pt|barrote}}, {{W+|pt|biga}}, {{W+|pt|trave}}
* {{Z|ru}}: {{W+|ru|балка}}, {{W+|ru|бревно}}, {{W+|ru|брус}}
* {{Z|es}}: {{W+|es|viga}}, {{W+|es|trabe}}, {{W+|es|traviesa}}
* {{Z|sv}}: {{W+|sv|balk}}, {{W+|sv|bjälke}}
* {{Z|tr}}: {{W+|tr|mertek}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=eekmrt|termek}}
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[karîte]], [[max]], [[kêran]], [[beşt]], [[ribanek]], [[nîre]], [[şeqle]], [[girş]], [[onî]], [[gelender]], [[mirdiyaq]], [[garîte]], mertek
==== Anagram ====
* {{anagram|tr|a=eekmrt|kertme}}
3snsu844mjec5mwgksu92pzbuqgmyvt
komentar
0
23938
6531708
6441918
2026-09-15T18:32:47Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531708
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[zelal|Zelal]]kirina [[bingeh]] û [[sedem]]ên [[mijar]]ekê yan [[bûyer]]ekê.
#: {{hevmane|ku|şirove|ravek|ravekirin|îzah|şerh|tefsîr|ronkirin}}
#* {{jêder|ku|Reklamek bernameya ''Panorama Hefteyê'' yan Rojhelat TV di Netkurd.com de, 9/2006|jêgirtin=Cemal Batun û Mûrad Ciwan rojev û bûyerên hefteyê di Rojhelat TV de her hefte '''komentar''' dikin.}}
# {{kontrol|rêz=erê|[[têbînî]], [[not]], [[ronkirin]], [[koment]]}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Nêzîk ====
{{kol3|ku
|koment
}}
==== Jê ====
{{kol3|ku
|komentarî
}}
==== Etîmolojî ====
Bi rêya fransî ''[[commentaire]]'' ji latînî ''[[commentarius]], [[commentarium]]''; herwiha bi îngilîzî ''[[commentary]]''
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W|tr|şerh}}
{{werger-bin}}
ryj6mb8a6o7huqa5cr85bdpsh00b9pf
6531722
6531708
2026-09-15T20:12:20Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531722
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[zelal|Zelalkirina]] [[bingeh]] û [[sedem]]ên [[mijar]]ekê yan [[bûyer]]ekê.
#: {{hevmane|ku|şirove|ravek|ravekirin|îzah|şerh|tefsîr|ronkirin}}
#* {{jêder|ku|Reklamek bernameya ''Panorama Hefteyê'' yan Rojhelat TV di Netkurd.com de, 9/2006|jêgirtin=Cemal Batun û Mûrad Ciwan rojev û bûyerên hefteyê di Rojhelat TV de her hefte '''komentar''' dikin.}}
# {{kontrol|rêz=erê|[[têbînî]], [[not]], [[ronkirin]], [[koment]]}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Nêzîk ====
{{kol3|ku
|koment
}}
==== Jê ====
{{kol3|ku
|komentarî
}}
==== Etîmolojî ====
Bi rêya fransî ''[[commentaire]]'' ji latînî ''[[commentarius]], [[commentarium]]''; herwiha bi îngilîzî ''[[commentary]]''
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W|tr|şerh}}
{{werger-bin}}
mrgi3k60mt3d09knah91wja7sj4x9hw
prîvat
0
27735
6531747
6445240
2026-09-15T20:42:11Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531747
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Rengdêr ===
{{ku-tewîn-rd|prîvat}}
{{rengdêr|ku}}
# Tişta/ê [[kes]]ekî yan çend kesan lê ne ya hemû [[xelk]]ê.
#: {{hevmane|ku|şexsî|ferdî|arizî|personal|întîm}}
#* {{jêder|ku|Îbrahîm Mihemed Şêxo li ser babê xwe Mihemed Şêxo li gor Amude.de, 3/2006|jêgirtin=Jiyana wî ya hunerî û ya '''prîvat''' ji hev cuda bûn. Ku ew bi me re diştexilî, bêhna wî tim teng bû. Lê dema li muzîkê dixist, ew yekî din bû. Te digot qey ev zilam ne ew zilam e.}}
# {{kontrol|rêz=erê|[[kesîn]], [[kesane]]}}
==== Jê ====
{{kol3|ku
|arpîvatî
}}
==== Etîmolojî ====
Ji latînî
==== Werger ====
{{werger-ser}}
* {{Z|fi}}: {{W+|fi|henkilökohtainen}}, {{W+|fi|yksityinen}}
* {{Z|fr}}: {{W+|fr|privé}}
* {{Z|en}}: {{W+|en|personal}}, {{W+|en|private}}
* {{Z|tr}}: {{W+|tr|kişisel}}, {{W+|tr|şahsi}}
{{werger-bin}}
5c6x1u1l3ebd2qfs2xbumilinoe4xn2
efor
0
30213
6531713
6447905
2026-09-15T18:34:52Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531713
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/ɛˈfoːɾ/}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[têkoşîn]], [[xebat]], [[ked]], [[hewl]], [[mihawele]], [[cehd]], [[pîkol]], [[berxwedan]], [[rênc]], [[renc]]
#* {{jêder-websîte|ku|jêgirtin=Wekî gelek kesan ez jî dixwazim çi '''efora''' min heye ji bo pexşanê bidim der.|nivîskar=Cîhan Roj|sernav=Peyv û pêsîr|websîte=Diyarname.com|sal=12/2006}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
Ji {{deyn|fr|ku|effort}} ji {{hv|la|ex-}} + [[fortis]]
==== Anagram ====
* {{anagram|ku|a=efor|Fero}}
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[ked]], [[cehd]]
i4qy5tje2hzlu496b2et42dog7g9n9g
damilîn
0
30760
6531772
6448328
2026-09-16T05:32:22Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531772
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Lêker ===
{{lêker|ku|c=ngh}}
# [[korîşkî]] bûn, [[qirç]] bûn, [[miçîn]]
#* {{jêder-websîte|ku|jêgirtin=“'''Damilîn'''” di tirkî de “yumulma” ye û dema mirov xwe bide tiştekî û pê dilebike tê bikaranîn, “Ez '''damilîm''' xwarinê û min xwar”.|nivîskar=Jîr DIlovan|sernav=Di pêşketina ziman de girîngiya wergerê|websîte=Welat.com|sal=1/2006}}
==== Tewandin ====
: da-mil-
==== Bi alfabeyên din ====
* {{ku-ar|داملین}}
==== Formên alternatîv ====
{{kol3|ku
|damiliyan
|damilyan
}}
==== Jê ====
{{kol3|ku
|damiliyayî
|damilî
}}
==== Têkildar ====
{{kol3|ku
|damilandin
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|tr}}:{{W+|tr|yumuklaşma}}, {{W+|tr|yumuklaşmak}}, {{W+|tr|yumulma}}, {{W+|tr|yumulmak}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# {{mane?|ku}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=adiîlmn|dilmanî|mîladin}}
gqtv98wykbdzgtbjmtpzdfmwinz3sge
senaryo
0
32556
6531700
6450043
2026-09-15T18:28:10Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) --bêçavkanî [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531700
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[fîlmname]]
# [[bernivîs]]a [[film]]ekê yan [[pirtûk]]ekê
#* {{jêder|ku|Jennifer Lopez li gor Nefel.com, 2/2007|jêgirtin="Ji ber ko '''senaryo''' ji jiyana rastîn gelek kuştina jin û keçan tesîr li min kir. Berî ez '''senaryoyê''' bixwînim min bawer nedikir tiştên wisa bibin. Armanca min ew bû ko bi vê rolê mirovan di vî warî de bikijînim qadeke xesas, balê bikişînim ser bûyerên bi vî rengî û mirovan ronî bikim. Ez hêvî dikim di vî warî de pêşketinên baş pêk bên."}}
# [[pêşbînî|pêşbîniya]] [[bûyer]]ên [[pêşeroj]]ê
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|sinaryo
|sînaryo
}}
==== Jê ====
{{kol3|ku
|senaryovan
|senaryovanî
|senaryoyî
}}
==== Etîmolojî ====
Ji {{ji|it}} [[scenario]] ji {{ji|la}} [[scaena]] ji {{ji|el}} [[σκηνή]] ([[dik]], [[şano]]).
==== Bi zaravayên din ====
* soranî: [[sînaryo]]
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Drehbuch|n}}
* {{Z|ar}}: {{W-|ar|سيناريو}}
* {{Z|fa}}: {{W+|fa|سناریو|guman=erê}}
* {{Z|fi}}: {{W+|fi|skenaario}}, {{W+|fi|käsikirjoitus}}
* {{Z|fr}}: {{W+|fr|scénario|m}}
* {{Z|nl}}: {{W+|nl|draaiboek|n}}
* {{Z|en}}: {{W+|en|scenario}}, {{W+|en|screenplay}}, {{W+|en|continuity}}
* {{Z|pt}}: {{W+|pt|cenário}}
* {{Z|sv}}: {{W+|sv|scenario|n}}
* {{Z|tr}}: {{W-|tr|#Tirkî}}
{{werger-bin}}
== {{ziman|tr}} ==
=== Bilêvkirin ===
* {{deng|tr|LL-Q256 (tur)-ToprakM-senaryo.wav|Deng|dever=}}
=== Navdêr ===
{{navdêr|tr}}
# [[#Kurdî|senaryo]]
j8zjks6rl37u4e0nh4dcx21fbvewv5o
chauffeur
0
34092
6531739
6349372
2026-09-15T20:36:37Z
Wikihez
7344
/* Dûnde */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:chauffeur|en.wikt]]
6531739
wikitext
text/x-wiki
== {{ziman|fr}} ==
=== Bilêvkirin ===
* {{deng|fr|LL-Q150 (fra)-Poslovitch-chauffeur.wav|Deng|dever=Cornimont, Fransa}}
=== Navdêr ===
{{navdêr|fr|z=n}}
# [[şofêr]], [[ajovan]], [[ajokar]], [[sayêq]]
==== Etîmolojî ====
Ji {{qertaf|fr|chauffer|-eur}}.
==== Dûnde ====
{{stûn|
* {{dû|deyn=1|ca|xofer|xòfer}}
* {{dû|cs|šofér|deyn=1}}
* {{dû|da|chauffør|deyn=1}}
* {{dardû|nl|chauffeur|deyn=1}}
* {{dû|en|chauffeur|deyn=1}}
* {{dû|eo|ŝoforo|ji=1}}
* {{dû|de|Chauffeur|deyn=1}}
* {{dû|hu|sofőr|deyn=1}}
* {{dû|it|chauffeur|deyn=1}}
* {{dû|jv|chauffeur|deyn=1|q=New Caledonian}}
* {{dû|lad|shofer|shofor|deyn=1}}
* {{dû|lv|šoferis|deyn=1}}
* {{dû|ln|sofɛ́lɛ|deyn=1}}
* {{dû|lt|šoferis|deyn=1}}
* {{dû|ary|شيفور|deyn=1}}
* {{dû|no|sjåfør|deyn=1}}
* {{dû|fa|شوفر|tr=šufer|deyn=1}}
* {{dû|pl|szofer|deyn=1}}
* {{dû|pt|chofer|deyn=1|alts=1}}
* {{dû|ro|șofer|deyn=1}}
* {{dardû|ru|шофёр|deyn=1}}
* {{dû|sh|шофер|cuda=шо̀фе̄р m|sc=Cyrl|deyn=1}}
* {{dû|es|chofer|deyn=1}}
* {{dû|sv|chaufför|deyn=1}}
* {{dû|th|โชเฟอร์|deyn=1}}
* {{dû|tr|şoför|deyn=1}}
}}
== {{ziman|nl}} ==
=== Navdêr ===
{{navdêr|nl|z=n}}
# {{mijar|nl|pîşe}} {{g|ku|şofêr}}, {{g|ku|ajovan}}
=== Çavkanî ===
{{rûpela wergerê|nl|şofêr}}
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|En-us-chauffeur.ogg|dever=DYA}}
=== Navdêr ===
{{navdêr|en}}
# [[hajo]], [[ajo]] <ref>Ferhenga Kurdî-Îngîlîzî ya Salah Sadallah</ref>
=== Çavkanî ===
{{çavkanî}}
8t478km5o7ejna2dar3efuikvg6sbj0
6531740
6531739
2026-09-15T20:36:54Z
Wikihez
7344
/* Dûnde */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531740
wikitext
text/x-wiki
== {{ziman|fr}} ==
=== Bilêvkirin ===
* {{deng|fr|LL-Q150 (fra)-Poslovitch-chauffeur.wav|Deng|dever=Cornimont, Fransa}}
=== Navdêr ===
{{navdêr|fr|z=n}}
# [[şofêr]], [[ajovan]], [[ajokar]], [[sayêq]]
==== Etîmolojî ====
Ji {{qertaf|fr|chauffer|-eur}}.
==== Dûnde ====
{{stûn|
* {{dû|kurmancî|şofêr|deyn=1}}
* {{dû|deyn=1|ca|xofer|xòfer}}
* {{dû|cs|šofér|deyn=1}}
* {{dû|da|chauffør|deyn=1}}
* {{dardû|nl|chauffeur|deyn=1}}
* {{dû|en|chauffeur|deyn=1}}
* {{dû|eo|ŝoforo|ji=1}}
* {{dû|de|Chauffeur|deyn=1}}
* {{dû|hu|sofőr|deyn=1}}
* {{dû|it|chauffeur|deyn=1}}
* {{dû|jv|chauffeur|deyn=1|q=New Caledonian}}
* {{dû|lad|shofer|shofor|deyn=1}}
* {{dû|lv|šoferis|deyn=1}}
* {{dû|ln|sofɛ́lɛ|deyn=1}}
* {{dû|lt|šoferis|deyn=1}}
* {{dû|ary|شيفور|deyn=1}}
* {{dû|no|sjåfør|deyn=1}}
* {{dû|fa|شوفر|tr=šufer|deyn=1}}
* {{dû|pl|szofer|deyn=1}}
* {{dû|pt|chofer|deyn=1|alts=1}}
* {{dû|ro|șofer|deyn=1}}
* {{dardû|ru|шофёр|deyn=1}}
* {{dû|sh|шофер|cuda=шо̀фе̄р m|sc=Cyrl|deyn=1}}
* {{dû|es|chofer|deyn=1}}
* {{dû|sv|chaufför|deyn=1}}
* {{dû|th|โชเฟอร์|deyn=1}}
* {{dû|tr|şoför|deyn=1}}
}}
== {{ziman|nl}} ==
=== Navdêr ===
{{navdêr|nl|z=n}}
# {{mijar|nl|pîşe}} {{g|ku|şofêr}}, {{g|ku|ajovan}}
=== Çavkanî ===
{{rûpela wergerê|nl|şofêr}}
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|En-us-chauffeur.ogg|dever=DYA}}
=== Navdêr ===
{{navdêr|en}}
# [[hajo]], [[ajo]] <ref>Ferhenga Kurdî-Îngîlîzî ya Salah Sadallah</ref>
=== Çavkanî ===
{{çavkanî}}
pt6q5xrmdaky0r2sccvg4ypt0388fcd
6531741
6531740
2026-09-15T20:37:02Z
Wikihez
7344
/* Dûnde */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531741
wikitext
text/x-wiki
== {{ziman|fr}} ==
=== Bilêvkirin ===
* {{deng|fr|LL-Q150 (fra)-Poslovitch-chauffeur.wav|Deng|dever=Cornimont, Fransa}}
=== Navdêr ===
{{navdêr|fr|z=n}}
# [[şofêr]], [[ajovan]], [[ajokar]], [[sayêq]]
==== Etîmolojî ====
Ji {{qertaf|fr|chauffer|-eur}}.
==== Dûnde ====
{{stûn|
* {{dû|ku|şofêr|deyn=1}}
* {{dû|deyn=1|ca|xofer|xòfer}}
* {{dû|cs|šofér|deyn=1}}
* {{dû|da|chauffør|deyn=1}}
* {{dardû|nl|chauffeur|deyn=1}}
* {{dû|en|chauffeur|deyn=1}}
* {{dû|eo|ŝoforo|ji=1}}
* {{dû|de|Chauffeur|deyn=1}}
* {{dû|hu|sofőr|deyn=1}}
* {{dû|it|chauffeur|deyn=1}}
* {{dû|jv|chauffeur|deyn=1|q=New Caledonian}}
* {{dû|lad|shofer|shofor|deyn=1}}
* {{dû|lv|šoferis|deyn=1}}
* {{dû|ln|sofɛ́lɛ|deyn=1}}
* {{dû|lt|šoferis|deyn=1}}
* {{dû|ary|شيفور|deyn=1}}
* {{dû|no|sjåfør|deyn=1}}
* {{dû|fa|شوفر|tr=šufer|deyn=1}}
* {{dû|pl|szofer|deyn=1}}
* {{dû|pt|chofer|deyn=1|alts=1}}
* {{dû|ro|șofer|deyn=1}}
* {{dardû|ru|шофёр|deyn=1}}
* {{dû|sh|шофер|cuda=шо̀фе̄р m|sc=Cyrl|deyn=1}}
* {{dû|es|chofer|deyn=1}}
* {{dû|sv|chaufför|deyn=1}}
* {{dû|th|โชเฟอร์|deyn=1}}
* {{dû|tr|şoför|deyn=1}}
}}
== {{ziman|nl}} ==
=== Navdêr ===
{{navdêr|nl|z=n}}
# {{mijar|nl|pîşe}} {{g|ku|şofêr}}, {{g|ku|ajovan}}
=== Çavkanî ===
{{rûpela wergerê|nl|şofêr}}
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|En-us-chauffeur.ogg|dever=DYA}}
=== Navdêr ===
{{navdêr|en}}
# [[hajo]], [[ajo]] <ref>Ferhenga Kurdî-Îngîlîzî ya Salah Sadallah</ref>
=== Çavkanî ===
{{çavkanî}}
mez355gn9n2dabknto6zgh1jh8cs10e
konsantrasyon
0
38463
6531711
6457510
2026-09-15T18:34:14Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531711
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[konsantrebûn]], [[bal]]a xwe dan tiştekî û xwe bi wî tiştî ve [[mijûl]] kirin <br>(û tiştên din [[piştguh]] kirin)
#* {{jêder-websîte|ku|jêgirtin=“Vegotina xewnekê” ya bi şiklê wênekirin an jî helbestkirinê qabîliyetek e, lê ji “dîtina xewnekê” re qabilîyet ne pêwîst e; lê xeyal ji ber ku '''konsantrasyon''' û medîtasyonekê dixwaze û ji ber ku '''konsantrasyon''' û medîtasyon jî alavên hunerê ne û huner bi qabîliyetê çêdibe, xeyalkirin qabîliyetek e...|nivîskar=Rênas Jiyan|sernav=Xewn û xeyal|websîte=Rojava.net|sal=2007}}
# [[berhev]]kirin, [[civandin]], [[kom]]kirin
# terkîz
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|konsentrasyon
}}
==== Têkildar ====
{{kol3|ku
|konsantre
}}
==== Etîmolojî ====
Ji {{deyn|fr|ku|concentration}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W|de|Konzentration|f}}
* {{Z|fi}}: {{W+|fi|keskittyminen}}
* {{Z|fr}}: {{W+|fr|concentration}}
* {{Z|he}}: {{W+|he|ריכוז}}
* {{Z|en}}: {{W+|en|concentration}}
* {{Z|it}}: {{W+|it|concentrazione}}
* {{Z|pt}}: {{W|pt|concentração|f}}
* {{Z|es}}: {{W+|es|concentración}}
* {{Z|sv}}: {{W+|sv|konsentration}}
* {{Z|tr}}: {{W+|tr|konsantrasyon}}, {{W|tr|dikkat toplaşımı|guman=erê}}
{{werger-bin}}
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[tîrbûn]]
# [[lêhûrbûn]], [[têfîkirîn]], konsantrasyon
2mo4n8a9xfac2mk08x8ipwm8bfmiexg
bîranîn
0
38571
6531774
6457640
2026-09-16T05:52:55Z
Wikihez
7344
Kurmancî/Navdêr: mane, hevmane hatin guhartin, maneyek hate rakirin (DefSideBoxes)
6531774
wikitext
text/x-wiki
{{bnr-l|bîr anîn}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/biːɾɑːˈniːn/}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-bîranîn.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[hedîse|Hedîse]]yên [[berê]] yên ku tên bîra yekî.
#: {{hevmane|ku|bîrhatin|xatirat|serpêhatî|serborî|serguzeşte}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Bi alfabeyên din ====
* {{ku-ar|بیرئانین}}
==== Formên alternatîv ====
* [[bibîranîn]]
==== Etîmolojî ====
Ji {{qertaf|ku|bîr|anîn}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Erinnerung|m}}, {{W+|de|Denkschrift|f|guman=erê}}
* {{Z|ar}}: {{W-|ar|ذكرى}}, {{W-|ar|تذكار|guman=erê}}, {{W-|ar|الإستذكار|guman=erê}}, {{W-|ar|التذاكر|guman=erê}}, {{W-|ar|التذكرة|guman=erê}}
* {{Z|fa}}: {{W+|fa|ذکر کردن}}, {{W+|fa|یادبود}}, {{W+|fa|بزرگداشت|guman=erê}}
* {{Z|fi}}: {{W-|fi|muistoja}}, {{W-|fi|muistot}}, {{W+|fi|muistaa}}
* {{Z|en}}: {{W+|en|memorial}}, {{W+|en|reminiscence}}, {{W+|en|memorandum|guman=erê}}, {{W+|en|memory}}
* {{Z|es}}: {{W+|es|recuerdo}}
* {{Z|tr}}: {{W+|tr|anı}}, {{W+|tr|anıt}}, {{W+|tr|anma}}, {{W+|tr|yad}}, {{W+|tr|zikir}}, {{W+|tr|hatıra}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=abîînnr|rabînîn}}
l3gk2naci8p8pjddzimdlii2miwjv9t
6531782
6531774
2026-09-16T07:12:09Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531782
wikitext
text/x-wiki
{{bnr-l|bîr anîn}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/biːɾɑːˈniːn/}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-bîranîn.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[hedîse|Hedîseyên]] [[berê]] yên ku tên bîra yekî.
#: {{hevmane|ku|bîrhatin|xatirat|serpêhatî|serborî|serguzeşte}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Bi alfabeyên din ====
* {{ku-ar|بیرئانین}}
==== Formên alternatîv ====
* [[bibîranîn]]
==== Etîmolojî ====
Ji {{qertaf|ku|bîr|anîn}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Erinnerung|m}}, {{W+|de|Denkschrift|f|guman=erê}}
* {{Z|ar}}: {{W-|ar|ذكرى}}, {{W-|ar|تذكار|guman=erê}}, {{W-|ar|الإستذكار|guman=erê}}, {{W-|ar|التذاكر|guman=erê}}, {{W-|ar|التذكرة|guman=erê}}
* {{Z|fa}}: {{W+|fa|ذکر کردن}}, {{W+|fa|یادبود}}, {{W+|fa|بزرگداشت|guman=erê}}
* {{Z|fi}}: {{W-|fi|muistoja}}, {{W-|fi|muistot}}, {{W+|fi|muistaa}}
* {{Z|en}}: {{W+|en|memorial}}, {{W+|en|reminiscence}}, {{W+|en|memorandum|guman=erê}}, {{W+|en|memory}}
* {{Z|es}}: {{W+|es|recuerdo}}
* {{Z|tr}}: {{W+|tr|anı}}, {{W+|tr|anıt}}, {{W+|tr|anma}}, {{W+|tr|yad}}, {{W+|tr|zikir}}, {{W+|tr|hatıra}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=abîînnr|rabînîn}}
hi2sy88je7mvyvgdqr1j69k439hvjpm
eşqiya
0
40928
6531758
6461213
2026-09-15T21:39:22Z
Wikihez
7344
Kurmancî/Navdêr: mane, hevmane hatin guhartin, beşa «Tewandin» hate zêdekirin (DefSideBoxes)
6531758
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=mn}}
# Kesa/ê ku bi [[darê zorê]] tiştekî ji [[xwedan]]ên wê [[standin|distîne]] û [[revandin|direvîne]].
#: {{hevmane|ku|rêbir|keleş|rêgir|heydûd|nijdevan|cerdevan|çete}}
# [[revok]], [[firar]], [[harib]]
# ''[[pîşe|Pîşê]] min çi bû li ser van çiya <br>Vîna Kurdistan kirbûm '''eşqiya''''' (Cegerxwîn, 1903 - 1984)
==== Tewandin ====
{{ku-tewîn-nav|mê û nêr}}
==== Jê ====
* [[eşqiyatî]]
* [[eşqiyayî]]
==== Etîmolojî ====
Ji {{ji|ar|ku|أشقياء|tr=aşḳiyāˀ|wate=feqîr, bedbext}} ji {{g|ar|شَقِيّ|tr=şaḳīy|w=bedbext}}.
<small>'''Çavkanî:''' {{malper|nivîskar=Sevan NİŞANYAN|url=https://www.nisanyansozluk.com/?k=e%C5%9Fk%C4%B1ya&lnk=1|sernav=Eşkıya|malper=nisanyansozluk.com|ziman=ku}}</small>
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W|de|Bandit|m}}, {{W|de|Räuber|m|guman=erê}}
* {{Z|fi}}: {{W+|fi|kaappaaja}}
* {{Z|en}}: {{W+|en|pirate}}, {{W+|en|hijacker}}, {{W+|en|rapparee}}, {{W+|en|robber}}
* {{Z|tr}}: {{W+|tr|haydut}}, {{W+|tr|korsan}}, {{W+|tr|şaki}}, {{W+|tr|eşkiya}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=aeiqşy|şiqeya}}
a14z0n6y3kncu69hl7cjb42jxbnj50e
6531759
6531758
2026-09-15T21:41:15Z
Wikihez
7344
/* Navdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531759
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=mn}}
# Kesa/ê ku bi [[darê zorê]] tiştekî ji [[xwedan]]ên wê [[standin|distîne]] û [[revandin|direvîne]].
#: {{hevmane|ku|rêbir|keleş|rêgir|heydûd|nijdevan|cerdevan|çete}}
# [[revok]], [[firar]], [[harib]]
#* {{jêder|ku|jg=[[pîşe|Pîşê]] min çi bû li ser van çiya <br>Vîna Kurdistan kirbûm '''eşqiya'''|Cegerxwîn, 1903 - 1984}}
==== Tewandin ====
{{ku-tewîn-nav|mê û nêr}}
==== Jê ====
* [[eşqiyatî]]
* [[eşqiyayî]]
==== Etîmolojî ====
Ji {{ji|ar|ku|أشقياء|tr=aşḳiyāˀ|wate=feqîr, bedbext}} ji {{g|ar|شَقِيّ|tr=şaḳīy|w=bedbext}}.
<small>'''Çavkanî:''' {{malper|nivîskar=Sevan NİŞANYAN|url=https://www.nisanyansozluk.com/?k=e%C5%9Fk%C4%B1ya&lnk=1|sernav=Eşkıya|malper=nisanyansozluk.com|ziman=ku}}</small>
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W|de|Bandit|m}}, {{W|de|Räuber|m|guman=erê}}
* {{Z|fi}}: {{W+|fi|kaappaaja}}
* {{Z|en}}: {{W+|en|pirate}}, {{W+|en|hijacker}}, {{W+|en|rapparee}}, {{W+|en|robber}}
* {{Z|tr}}: {{W+|tr|haydut}}, {{W+|tr|korsan}}, {{W+|tr|şaki}}, {{W+|tr|eşkiya}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=aeiqşy|şiqeya}}
618xeu1vj1uybl9elofvzm4joq2pbp9
6531760
6531759
2026-09-15T21:42:48Z
Wikihez
7344
/* Navdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531760
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=mn}}
# Kesa/ê ku bi [[darê zorê]] tiştekî ji [[xwedan]]ên wê [[standin|distîne]] û [[revandin|direvîne]].
#: {{hevmane|ku|rêbir|keleş|rêgir|heydûd|nijdevan|cerdevan|çete}}
# [[revok]], [[firar]], [[harib]]
#* {{jêder-kitêb|ku|jg=[[pîşe|Pîşê]] min çi bû li ser van çiya <br>Vîna Kurdistan kirbûm '''eşqiya'''|nivîskar=w:Cegerxwîn}}
==== Tewandin ====
{{ku-tewîn-nav|mê û nêr}}
==== Jê ====
* [[eşqiyatî]]
* [[eşqiyayî]]
==== Etîmolojî ====
Ji {{ji|ar|ku|أشقياء|tr=aşḳiyāˀ|wate=feqîr, bedbext}} ji {{g|ar|شَقِيّ|tr=şaḳīy|w=bedbext}}.
<small>'''Çavkanî:''' {{malper|nivîskar=Sevan NİŞANYAN|url=https://www.nisanyansozluk.com/?k=e%C5%9Fk%C4%B1ya&lnk=1|sernav=Eşkıya|malper=nisanyansozluk.com|ziman=ku}}</small>
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W|de|Bandit|m}}, {{W|de|Räuber|m|guman=erê}}
* {{Z|fi}}: {{W+|fi|kaappaaja}}
* {{Z|en}}: {{W+|en|pirate}}, {{W+|en|hijacker}}, {{W+|en|rapparee}}, {{W+|en|robber}}
* {{Z|tr}}: {{W+|tr|haydut}}, {{W+|tr|korsan}}, {{W+|tr|şaki}}, {{W+|tr|eşkiya}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=aeiqşy|şiqeya}}
0utge287q46o24n6bnyskyc504nkhw6
çoxik
0
41115
6531779
6461491
2026-09-16T07:07:25Z
Wikihez
7344
/* Navdêr */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: sererastkirin
6531779
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=n}}
# [[çakêt|Çakêt]]ên [[bêhuçk]] yan [[huçik]][[kurt]]; [[gumlek]]ên [[huçik]][[kurt]] (bi taybetî yên [[stûr]] yên ji [[hirî|hiriyê]]).
#: {{hevmane|ku|qutik|êlek}}
#* {{jêder-vîdeo|ku|hunermend=Erdawan Zaxoyi|platform=w:Youtube|sernav=Erdawan Zaxoyi - Çoxe Minoارده وان زاخۆی - چووخکێ منو|tarîx=2015-02-19|url=https://www.youtube.com/watch?v=dspX2PBd_aU|tarîxa-gihiştinê=2026-09-16|jêgirtin='''Çoxik'''ê mino <br> Çoxê mino hiriya berana deyî malê mino hiriya berana <br> Dav û ristin ji destên nezanan deyî dav û ristin ji destên nezanan <br> '''Çoxik''' teng e nagirit milano deyî malê teng e nagirit milano <br> Çoxê mino hiriya bizina deyî malê mino hiriya bizina <br> Dav û ristin ji destên bîjinan dey dav û ristin ji destên bîjinan <br> '''Çoxik''' teng e nagirit bazinan deyî malê teng e nagirit bazinan <br> Çoxê mino hiriya karikan deyî malê mino hiriya karikan <br> Dav û risitin ji destên keçikan deyî dav û ristin ji destên kiçikan <br> '''Çoxik''' teng e nagirit memikan deyî malê teng e na girit memikan}}
==== Bi alfabeyên din ====
* {{ku-ar|چۆخک}}
==== Formên alternatîv ====
* {{g|ku|çox}}
* {{g|ku|çoxk}}
==== Etîmolojî ====
Hevreha farisî gîlekî ''çoxe'' û {{g|fa|چوخه|tr=çûxê}} ku wek [[çuha]] ketibû tirkî jî. Ne diyar e ka bingehê wê ji {{ji|ira}} yan ji zimanekê din e.
==== Werger ====
{{werger-ser}}
* {{Z|ar}}: {{W-|ar|سترة}}, {{W-|ar|يلك}}
* {{Z|en}}: {{W+|en|vest}}, {{W+|en|broadcloth|guman=erê}}, {{W-|en|shepherd's goatshair waistcoat}}
* {{Z|tr}}: {{W+|tr|çuha}}, {{W+|tr|yelek|guman=erê}}, {{W+|tr|kutnu|guman=erê}}
{{werger-bin}}
==== Çavkanî ====
{{çavkanî}}
h4mgv7edzomyztxh4k6b0k4rdamv0t5
6531786
6531779
2026-09-16T07:27:45Z
Wikihez
7344
/* Navdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531786
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=n}}
# [[çakêt|Çakêt]]ên [[bêhuçk]] yan [[huçik]][[kurt]]; [[gumlek]]ên [[huçik]][[kurt]] (bi taybetî yên [[stûr]] yên ji [[hirî|hiriyê]]).
#: {{hevmane|ku|qutik|êlek}}
#* {{jêder-vîdeo|ku|hunermend=Erdawan Zaxoyi|platform=w:Youtube|sernav=Çoxe Mino|tarîx=2015-02-19|url=https://www.youtube.com/watch?v=dspX2PBd_aU|tarîxa-gihiştinê=2026-09-16|jêgirtin='''Çoxik'''ê mino <br> Çoxê mino hiriya berana deyî malê mino hiriya berana <br> Dav û ristin ji destên nezanan deyî dav û ristin ji destên nezanan <br> '''Çoxik''' teng e nagirit milano deyî malê teng e nagirit milano <br> Çoxê mino hiriya bizina deyî malê mino hiriya bizina <br> Dav û ristin ji destên bîjinan dey dav û ristin ji destên bîjinan <br> '''Çoxik''' teng e nagirit bazinan deyî malê teng e nagirit bazinan <br> Çoxê mino hiriya karikan deyî malê mino hiriya karikan <br> Dav û risitin ji destên keçikan deyî dav û ristin ji destên kiçikan <br> '''Çoxik''' teng e nagirit memikan deyî malê teng e na girit memikan}}
==== Bi alfabeyên din ====
* {{ku-ar|چۆخک}}
==== Formên alternatîv ====
* {{g|ku|çox}}
* {{g|ku|çoxk}}
==== Etîmolojî ====
Hevreha farisî gîlekî ''çoxe'' û {{g|fa|چوخه|tr=çûxê}} ku wek [[çuha]] ketibû tirkî jî. Ne diyar e ka bingehê wê ji {{ji|ira}} yan ji zimanekê din e.
==== Werger ====
{{werger-ser}}
* {{Z|ar}}: {{W-|ar|سترة}}, {{W-|ar|يلك}}
* {{Z|en}}: {{W+|en|vest}}, {{W+|en|broadcloth|guman=erê}}, {{W-|en|shepherd's goatshair waistcoat}}
* {{Z|tr}}: {{W+|tr|çuha}}, {{W+|tr|yelek|guman=erê}}, {{W+|tr|kutnu|guman=erê}}
{{werger-bin}}
==== Çavkanî ====
{{çavkanî}}
649bh6g9v458d11ge35aramq1cu61je
6531787
6531786
2026-09-16T08:12:08Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531787
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=n}}
# [[çakêt|Çakêtên]] [[bêhuçk]] yan [[huçik]][[kurt]]; [[gumlek]]ên [[huçik]][[kurt]] (bi taybetî yên [[stûr]] yên ji [[hirî|hiriyê]]).
#: {{hevmane|ku|qutik|êlek}}
#* {{jêder-vîdeo|ku|hunermend=Erdawan Zaxoyi|platform=w:Youtube|sernav=Çoxe Mino|tarîx=2015-02-19|url=https://www.youtube.com/watch?v=dspX2PBd_aU|tarîxa-gihiştinê=2026-09-16|jêgirtin='''Çoxik'''ê mino <br> Çoxê mino hiriya berana deyî malê mino hiriya berana <br> Dav û ristin ji destên nezanan deyî dav û ristin ji destên nezanan <br> '''Çoxik''' teng e nagirit milano deyî malê teng e nagirit milano <br> Çoxê mino hiriya bizina deyî malê mino hiriya bizina <br> Dav û ristin ji destên bîjinan dey dav û ristin ji destên bîjinan <br> '''Çoxik''' teng e nagirit bazinan deyî malê teng e nagirit bazinan <br> Çoxê mino hiriya karikan deyî malê mino hiriya karikan <br> Dav û risitin ji destên keçikan deyî dav û ristin ji destên kiçikan <br> '''Çoxik''' teng e nagirit memikan deyî malê teng e na girit memikan}}
==== Bi alfabeyên din ====
* {{ku-ar|چۆخک}}
==== Formên alternatîv ====
* {{g|ku|çox}}
* {{g|ku|çoxk}}
==== Etîmolojî ====
Hevreha farisî gîlekî ''çoxe'' û {{g|fa|چوخه|tr=çûxê}} ku wek [[çuha]] ketibû tirkî jî. Ne diyar e ka bingehê wê ji {{ji|ira}} yan ji zimanekê din e.
==== Werger ====
{{werger-ser}}
* {{Z|ar}}: {{W-|ar|سترة}}, {{W-|ar|يلك}}
* {{Z|en}}: {{W+|en|vest}}, {{W+|en|broadcloth|guman=erê}}, {{W-|en|shepherd's goatshair waistcoat}}
* {{Z|tr}}: {{W+|tr|çuha}}, {{W+|tr|yelek|guman=erê}}, {{W+|tr|kutnu|guman=erê}}
{{werger-bin}}
==== Çavkanî ====
{{çavkanî}}
0jqv9gt4afp2cvq42urrwsi36mup38i
retorîk
0
42230
6531773
6463276
2026-09-16T05:33:26Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531773
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[huner|Huner]]a [[axiftin]]a [[bedew]] û [[qayil]]kirina [[guhdar]]an yan [[xwendevan]]an.
#: {{hevmane|ku|rewanbêjî|xweşbêjî|[[xweş]][[peyivîn]]|belaxet|xîtabet}}
# [[peyv|Peyv]] û [[gotin]]ên [[spehî]] lê ne [[rast]], [[peyivîn]]a [[berbad]] ya ku li gel [[rastî|rastiyê]] [[guncîn|nagunce]].
#* {{jêder|ku|Netkurd.com, 6/2010|jêgirtin=Hukumeta tirk '''retorîkek''' nû bikar tîne. Di vê '''retorîkê''' de dijminatiya Îsraîlê û antîsemîtîzm berçav dibe.}}
# [[awa|Awa]]yê [[axiftin]]a kesekî.
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|rêtorîk
}}
==== Jê ====
{{kol3|ku
|retorîkî
}}
==== Etîmolojî ====
Ji {{ji|el}} [[ῥητορικὴ]] ([[huner]]a [[axiftin]]ê).
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Rhetorik|f}}
* {{Z|bg}}: {{W+|bg|реторика}}
* {{Z|zh}}: {{W+|zh|修辭}}
* {{Z|fi}}: {{W+|fi|retoriikka}}, {{W+|fi|kaunopuheisuus}}
* {{Z|fr}}: {{W+|fr|rhétorique}}
* {{Z|nl}}: {{W+|nl|retorica}}, {{W+|nl|welsprekendheid}}
* {{Z|en}}: {{W+|en|rhetoric}}
* {{Z|it}}: {{W+|it|retorica|f}}
* {{Z|ja}}: {{W-|ja|修辞技法}}
* {{Z|la}}: {{W-|la|ars rhetorica}}
* {{Z|pl}}: {{W+|pl|retoryka}}
* {{Z|pt}}: {{W+|pt|retórica}}
* {{Z|es}}: {{W+|es|retórica|f}}
* {{Z|sv}}: {{W+|sv|retorik}}
* {{Z|tr}}: {{W+|tr|belâgat}}, {{W+|tr|retorik}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=eîkorrt|rektorî|tîrkero}}
hksjp95oaxmtxu83q9vjxct0e79tbkq
6531775
6531773
2026-09-16T05:56:38Z
Wikihez
7344
/* Etîmolojî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531775
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[huner|Huner]]a [[axiftin]]a [[bedew]] û [[qayil]]kirina [[guhdar]]an yan [[xwendevan]]an.
#: {{hevmane|ku|rewanbêjî|xweşbêjî|[[xweş]][[peyivîn]]|belaxet|xîtabet}}
# [[peyv|Peyv]] û [[gotin]]ên [[spehî]] lê ne [[rast]], [[peyivîn]]a [[berbad]] ya ku li gel [[rastî|rastiyê]] [[guncîn|nagunce]].
#* {{jêder|ku|Netkurd.com, 6/2010|jêgirtin=Hukumeta tirk '''retorîkek''' nû bikar tîne. Di vê '''retorîkê''' de dijminatiya Îsraîlê û antîsemîtîzm berçav dibe.}}
# [[awa|Awa]]yê [[axiftin]]a kesekî.
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|rêtorîk
}}
==== Jê ====
{{kol3|ku
|retorîkî
}}
==== Etîmolojî ====
Ji {{ji|el|ku|ῥητορικὴ|t=[[huner]]a [[axiftin]]ê}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Rhetorik|f}}
* {{Z|bg}}: {{W+|bg|реторика}}
* {{Z|zh}}: {{W+|zh|修辭}}
* {{Z|fi}}: {{W+|fi|retoriikka}}, {{W+|fi|kaunopuheisuus}}
* {{Z|fr}}: {{W+|fr|rhétorique}}
* {{Z|nl}}: {{W+|nl|retorica}}, {{W+|nl|welsprekendheid}}
* {{Z|en}}: {{W+|en|rhetoric}}
* {{Z|it}}: {{W+|it|retorica|f}}
* {{Z|ja}}: {{W-|ja|修辞技法}}
* {{Z|la}}: {{W-|la|ars rhetorica}}
* {{Z|pl}}: {{W+|pl|retoryka}}
* {{Z|pt}}: {{W+|pt|retórica}}
* {{Z|es}}: {{W+|es|retórica|f}}
* {{Z|sv}}: {{W+|sv|retorik}}
* {{Z|tr}}: {{W+|tr|belâgat}}, {{W+|tr|retorik}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=eîkorrt|rektorî|tîrkero}}
0toficqi3s272ccyfqei02i18qx1v1e
6531781
6531775
2026-09-16T07:12:06Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531781
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[huner|Hunera]] [[axiftin]]a [[bedew]] û [[qayil]]kirina [[guhdar]]an yan [[xwendevan]]an.
#: {{hevmane|ku|rewanbêjî|xweşbêjî|[[xweş]][[peyivîn]]|belaxet|xîtabet}}
# [[peyv|Peyv]] û [[gotin]]ên [[spehî]] lê ne [[rast]], [[peyivîn]]a [[berbad]] ya ku li gel [[rastî|rastiyê]] [[guncîn|nagunce]].
#* {{jêder|ku|Netkurd.com, 6/2010|jêgirtin=Hukumeta tirk '''retorîkek''' nû bikar tîne. Di vê '''retorîkê''' de dijminatiya Îsraîlê û antîsemîtîzm berçav dibe.}}
# [[awa|Awayê]] [[axiftin]]a kesekî.
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|rêtorîk
}}
==== Jê ====
{{kol3|ku
|retorîkî
}}
==== Etîmolojî ====
Ji {{ji|el|ku|ῥητορικὴ|t=[[huner]]a [[axiftin]]ê}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Rhetorik|f}}
* {{Z|bg}}: {{W+|bg|реторика}}
* {{Z|zh}}: {{W+|zh|修辭}}
* {{Z|fi}}: {{W+|fi|retoriikka}}, {{W+|fi|kaunopuheisuus}}
* {{Z|fr}}: {{W+|fr|rhétorique}}
* {{Z|nl}}: {{W+|nl|retorica}}, {{W+|nl|welsprekendheid}}
* {{Z|en}}: {{W+|en|rhetoric}}
* {{Z|it}}: {{W+|it|retorica|f}}
* {{Z|ja}}: {{W-|ja|修辞技法}}
* {{Z|la}}: {{W-|la|ars rhetorica}}
* {{Z|pl}}: {{W+|pl|retoryka}}
* {{Z|pt}}: {{W+|pt|retórica}}
* {{Z|es}}: {{W+|es|retórica|f}}
* {{Z|sv}}: {{W+|sv|retorik}}
* {{Z|tr}}: {{W+|tr|belâgat}}, {{W+|tr|retorik}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=eîkorrt|rektorî|tîrkero}}
3c00wnhxzuh12ez1oaykrqyeu85wh7z
qudsiyet
0
46253
6531733
6468150
2026-09-15T20:29:52Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531733
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# Tişta/ê ku [[pîroz]] e anku [[qudsî]] ye.
#: {{hevmane|ku|pîrozî|pîrozwerî|miqedisî}}
#* {{jêder|ku|daxwiya Serokatiya Herêma Kurdistanê, 18.10.2007 li gor Nefel.com|jêgirtin=Bi qenaeta me wisa ye û tecribeya salên borî jî îsbat kiriye ko ew mesele bi şerî nayêt çareser kirin, lê belê di heman demê de em pir vekirî ji hemî aliyan re dibêjin ko her bi çi bihaneyê be heger destdirêjî li herêmê û tecribeya Kurdistanê bêt kirin me amadehiyeka temam heye ko em parastinê li tecribeya demokratîk û kerameta miletê xwe û '''qudsiyeta''' welatê xwe bikin.}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|qudsîtî
}}
==== Etîmolojî ====
Ji {{ji|ar|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Heiligkeit}}
* {{Z|arc}}:* {{Z|zh}}: {{W+|zh|神圣}}, {{W+|zh|纯洁}}, {{W-|zh|洁白}}
* {{Z|fi}}: {{W+|fi|pyhyys}}
* {{Z|en}}: {{W+|en|sanctity}}, {{W+|en|sacredness}}, {{W+|en|holiness}}, {{W+|en|halidom}}, {{W+|en|blessedness}}
* {{Z|tr}}: {{W+|tr|kutsallık}}, {{W+|tr|kutsiyet}}
{{werger-bin}}
4jc0lw9fnhv7avebwjssx9yo5fawu6b
6531734
6531733
2026-09-15T20:30:42Z
Wikihez
7344
([[m:Special:MyLanguage/User:Jon Harald Søby/diffedit|diffedit]])
6531734
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# Tişta/ê ku [[pîroz]] e anku [[qudsî]] ye.
#: {{hevmane|ku|pîrozî|pîrozwerî|miqedisî}}
#* {{jêder|ku|daxwiya Serokatiya Herêma Kurdistanê, 18.10.2007, li gor Nefel.com|jêgirtin=Bi qenaeta me wisa ye û tecribeya salên borî jî îsbat kiriye ko ew mesele bi şerî nayêt çareser kirin, lê belê di heman demê de em pir vekirî ji hemî aliyan re dibêjin ko her bi çi bihaneyê be heger destdirêjî li herêmê û tecribeya Kurdistanê bêt kirin me amadehiyeka temam heye ko em parastinê li tecribeya demokratîk û kerameta miletê xwe û '''qudsiyeta''' welatê xwe bikin.}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|qudsîtî
}}
==== Etîmolojî ====
Ji {{ji|ar|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Heiligkeit}}
* {{Z|arc}}:* {{Z|zh}}: {{W+|zh|神圣}}, {{W+|zh|纯洁}}, {{W-|zh|洁白}}
* {{Z|fi}}: {{W+|fi|pyhyys}}
* {{Z|en}}: {{W+|en|sanctity}}, {{W+|en|sacredness}}, {{W+|en|holiness}}, {{W+|en|halidom}}, {{W+|en|blessedness}}
* {{Z|tr}}: {{W+|tr|kutsallık}}, {{W+|tr|kutsiyet}}
{{werger-bin}}
5dyaud3fict1p3tl6syfa5yo2mxbgdu
harem
0
46504
6531776
6468511
2026-09-16T07:01:56Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531776
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[kom]]a [[jin]]ên kesekî(li hin welatên ku [[pirjinî]] [[pejirbar]] e)
#* {{jêder-websîte|ku|jêgirtin=Li Dergûşa şaristaniyê erf û edetên ereban ên qirêj û paþverû li ser navê îslamiyetê hatine pêkanîn û jin pêşî bûne “'''harema''' mêran” û paşê bûne “milk” û “namûs”a mêran. Paşê “mêran” navê “'''harem'''”ê kirin “zewac” û bi 4 jinan re bi dilekî rehet “zewicîn”. Lewre ew “jinên” wan bûn. Ew ên ku derfet û îmkanên wan hebûn jî, pirtir jin anîn û di bin navê “Zewac”ê de “'''harem'''”a xwe zêde kirin. 364 jinên Nasruddewle, padîşahê Dewleta Merwaniyan hebû...|nivîskar=Bîrgul Özbariş ARDA|sernav=Bersiva [bo] Eskerê Boyîk: Dema Hareman derbas bû!|websîte=Amidakurd.com|sal=11/2007}}
# [[ode]]ya [[jin]]an (ya ku li [[zelam]]ên [[biyanî]] [[qedexe]] ye)
# [[kom]]a [[heywan]]ên [[mê]] yên [[li gel]] [[heywan]]ek [[nêr]]
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|herem
|heremxane
}}
==== Etîmolojî ====
Ji {{ji|ar|ku|حرم|cuda=حَرَم}}.
==== Bide ber ====
{{kol3|ku
|herêm
}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W|de|Harem|m}}
* {{Z|bg}}: {{W|bg|харем#Bulgarian|m}}
* {{Z|cs}}: {{W|cs|harém|m}}
* {{Z|zh}}: {{W+|zh|妻妾}}, {{W+|zh|后宫}}, {{W+|zh|後宮}}
* {{Z|da}}: {{W|da|harem}}
* {{Z|ar}}: {{W|ar|حريم}}, {{W|ar|حرم|guman=erê}}
* {{Z|et}}: {{W+|et|haarem}}
* {{Z|fa}}: {{W|fa|حریم}}
* {{Z|fi}}: {{W+|fi|haaremi}}
* {{Z|fr}}: {{W|fr|harem|m}}
* {{Z|hi}}: {{W-|hi|महल-सरा|tr=mahal-sarā}}, {{W-|hi|हरम-सरा|tr=haram-sarā}}
* {{Z|nl}}: {{W|nl|harem|m}}, {{W+|nl|vrouwenverblijf}}
* {{Z|he}}: {{W+|he|הרמון}}
* {{Z|en}}: {{W+|en|harem}}
* {{Z|it}}: {{W|it|harem}}
* {{Z|ja}}: {{W+|ja|ハーレム}}
* {{Z|ko}}: {{W-|ko|하렘|tr=harem}}, {{W-|ko|후궁|tr=hugung}}
* {{Z|hu}}: {{W+|hu|hárem}}
* {{Z|pt}}: {{W|pt|harém|m}}
* {{Z|ru}}: {{W|ru|гарем|m}}
* {{Z|sh}}: {{W|sh|harem#Bosnian|m}}
* {{Z|sh}}: {{W|sh|harem|m}}
* {{Z|sh}}:
*: Kirîlî: [[харем#Serbian|харем]] {{mê}}
*: Latînî: {{W|la|harem#Serbian|m}}
* {{Z|es}}: {{W+|es|harén}}
* {{Z|sv}}: {{W|sv|harem|n}}
* {{Z|tr}}: {{W+|tr|harem}}
* {{Z|ur}}: {{W|ur|محل سرا|tr=mahal-sarā}}, {{W|ur|حرم سرا|tr=haram-sarā}}
* {{Z|el}}: {{W+|el|χαρέμι}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=aehmr|ehram|ermah|heram|mehra|merha|rehma}}
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# harem
# [[herem]]
# [[jin]], kulfet kâhyası kehyayê heremê
==== Anagram ====
* {{anagram|tr|a=aehmr|ehram}}
i5tjxztwi98t6z9d979367m9k538b5h
cidiyet
0
47059
6531714
6469037
2026-09-15T18:35:10Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531714
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[cidîtî]], [[jidilî]], [[samîmîtî]], [[cidî]]bûn
#* {{jêder-websîte|ku|jêgirtin=Daku ez bi xwe ji pêvajoya polîtîkaya ku çêdibe zêde dûr nînim, carcarina xwe bi xwe re dibêjim: Gelo mirov çawan dikare ewqas bê '''cidiyet''' bibe û di derbarê heyiyên xwe yên esasî de weha bê pîvan bipeyîve? Bersîv nîne! Ev kambaxiya nexweşiyeke pir sosret e di nava kurdan de û yeke ”kurdî” ye.|nivîskar=Beyhanî Şahîn|sernav=No, Kurdiya di mêjiyên mirî de, No!|websîte=Amidakurd.com|sal=2007}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|ciddiyet
}}
==== Etîmolojî ====
Ji {{deyn|ar|ku}}: [[cidî]] + [[-yet]]
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Ernst|n}}
* {{Z|fa}}: {{W+|fa|جدیت}}
* {{Z|fi}}: {{W+|fi|vakavuus}}
* {{Z|en}}: {{W+|en|seriousness}}
* {{Z|tr}}: {{W+|tr|ciddiyet}}
{{werger-bin}}
jw7e9anug69md6ob9tax3oom8z6ntuw
krismas
0
47793
6531743
6469666
2026-09-15T20:39:29Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531743
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[cejn|Cejn]]a [[welidîn]]a [[Îsa]] [[Mesîh]]; her sal roja 25.12.
#: {{hevmane|ku|gaxan|noel}}
#* {{jêder|ku|jêgirtin=Hat zanîn ku wê li bajarê Bexdadê, dareke pir mezin ya Krîsmasê ango ya herî mezin li Rojhilata Navîn li hundir baxa Zewrayê bê danîn daku ahenga serê salê li gel bête pîrozkirin.|pukmedia.com 12/2015}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|krîsmas
|krismis
}}
==== Jê ====
{{kol3|ku
|krismasî
}}
==== Têkildar ====
{{kol3|ku
|Mewlûd
}}
==== Etîmolojî ====
Deyn ji {{deyn|en|ku|Christmas}} ji [[Christ]] ([[Mesîh]]) + [[mass]] ([[cejn]])
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W|af|Kersfees}}
* {{Z|de}}: {{W|de|Weihnachten}} {{nt}}
* {{Z|be}}: {{W|be|Нараджэньне}} (Naradjèn'nié)
* {{Z|br}}: {{W|br|Nedeleg}}
* {{Z|bg}}: {{W-|bg|Рождество Христово|tr=Rojdestvo Xrîstovo}}, {{W|bg|Коледа}} (Koliéda)
* {{Z|cs}}: {{W|cs|Vánoce}}
* {{Z|zh}}: {{W-|zh|圣诞节|tr=Shèng dàn jié}}, {{W|zh|圣诞节}} (Chèungdàne.djyái)
* {{Z|da}}: {{W|da|jul}}
* {{Z|id}}: {{W|id|Natal}}
* {{Z|ar}}: {{W-|ar|عيد الميلا|tr='eyd el-mîlad}}, {{W|ar|عيد ميلاد|guman=erê}}
* {{Z|hy}}: {{W|hy|Սուրբ Ծնունդ}}, {{W|hy|Ծնունտ}} (Dznount)
* {{Z|eo}}: {{W|eo|Kristnasko}}
* {{Z|fa}}: {{W-|fa|عید میلاد مسی|tr='eyd mîlad Mesîh}}, {{W-|fa|عید نوئل|tr='eyd noel}}
* {{Z|fi}}: {{W|fi|joulu}}
* {{Z|fr}}: {{W|fr|noël}}
* {{Z|hi}}: {{W-|hi|क्रिसमस|tr=Krismas}}, {{W-|hi|बड़ा दिन|tr=ba.rā din}}
* {{Z|nl}}: {{W|nl|Kerstmis}}
* {{Z|he}}: {{W|he|חג המולד}} (Hag Emvlad)
* {{Z|ang}}: {{W|ang|Crīstesmæsse}}
* {{Z|en}}: {{W|en|Christmas}}
* {{Z|ga}}: {{W|ga|An Nollaig}}
* {{Z|it}}: {{W|it|Natale}}
* {{Z|is}}: {{W|is|Jól}}
* {{Z|ja}}: {{W|ja|クリスマス}} (Kurisumasu)
* {{Z|jv}}: {{W|jv|Natal}}
* {{Z|ca}}: {{W|ca|Nadal}}
* {{Z|ko}}: {{W|ko|크리스마스}} (Keuriseumaseu), {{W-|ko|크리스마스|tr=K'ŭrisŭmasŭ}}
* {{Z|la}}: {{W|la|Christi Natalis}}
* {{Z|lt}}: {{W|lt|Kalėdos}}
* {{Z|lb}}: {{W|lb|Chrëschtdag}}
* {{Z|ms}}: {{W|ms|Krismas}}
* {{Z|hu}}: {{W|hu|Karácsony}}
* {{Z|nds}}: {{W|nds|Wiehnacht}}
* {{Z|no}}: {{W|no|Jul}}
* {{Z|oc}}: {{W|oc|Nadal}}
* {{Z|pl}}: {{W|pl|Narodzenie}}
* {{Z|pt}}: {{W|pt|Natal}}
* {{Z|rm}}: {{W|rm|Nadal}}
* {{Z|ro}}: {{W|ro|Crăciun}}
* {{Z|ru}}: {{W|ru|Рождество}} (Rajdiéstvo)
* {{Z|scn}}: {{W|scn|Natali}}
* {{Z|sh}}: {{W|sh|Božić}}
* {{Z|sh}}: {{W|sh|Божић}} (Bojits’)
* {{Z|es}}: {{W|es|Navidad}}
* {{Z|sv}}: {{W|sv|jul}}
* {{Z|th}}: {{W|th|คริสต์มาส}} (Kʰris.ma:s)
* {{Z|tr}}: {{W|tr|Noel}}
* {{Z|uk}}: {{W|uk|Різдво}} (Rîzdvo)
* {{Z|cy}}: {{W|cy|Nadolig}}
* {{Z|el}}: {{W|el|Χριστούγεννα}} (Ristoíyènna)
{{werger-bin}}
szruhvolurkp73if4d8p7pctuyi1kk0
6531767
6531743
2026-09-15T22:12:09Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531767
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[cejn|Cejna]] [[welidîn]]a [[Îsa]] [[Mesîh]]; her sal roja 25.12.
#: {{hevmane|ku|gaxan|noel}}
#* {{jêder|ku|jêgirtin=Hat zanîn ku wê li bajarê Bexdadê, dareke pir mezin ya Krîsmasê ango ya herî mezin li Rojhilata Navîn li hundir baxa Zewrayê bê danîn daku ahenga serê salê li gel bête pîrozkirin.|pukmedia.com 12/2015}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|krîsmas
|krismis
}}
==== Jê ====
{{kol3|ku
|krismasî
}}
==== Têkildar ====
{{kol3|ku
|Mewlûd
}}
==== Etîmolojî ====
Deyn ji {{deyn|en|ku|Christmas}} ji [[Christ]] ([[Mesîh]]) + [[mass]] ([[cejn]])
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W|af|Kersfees}}
* {{Z|de}}: {{W|de|Weihnachten}} {{nt}}
* {{Z|be}}: {{W|be|Нараджэньне}} (Naradjèn'nié)
* {{Z|br}}: {{W|br|Nedeleg}}
* {{Z|bg}}: {{W-|bg|Рождество Христово|tr=Rojdestvo Xrîstovo}}, {{W|bg|Коледа}} (Koliéda)
* {{Z|cs}}: {{W|cs|Vánoce}}
* {{Z|zh}}: {{W-|zh|圣诞节|tr=Shèng dàn jié}}, {{W|zh|圣诞节}} (Chèungdàne.djyái)
* {{Z|da}}: {{W|da|jul}}
* {{Z|id}}: {{W|id|Natal}}
* {{Z|ar}}: {{W-|ar|عيد الميلا|tr='eyd el-mîlad}}, {{W|ar|عيد ميلاد|guman=erê}}
* {{Z|hy}}: {{W|hy|Սուրբ Ծնունդ}}, {{W|hy|Ծնունտ}} (Dznount)
* {{Z|eo}}: {{W|eo|Kristnasko}}
* {{Z|fa}}: {{W-|fa|عید میلاد مسی|tr='eyd mîlad Mesîh}}, {{W-|fa|عید نوئل|tr='eyd noel}}
* {{Z|fi}}: {{W|fi|joulu}}
* {{Z|fr}}: {{W|fr|noël}}
* {{Z|hi}}: {{W-|hi|क्रिसमस|tr=Krismas}}, {{W-|hi|बड़ा दिन|tr=ba.rā din}}
* {{Z|nl}}: {{W|nl|Kerstmis}}
* {{Z|he}}: {{W|he|חג המולד}} (Hag Emvlad)
* {{Z|ang}}: {{W|ang|Crīstesmæsse}}
* {{Z|en}}: {{W|en|Christmas}}
* {{Z|ga}}: {{W|ga|An Nollaig}}
* {{Z|it}}: {{W|it|Natale}}
* {{Z|is}}: {{W|is|Jól}}
* {{Z|ja}}: {{W|ja|クリスマス}} (Kurisumasu)
* {{Z|jv}}: {{W|jv|Natal}}
* {{Z|ca}}: {{W|ca|Nadal}}
* {{Z|ko}}: {{W|ko|크리스마스}} (Keuriseumaseu), {{W-|ko|크리스마스|tr=K'ŭrisŭmasŭ}}
* {{Z|la}}: {{W|la|Christi Natalis}}
* {{Z|lt}}: {{W|lt|Kalėdos}}
* {{Z|lb}}: {{W|lb|Chrëschtdag}}
* {{Z|ms}}: {{W|ms|Krismas}}
* {{Z|hu}}: {{W|hu|Karácsony}}
* {{Z|nds}}: {{W|nds|Wiehnacht}}
* {{Z|no}}: {{W|no|Jul}}
* {{Z|oc}}: {{W|oc|Nadal}}
* {{Z|pl}}: {{W|pl|Narodzenie}}
* {{Z|pt}}: {{W|pt|Natal}}
* {{Z|rm}}: {{W|rm|Nadal}}
* {{Z|ro}}: {{W|ro|Crăciun}}
* {{Z|ru}}: {{W|ru|Рождество}} (Rajdiéstvo)
* {{Z|scn}}: {{W|scn|Natali}}
* {{Z|sh}}: {{W|sh|Božić}}
* {{Z|sh}}: {{W|sh|Божић}} (Bojits’)
* {{Z|es}}: {{W|es|Navidad}}
* {{Z|sv}}: {{W|sv|jul}}
* {{Z|th}}: {{W|th|คริสต์มาส}} (Kʰris.ma:s)
* {{Z|tr}}: {{W|tr|Noel}}
* {{Z|uk}}: {{W|uk|Різдво}} (Rîzdvo)
* {{Z|cy}}: {{W|cy|Nadolig}}
* {{Z|el}}: {{W|el|Χριστούγεννα}} (Ristoíyènna)
{{werger-bin}}
oq7jh758q30c4hq1wy32eag593iyuk7
melûm
0
48874
6531702
6470813
2026-09-15T18:28:54Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531702
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Rengdêr ===
{{ku-tewîn-rd|melûm}}
{{rengdêr|ku}}
# Tişta/ê ku [[gelek]] [[baş]] tê [[zanîn]].
#: {{hevmane|ku|diyar|xwiya|aşkere|berçav|belî}}
#* {{jêder|ku|Hunermend Xanemîr di hevpeyivînekê de li gel Mustafa Gazî, Netkurd.com, 2/2008|jêgirtin=Ez ji xeynî muzîkê tu karekî din nakim. Lê belê ez dikarim viya bibêjim em di vê mijarê de gelek zorî û zehmetiyan dikişînin, rewşa me ya aboriyê qet ne baş e, lewre ji bav û kalê me ne pîşeyek ne jî tu mîratek ji me re ma. Rewşa jiyana hunermendên Kurd '''melûm''' e. Bi rastî ev rewşa neyînî zorê dide me. Were 22 salan ji bo muzîka Kurdî keda xwe birjîne, xwe feda bike, tenê bi vî karî mijûl bibe û bi pişt re jî here di karekî bê eleqe de bixebite, ma ewha dibe?}}
# {{kontrol|rêz=erê|[[aşkira]], [[xuya]], [[zelal]]}}
==== Formên alternatîv ====
{{kol3|ku
|malûm
|me'lûm
|mahlûm
|mehlûm
|me'lîm
}}
==== Jê ====
{{kol3|ku
|melûmat
|melûmî
|melûmîtî
|melûmtî
}}
==== Etîmolojî ====
Ji {{deyn|ar|ku}}, têkildarî [[ilm]]
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|anschaulig}}, {{W+|de|augenscheinlich}}, {{W+|de|ersichtlich}}, {{W+|de|evident}}, {{W+|de|offenbar}}, {{W+|de|offenkundig}}, {{W+|de|einleuchtend}}
* {{Z|eo}}: {{W+|eo|evidenta}}
* {{Z|fa}}: {{W+|fa|معلوم}}
* {{Z|fo}}: {{W+|fo|eyðsæddur}}
* {{Z|fi}}: {{W+|fi|ilmeinen}}, {{W+|fi|selvä}}
* {{Z|fr}}: {{W+|fr|obvie}}, {{W+|fr|évident}}
* {{Z|nl}}: {{W+|nl|evident}}, {{W+|nl|voor de hand liggend}}, {{W+|nl|apert}}, {{W+|nl|duidelijk}}, {{W+|nl|kennelijk}}, {{W+|nl|klaarblijkelijk}}, {{W+|nl|uitgesproken}}
* {{Z|en}}: {{W+|en|evident}}, {{W+|en|obvious}}, {{W+|en|clear}}, {{W+|en|definite}}, {{W+|en|demonstrable}}, {{W+|en|patent}}, {{W+|en|apparent}}
* {{Z|it}}: {{W+|it|chiaro}}, {{W+|it|evidente}}, {{W+|it|palese}}
* {{Z|ca}}: {{W+|ca|evident}}
* {{Z|ms}}: {{W+|ms|terbukti}}
* {{Z|pt}}: {{W+|pt|evidente}}, {{W+|pt|inequívoco}}, {{W+|pt|manifesto}}, {{W+|pt|óbvio}}, {{W+|pt|patente}}
* {{Z|es}}: {{W+|es|evidente}}
* {{Z|sv}}: {{W+|sv|synbarligen}}, {{W+|sv|uppenbar}}
* {{Z|tr}}: {{W+|tr|aşikâr}}, {{W+|tr|belirgin}}, {{W+|tr|belli}}, {{W+|tr|bilinen}}, {{W+|tr|malum}}
{{werger-bin}}
j1qyhse2kp2k1ak2dsjef5o4wxn5n8n
tidark
0
49107
6531750
6470990
2026-09-15T20:43:23Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531750
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[tedbîr]], [[plan]], xwe[[amade]]kirina bo [[kar]]ekî
#* {{jêder-websîte|ku|jêgirtin=Wexta hêdî hêdî payîz bi ser me de tê, em jî tivdarekên xwe yên zivistanê dikin. Evana jî goştê hişk, mastê suzme, tirşoya ga, uzelik, kereng, xatûn barmaxî, qelî, rûnê helandî toraq û penêr, û çortan. Heke em payîzan van tivdarekên xwe nekin, em ê zivistanê hinekî xizaniyê bikişînin.|nivîskar=Yildiz Gultekîn|sernav=Çortanê Metê|websîte=Azadiyawelat.com|sal=3/2008}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
|tidarek
|tidarik
|tivdarek
|tivdarik
|tivdark
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Bereitstellung|f}}, {{W+|de|Besorgung|f}}, {{W+|de|Vorbereitung|f}}
* {{Z|en}}: {{W+|en|supplier|guman=erê}}
* {{Z|tr}}: {{W+|tr|tedarik}}, {{W+|tr|hazırlık|guman=erê}}, {{W+|tr|levazım|guman=erê}}
{{werger-bin}}
ity7gtsnt4l821vsl2f741wlfsto2ub
-lojî
0
49176
6531643
6244482
2026-09-15T17:01:26Z
Wikihez
7344
/* Etîmolojî */
6531643
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Paşgir ===
{{paşgir|ku}}
# [[-nasî]], [[-zanî]]
#: ''[[fîlolojî]], [[îdeolojî]], [[jerontolojî]], [[teknolojî]]''
==== Formên alternatîv ====
* [[-logî]]
==== Têkildar ====
* [[-log]]
==== Jê ====
{{paşgirbibîne|ku}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|fr:-logie<id:beşa lêkolînê>|tree=1}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|-logië}}
* {{Z|sq}}: {{W-|sq|-logjia}}
* {{Z|de}}: {{W+|de|-kunde|m}}, {{W+|de|Lehre|m|cuda=-lehre}}, {{W+|de|Wissenschaft|m|cuda=-wissenschaft}}, {{W+|de|-logie|m}}, {{W+|de|-ik|m}}
* {{Z|zgh}}: {{W-|zgh|ⵜⴰⵙⵏ-}}, {{W-|zgh|ⵜⵓⵙⵏ-}}
* {{Z|an}}: {{W-|an|-lochía|m}}
* {{Z|ast}}: {{W-|ast|-loxía|m}}
* {{Z|az}}: {{W-|az|-şünaslıq}}, {{W-|az|-logiya}}
* {{Z|be}}: {{W-|be|-ло́гія|m}}
* {{Z|br}}: {{W-|br|-loji|m}}
* {{Z|bg}}: {{W-|bg|-ло́гия|m}}
* {{Z|cs}}: {{W-|cs|-logie|m}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|學}}
*: {{Z|cmn}}: {{W+|cmn|學|tr=xué}}
* {{Z|da}}: {{W+|da|-logi|g}}
* {{Z|gsw}}: {{W-|gsw|-logie|m}}
* {{Z|id}}: {{W-|id|-logi}}
* {{Z|ar}}: {{W-|ar|لوجيا|m|cuda=ـلُوجِيَا}}, {{W+|ar|علم|n|cuda=عِلْم اَلـ}}
* {{Z|myv}}: {{W-|myv|-тона}}
* {{Z|eo}}: {{W-|eo|-logio}}
* {{Z|et}}: {{W-|et|-loogia}}
* {{Z|fa}}: {{W+|fa|شناسی|tr=-šenâsi}}, {{W-|fa|لوژی|tr=-loži|cuda=ـلوژی}}
* {{Z|fo}}: {{W-|fo|-frøði|m}}
* {{Z|fi}}: {{W+|fi|-logia}}, {{W+|fi|oppi|cuda=-oppi}}
* {{Z|fr}}: {{W+|fr|-logie|m}}
* {{Z|gl}}: {{W+|gl|-loxía|m}}
* {{Z|ka}}: {{W+|ka|-ლოგია}}
* {{Z|hi}}: {{W+|hi|ज्ञान|n|cuda=-ज्ञान}}
* {{Z|nl}}: {{W+|nl|-logie|m}}, {{W-|nl|-kunde}}, {{W-|nl|-leer}}
* {{Z|he}}: {{W+|he|־לוגיה|tr=-logya}}
* {{Z|io}}: {{W-|io|-logio}}
* {{Z|ang}}: {{W-|ang|-cræft|n}}
* {{Z|en}}: {{W+|en|-logy}}
* {{Z|ia}}: {{W-|ia|-logia}}
* {{Z|ga}}: {{W-|ga|eolaíocht|m}}
* {{Z|it}}: {{W+|it|-logia|m}}
* {{Z|is}}: {{W-|is|-fræði}}
* {{Z|ja}}: {{W+|ja|学|tr=-がく, -gaku}}, {{W-|ja|ロジー|tr=-rojī|cuda=-ロジー}}
* {{Z|ca}}: {{W+|ca|-logia|m}}
* {{Z|ko}}: {{W+|ko|학}}
* {{Z|co}}: {{W-|co|-lugia|m}}
* {{Z|la}}: {{W-|la|-logia|m}}
* {{Z|lv}}: {{W-|lv|-logija|m}}
* {{Z|lt}}: {{W-|lt|-logija|m}}
* {{Z|lb}}: {{W-|lb|-logie|m}}
* {{Z|mk}}: {{W-|mk|-логија|m}}
* {{Z|ms}}: {{W+|ms|-logi}}, {{W-|ms|kaji}}
* {{Z|hu}}: {{W-|hu|-lógia}}, {{W-|hu|-ászat}}, {{W-|hu|-észet}}, {{W+|hu|tan|cuda=-tan}}, {{W+|hu|tudomány|cuda=-tudomány}}
* {{Z|mn}}: {{W+|mn|зүй}}
* {{Z|nv}}: {{W-|nv|naalkaah}}
* {{Z|nds}}: {{W-|nds|-logie|m}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|-logi}}
* {{Z|oc}}: {{W-|oc|-logia|m}}
* {{Z|ug}}: {{W-|ug|ـشۇناسلىق}}
* {{Z|uz}}: {{W-|uz|-shunoslik}}, {{W-|uz|-logiya}}
* {{Z|pl}}: {{W+|pl|-logia|m}}
* {{Z|pt}}: {{W+|pt|-logia|m}}
* {{Z|kk}}: {{W-|kk|-тану}}
* {{Z|ro}}: {{W-|ro|-logie|m}}
* {{Z|ru}}: {{W+|ru|-ло́гия|m}}, {{W+|ru|ведение|nt|cuda=-ве́дение}}, {{W+|ru|знание|nt|cuda=-зна́ние}}
* {{Z|se}}: {{W-|se|-logiija}}
* {{Z|sc}}: {{W-|sc|-logia|m}}
* {{Z|ceb}}: {{W-|ceb|-lohia}}
* {{Z|scn}}: {{W+|scn|-luggìa|m}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|-ло̀гија|m}}
*: Latînî: {{W-|sh|-lògija|m}}
* {{Z|sk}}: {{W-|sk|-lógia|m}}
* {{Z|sl}}: {{W-|sl|-logija|m}}
* {{Z|es}}: {{W+|es|-logía|m}}
* {{Z|su}}: {{W-|su|-logi}}
* {{Z|sv}}: {{W+|sv|-logi|g}}
* {{Z|tg}}: {{W-|tg|-шиносӣ}}
* {{Z|tl}}: {{W-|tl|-lohia}}
* {{Z|ta}}: {{W-|ta|-இயல்}}
* {{Z|th}}: {{W+|th|วิทยา|cuda=-วิทยา|tr=-wíttáyaa}}
* {{Z|tr}}: {{W+|tr|-loji}}
* {{Z|ur}}: {{W-|ur|یات|tr=-iyāt}}
* {{Z|uk}}: {{W-|uk|-ло́гія|m}}
* {{Z|vi}}: {{W+|vi|học}}
* {{Z|wa}}: {{W+|wa|-lodjeye|m}}
* {{Z|cy}}: {{W-|cy|-leg}}
* {{Z|km}}: {{W+|km|វិទ្យា|cuda=-វិទ្យា}}, {{W-|km|សាស្ត្រ|cuda=-សាស្ត្រ}}
* {{Z|grc}}: {{W-|grc|-λογία|m}}
* {{Z|el}}: {{W+|el|-λογία|m}}
{{werger-bin}}
:{{werger çavkanî|en|-logy|beş=Ev qismê Wergerê|oldid=75751475}}
d7sc6bq00wbvfsl12bgfvj8vjriqka9
6531644
6531643
2026-09-15T17:01:39Z
Wikihez
7344
/* Etîmolojî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531644
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Paşgir ===
{{paşgir|ku}}
# [[-nasî]], [[-zanî]]
#: ''[[fîlolojî]], [[îdeolojî]], [[jerontolojî]], [[teknolojî]]''
==== Formên alternatîv ====
* [[-logî]]
==== Têkildar ====
* [[-log]]
==== Jê ====
{{paşgirbibîne|ku}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|fr:-logie<id:beşa lêkolînê>|metn=++|dar=1}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|-logië}}
* {{Z|sq}}: {{W-|sq|-logjia}}
* {{Z|de}}: {{W+|de|-kunde|m}}, {{W+|de|Lehre|m|cuda=-lehre}}, {{W+|de|Wissenschaft|m|cuda=-wissenschaft}}, {{W+|de|-logie|m}}, {{W+|de|-ik|m}}
* {{Z|zgh}}: {{W-|zgh|ⵜⴰⵙⵏ-}}, {{W-|zgh|ⵜⵓⵙⵏ-}}
* {{Z|an}}: {{W-|an|-lochía|m}}
* {{Z|ast}}: {{W-|ast|-loxía|m}}
* {{Z|az}}: {{W-|az|-şünaslıq}}, {{W-|az|-logiya}}
* {{Z|be}}: {{W-|be|-ло́гія|m}}
* {{Z|br}}: {{W-|br|-loji|m}}
* {{Z|bg}}: {{W-|bg|-ло́гия|m}}
* {{Z|cs}}: {{W-|cs|-logie|m}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|學}}
*: {{Z|cmn}}: {{W+|cmn|學|tr=xué}}
* {{Z|da}}: {{W+|da|-logi|g}}
* {{Z|gsw}}: {{W-|gsw|-logie|m}}
* {{Z|id}}: {{W-|id|-logi}}
* {{Z|ar}}: {{W-|ar|لوجيا|m|cuda=ـلُوجِيَا}}, {{W+|ar|علم|n|cuda=عِلْم اَلـ}}
* {{Z|myv}}: {{W-|myv|-тона}}
* {{Z|eo}}: {{W-|eo|-logio}}
* {{Z|et}}: {{W-|et|-loogia}}
* {{Z|fa}}: {{W+|fa|شناسی|tr=-šenâsi}}, {{W-|fa|لوژی|tr=-loži|cuda=ـلوژی}}
* {{Z|fo}}: {{W-|fo|-frøði|m}}
* {{Z|fi}}: {{W+|fi|-logia}}, {{W+|fi|oppi|cuda=-oppi}}
* {{Z|fr}}: {{W+|fr|-logie|m}}
* {{Z|gl}}: {{W+|gl|-loxía|m}}
* {{Z|ka}}: {{W+|ka|-ლოგია}}
* {{Z|hi}}: {{W+|hi|ज्ञान|n|cuda=-ज्ञान}}
* {{Z|nl}}: {{W+|nl|-logie|m}}, {{W-|nl|-kunde}}, {{W-|nl|-leer}}
* {{Z|he}}: {{W+|he|־לוגיה|tr=-logya}}
* {{Z|io}}: {{W-|io|-logio}}
* {{Z|ang}}: {{W-|ang|-cræft|n}}
* {{Z|en}}: {{W+|en|-logy}}
* {{Z|ia}}: {{W-|ia|-logia}}
* {{Z|ga}}: {{W-|ga|eolaíocht|m}}
* {{Z|it}}: {{W+|it|-logia|m}}
* {{Z|is}}: {{W-|is|-fræði}}
* {{Z|ja}}: {{W+|ja|学|tr=-がく, -gaku}}, {{W-|ja|ロジー|tr=-rojī|cuda=-ロジー}}
* {{Z|ca}}: {{W+|ca|-logia|m}}
* {{Z|ko}}: {{W+|ko|학}}
* {{Z|co}}: {{W-|co|-lugia|m}}
* {{Z|la}}: {{W-|la|-logia|m}}
* {{Z|lv}}: {{W-|lv|-logija|m}}
* {{Z|lt}}: {{W-|lt|-logija|m}}
* {{Z|lb}}: {{W-|lb|-logie|m}}
* {{Z|mk}}: {{W-|mk|-логија|m}}
* {{Z|ms}}: {{W+|ms|-logi}}, {{W-|ms|kaji}}
* {{Z|hu}}: {{W-|hu|-lógia}}, {{W-|hu|-ászat}}, {{W-|hu|-észet}}, {{W+|hu|tan|cuda=-tan}}, {{W+|hu|tudomány|cuda=-tudomány}}
* {{Z|mn}}: {{W+|mn|зүй}}
* {{Z|nv}}: {{W-|nv|naalkaah}}
* {{Z|nds}}: {{W-|nds|-logie|m}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|-logi}}
* {{Z|oc}}: {{W-|oc|-logia|m}}
* {{Z|ug}}: {{W-|ug|ـشۇناسلىق}}
* {{Z|uz}}: {{W-|uz|-shunoslik}}, {{W-|uz|-logiya}}
* {{Z|pl}}: {{W+|pl|-logia|m}}
* {{Z|pt}}: {{W+|pt|-logia|m}}
* {{Z|kk}}: {{W-|kk|-тану}}
* {{Z|ro}}: {{W-|ro|-logie|m}}
* {{Z|ru}}: {{W+|ru|-ло́гия|m}}, {{W+|ru|ведение|nt|cuda=-ве́дение}}, {{W+|ru|знание|nt|cuda=-зна́ние}}
* {{Z|se}}: {{W-|se|-logiija}}
* {{Z|sc}}: {{W-|sc|-logia|m}}
* {{Z|ceb}}: {{W-|ceb|-lohia}}
* {{Z|scn}}: {{W+|scn|-luggìa|m}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|-ло̀гија|m}}
*: Latînî: {{W-|sh|-lògija|m}}
* {{Z|sk}}: {{W-|sk|-lógia|m}}
* {{Z|sl}}: {{W-|sl|-logija|m}}
* {{Z|es}}: {{W+|es|-logía|m}}
* {{Z|su}}: {{W-|su|-logi}}
* {{Z|sv}}: {{W+|sv|-logi|g}}
* {{Z|tg}}: {{W-|tg|-шиносӣ}}
* {{Z|tl}}: {{W-|tl|-lohia}}
* {{Z|ta}}: {{W-|ta|-இயல்}}
* {{Z|th}}: {{W+|th|วิทยา|cuda=-วิทยา|tr=-wíttáyaa}}
* {{Z|tr}}: {{W+|tr|-loji}}
* {{Z|ur}}: {{W-|ur|یات|tr=-iyāt}}
* {{Z|uk}}: {{W-|uk|-ло́гія|m}}
* {{Z|vi}}: {{W+|vi|học}}
* {{Z|wa}}: {{W+|wa|-lodjeye|m}}
* {{Z|cy}}: {{W-|cy|-leg}}
* {{Z|km}}: {{W+|km|វិទ្យា|cuda=-វិទ្យា}}, {{W-|km|សាស្ត្រ|cuda=-សាស្ត្រ}}
* {{Z|grc}}: {{W-|grc|-λογία|m}}
* {{Z|el}}: {{W+|el|-λογία|m}}
{{werger-bin}}
:{{werger çavkanî|en|-logy|beş=Ev qismê Wergerê|oldid=75751475}}
okytxkzqyfo5vcxpgog1nn4lq9z8178
6531671
6531644
2026-09-15T17:41:06Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531671
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Paşgir ===
{{paşgir|ku}}
# [[-nasî]], [[-zanî]]
#: ''[[fîlolojî]], [[îdeolojî]], [[jerontolojî]], [[teknolojî]]''
==== Formên alternatîv ====
* [[-logî]]
==== Jê ====
{{paşgirbibîne|ku}}
==== Têkildar ====
* [[-log]]
==== Etîmolojî ====
{{etîmon|ku|:deyn|fr:-logie<id:beşa lêkolînê>|metn=++|dar=1}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|-logië}}
* {{Z|sq}}: {{W-|sq|-logjia}}
* {{Z|de}}: {{W+|de|-kunde|m}}, {{W+|de|Lehre|m|cuda=-lehre}}, {{W+|de|Wissenschaft|m|cuda=-wissenschaft}}, {{W+|de|-logie|m}}, {{W+|de|-ik|m}}
* {{Z|zgh}}: {{W-|zgh|ⵜⴰⵙⵏ-}}, {{W-|zgh|ⵜⵓⵙⵏ-}}
* {{Z|an}}: {{W-|an|-lochía|m}}
* {{Z|ast}}: {{W-|ast|-loxía|m}}
* {{Z|az}}: {{W-|az|-şünaslıq}}, {{W-|az|-logiya}}
* {{Z|be}}: {{W-|be|-ло́гія|m}}
* {{Z|br}}: {{W-|br|-loji|m}}
* {{Z|bg}}: {{W-|bg|-ло́гия|m}}
* {{Z|cs}}: {{W-|cs|-logie|m}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|學}}
*: {{Z|cmn}}: {{W+|cmn|學|tr=xué}}
* {{Z|da}}: {{W+|da|-logi|g}}
* {{Z|gsw}}: {{W-|gsw|-logie|m}}
* {{Z|id}}: {{W-|id|-logi}}
* {{Z|ar}}: {{W-|ar|لوجيا|m|cuda=ـلُوجِيَا}}, {{W+|ar|علم|n|cuda=عِلْم اَلـ}}
* {{Z|myv}}: {{W-|myv|-тона}}
* {{Z|eo}}: {{W-|eo|-logio}}
* {{Z|et}}: {{W-|et|-loogia}}
* {{Z|fa}}: {{W+|fa|شناسی|tr=-šenâsi}}, {{W-|fa|لوژی|tr=-loži|cuda=ـلوژی}}
* {{Z|fo}}: {{W-|fo|-frøði|m}}
* {{Z|fi}}: {{W+|fi|-logia}}, {{W+|fi|oppi|cuda=-oppi}}
* {{Z|fr}}: {{W+|fr|-logie|m}}
* {{Z|gl}}: {{W+|gl|-loxía|m}}
* {{Z|ka}}: {{W+|ka|-ლოგია}}
* {{Z|hi}}: {{W+|hi|ज्ञान|n|cuda=-ज्ञान}}
* {{Z|nl}}: {{W+|nl|-logie|m}}, {{W-|nl|-kunde}}, {{W-|nl|-leer}}
* {{Z|he}}: {{W+|he|־לוגיה|tr=-logya}}
* {{Z|io}}: {{W-|io|-logio}}
* {{Z|ang}}: {{W-|ang|-cræft|n}}
* {{Z|en}}: {{W+|en|-logy}}
* {{Z|ia}}: {{W-|ia|-logia}}
* {{Z|ga}}: {{W-|ga|eolaíocht|m}}
* {{Z|it}}: {{W+|it|-logia|m}}
* {{Z|is}}: {{W-|is|-fræði}}
* {{Z|ja}}: {{W+|ja|学|tr=-がく, -gaku}}, {{W-|ja|ロジー|tr=-rojī|cuda=-ロジー}}
* {{Z|ca}}: {{W+|ca|-logia|m}}
* {{Z|ko}}: {{W+|ko|학}}
* {{Z|co}}: {{W-|co|-lugia|m}}
* {{Z|la}}: {{W-|la|-logia|m}}
* {{Z|lv}}: {{W-|lv|-logija|m}}
* {{Z|lt}}: {{W-|lt|-logija|m}}
* {{Z|lb}}: {{W-|lb|-logie|m}}
* {{Z|mk}}: {{W-|mk|-логија|m}}
* {{Z|ms}}: {{W+|ms|-logi}}, {{W-|ms|kaji}}
* {{Z|hu}}: {{W-|hu|-lógia}}, {{W-|hu|-ászat}}, {{W-|hu|-észet}}, {{W+|hu|tan|cuda=-tan}}, {{W+|hu|tudomány|cuda=-tudomány}}
* {{Z|mn}}: {{W+|mn|зүй}}
* {{Z|nv}}: {{W-|nv|naalkaah}}
* {{Z|nds}}: {{W-|nds|-logie|m}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|-logi}}
* {{Z|oc}}: {{W-|oc|-logia|m}}
* {{Z|ug}}: {{W-|ug|ـشۇناسلىق}}
* {{Z|uz}}: {{W-|uz|-shunoslik}}, {{W-|uz|-logiya}}
* {{Z|pl}}: {{W+|pl|-logia|m}}
* {{Z|pt}}: {{W+|pt|-logia|m}}
* {{Z|kk}}: {{W-|kk|-тану}}
* {{Z|ro}}: {{W-|ro|-logie|m}}
* {{Z|ru}}: {{W+|ru|-ло́гия|m}}, {{W+|ru|ведение|nt|cuda=-ве́дение}}, {{W+|ru|знание|nt|cuda=-зна́ние}}
* {{Z|se}}: {{W-|se|-logiija}}
* {{Z|sc}}: {{W-|sc|-logia|m}}
* {{Z|ceb}}: {{W-|ceb|-lohia}}
* {{Z|scn}}: {{W+|scn|-luggìa|m}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|-ло̀гија|m}}
*: Latînî: {{W-|sh|-lògija|m}}
* {{Z|sk}}: {{W-|sk|-lógia|m}}
* {{Z|sl}}: {{W-|sl|-logija|m}}
* {{Z|es}}: {{W+|es|-logía|m}}
* {{Z|su}}: {{W-|su|-logi}}
* {{Z|sv}}: {{W+|sv|-logi|g}}
* {{Z|tg}}: {{W-|tg|-шиносӣ}}
* {{Z|tl}}: {{W-|tl|-lohia}}
* {{Z|ta}}: {{W-|ta|-இயல்}}
* {{Z|th}}: {{W+|th|วิทยา|cuda=-วิทยา|tr=-wíttáyaa}}
* {{Z|tr}}: {{W+|tr|-loji}}
* {{Z|ur}}: {{W-|ur|یات|tr=-iyāt}}
* {{Z|uk}}: {{W-|uk|-ло́гія|m}}
* {{Z|vi}}: {{W+|vi|học}}
* {{Z|wa}}: {{W+|wa|-lodjeye|m}}
* {{Z|cy}}: {{W-|cy|-leg}}
* {{Z|km}}: {{W+|km|វិទ្យា|cuda=-វិទ្យា}}, {{W-|km|សាស្ត្រ|cuda=-សាស្ត្រ}}
* {{Z|grc}}: {{W-|grc|-λογία|m}}
* {{Z|el}}: {{W+|el|-λογία|m}}
{{werger-bin}}
:{{werger çavkanî|en|-logy|beş=Ev qismê Wergerê|oldid=75751475}}
qyprby9hkkabbbofde37uruknwa0kih
terror
0
49621
6531755
6471565
2026-09-15T21:27:42Z
Wikihez
7344
Îngilîzî/Navdêr: beşa «Etîmolojî» hate zêdekirin (DefSideBoxes)
6531755
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
:{{ku-tîp|teror}}
* {{deng|en|en-us-terror.ogg|dever=DYA}}
=== Navdêr ===
{{navdêr|en}}
# [[teror]], [[terorîzm]], [[sehm]]
# [[tirs]], [[erjeng]]
==== Têkildar ====
{{kol3|en
|terrorism
|terrorist
}}
==== Etîmolojî ====
{{etîmon|en|:mîr|enm:terrour<id:teror>|id=teror|metn=+|dar=1}}
b05wxi07r09xxmc991qo9f7w8vldnpc
konfederasyon
0
50327
6531749
6472393
2026-09-15T20:43:12Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531749
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[yekîtî|Yekîtiya]] navbera hin [[dewlet]]an yan [[wîlayet]]an yên [[rêkxistin]]ên [[siyasî]].
#: {{hevmane|ku|komhevî}}
#* {{jêder|ku|Nefel.com, 7/2008|jêgirtin=Li gor Abdullah Ocalanî divêt kurdan desthilateka xurt ya kurdistanî li bakurê Kurdistanê nebe û hertim girêdayî Enqereyê bin. Lewra jî Abdullah Ocalan û DTP aşkera li dijî federasyon û '''konfederasyonê''' derdikevin û her carê formûlasyoneka nû ko ti qîmetê wê nîne derdixin pêş.}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Têkildar ====
{{kol3|ku
|konfederal
|konfederalî
|konfederalîst
|konfederalîstî
|konfederalîstîk
|konfederalîtî
|konfederalîzm
|konfederaltî
|federal
|federasyon
}}
==== Etîmolojî ====
Bi rêya {{ji|fr|ku|confédération}} ji latînî.
==== Werger ====
{{werger-ser}}
* {{Z|fa}}: {{W+|fa|کنفدراسیون}}
* {{Z|fi}}: {{W+|fi|konfederaatio}}
* {{Z|fr}}: {{W+|fr|confédération}}
* {{Z|en}}: {{W+|en|confederation}}
* {{Z|tr}}: {{W+|tr|konfederasyon}}, {{W+|tr|genbirlik}}
{{werger-bin}}
9mq6zqb1mtswvu6scfo6mce9dsf1o8e
çixandin
0
50348
6531709
6472415
2026-09-15T18:33:06Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531709
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Lêker ===
{{ku-tewîn-lk|çixandin|form=gerguhêz|niha=çixîn|borî=çixand}}
{{lêker|ku|c=x-gh}}
# bi [[xirab]]î [[behs]]ê kesekî kirin, <br>kesek dan [[şerm]]ê, <br>[[dijûn]] anku [[xeber]] gotin kesekî, <br>kesek [[sil]] kirin, [[xeyidandin]]
#* {{jêder|ku|jêgirtin=Kurê min, hişyar be kesî '''neçixîne'''!|Îbrahîm Remezan Zaxoyî: ''Ferhenga Pîşkên Kurdî'', weşanxaneya Spîrêz, Dihok 2007, r. 57}}
==== Tewandin ====
:-çixîn-
==== Bi alfabeyên din ====
* {{ku-ar|چخاندن}}
==== Jê ====
{{kol3|ku
|çixandî
|çixîner
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Bide ber ====
{{kol3|ku
|cewir
}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|beledig}}
* {{Z|de}}: {{W+|de|beleidigen}}, {{W+|de|schelten}}, {{W+|de|schimpfen}}, {{W+|de|beschimpfen}}
* {{Z|da}}: {{W-|da|fornærme}}, {{W-|da|skælde}}
* {{Z|eo}}: {{W-|eo|insulti}}
* {{Z|fi}}: {{W+|fi|suututtaa}}, {{W+|fi|loukata}}
* {{Z|fr}}: {{W+|fr|insulter}}
* {{Z|fy}}: {{W-|fy|rache}}
* {{Z|nl}}: {{W+|nl|affronteren}}, {{W+|nl|beledigen}}, {{W+|nl|krenken}}
* {{Z|io}}: {{W+|io|insultar}}
* {{Z|en}}: to {{W+|en|insult}}, to {{W+|en|offend}}, to {{W+|en|curse}}, to {{W+|en|revile}}, to {{W+|en|affront}}
* {{Z|it}}: {{W+|it|insultare}}, {{W+|it|offendere}}
* {{Z|is}}: {{W-|is|skamma}}
* {{Z|ca}}: {{W+|ca|insultar}}
* {{Z|la}}: {{W-|la|insultare}}
* {{Z|no}}: {{W-|no|skjelle ut}}
* {{Z|pap}}: {{W-|pap|insultá}}, {{W-|pap|ofendé}}
* {{Z|pt}}: {{W+|pt|injuriar}}, {{W+|pt|insultar}}
* {{Z|ru}}: {{W+|ru|бранить}}
* {{Z|srn}}: {{W-|srn|afrontu}}
* {{Z|es}}: {{W+|es|insultar}}
* {{Z|sv}}: {{W+|sv|skälla ut}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# {{mane?|ku}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
{{werger-bin}}
67gm10glcw7mki3p6jn75gtyw3n09h3
6531710
6531709
2026-09-15T18:33:22Z
Wikihez
7344
/* Lêker */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531710
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Lêker ===
{{ku-tewîn-lk|çixandin|form=gerguhêz|niha=çixîn|borî=çixand}}
{{lêker|ku|c=x-gh}}
# bi [[xirab]]î [[behs]]ê kesekî kirin, <br>kesek dan [[şerm]]ê, <br>[[dijûn]] anku [[xeber]] gotin kesekî, <br>kesek [[sil]] kirin, [[xeyidandin]]
#* {{jêder|ku|jêgirtin=Kurê min, hişyar be kesî '''neçixîne'''!|Îbrahîm Remezan Zaxoyî: ''Ferhenga Pîşkên Kurdî'', weşanxaneya Spîrêz, Dihok, 2007, r. 57}}
==== Tewandin ====
:-çixîn-
==== Bi alfabeyên din ====
* {{ku-ar|چخاندن}}
==== Jê ====
{{kol3|ku
|çixandî
|çixîner
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Bide ber ====
{{kol3|ku
|cewir
}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|beledig}}
* {{Z|de}}: {{W+|de|beleidigen}}, {{W+|de|schelten}}, {{W+|de|schimpfen}}, {{W+|de|beschimpfen}}
* {{Z|da}}: {{W-|da|fornærme}}, {{W-|da|skælde}}
* {{Z|eo}}: {{W-|eo|insulti}}
* {{Z|fi}}: {{W+|fi|suututtaa}}, {{W+|fi|loukata}}
* {{Z|fr}}: {{W+|fr|insulter}}
* {{Z|fy}}: {{W-|fy|rache}}
* {{Z|nl}}: {{W+|nl|affronteren}}, {{W+|nl|beledigen}}, {{W+|nl|krenken}}
* {{Z|io}}: {{W+|io|insultar}}
* {{Z|en}}: to {{W+|en|insult}}, to {{W+|en|offend}}, to {{W+|en|curse}}, to {{W+|en|revile}}, to {{W+|en|affront}}
* {{Z|it}}: {{W+|it|insultare}}, {{W+|it|offendere}}
* {{Z|is}}: {{W-|is|skamma}}
* {{Z|ca}}: {{W+|ca|insultar}}
* {{Z|la}}: {{W-|la|insultare}}
* {{Z|no}}: {{W-|no|skjelle ut}}
* {{Z|pap}}: {{W-|pap|insultá}}, {{W-|pap|ofendé}}
* {{Z|pt}}: {{W+|pt|injuriar}}, {{W+|pt|insultar}}
* {{Z|ru}}: {{W+|ru|бранить}}
* {{Z|srn}}: {{W-|srn|afrontu}}
* {{Z|es}}: {{W+|es|insultar}}
* {{Z|sv}}: {{W+|sv|skälla ut}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# {{mane?|ku}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
{{werger-bin}}
d1zot9niqjhmp3yru50ja5pf234qloi
sibit
0
50883
6531712
6472770
2026-09-15T18:34:34Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531712
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Illustration Nasturtium officinale0.jpg|thumb|250px|sibit]]
{{navdêr|ku|z=m}}
# [[biharat|Biharat]] û [[giya]]yek [[xwarin]]ê yê [[bêhnxweş]] e.
#: {{hevmane|ku|pîz|tûzik|tûzmask|kîzmas}}
#* {{jêder|ku|Bavê Jîndar, Lotikxane.com, 11/2008|jêgirtin='''Sibit''' biharat û giyayek xwarinê ya bêhnxweş e. Bejna wê 30- 70 cm bilind dibe. Naftenga wê kesk an jî keskeşîn e, wek movikan li ser hev bilind dibe. Nava navtenga wê vala ye. Ji navtengê çiq jê belav dibin. Bi çiqan ve pelên kesk yên zirav wek tayê birçik e. Di nîvê havînê de kulîlkên wê yên bêhnxweş, zerîn wek ûşî û sîvankan xwe digehînin hev. Bizrê wê pahn û hêkanî (peyzî), di nava wan kulîlkan de ne. Rengê wan esmerê qahweyî ne. Di TIB ê de tovê wê jibo derman û parfûman tê bikaranîn.}}
# {{kontrol|rêz=erê|[[kîzmas]], [[mendê]], [[mendik]]}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|sebit
|sebt
|sibt
}}
==== Navê zanistî ====
* {{g|ku|Nasturtium officinale}}
==== Jê ====
{{kol3|ku
|sibitî
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Brunnenkresse|f}}
* {{Z|cs}}: {{W+|cs|řeřicha}}
* {{Z|eo}}: {{W-|eo|akvokreso}}
* {{Z|fa}}: {{W-|fa|شاهی آبی}}
* {{Z|fi}}: {{W+|fi|vesikrassi}}
* {{Z|fr}}: {{W-|fr|cresson d'eau}}, {{W+|fr|cresson de fontaine}}
* {{Z|nl}}: {{W+|nl|waterkers}}
* {{Z|en}}: {{W+|en|watercress}}
* {{Z|it}}: {{W-|it|crescione di fonte}}, {{W-|it|crescione acquatico}}, {{W-|it|crescione}}
* {{Z|hu}}: {{W+|hu|vizitorma}}
* {{Z|pt}}: {{W+|pt|agrião}}, {{W-|pt|agriões-de-água}}, {{W-|pt|agriões-das-fontes}}
* {{Z|ru}}: {{W-|ru|кресс водяной}}
* {{Z|es}}: {{W+|es|berro}}
* {{Z|sv}}: {{W-|sv|vattenkrasse}}
* {{Z|tr}}: {{W+|tr|su teresi}}
* {{Z|uk}}: {{W-|uk|водяний крес}}
{{werger-bin}}
k2zjbosl32qlciu7d6kl7120tohdbii
kêbirk
0
52191
6531771
6310870
2026-09-16T05:31:52Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531771
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[yarî|Yariya]] yan [[kar]]ê ku du yan zêdetir kes dikin daku [[diyar]] bibe ka [[kîjan]] [[zîrek]]tir e.
#: {{hevmane|ku|pêşbazî|pêşbirk|hevrikî|berebezî|berêkane|hawirkanî|riberiz|kêbirk|milmilane|dijberî|reqabet|misabiqe|sîbaq}}
#: {{nm|ku|'''kêbirka''' bezînê}}
#: {{nm|ku|'''kêbirka''' helbestan}}
#* {{jêder|ku|Rojnameya Rûdaw ji Elî Fileyhî dipirse, Rudaw.net, 7/2009|jêgirtin=Rûdaw: Meha bê li Herêma Kurdistan’ê hilbijartinên parlemen û serokatiya herêmê tên encamdayîn. Di nav wan lîsteyan de lîsteyek bi navê Lîsta Destûrî heye ku '''kêbirkê''' dike, erê vê lîsteyê têkilî bi partiya we ve heye?}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Jê ====
{{kol3|ku
|kêbirkane
|kêbirkî
|kêbirkvan
|kêbirkvanî
}}
==== Etîmolojî ====
[[kê]] + [[birin]] - [[-in]] + [[-k]]
==== Werger ====
{{werger-ser}}
* {{Z|fi}}: {{W+|fi|kilpailu}}, {{W+|fi|kisa}}
* {{Z|fr}}: {{W|fr|concours|m}}, {{W|fr|compétition|f}}
* {{Z|nl}}: {{W|nl|competitie|f}}, {{W|nl|toernooi|n}}
* {{Z|en}}: {{W+|en|competition}}, {{W+|en|contest}}
* {{Z|it}}: {{W|it|concorso|m}}, {{W|it|competizione|f}}, {{W|it|gara|f}}
* {{Z|ca}}: {{W+|ca|competició}}
* {{Z|ml}}: {{W-|ml|മത്സരം|tr=malsaram}}
* {{Z|pt}}: {{W|pt|concurso|m}}, {{W|pt|competição|f}}
* {{Z|sv}}: {{W|sv|tävling|c}}
* {{Z|tr}}: {{W+|tr|müsabaka}}, {{W+|tr|yarış}}
{{werger-bin}}
1tvzf7ded6k4ztamoyz1x1eatmtimp6
vebuhtin
0
52527
6531736
6474308
2026-09-15T20:33:23Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531736
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Lêker ===
{{lêker|ku|c=p-gh}}
# [[berpirsiyarî|Berpirsiyariya]] [[kar]]ekî dan kesekî; kesek [[wezîfedar]] kirin.
#: {{hevmane|ku|veşandin|tayîn kirin}}
#* {{jêder|ku|Zeynelabidîn Zinar di hevpeyivînekê de li gel Gabar Çiyan, Netkurd.com, 7/2009|jêgirtin=Dema Selîm Temo Zanîngeha Enqerê qedandiye, doktora xwe jî her li ser wêjeya kurdî daye. Niha Saziya Xwendina Bala (YOK) ew vebuhtî kiriye ku li Mêrdînê projeya Ziman û Edebiyata Kurdî amade bike.}}
==== Tewandin ====
:ve-buh-
==== Formên alternatîv ====
{{kol3|ku
|vebihandin
|vebihtin
|vebotin
|vebuhandin
}}
==== Jê ====
{{kol3|ku
|vebuher
|vebuhtî
}}
==== Etîmolojî ====
{{bnr|بووژان}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|berufen}}, {{W+|de|abgrenzen}}, {{W+|de|ansetzen}}, {{W+|de|aussetzen}}, {{W+|de|bestimmen}}, {{W+|de|einsetzen}}, {{W+|de|ernennen}}, {{W+|de|festsetzen}}
* {{Z|fa}}: {{W+|fa|تعیین کردن}}, {{W-|fa|کاندید کردن}}, {{W+|fa|معرفی کردن}}, {{W-|fa|منتصب کردن}}, {{W-|fa|نامزد کردن}}
* {{Z|fi}}: {{W+|fi|nimittää}}
* {{Z|fr}}: {{W+|fr|déterminer}}, {{W+|fr|désigner}}, {{W+|fr|déplacer}}, {{W+|fr|fixer}}, {{W+|fr|installer}}, {{W+|fr|nommer}}
* {{Z|en}}: to {{W+|en|appoint}}, to {{W+|en|nominate}}, to [[asign]] someone to a [[post]], {{W+|en|relegate}}
* {{Z|tr}}: {{W-|tr|tayin etmek}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# {{mane?|ku}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|tayin}}
{{werger-bin}}
eeonaipazrnlw6sxezvd2p4ek2pgej8
6531745
6531736
2026-09-15T20:41:05Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531745
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Lêker ===
{{lêker|ku|c=p-gh}}
# [[berpirsiyarî|Berpirsiyariya]] [[kar]]ekî dan kesekî; kesek [[wezîfedar]] kirin.
#: {{hevmane|ku|veşandin|tayîn kirin}}
#* {{jêder|ku|Zeynelabidîn Zinar di hevpeyivînekê de li gel Gabar Çiyan, Netkurd.com, 7/2009|jêgirtin=Dema Selîm Temo Zanîngeha Enqerê qedandiye, doktora xwe jî her li ser wêjeya kurdî daye. Niha Saziya Xwendina Bala (YOK) ew vebuhtî kiriye ku li Mêrdînê projeya Ziman û Edebiyata Kurdî amade bike.}}
==== Tewandin ====
:ve-buh-
==== Formên alternatîv ====
{{kol3|ku
|vebihandin
|vebihtin
|vebotin
|vebuhandin
}}
==== Jê ====
{{kol3|ku
|vebuher
|vebuhtî
}}
==== Etîmolojî ====
{{bnr|بووژان}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|berufen}}, {{W+|de|abgrenzen}}, {{W+|de|ansetzen}}, {{W+|de|aussetzen}}, {{W+|de|bestimmen}}, {{W+|de|einsetzen}}, {{W+|de|ernennen}}, {{W+|de|festsetzen}}
* {{Z|fa}}: {{W+|fa|تعیین کردن}}, {{W-|fa|کاندید کردن}}, {{W+|fa|معرفی کردن}}, {{W-|fa|منتصب کردن}}, {{W-|fa|نامزد کردن}}
* {{Z|fi}}: {{W+|fi|nimittää}}
* {{Z|fr}}: {{W+|fr|déterminer}}, {{W+|fr|désigner}}, {{W+|fr|déplacer}}, {{W+|fr|fixer}}, {{W+|fr|installer}}, {{W+|fr|nommer}}
* {{Z|en}}: to {{W+|en|appoint}}, to {{W+|en|nominate}}, to [[asign]] someone to a [[post]], {{W+|en|relegate}}
* {{Z|tr}}: {{W-|tr|tayin etmek}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# {{mane?|ku}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|tayin}}
{{werger-bin}}
mahzdv59wf395cso2bcxvp4f6axfjjr
lezok
0
52795
6531780
6474543
2026-09-16T07:10:29Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531780
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Rengdêr ===
{{ku-tewîn-rd|lezok}}
{{rengdêr|ku}}
# [[lezgîn]], [[bilez]], bi[[lez]], bi [[lez]], [[acîl]], [[zû]]
#* {{jêder-websîte|ku|jêgirtin=Ji hêla din de, ew Lêgervanê, ku biryarên '''lezok''' distîne, dê hem mafxwerê mafên hevpîşeyên xwe bête xuyakirin, û hem jî dê ziyanên giran, bê hemdî xwe, bigihîne dîrok û gelê xwe.|nivîskar=Heyder Omer|sernav=Çima li ba xelkê rûmet e û li ba me robet e?!|websîte=Diyarname.com|sal=7/2009}}
==== Jê ====
{{kol3|ku
|lezokane
|lezokî
|lezokîtî
|lezoktî
}}
==== Etîmolojî ====
Ji {{paş|ku|lez|-ok}} yan [[lezîn]] - [[-în]] + [[-ok]]
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W|de|eilig}}, {{W|de|voreilig|guman=erê}}
* {{Z|ar}}: {{W|ar|العجول|guman=erê}}
* {{Z|en}}: {{W+|en|volant}}, {{W|en|lunger|guman=erê}}, {{W|en|quickie|guman=erê}}, {{W|en|on the wings of the wind|guman=erê}}, {{W|en|overhasty|guman=erê}}, {{W|en|over-hasty|guman=erê}}
* {{Z|tr}}: {{W+|tr|hızlı}}, {{W+|tr|aceleci}}
{{werger-bin}}
skm35xs755vuuzxydkk4ga8lr7bmkbg
zelq
0
52876
6531735
6474601
2026-09-15T20:31:54Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531735
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/zɛlq/}}
* {{kîte|ku|zelq}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# Paşmayîya xwarinê yan jî pîsatiya ku ji qula firrindeyan (mirîşk, hemû çivîk, [[elok]]) derdikeve
#: {{nm|ku|Fatê li sola Kemalo nerî ku bi zelqe, wê çaxê bi dizê [[hêk]]a hisiya}}
#* {{jêder|ku|kulilk.com, 07/2008|jêgirtin=Mirîşka bigere wê lingê wê bi '''zelq''' be}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Bide ber ====
{{kol3|ku
|rêx
|gû
|pişkul
}}
==== Werger ====
{{werger-ser}}
* {{Z|en}}: {{W-|en|excrement of birds}}, {{W+|en|adhesion}}
* {{Z|tr}}: {{W+|tr|sağı}}, {{W+|tr|yapışma}}
{{werger-bin}}
nhlyaaxkmk3pk7nrssoe9r96vxtuc3v
cotgeh
0
55564
6531763
6477261
2026-09-15T21:47:50Z
Wikihez
7344
Kurmancî/Navdêr: mane, hevmane hatin guhartin, mane, hevmane, dijmane hatin guhartin, beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531763
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-cotgeh.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[cih|Cihê]] ku [[nemaze|nemaze]] [[kar|kar]] û [[bar|barên]] [[cotemenî|cotemeniyê]] [[debr|debr]] û [[dexl|dexlê]], [[zendarî|zendariyê]], [[terşdarî|terşdariyê]] û [[hwd|hwd]]. lê tê kirin.
#: {{hevmane|ku|kûfregeh}}
# [[kêlgeh|Kêlgeha]] [[tenê|tenê]] [[ji bo]] [[cot|cot]] [[hatiye|hatiye]] [[parastin|parastin]] [[daku|daku]] [[li wir]] [[dehl|dehl]] [[dan|dan]] bê [[reşandin|reşandin]] û [[çandin|çandin]].
#: {{hevmane|ku|zevî}}
#: {{dijmane|ku|mêrg}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
Ji {{paş|ku|cot|-geh}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Bauernhof|n}}
* {{Z|tr}}:{{W+|tr|çiflik}}, {{W+|tr|çiftlik|guman=erê}}
{{werger-bin}}
heql9akqyiym1fr8anyh6nhy2ikx9vg
6531769
6531763
2026-09-15T23:12:11Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531769
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-cotgeh.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[cih|Cihê]] ku [[nemaze]] [[kar]] û [[bar]]ên [[cotemenî|cotemeniyê]] [[debr]] û [[dexl]]ê, [[zendarî|zendariyê]], [[terşdarî|terşdariyê]] û [[hwd]]. lê tê kirin.
#: {{hevmane|ku|kûfregeh}}
# [[kêlgeh|Kêlgeha]] [[tenê]] [[ji bo]] [[cot]] [[hatiye]] [[parastin]] [[daku]] [[li wir]] [[dehl]] [[dan]] bê [[reşandin]] û [[çandin]].
#: {{hevmane|ku|zevî}}
#: {{dijmane|ku|mêrg}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
Ji {{paş|ku|cot|-geh}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Bauernhof|n}}
* {{Z|tr}}:{{W+|tr|çiflik}}, {{W+|tr|çiftlik|guman=erê}}
{{werger-bin}}
dwpqp85qv09n6cyhs36rirq9qsqwbal
erotîzm
0
57562
6531668
6306172
2026-09-15T17:34:20Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531668
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Lingerie.jpg|thumb|160px|hin cilên erotîk]]
{{navdêr|ku|z=m}}
# [[babet]] û [[mijar]]ên [[erotîk]], [[evînî]], [[seks]]
#* {{jêder|ku|jêgirtin=Ji bo keşifkirina berhemên dawî yên wêje û muzîkî yên bi zimanê baskî, Salona Durango "Fûara Muzîk û Pirtûkan" li dar dixe. Mijara fûarê ya îsal '''erotîzm''' e û welatê mêvan jî Kurdistan e.|Diyarname.com, 11/2010}}
==== Tewandin ====
{{ku-tewîn-nav|mê|y}}
==== Jê ====
{{kol3|ku
|otoerotîzm
|homoerotîzm
}}
==== Etîmolojî ====
Bi rêya {{hv|fr|érotisme}} ji {{hv|<strong class="error"><span class="scribunto-error mw-scribunto-error-ceee2e39">Xeletiya Luayê di Modul:parameters de, li ser rêza 692: Parameter 1 must be a valid language or etymology language name; the value "yewnaniya kevn" is not valid. See [[WT:LOL]] and [[WT:LOL/E]]..</span></strong>|Ἔρως}} ([[Eros]], [[xweda]]yê [[evîn]]ê ji [[mîtolojî|mîtolojiya]] [[yûnanî]] de) ji [[ἔρως]] ([[evîn]], [[hez]]kirin) + [[-îzm]]
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W|de|Erotik|f}}
* {{Z|bg}}: {{W|bg|Еротика}}
* {{Z|cs}}: {{W|cs|erotika}}
* {{Z|zh}}: {{W|zh|情色}} (qíng sè), {{W|zh|性感}} (xìng gǎn)
* {{Z|da}}: {{W|da|erotik}}
* {{Z|id}}: {{W|id|erotisme}}
* {{Z|eo}}: {{W|eo|erotiko}}
* {{Z|et}}: {{W|et|erootika}}
* {{Z|fi}}: {{W|fi|erotiikka}}
* {{Z|fr}}: {{W|fr|érotisme}}
* {{Z|nl}}: {{W|nl|erotieka}}
* {{Z|he}}: {{W|he|ארוטיקה}}
* {{Z|en}}: {{W|en|eroticism}}
* {{Z|ia}}: {{W|ia|erotica}}
* {{Z|it}}: {{W|it|erotismo}}
* {{Z|lt}}: {{W|lt|erotik}}
* {{Z|hu}}: {{W|hu|erotika}}
* {{Z|no}}: {{W|no|erotikk}}
* {{Z|pl}}: {{W|pl|erotyka}}, {{W|pl|erotyzm}}
* {{Z|pt}}: {{W|pt|erotismo}}
* {{Z|ro}}: {{W|ro|erotism}}
* {{Z|ru}}: {{W|ru|эротика}} (erotika)
* {{Z|sh}}: {{W|sh|eротика}} (erotika)
* {{Z|es}}: {{W|es|erotismo}}
* {{Z|sv}}: {{W|sv|erotik}}
* {{Z|tr}}: {{W|tr|erotizm}}, {{W|tr|erosçuluk}}, {{W|tr|şehvaniyet}}, {{W|tr|kösnüllük}}
{{werger-bin}}
mafxt2wi5c8pw9jzj3qywwlqdb5luif
6531706
6531668
2026-09-15T18:31:49Z
Wikihez
7344
/* Etîmolojî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531706
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Lingerie.jpg|thumb|160px|hin cilên erotîk]]
{{navdêr|ku|z=m}}
# [[babet]] û [[mijar]]ên [[erotîk]], [[evînî]], [[seks]]
#* {{jêder|ku|jêgirtin=Ji bo keşifkirina berhemên dawî yên wêje û muzîkî yên bi zimanê baskî, Salona Durango "Fûara Muzîk û Pirtûkan" li dar dixe. Mijara fûarê ya îsal '''erotîzm''' e û welatê mêvan jî Kurdistan e.|Diyarname.com, 11/2010}}
==== Tewandin ====
{{ku-tewîn-nav|mê|y}}
==== Jê ====
{{kol3|ku
|otoerotîzm
|homoerotîzm
}}
==== Etîmolojî ====
Bi rêya {{ji|fr|ku|érotisme}} ji {{ji|grc|ku|Ἔρως}} ([[Eros]], [[xweda]]yê [[evîn]]ê ji [[mîtolojî|mîtolojiya]] [[yûnanî]] de) ji [[ἔρως]] ([[evîn]], [[hez]]kirin) + [[-îzm]]
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W|de|Erotik|f}}
* {{Z|bg}}: {{W|bg|Еротика}}
* {{Z|cs}}: {{W|cs|erotika}}
* {{Z|zh}}: {{W|zh|情色}} (qíng sè), {{W|zh|性感}} (xìng gǎn)
* {{Z|da}}: {{W|da|erotik}}
* {{Z|id}}: {{W|id|erotisme}}
* {{Z|eo}}: {{W|eo|erotiko}}
* {{Z|et}}: {{W|et|erootika}}
* {{Z|fi}}: {{W|fi|erotiikka}}
* {{Z|fr}}: {{W|fr|érotisme}}
* {{Z|nl}}: {{W|nl|erotieka}}
* {{Z|he}}: {{W|he|ארוטיקה}}
* {{Z|en}}: {{W|en|eroticism}}
* {{Z|ia}}: {{W|ia|erotica}}
* {{Z|it}}: {{W|it|erotismo}}
* {{Z|lt}}: {{W|lt|erotik}}
* {{Z|hu}}: {{W|hu|erotika}}
* {{Z|no}}: {{W|no|erotikk}}
* {{Z|pl}}: {{W|pl|erotyka}}, {{W|pl|erotyzm}}
* {{Z|pt}}: {{W|pt|erotismo}}
* {{Z|ro}}: {{W|ro|erotism}}
* {{Z|ru}}: {{W|ru|эротика}} (erotika)
* {{Z|sh}}: {{W|sh|eротика}} (erotika)
* {{Z|es}}: {{W|es|erotismo}}
* {{Z|sv}}: {{W|sv|erotik}}
* {{Z|tr}}: {{W|tr|erotizm}}, {{W|tr|erosçuluk}}, {{W|tr|şehvaniyet}}, {{W|tr|kösnüllük}}
{{werger-bin}}
cfcip4bzbi5v8lpkpi2rfw8s3o9rjgl
prosedûr
0
57899
6531701
6314952
2026-09-15T18:28:38Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531701
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[awa|Awa]]yê [[meşandin]]a yan [[meşîn]]a [[kar]]ekî.
#: {{hevmane|ku|rêbaz|meşandin|birêvebirin|azîne|metod|prose|pêvajo}}
#* {{jêder|ku|Netkurd.com, 12/2010|jêgirtin=Wezîrê karûbarên hundurîn ê Tirkiyeyê Beşîr Atalay vê sibehê di kanala NTV de ragihand ku li her derê gundî bi daxwaza vegerandin an guhertina navên gundên xwe dikarin biçin referandumê. Gava bi referandumê qebûl bû ew dikarin bi nameyekê serdana qeymeqam bikin, qeymeqam ê nameyê verêke walî, walî jî ê verêke nik min ez ê bi îmzayekê navê wan xwestî qebûl bikim. Wezîr Atalay ragihand ku mesela guhertina navan bi vê prosedurê dimeşe û ew ne karê belediyeyan e.}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|prosedur
}}
==== Etîmolojî ====
Bi rêya fransî [[procédure]] yan îngilîzî [[procedure]] ji latînî [[procedere]] ([[pêşve çûn]], [[pêş ketin]])
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Prozedur|f}}, {{W+|de|Prozedere|n}}, {{W+|de|Ablauf|m}}, {{W+|de|Vorgehensweise|f|guman=erê}}
* {{Z|cs}}: {{W+|cs|postup|m}}
* {{Z|zh}}: {{W-|zh|做法|tr=zuò fǎ}}
* {{Z|ar}}: {{W-|ar|إجراءات}}
* {{Z|fi}}: {{W+|fi|toimintatapa}}, {{W+|fi|menetelmä}}, {{W+|fi|menettely}}
* {{Z|fr}}: {{W+|fr|procédé|m}}
* {{Z|nl}}: {{W+|nl|procedure|f}}
* {{Z|en}}: {{W+|en|procedure}}, {{W+|en|draw}}
* {{Z|it}}: {{W+|it|procedura|f}}
* {{Z|ja}}: [[手段]] ([[しゅだん]], [[shudan]]), {{W+|ja|やり方|tr=[[やりかた]], yarikata}}
* {{Z|ca}}: {{W+|ca|procediment|m}}
* {{Z|hu}}: {{W+|hu|eljárás}}
* {{Z|pt}}: {{W+|pt|procedimento|m}}
* {{Z|ro}}: {{W+|ro|procedură|f}}, {{W+|ro|procedeu|n}}
* {{Z|es}}: {{W+|es|procedimiento|m}}
* {{Z|sw}}: {{W+|sw|mtindo|xs=Swahili}}
* {{Z|tr}}: {{W+|tr|prosedür}}
* {{Z|el}}: {{W+|el|διαδικασία|tr=diadikasía|f}}
{{werger-bin}}
tujbnrxbxmv5ihdpuji2t1swry7swq2
6531723
6531701
2026-09-15T20:12:23Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531723
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[awa|Awayê]] [[meşandin]]a yan [[meşîn]]a [[kar]]ekî.
#: {{hevmane|ku|rêbaz|meşandin|birêvebirin|azîne|metod|prose|pêvajo}}
#* {{jêder|ku|Netkurd.com, 12/2010|jêgirtin=Wezîrê karûbarên hundurîn ê Tirkiyeyê Beşîr Atalay vê sibehê di kanala NTV de ragihand ku li her derê gundî bi daxwaza vegerandin an guhertina navên gundên xwe dikarin biçin referandumê. Gava bi referandumê qebûl bû ew dikarin bi nameyekê serdana qeymeqam bikin, qeymeqam ê nameyê verêke walî, walî jî ê verêke nik min ez ê bi îmzayekê navê wan xwestî qebûl bikim. Wezîr Atalay ragihand ku mesela guhertina navan bi vê prosedurê dimeşe û ew ne karê belediyeyan e.}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|prosedur
}}
==== Etîmolojî ====
Bi rêya fransî [[procédure]] yan îngilîzî [[procedure]] ji latînî [[procedere]] ([[pêşve çûn]], [[pêş ketin]])
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Prozedur|f}}, {{W+|de|Prozedere|n}}, {{W+|de|Ablauf|m}}, {{W+|de|Vorgehensweise|f|guman=erê}}
* {{Z|cs}}: {{W+|cs|postup|m}}
* {{Z|zh}}: {{W-|zh|做法|tr=zuò fǎ}}
* {{Z|ar}}: {{W-|ar|إجراءات}}
* {{Z|fi}}: {{W+|fi|toimintatapa}}, {{W+|fi|menetelmä}}, {{W+|fi|menettely}}
* {{Z|fr}}: {{W+|fr|procédé|m}}
* {{Z|nl}}: {{W+|nl|procedure|f}}
* {{Z|en}}: {{W+|en|procedure}}, {{W+|en|draw}}
* {{Z|it}}: {{W+|it|procedura|f}}
* {{Z|ja}}: [[手段]] ([[しゅだん]], [[shudan]]), {{W+|ja|やり方|tr=[[やりかた]], yarikata}}
* {{Z|ca}}: {{W+|ca|procediment|m}}
* {{Z|hu}}: {{W+|hu|eljárás}}
* {{Z|pt}}: {{W+|pt|procedimento|m}}
* {{Z|ro}}: {{W+|ro|procedură|f}}, {{W+|ro|procedeu|n}}
* {{Z|es}}: {{W+|es|procedimiento|m}}
* {{Z|sw}}: {{W+|sw|mtindo|xs=Swahili}}
* {{Z|tr}}: {{W+|tr|prosedür}}
* {{Z|el}}: {{W+|el|διαδικασία|tr=diadikasía|f}}
{{werger-bin}}
khag8i75mioass0ax4karo4f34ux61g
rehend
0
59579
6531744
6325250
2026-09-15T20:40:39Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531744
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[dirêjahî|Dirêjahî]] û [[bilindahî]] û [[firehî|firehiya]] tiştekî.
#* {{jêder|ku|jêgirtin=Pêwîst e ku xwerêxistinkirina sîstema KCKê ya çar '''rehendî''' (dört boyutlu) were pejirandin.|Hezenparastin.com}}
# [[perçe|Perçeyek]] taybet ya [[rewş]] an jî [[hêl]]ekî ku li ser fikr û ramanê mirov bandorê çêdike.
#: {{nm|ku|Eger tu ji vê '''rehendê''' li ser mijarê hûr bî, tu wê bibînî ku hin mîrên kurdan berî sed salan xwedî dîwana helbestên tirkî bûne...}}
# [[alî]], [[layen]]<ref>{{pirtûk|nivîskar=Ergin Öpengin|sernav=Rewşa kurdî ya sosyolenguîstîk li Tirkiyeyê, Ferhengoka têgehên zimmanasiya civakî|weşanxane={{w|Weşanên Avesta|Avesta}}|sal=2011|çap=1|rûpel=214|isbn=978-605-5585-71-6}}</ref>
#* {{jêder|ku|jêgirtin=Di nav dorbenda zagona navnetewî da konsepta mafê biryardana çarenûs piştî gelek salan guheriye. Ev guherîn ji aliyekî ve '''rehende'''ka derveyî eşkere dike ku bi cudaxwaziyê ve têkildar e; ji aliyê din ve jî rehendeka hindirîn eşkere dike ku vê yekê rê daye demokrasiya beşdar û parastina kêmaniyan di konteksta pevrejiyaneka piranîgir/pluralîst di nav sînorên dewletê da.| Kovara "Kurdish Studies" Volume: 1, No: 1, rp:5-27}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Jê ====
{{kol3|ku
|1-rehendî,yekrehendî
|2-rehendî,durehendî
|3-rehendî,sêrehendî
|4-rehendî,çarrehendî
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Proportion|f}}, {{W+|de|Dimension|f}}
* {{Z|bn}}: {{W-|bn|মাত্রা}}
* {{Z|bg}}: {{W+|bg|размер|m}}
* {{Z|cs}}: {{W+|cs|rozměr|m}}
* {{Z|da}}: {{W+|da|dimension|c}}
* {{Z|fr}}: {{W+|fr|dimension|f}}
* {{Z|gd}}: {{W-|gd|tomhas|m}}
* {{Z|nl}}: {{W+|nl|dimensie|f}}, {{W+|nl|afmeting|f}}
* {{Z|he}}: {{W-|he|מימד|tr=meimad}}
* {{Z|en}}: {{W+|en|dimension}}, {{W+|en|aspect}}
* {{Z|it}}: {{W+|it|dimensione|f}}
* {{Z|ja}}: {{W+|ja|面積|tr=[[めんせき]], menseki}}
* {{Z|ca}}: {{W+|ca|dimensió|f}}
* {{Z|pt}}: {{W+|pt|dimensão}}
* {{Z|ro}}: {{W+|ro|dimensiune|f}}, {{W-|ro|dimensiuni|p}}
* {{Z|ru}}: {{W+|ru|размер|tr=razmér|m}}
* {{Z|sd}}: {{W+|sd|ماپَ|tr=maapa}}
* {{Z|sl}}: {{W-|sl|dimenzija|f}}
* {{Z|es}}: {{W+|es|dimensión|f}}
* {{Z|sv}}: {{W+|sv|dimension|c}}
* {{Z|tr}}: {{W+|tr|boyut}}
{{werger-bin}}
=== Çavkanî ===
{{çavkanî}}
0t7sm8pxb2cb7rg4lq3y727echvktad
koń
0
61164
6531647
6041445
2026-09-15T17:05:48Z
~2026-49765-60
15374
6531647
wikitext
text/x-wiki
== {{ziman|pl}} ==
=== Bilêvkirin ===
* {{deng|pl|Pl-{{PAGENAME}}.ogg|Deng}}
* {{deng|pl|LL-Q809 (pol)-Gower-koń.wav|Deng|dever=Bytom, Polonya}}
=== Navdêr ===
{{navdêr|pl|z=}}
# [[hesp]]
3o0fchaoso8woreb3fkf4ylyntn4anv
6531666
6531647
2026-09-15T17:29:11Z
Balyozbot
7867
[[Bikarhêner:Wikihez/skrîpt/py/dengbot.py|Bot]]: Formata parametreya "dever" hat guhartin
6531666
wikitext
text/x-wiki
== {{ziman|pl}} ==
=== Bilêvkirin ===
* {{deng|pl|Pl-{{PAGENAME}}.ogg}}
* {{deng|pl|LL-Q809 (pol)-Gower-koń.wav|Deng|dever=Bytom, Polonya}}
=== Navdêr ===
{{navdêr|pl|z=}}
# [[hesp]]
gsf7h8fs7azpheakeweauwmu8lmcn9f
posat
0
62420
6531742
6385269
2026-09-15T20:38:55Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531742
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[fîşek]], [[cebilxane]], [[pos]], [[gulle]], [[berik]]ên [[sîleh]]an
#* {{jêder-websîte|ku|jêgirtin=-Hikûmeta Tirk ji mêj ve şivîşkan ji me dike. Heya em bikarin bê tirs bixebitin û xebata xwe ji çavên neyar veşêrin, divê ji rûve em xwe ji aliyê hikûmetê bidin xuya kirin. Min divê çend têlgirafan bidim Anqerê ku: “Leşkerê Şêx Seîd nîzîkî me dibe, ji bo em bikarin xwe li pêş wî bigirin û nehêlin şûreş bigehê erdê me, em hêvî dikin ji bo çê kirina alayiyên milîsan di nava welêt de desturê bidin”. Ji ber ku leşkerê wan hindike wê destûra me bidin. Hîngê em dê bikarin bê tirs peyayên xwe di qezayên Kexte û Pêtirgeyê de bicivînin. Heye ku hikûmet him çek û '''posat''' jî bide me. çi gava Şêx Seîd Sêwerek sitand, em jî dê rabin. Ez bawerim ku holê em bikarin bi kêrî xwe û welêt bûn û ji bin destê Tirkan derkevin.|sernav=Bîranînên Osman Sebrî (1905 – 1993), birrê yekê, "Şoreş"|amadekar=Dilawer Zengî|websîte=Tirej.name|tarîxa-gihiştinê=25.10.2011}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|fa}}: {{W+|fa|ملزومات}}, {{W-|fa|وسایل لازم}}
* {{Z|tr}}: {{W+|tr|pusat}}, {{W+|tr|gereç}}, {{W+|tr|mühimmat}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=aopst|patos}}
mzuu55mv5gp9doww5wfogc1vtl6u3og
gangol
0
62732
6531705
6307056
2026-09-15T18:31:00Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531705
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Hammockonaboat.jpg|thumb|170px|jinek di hêlûçekê de]]
{{navdêr|ku|z=m}}
# Tişta/ê her [[ser]]ek wê / wî bi tiştekî ve hatiye [[girê]]dan lê ew bi xwe [[sist]] li [[hewa]] maye bo ku mirov [[rûniştin|rûne]] yan xwe [[pal]] de ser û xwe tê de [[hejandin|bihejîne]] anku [[colandin|bicolîne]].
#: {{hevmane|ku|colank|çincîr|hêlûç|kêşik|hêlanok|gengola|hamak}}
#* {{jêder|ku|jêgirtin=Em jî (=di devoka Diyarbekirê de) dibêjin ''''gangol''''.|Aykut Togcu di koma "Zimanê Kurdî" de li ser [[Rûname]]yê, 1/2012}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|gengola
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Bide ber ====
{{kol3|ku
|hêsk
|landik
}}
==== Werger ====
{{werger-ser}}
* {{Z|sq}}: {{W+|sq|shtrat i varur}}
* {{Z|de}}: {{W|de|Hängematte|f}}
* {{Z|bg}}: {{W-|bg|хамак|tr=khamak}}
* {{Z|cs}}: {{W|cs|houpací síť|f}}
* {{Z|da}}: {{W+|da|hængekøje}}
* {{Z|ar}}: {{W|ar|ارجوحة شبكية}} {{IPAchar|(’urjūħa šabakíyya)}}
* {{Z|hy}}: {{W-|hy|ճոճ|tr=čoč}}
* {{Z|eo}}: {{W|eo|hamako|xs=Esperanto}}
* {{Z|et}}: {{W+|et|võrkkiik}}
* {{Z|fa}}: {{W+|fa|بانوج}}
* {{Z|fi}}: {{W+|fi|riippumatto}}
* {{Z|fr}}: {{W|fr|hamac|m}}
* {{Z|ka}}: {{W|ka|ჰამაკი|tr=hamaki}}
* {{Z|hi}}: {{W+|hi|दो खूँटियों से बंधा जालीदार झूला}}
* {{Z|nl}}: {{W|nl|hangmat|f}}
* {{Z|he}}: {{W-|he|ערסל|tr=arsal}}
* {{Z|en}}: {{W+|en|hammock}}
* [[îstmûs zapotekî]]: [[guixhe]]
* {{Z|it}}: {{W|it|amaca|f}}
* {{Z|ja}}: {{W-|ja|ハンモック|tr=hanmokku}}
* {{Z|ca}}: {{W|ca|hamaca|f}}
* {{Z|ko}}: {{W-|ko|해먹|tr=haemeok}}
* {{Z|la}}: {{W+|la|lectulus pensilis}}
* {{Z|lv}}: {{W|lv|šūpuļtīkls|m}}
* [[lawsî]]: {{W|lo|ເປ|tr=pee|sc=Laoo|xs=Lao}}
* {{Z|lt}}: {{W|lt|hamakas|m}}
* {{Z|mk}}: {{W|mk|tr=léžalka|лежалка|f}}
* {{Z|hu}}: {{W+|hu|függőágy}}
* {{Z|no}}: {{W|no|hengekøye|f}}
* {{Z|pl}}: {{W|pl|hamak|m}}
* {{Z|pt}}: {{W|pt|rede|f}}
* {{Z|ro}}: {{W|ro|hamac|n}}
* {{Z|ru}}: {{W|ru|tr=gamák|гамак|m}}
* {{Z|sh}}: {{W|sh|visaljka|f}}
* {{Z|sk}}: {{W|sk|hojdacia sieť|f}}
* {{Z|sl}}: {{W|sl|viseča mreža|f}}
* {{Z|es}}: {{W|es|hamaca|f}}, [[hamaca paraguaya]]
* {{Z|sv}}: {{W|sv|hängmatta|c}}
* {{Z|th}}: {{W|th|เปลญวน}} (bplay yuan)
* [[tamilî]]: [[கித்தான்கட்டில்]] (kittankattil)
* {{Z|tr}}: {{W+|tr|hamak}}
* {{Z|uk}}: {{W|uk|tr=hamák|гамак|m}}, {{W|uk|підвісне ліжко|tr=pidvísne lížko}}
* {{Z|vi}}: {{W|vi|cái võng|xs=Vietnamese}}
{{werger-bin}}
isurhaltj91qx1gii84ocdsvpsi1tdb
greyfûrt
0
62738
6531746
6326873
2026-09-15T20:41:33Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531746
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Pomelos - Grapefruits.jpg|thumb|250px|greyfûrt]]
{{navdêr|ku|z=m}}
# {{e|ku|fêkî}} [[narincî|Narinciyeke]] wek [[pirteqal]]an e lê [[goşt]]ê wê [[sor]] û [[tehl]]tir e.
#: {{hevmane|ku|sindî|lîmona bihiştê|pistanbûk|greyp}}
# {{e|ku|dar}} [[dar|Dar]]a ku ew [[mêwe]] pê ve [[şên]] dibe.
#* {{jêder-websîte|ku|jêgirtin='''Greyfûrt''' riwakek daran e, ji malbata narinciyan e. Fêqîyek tama wê tahlemiz e. Dara wê 6-15 metro bilind dibe. Belgên wê keskê tarî, biriqok, kevilane, mezinahîya belgan bi qasî 15 cm hêkane û naweşin. Kulîlkên wê wek hûşîyan spî û bi hev re ne. Di nava kulîlkê de navikek û tozdanka dolkirinê heye. Ji wê kulîlkê fêqîyek ji pirteqalan mezintir, serûbinê wê hinekî dewisandî (pahn) û qaşilzirav, têravî û têrgoşt çêdibe. Aha ji wê fêqîyê re '''greyfûrt''' tê gotin. Rengê wan spîyane, peyazî an jî sorane ne. Her weha rengê goştê fêqî ji wisa nin. Bixwe rengê goştê wê dide qaşil. Du cûreyên wê hene, yên bi sîsik û yên bê sîsik. Lê yên herî bi qîmet yên goştsor in. Bi piranîya zimanên din jî ew nav lê tê kirin (bê guman navên wê yên şibandî jî hene, wek memikên keçan, fêqîya biheştê, pirteqala tahl û hwd.) Fêqîyek gellek faydedar e, lê nabit bi dermanan re bê xwarin ( ji ber ku bandora dermanan zêde bilind dike, dibe sedema (mirin jî têde) xisardayînên mezin.|nivîskar=Bavê Jîndar|sernav=Greyfûrt|websîte=Lotikxane.com|sal=11/2011}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|greypfrût
}}
==== Jê ====
{{kol3|ku
|dargreyfûrt
}}
==== Etîmolojî ====
Deyn ji {{deyn|en|ku|grapefruit}} ji [[grape]] ([[tirî]]) + [[fruit]] yan jî ji [[great]] [[fruit]] ("fêkiyê hêja"). Ji ber zêdebûna herfên bêdeng di bilêvkirina peyva resen ya îngilîzî de, di kurdî û tirkî de bo hêsankirina gotinê "p" bi "f" ve heliye û herwiha "grey'''frû'''t" bûye "gre'''yfû'''rt".
==== Werger ====
{{werger-ser}}
* [[alabamayî]]: [[yalàaha kaskaha]]
* {{Z|sh}}: {{W+|sh|arabisagar}}
* {{ca}} [[aranja]] {{mê}}
* {{Z|ce}}: {{W+|ce|грейпфрукт}}
* {{Z|da}}: {{W+|da|grapefrugt}}, {{W+|da|grape}}
* {{Z|de}}: {{W|de|Grapefruit|f}}, {{W|de|Pampelmuse|f}}
* {{Z|eo}}: {{W|eo|grapfrukto|xs=Esperanto}}
* {{Z|fi}}: {{W+|fi|greippi}}
* {{Z|fr}}: {{W|fr|pamplemousse|m}}, {{W|fr|pomélo|m}}
* {{Z|hi}}: {{W-|hi|मौसमी|tr=mausamī}}, {{W-|hi|छोटा चकोतरा|tr=chōṭā cakōtarā}}
* {{Z|nl}}: {{W|nl|grapefruit|m}}
* {{Z|io}}: {{W+|io|pompelmuso}}
* {{Z|he}}: {{W|he|אשכולית|tr=eshkolít|f}}
* {{Z|ilo}}: {{W+|ilo|lukban}}
* {{Z|id}}: [[jeruk]] [[bali]]
* {{Z|en}}: {{W+|en|grapefruit}}
* {{Z|it}}: {{W-|it|pompelmo|tr=tree & fruit}}
* {{Z|is}}: {{W|is|greipaldin|n}}, {{W|is|greip|f}}, {{W|is|tröllaldin|n}}
* {{Z|ja}}: {{W-|ja|グレープフルーツ|tr=gurēpu-furūtsu}}
* {{Z|sh}}: {{W|sh|grejpfrut|m}}, {{W|sh|limunika|f}}
* {{Z|mk}}: {{W|mk|грејпфрут|tr=gréjpfrut|m}}, {{W|mk|цитрон|tr=citrón|m}}
* [[maltayî]]Maltese: [[lumi-larinġ]]
* {{Z|nv}}: {{W+|nv|chʼil łitsxooí ntsxaaígíí}}
* {{Z|pl}}: {{W|pl|grejpfrut|m}}
* {{Z|pt}}: {{W|pt|toranja|f}}, {{W|pt|toronja|f}}
* {{Z|ru}}: {{W|ru|грейпфрут|tr=gr'ejpfrút|m}}
* {{Z|sk}}: {{W|sk|grenivka|f}}
* {{Z|es}}: {{W|es|toronja|f}}, {{W|es|pomelo|m}}
* {{Z|sv}}: {{W+|sv|balungi}}, {{W|sv|grape|c}}, {{W|sv|grapefrukt|c}}
* [[tagalogî]]: [[suha']]
* [[tawsûgî]]: [[suwa' gadja]]
* {{Z|tr}}: {{W+|tr|greyfurt}}, {{W+|tr|altın top}}
{{werger-bin}}
0wrhc7t1ubz6w4g53qkvn5q63unrw6s
6531766
6531746
2026-09-15T22:12:06Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531766
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Pomelos - Grapefruits.jpg|thumb|250px|greyfûrt]]
{{navdêr|ku|z=m}}
# {{e|ku|fêkî}} [[narincî|Narinciyeke]] wek [[pirteqal]]an e lê [[goşt]]ê wê [[sor]] û [[tehl]]tir e.
#: {{hevmane|ku|sindî|lîmona bihiştê|pistanbûk|greyp}}
# {{e|ku|dar}} [[dar|Dara]] ku ew [[mêwe]] pê ve [[şên]] dibe.
#* {{jêder-websîte|ku|jêgirtin='''Greyfûrt''' riwakek daran e, ji malbata narinciyan e. Fêqîyek tama wê tahlemiz e. Dara wê 6-15 metro bilind dibe. Belgên wê keskê tarî, biriqok, kevilane, mezinahîya belgan bi qasî 15 cm hêkane û naweşin. Kulîlkên wê wek hûşîyan spî û bi hev re ne. Di nava kulîlkê de navikek û tozdanka dolkirinê heye. Ji wê kulîlkê fêqîyek ji pirteqalan mezintir, serûbinê wê hinekî dewisandî (pahn) û qaşilzirav, têravî û têrgoşt çêdibe. Aha ji wê fêqîyê re '''greyfûrt''' tê gotin. Rengê wan spîyane, peyazî an jî sorane ne. Her weha rengê goştê fêqî ji wisa nin. Bixwe rengê goştê wê dide qaşil. Du cûreyên wê hene, yên bi sîsik û yên bê sîsik. Lê yên herî bi qîmet yên goştsor in. Bi piranîya zimanên din jî ew nav lê tê kirin (bê guman navên wê yên şibandî jî hene, wek memikên keçan, fêqîya biheştê, pirteqala tahl û hwd.) Fêqîyek gellek faydedar e, lê nabit bi dermanan re bê xwarin ( ji ber ku bandora dermanan zêde bilind dike, dibe sedema (mirin jî têde) xisardayînên mezin.|nivîskar=Bavê Jîndar|sernav=Greyfûrt|websîte=Lotikxane.com|sal=11/2011}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|greypfrût
}}
==== Jê ====
{{kol3|ku
|dargreyfûrt
}}
==== Etîmolojî ====
Deyn ji {{deyn|en|ku|grapefruit}} ji [[grape]] ([[tirî]]) + [[fruit]] yan jî ji [[great]] [[fruit]] ("fêkiyê hêja"). Ji ber zêdebûna herfên bêdeng di bilêvkirina peyva resen ya îngilîzî de, di kurdî û tirkî de bo hêsankirina gotinê "p" bi "f" ve heliye û herwiha "grey'''frû'''t" bûye "gre'''yfû'''rt".
==== Werger ====
{{werger-ser}}
* [[alabamayî]]: [[yalàaha kaskaha]]
* {{Z|sh}}: {{W+|sh|arabisagar}}
* {{ca}} [[aranja]] {{mê}}
* {{Z|ce}}: {{W+|ce|грейпфрукт}}
* {{Z|da}}: {{W+|da|grapefrugt}}, {{W+|da|grape}}
* {{Z|de}}: {{W|de|Grapefruit|f}}, {{W|de|Pampelmuse|f}}
* {{Z|eo}}: {{W|eo|grapfrukto|xs=Esperanto}}
* {{Z|fi}}: {{W+|fi|greippi}}
* {{Z|fr}}: {{W|fr|pamplemousse|m}}, {{W|fr|pomélo|m}}
* {{Z|hi}}: {{W-|hi|मौसमी|tr=mausamī}}, {{W-|hi|छोटा चकोतरा|tr=chōṭā cakōtarā}}
* {{Z|nl}}: {{W|nl|grapefruit|m}}
* {{Z|io}}: {{W+|io|pompelmuso}}
* {{Z|he}}: {{W|he|אשכולית|tr=eshkolít|f}}
* {{Z|ilo}}: {{W+|ilo|lukban}}
* {{Z|id}}: [[jeruk]] [[bali]]
* {{Z|en}}: {{W+|en|grapefruit}}
* {{Z|it}}: {{W-|it|pompelmo|tr=tree & fruit}}
* {{Z|is}}: {{W|is|greipaldin|n}}, {{W|is|greip|f}}, {{W|is|tröllaldin|n}}
* {{Z|ja}}: {{W-|ja|グレープフルーツ|tr=gurēpu-furūtsu}}
* {{Z|sh}}: {{W|sh|grejpfrut|m}}, {{W|sh|limunika|f}}
* {{Z|mk}}: {{W|mk|грејпфрут|tr=gréjpfrut|m}}, {{W|mk|цитрон|tr=citrón|m}}
* [[maltayî]]Maltese: [[lumi-larinġ]]
* {{Z|nv}}: {{W+|nv|chʼil łitsxooí ntsxaaígíí}}
* {{Z|pl}}: {{W|pl|grejpfrut|m}}
* {{Z|pt}}: {{W|pt|toranja|f}}, {{W|pt|toronja|f}}
* {{Z|ru}}: {{W|ru|грейпфрут|tr=gr'ejpfrút|m}}
* {{Z|sk}}: {{W|sk|grenivka|f}}
* {{Z|es}}: {{W|es|toronja|f}}, {{W|es|pomelo|m}}
* {{Z|sv}}: {{W+|sv|balungi}}, {{W|sv|grape|c}}, {{W|sv|grapefrukt|c}}
* [[tagalogî]]: [[suha']]
* [[tawsûgî]]: [[suwa' gadja]]
* {{Z|tr}}: {{W+|tr|greyfurt}}, {{W+|tr|altın top}}
{{werger-bin}}
b290qc91q9czwvo3205vtxmzv195p1h
tevnpîrk
0
63178
6531764
6319025
2026-09-15T21:50:43Z
Wikihez
7344
Kurmancî/Navdêr: mane, hevmane hatin guhartin, beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531764
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA|t(h)evnpîrk}}, تەڤنپیرک
=== Navdêr ===
[[Wêne:Brachypelma edit.jpg|thumb|250px|tevnpîrkek]]
{{navdêr|ku|z=m}}
# {{f|ku|buxik}} [[kêz|Kêz]]ek [[gir]] ya bi [[heşt]] [[ling]]an e û [[hêlîn]]a wê wek [[tevn]]î ye û dikare bi mirovan [[pê vedan|vede]].
#: {{hevmane|ku|pîrtevînk|pîrepind|pîropind|pîrik|pîrhevok|pîrepit|dapîrûşk|pîrê|pindepîr|pîrebok|pîrpîruk|kundiroşk|gangol|pêndek|pîrhebok}}
==== Tewandin ====
{{ku-tewîn-nav/nû|mê}}
==== Notên bikaranînê ====
Ji bilî hinan ([[tevnpîrk]], [[pîrelik]]...) hemû bêjeyên ku ramana "pîrejina cazûger" didin herwihan dikarin bo "tevnpîrk" bi kar werin. Dibe ku hoya vê bibin baweriyên kevnar ku wan têkilî hevdî dikir.
====Bi alfabeyên din====
* {{ku-ar|تهڤنپیرک}}
==== Formên alternatîv ====
* [[tevinpîr]]
* [[tevinpîrk]]
* [[tevnepîrk]]
==== Etîmolojî ====
{{etîmon|ku|:qf|tevn|pîr|-k|dar=1|metn=++}}
==== Bi zaravayên din ====
* kurmancî: konê pîrê, pindir, pilindir, pênderk, tevinpîrik, pîrtevîng, tevnepîr, pilingir, pîrê, pîrkê, pîrik, pîrhevok, pîrhebok, pîrevok, pîrebok, dapîroşk
* soranî: kakllemêşan, kaklêmûşan, calcalûke,culeyîkere, pespesekole, calcaloke, dapîroçke, helepeje
* zazakî: pîrik, pîrike, pîre
* kelhûrî:
* hewramî: kellkellemêş, kaklêmûşan, pespesekollê
* lekî: rûtêla
==== Werger ====
{{werger-ser}}
* {{Z|abx}}: {{W-|abx|lawaʼ}}
* {{Z|af}}: {{W-|af|spinnekop}}
* {{Z|sq}}: {{W+|sq|merimangë}}
* {{Z|de}}: {{W+|de|Spinne|f}}, {{W+|de|Tarantel|f|guman=erê}}
* {{Z|amu}}: {{W-|amu|kítònkwé}}
* {{Z|rup}}: {{W+|rup|pangu}}
* {{Z|az}}: {{W+|az|hörümçək}}
* {{Z|eu}}: {{W+|eu|armiarma}}
* {{Z|bn}}: {{W+|bn|মাকড়সা}}
* {{Z|bg}}: {{W+|bg|паяк|R=pajak}}
* {{Z|cs}}: {{W+|cs|pavouk}}
* {{Z|chr}}: {{W+|chr|ᎧᎾᏁᏍᎩ|R=kananesgi}}
* {{Z|cic}}: {{W-|cic|cholhkan}}, {{W-|cic|choklhan}}
* {{Z|zh}}: {{W+|zh|蜘蛛|R=zhīzhū}}
* {{Z|da}}: {{W+|da|edderkop}}
* {{Z|id}}: {{W-|id|labah}}
* {{Z|ar}}: {{W+|ar|عنكبوت}} ('enkebût)
* {{Z|hy}}: {{W+|hy|սարդ|R=sard}}
* {{Z|myv}}: {{W-|myv|шанжав|R=šanžav}} ; {{W-|myv|пондо мукоро|R=pondo mukoro}}
* {{Z|eo}}: {{W+|eo|araneo}}
* {{Z|et}}: {{W+|et|ämblik}}
* {{Z|fa}}: {{W+|fa|عنکبوت|tr='enkebût}}, {{W-|fa|تارتنک}} (tartenek)
* {{Z|fo}}: {{W-|fo|eiturkoppur}}
* {{Z|fi}}: {{W+|fi|hämähäkki}}
* {{Z|fr}}: {{W+|fr|araignée}}
* {{Z|gd}}: {{W-|gd|damhan-allaidh}}, {{W-|gd|fìgheadair}}
* {{Z|ka}}: {{W-|ka|ობობა|R=oboba}}
* {{Z|hi}}: {{W+|hi|मकड़ा}}
* {{Z|nl}}: {{W+|nl|spin|m}}
* {{Z|he}}: {{W-|he|עכבישאים|R=aïkbyšaym}}
* {{Z|en}}: {{W+|en|spider}}, {{W+|en|cobweb|guman=erê}}, {{W+|en|spider's web|guman=erê}}
* {{Z|iu}}: {{W+|iu|ᐋᓯᕙᖅ|R=[[aasivaq]]}}
* {{Z|ga}}: {{W-|ga|damhán alla}}, {{W-|ga|ruán alla}}, {{W-|ga|fíodóir}}
* {{Z|it}}: {{W+|it|ragno}}
* {{Z|is}}: {{W+|is|könguló}}
* {{Z|ja}}: {{W+|ja|蜘蛛|R=くも}} (kumo), スパイダー (supaidā)
* {{Z|ca}}: {{W+|ca|aranya}}
* {{Z|qu}}: {{W-|qu|kusikusi}}; {{W-|qu|apasanka}}
* {{Z|ko}}: {{W+|ko|거미|R=geomi}}
* {{Z|lv}}: {{W-|lv|zirneklis}}
* {{Z|ln}}: {{W-|ln|alúlu}}
* {{Z|lt}}: {{W+|lt|voras}}
* {{Z|mk}}: {{W-|mk|пајак|R=pájak}}
* {{Z|ms}}: {{W-|ms|labah-labah}}
* {{Z|mt}}: {{W-|mt|brimba}}
* {{Z|mi}}: {{W-|mi|puawerewere}}, {{W-|mi|puungaawerewere}}, {{W-|mi|puungaiwerewere}}, {{W-|mi|puuwerewere}}, {{W-|mi|tuuturi}}
* {{Z|yua}}: {{W-|yua|am}}
* {{Z|hu}}: {{W+|hu|pók}}
* {{Z|mn}}: {{W+|mn|аалз|R=aalz}}
* {{Z|nah}}: {{W+|nah|tocatl}}
* {{Z|nv}}: {{W-|nv|na'ashjé'ii'}}
* {{Z|ne}}: {{W-|ne|माकुरो|R=mākuro}}
* {{Z|no}}: {{W+|no|edderkopp}}
* {{Z|oc}}: {{W+|oc|aranha}}
* {{Z|pap}}: {{W-|pap|araña}}, {{W-|pap|haraña}}
* {{Z|pl}}: {{W+|pl|pająk}}
* {{Z|pt}}: {{W+|pt|aranha}}
* {{Z|ro}}: {{W+|ro|păianjen}}
* {{Z|ru}}: {{W+|ru|паук|R=paouk}}
* {{Z|se}}: {{W-|se|heavdni}}
* {{Z|sm}}: {{W-|sm|'apogāleveleve}}
* {{Z|ceb}}: {{W-|ceb|damang}}
* {{Z|srn}}: {{W-|srn|anansi}}
* {{Z|sh}}: {{W+|sh|pauk|m}}, {{W+|sh|pȁūk}}
* {{Z|sh}}: {{W-|sh|паук|R=pauk}}
* {{Z|sk}}: {{W+|sk|pavúk}}
* {{Z|sl}}: {{W+|sl|pajek}}
* {{Z|so}}: {{W+|so|caaro}}
* {{Z|dsb}}: {{W-|dsb|pawk}}
* {{Z|hsb}}: {{W-|hsb|pawk}}
* {{Z|es}}: {{W+|es|araña}}
* {{Z|sw}}: {{W+|sw|buibui}}
* {{Z|sv}}: {{W+|sv|spindel}}
* {{Z|tl}}: {{W-|tl|gagambá}}, {{W-|tl|álalawa}}
* {{Z|ta}}: {{W+|ta|சிலந்தி|R=silanti}}
* {{Z|te}}: {{W+|te|సాలీడు|R=saaleeDu}}
* {{Z|tt}}: {{W-|tt|ürmäküç}}
* {{Z|tr}}: {{W+|tr|örümcek}}, {{W+|tr|ağ}}, {{W+|tr|örümcek ağı|guman=erê}}
* {{Z|uk}}: {{W-|uk|павук}}
* {{Z|vi}}: {{W+|vi|nhện}}, {{W-|vi|con nhện}}
* {{Z|vo}}: {{W+|vo|spulaf}}
* {{Z|cy}}: {{W-|cy|corryn|m}}, {{W-|cy|pryf copyn}}
* {{Z|km}}: {{W+|km|ពីងពាង|R=bpīng bpīəng}}
* {{Z|yi}}: {{W-|yi|שפּין|R=shpin}}
* {{Z|yo}}: {{W-|yo|alantakun}}
* {{Z|el}}: {{W+|el|αράχνη|R=arákhni}}
* {{Z|zu}}: {{W+|zu|ulwembu}}, {{W+|zu|ululwembu}}
{{werger-bin}}
rsz2pxtr9ruw3wckyifcuw8gqwjmw2d
6531765
6531764
2026-09-15T21:51:16Z
Wikihez
7344
Kurmancî: beşa «Bilêvkirin» hate nûkirin; Kurmancî/Navdêr: beşa «Bi alfabeyên din» hate nûkirin (DefSideBoxes)
6531765
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA|t(h)evnpîrk}}
=== Navdêr ===
[[Wêne:Brachypelma edit.jpg|thumb|250px|tevnpîrkek]]
{{navdêr|ku|z=m}}
# {{f|ku|buxik}} [[kêz|Kêz]]ek [[gir]] ya bi [[heşt]] [[ling]]an e û [[hêlîn]]a wê wek [[tevn]]î ye û dikare bi mirovan [[pê vedan|vede]].
#: {{hevmane|ku|pîrtevînk|pîrepind|pîropind|pîrik|pîrhevok|pîrepit|dapîrûşk|pîrê|pindepîr|pîrebok|pîrpîruk|kundiroşk|gangol|pêndek|pîrhebok}}
==== Tewandin ====
{{ku-tewîn-nav/nû|mê}}
==== Notên bikaranînê ====
Ji bilî hinan ([[tevnpîrk]], [[pîrelik]]...) hemû bêjeyên ku ramana "pîrejina cazûger" didin herwihan dikarin bo "tevnpîrk" bi kar werin. Dibe ku hoya vê bibin baweriyên kevnar ku wan têkilî hevdî dikir.
====Bi alfabeyên din====
* {{ku-ar|تهڤنپیرک}}
==== Formên alternatîv ====
* [[tevinpîr]]
* [[tevinpîrk]]
* [[tevnepîrk]]
==== Etîmolojî ====
{{etîmon|ku|:qf|tevn|pîr|-k|dar=1|metn=++}}
==== Bi zaravayên din ====
* kurmancî: konê pîrê, pindir, pilindir, pênderk, tevinpîrik, pîrtevîng, tevnepîr, pilingir, pîrê, pîrkê, pîrik, pîrhevok, pîrhebok, pîrevok, pîrebok, dapîroşk
* soranî: kakllemêşan, kaklêmûşan, calcalûke,culeyîkere, pespesekole, calcaloke, dapîroçke, helepeje
* zazakî: pîrik, pîrike, pîre
* kelhûrî:
* hewramî: kellkellemêş, kaklêmûşan, pespesekollê
* lekî: rûtêla
==== Werger ====
{{werger-ser}}
* {{Z|abx}}: {{W-|abx|lawaʼ}}
* {{Z|af}}: {{W-|af|spinnekop}}
* {{Z|sq}}: {{W+|sq|merimangë}}
* {{Z|de}}: {{W+|de|Spinne|f}}, {{W+|de|Tarantel|f|guman=erê}}
* {{Z|amu}}: {{W-|amu|kítònkwé}}
* {{Z|rup}}: {{W+|rup|pangu}}
* {{Z|az}}: {{W+|az|hörümçək}}
* {{Z|eu}}: {{W+|eu|armiarma}}
* {{Z|bn}}: {{W+|bn|মাকড়সা}}
* {{Z|bg}}: {{W+|bg|паяк|R=pajak}}
* {{Z|cs}}: {{W+|cs|pavouk}}
* {{Z|chr}}: {{W+|chr|ᎧᎾᏁᏍᎩ|R=kananesgi}}
* {{Z|cic}}: {{W-|cic|cholhkan}}, {{W-|cic|choklhan}}
* {{Z|zh}}: {{W+|zh|蜘蛛|R=zhīzhū}}
* {{Z|da}}: {{W+|da|edderkop}}
* {{Z|id}}: {{W-|id|labah}}
* {{Z|ar}}: {{W+|ar|عنكبوت}} ('enkebût)
* {{Z|hy}}: {{W+|hy|սարդ|R=sard}}
* {{Z|myv}}: {{W-|myv|шанжав|R=šanžav}} ; {{W-|myv|пондо мукоро|R=pondo mukoro}}
* {{Z|eo}}: {{W+|eo|araneo}}
* {{Z|et}}: {{W+|et|ämblik}}
* {{Z|fa}}: {{W+|fa|عنکبوت|tr='enkebût}}, {{W-|fa|تارتنک}} (tartenek)
* {{Z|fo}}: {{W-|fo|eiturkoppur}}
* {{Z|fi}}: {{W+|fi|hämähäkki}}
* {{Z|fr}}: {{W+|fr|araignée}}
* {{Z|gd}}: {{W-|gd|damhan-allaidh}}, {{W-|gd|fìgheadair}}
* {{Z|ka}}: {{W-|ka|ობობა|R=oboba}}
* {{Z|hi}}: {{W+|hi|मकड़ा}}
* {{Z|nl}}: {{W+|nl|spin|m}}
* {{Z|he}}: {{W-|he|עכבישאים|R=aïkbyšaym}}
* {{Z|en}}: {{W+|en|spider}}, {{W+|en|cobweb|guman=erê}}, {{W+|en|spider's web|guman=erê}}
* {{Z|iu}}: {{W+|iu|ᐋᓯᕙᖅ|R=[[aasivaq]]}}
* {{Z|ga}}: {{W-|ga|damhán alla}}, {{W-|ga|ruán alla}}, {{W-|ga|fíodóir}}
* {{Z|it}}: {{W+|it|ragno}}
* {{Z|is}}: {{W+|is|könguló}}
* {{Z|ja}}: {{W+|ja|蜘蛛|R=くも}} (kumo), スパイダー (supaidā)
* {{Z|ca}}: {{W+|ca|aranya}}
* {{Z|qu}}: {{W-|qu|kusikusi}}; {{W-|qu|apasanka}}
* {{Z|ko}}: {{W+|ko|거미|R=geomi}}
* {{Z|lv}}: {{W-|lv|zirneklis}}
* {{Z|ln}}: {{W-|ln|alúlu}}
* {{Z|lt}}: {{W+|lt|voras}}
* {{Z|mk}}: {{W-|mk|пајак|R=pájak}}
* {{Z|ms}}: {{W-|ms|labah-labah}}
* {{Z|mt}}: {{W-|mt|brimba}}
* {{Z|mi}}: {{W-|mi|puawerewere}}, {{W-|mi|puungaawerewere}}, {{W-|mi|puungaiwerewere}}, {{W-|mi|puuwerewere}}, {{W-|mi|tuuturi}}
* {{Z|yua}}: {{W-|yua|am}}
* {{Z|hu}}: {{W+|hu|pók}}
* {{Z|mn}}: {{W+|mn|аалз|R=aalz}}
* {{Z|nah}}: {{W+|nah|tocatl}}
* {{Z|nv}}: {{W-|nv|na'ashjé'ii'}}
* {{Z|ne}}: {{W-|ne|माकुरो|R=mākuro}}
* {{Z|no}}: {{W+|no|edderkopp}}
* {{Z|oc}}: {{W+|oc|aranha}}
* {{Z|pap}}: {{W-|pap|araña}}, {{W-|pap|haraña}}
* {{Z|pl}}: {{W+|pl|pająk}}
* {{Z|pt}}: {{W+|pt|aranha}}
* {{Z|ro}}: {{W+|ro|păianjen}}
* {{Z|ru}}: {{W+|ru|паук|R=paouk}}
* {{Z|se}}: {{W-|se|heavdni}}
* {{Z|sm}}: {{W-|sm|'apogāleveleve}}
* {{Z|ceb}}: {{W-|ceb|damang}}
* {{Z|srn}}: {{W-|srn|anansi}}
* {{Z|sh}}: {{W+|sh|pauk|m}}, {{W+|sh|pȁūk}}
* {{Z|sh}}: {{W-|sh|паук|R=pauk}}
* {{Z|sk}}: {{W+|sk|pavúk}}
* {{Z|sl}}: {{W+|sl|pajek}}
* {{Z|so}}: {{W+|so|caaro}}
* {{Z|dsb}}: {{W-|dsb|pawk}}
* {{Z|hsb}}: {{W-|hsb|pawk}}
* {{Z|es}}: {{W+|es|araña}}
* {{Z|sw}}: {{W+|sw|buibui}}
* {{Z|sv}}: {{W+|sv|spindel}}
* {{Z|tl}}: {{W-|tl|gagambá}}, {{W-|tl|álalawa}}
* {{Z|ta}}: {{W+|ta|சிலந்தி|R=silanti}}
* {{Z|te}}: {{W+|te|సాలీడు|R=saaleeDu}}
* {{Z|tt}}: {{W-|tt|ürmäküç}}
* {{Z|tr}}: {{W+|tr|örümcek}}, {{W+|tr|ağ}}, {{W+|tr|örümcek ağı|guman=erê}}
* {{Z|uk}}: {{W-|uk|павук}}
* {{Z|vi}}: {{W+|vi|nhện}}, {{W-|vi|con nhện}}
* {{Z|vo}}: {{W+|vo|spulaf}}
* {{Z|cy}}: {{W-|cy|corryn|m}}, {{W-|cy|pryf copyn}}
* {{Z|km}}: {{W+|km|ពីងពាង|R=bpīng bpīəng}}
* {{Z|yi}}: {{W-|yi|שפּין|R=shpin}}
* {{Z|yo}}: {{W-|yo|alantakun}}
* {{Z|el}}: {{W+|el|αράχνη|R=arákhni}}
* {{Z|zu}}: {{W+|zu|ulwembu}}, {{W+|zu|ululwembu}}
{{werger-bin}}
sg82rvxwz5o9z8vjgd6wn7lb8w4vimi
6531768
6531765
2026-09-15T23:12:08Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir, Valahiya beşan rast kir, --Valahiyên nehewce, {{ku-tewîn-nav/nû}} → {{ku-tewîn-nav}}.)
6531768
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA|t(h)evnpîrk}}
=== Navdêr ===
[[Wêne:Brachypelma edit.jpg|thumb|250px|tevnpîrkek]]
{{navdêr|ku|z=m}}
# {{f|ku|buxik}} [[kêz|Kêzek]] [[gir]] ya bi [[heşt]] [[ling]]an e û [[hêlîn]]a wê wek [[tevn]]î ye û dikare bi mirovan [[pê vedan|vede]].
#: {{hevmane|ku|pîrtevînk|pîrepind|pîropind|pîrik|pîrhevok|pîrepit|dapîrûşk|pîrê|pindepîr|pîrebok|pîrpîruk|kundiroşk|gangol|pêndek|pîrhebok}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Notên bikaranînê ====
Ji bilî hinan ([[tevnpîrk]], [[pîrelik]]...) hemû bêjeyên ku ramana "pîrejina cazûger" didin herwihan dikarin bo "tevnpîrk" bi kar werin. Dibe ku hoya vê bibin baweriyên kevnar ku wan têkilî hevdî dikir.
==== Bi alfabeyên din ====
* {{ku-ar|تهڤنپیرک}}
==== Formên alternatîv ====
* [[tevinpîr]]
* [[tevinpîrk]]
* [[tevnepîrk]]
==== Etîmolojî ====
{{etîmon|ku|:qf|tevn|pîr|-k|dar=1|metn=++}}
==== Bi zaravayên din ====
* kurmancî: konê pîrê, pindir, pilindir, pênderk, tevinpîrik, pîrtevîng, tevnepîr, pilingir, pîrê, pîrkê, pîrik, pîrhevok, pîrhebok, pîrevok, pîrebok, dapîroşk
* soranî: kakllemêşan, kaklêmûşan, calcalûke,culeyîkere, pespesekole, calcaloke, dapîroçke, helepeje
* zazakî: pîrik, pîrike, pîre
* kelhûrî:
* hewramî: kellkellemêş, kaklêmûşan, pespesekollê
* lekî: rûtêla
==== Werger ====
{{werger-ser}}
* {{Z|abx}}: {{W-|abx|lawaʼ}}
* {{Z|af}}: {{W-|af|spinnekop}}
* {{Z|sq}}: {{W+|sq|merimangë}}
* {{Z|de}}: {{W+|de|Spinne|f}}, {{W+|de|Tarantel|f|guman=erê}}
* {{Z|amu}}: {{W-|amu|kítònkwé}}
* {{Z|rup}}: {{W+|rup|pangu}}
* {{Z|az}}: {{W+|az|hörümçək}}
* {{Z|eu}}: {{W+|eu|armiarma}}
* {{Z|bn}}: {{W+|bn|মাকড়সা}}
* {{Z|bg}}: {{W+|bg|паяк|R=pajak}}
* {{Z|cs}}: {{W+|cs|pavouk}}
* {{Z|chr}}: {{W+|chr|ᎧᎾᏁᏍᎩ|R=kananesgi}}
* {{Z|cic}}: {{W-|cic|cholhkan}}, {{W-|cic|choklhan}}
* {{Z|zh}}: {{W+|zh|蜘蛛|R=zhīzhū}}
* {{Z|da}}: {{W+|da|edderkop}}
* {{Z|id}}: {{W-|id|labah}}
* {{Z|ar}}: {{W+|ar|عنكبوت}} ('enkebût)
* {{Z|hy}}: {{W+|hy|սարդ|R=sard}}
* {{Z|myv}}: {{W-|myv|шанжав|R=šanžav}} ; {{W-|myv|пондо мукоро|R=pondo mukoro}}
* {{Z|eo}}: {{W+|eo|araneo}}
* {{Z|et}}: {{W+|et|ämblik}}
* {{Z|fa}}: {{W+|fa|عنکبوت|tr='enkebût}}, {{W-|fa|تارتنک}} (tartenek)
* {{Z|fo}}: {{W-|fo|eiturkoppur}}
* {{Z|fi}}: {{W+|fi|hämähäkki}}
* {{Z|fr}}: {{W+|fr|araignée}}
* {{Z|gd}}: {{W-|gd|damhan-allaidh}}, {{W-|gd|fìgheadair}}
* {{Z|ka}}: {{W-|ka|ობობა|R=oboba}}
* {{Z|hi}}: {{W+|hi|मकड़ा}}
* {{Z|nl}}: {{W+|nl|spin|m}}
* {{Z|he}}: {{W-|he|עכבישאים|R=aïkbyšaym}}
* {{Z|en}}: {{W+|en|spider}}, {{W+|en|cobweb|guman=erê}}, {{W+|en|spider's web|guman=erê}}
* {{Z|iu}}: {{W+|iu|ᐋᓯᕙᖅ|R=[[aasivaq]]}}
* {{Z|ga}}: {{W-|ga|damhán alla}}, {{W-|ga|ruán alla}}, {{W-|ga|fíodóir}}
* {{Z|it}}: {{W+|it|ragno}}
* {{Z|is}}: {{W+|is|könguló}}
* {{Z|ja}}: {{W+|ja|蜘蛛|R=くも}} (kumo), スパイダー (supaidā)
* {{Z|ca}}: {{W+|ca|aranya}}
* {{Z|qu}}: {{W-|qu|kusikusi}}; {{W-|qu|apasanka}}
* {{Z|ko}}: {{W+|ko|거미|R=geomi}}
* {{Z|lv}}: {{W-|lv|zirneklis}}
* {{Z|ln}}: {{W-|ln|alúlu}}
* {{Z|lt}}: {{W+|lt|voras}}
* {{Z|mk}}: {{W-|mk|пајак|R=pájak}}
* {{Z|ms}}: {{W-|ms|labah-labah}}
* {{Z|mt}}: {{W-|mt|brimba}}
* {{Z|mi}}: {{W-|mi|puawerewere}}, {{W-|mi|puungaawerewere}}, {{W-|mi|puungaiwerewere}}, {{W-|mi|puuwerewere}}, {{W-|mi|tuuturi}}
* {{Z|yua}}: {{W-|yua|am}}
* {{Z|hu}}: {{W+|hu|pók}}
* {{Z|mn}}: {{W+|mn|аалз|R=aalz}}
* {{Z|nah}}: {{W+|nah|tocatl}}
* {{Z|nv}}: {{W-|nv|na'ashjé'ii'}}
* {{Z|ne}}: {{W-|ne|माकुरो|R=mākuro}}
* {{Z|no}}: {{W+|no|edderkopp}}
* {{Z|oc}}: {{W+|oc|aranha}}
* {{Z|pap}}: {{W-|pap|araña}}, {{W-|pap|haraña}}
* {{Z|pl}}: {{W+|pl|pająk}}
* {{Z|pt}}: {{W+|pt|aranha}}
* {{Z|ro}}: {{W+|ro|păianjen}}
* {{Z|ru}}: {{W+|ru|паук|R=paouk}}
* {{Z|se}}: {{W-|se|heavdni}}
* {{Z|sm}}: {{W-|sm|'apogāleveleve}}
* {{Z|ceb}}: {{W-|ceb|damang}}
* {{Z|srn}}: {{W-|srn|anansi}}
* {{Z|sh}}: {{W+|sh|pauk|m}}, {{W+|sh|pȁūk}}
* {{Z|sh}}: {{W-|sh|паук|R=pauk}}
* {{Z|sk}}: {{W+|sk|pavúk}}
* {{Z|sl}}: {{W+|sl|pajek}}
* {{Z|so}}: {{W+|so|caaro}}
* {{Z|dsb}}: {{W-|dsb|pawk}}
* {{Z|hsb}}: {{W-|hsb|pawk}}
* {{Z|es}}: {{W+|es|araña}}
* {{Z|sw}}: {{W+|sw|buibui}}
* {{Z|sv}}: {{W+|sv|spindel}}
* {{Z|tl}}: {{W-|tl|gagambá}}, {{W-|tl|álalawa}}
* {{Z|ta}}: {{W+|ta|சிலந்தி|R=silanti}}
* {{Z|te}}: {{W+|te|సాలీడు|R=saaleeDu}}
* {{Z|tt}}: {{W-|tt|ürmäküç}}
* {{Z|tr}}: {{W+|tr|örümcek}}, {{W+|tr|ağ}}, {{W+|tr|örümcek ağı|guman=erê}}
* {{Z|uk}}: {{W-|uk|павук}}
* {{Z|vi}}: {{W+|vi|nhện}}, {{W-|vi|con nhện}}
* {{Z|vo}}: {{W+|vo|spulaf}}
* {{Z|cy}}: {{W-|cy|corryn|m}}, {{W-|cy|pryf copyn}}
* {{Z|km}}: {{W+|km|ពីងពាង|R=bpīng bpīəng}}
* {{Z|yi}}: {{W-|yi|שפּין|R=shpin}}
* {{Z|yo}}: {{W-|yo|alantakun}}
* {{Z|el}}: {{W+|el|αράχνη|R=arákhni}}
* {{Z|zu}}: {{W+|zu|ulwembu}}, {{W+|zu|ululwembu}}
{{werger-bin}}
b22tocp09st7bf31gv0ecekpearzdjb
zombî
0
63783
6531777
6339951
2026-09-16T07:02:27Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531777
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Zombie costume portrait.jpg|thumb|250px|kesekî ku xwe [[şibandin|şibandiye]] zombiyan]]
{{navdêr|ku|z=mn}}
# {{f|ku|fantezî}} [[mirov|Mirovên]] [[mirî]] yên ku [[gerîn|digerin]].
#* {{jêder|ku|jêgirtin=Mirov wek heywanekî ewitiye. Dûra kûçikê li malê girtiye û ew fetisandiye. Piştî kûçik fetisandinê, mirovê "'''zombî'''" goştê kûçik xwariye.|Avestakurd.net, 6/2012}}
# {{f|ku|mecazî}} Kesê ne [[li ser hişên xwe bûn|li ser hişên xwe]], [[bêhay]], [[nîv]][[mirov]].
==== Bi alfabeyên din ====
* {{ku-ar|زۆمبی}}
==== Etîmolojî ====
Deyn ji {{deyn|en|ku|zombie}} ji zimanekî [[bantûyî]], bide ber [[kîkongoyî]] "[[zumbi]]" ([[fetîş]]), [[kîmbundûyî]] "[[nzambi]]" ([[xweda]]) û [[folklor]]a [[Karîbya|karîbyayî]] "[[jumbee]]" ([[dêw]] yan [[cin]]). Belkî herwiha têkilî [[sombra]] (1. [[sî]], [[sîber]] 2. [[reşê şevê]], [[xeyalet]]) ya spanî jî be.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Zombie}} {{n}}
* {{Z|eu}}: {{W-|eu|zonbi}}
* {{Z|bg}}: {{W-|bg|Зомби}}
* {{Z|cs}}: {{W+|cs|zombie}}
* {{Z|zh}}: {{W-|zh|喪屍}}
* {{Z|da}}: {{W-|da|zombie}}
* {{Z|id}}: {{W+|id|zombi}}
* {{Z|ar}}: {{W-|ar|زومبي}}
* {{Z|eo}}: {{W-|eo|zombio}}
* {{Z|et}}: {{W+|et|zombie}}
* {{Z|fa}}: {{W-|fa|مرده متحرک}}
* {{Z|fi}}: {{W+|fi|zombi}}
* {{Z|fr}}: {{W+|fr|zombi}}
* {{Z|ht}}: {{W-|ht|zonbi}}
* {{Z|nl}}: {{W+|nl|zombie}}
* {{Z|he}}: {{W-|he|זומבי}}
* {{Z|en}}: {{W+|en|zombie}}
* {{Z|ga}}: {{W-|ga|zombaí}}
* {{Z|it}}: {{W+|it|zombie}}
* {{Z|is}}: {{W-|is|uppvakningur}}
* {{Z|ja}}: {{W+|ja|ゾンビ}}
* {{Z|ca}}: {{W-|ca|zombi}}
* {{Z|ko}}: {{W+|ko|좀비}}
* {{Z|lt}}: {{W-|lt|zombis}}
* {{Z|mk}}: {{W-|mk|Зомби}}
* {{Z|hu}}: {{W+|hu|zombi}}
* {{Z|nn}}: {{W-|nn|zombi}}
* {{Z|no}}: {{W+|no|zombie}}
* {{Z|uz}}: {{W+|uz|zombi}}
* {{Z|pap}}: {{W-|pap|zumbi}}
* {{Z|pl}}: {{W+|pl|zombie}}
* {{Z|pt}}: {{W+|pt|zumbi}}
* {{Z|ro}}: {{W+|ro|zombi}}
* {{Z|ru}}: {{W-|ru|Зомби}}
* {{Z|sh}}: {{W+|sh|zombi}}
* {{Z|sh}}: {{W-|sh|Зомби}}
* {{Z|sk}}: {{W-|sk|zombi}}
* {{Z|sl}}: {{W-|sl|zombi}}
* {{Z|es}}: {{W+|es|zombi}}
* {{Z|sv}}: {{W+|sv|zombie}}
* {{Z|tl}}: {{W-|tl|bangkay na buhay}}
* {{Z|th}}: {{W-|th|ซอมบี}}
* {{Z|tr}}: {{W+|tr|zombi}}
* {{Z|uk}}: {{W-|uk|Зомбі}}
* {{Z|vi}}: {{W+|vi|zombie}}
* {{Z|cy}}: {{W-|cy|Sombi}}
{{werger-bin}}
ip8w9snm7te9aou2gb9s6bgf4j4hck4
beschmutzen
0
116630
6531677
4107591
2026-09-15T18:02:51Z
Jeuwre
4694
/* Bilêvkirin */
6531677
wikitext
text/x-wiki
== {{ziman|de}} ==
=== Bilêvkirin ===
* {{IPA|de|[bəˈʃmʊt͡sn̩]}}
* {{deng|de|De-beschmutzen2.ogg|dever=Berlîn, Almanya}}
* {{deng|de|De-beschmutzen.ogg|Deng}}
* {{kîte|de|be|schmut|zen}}
=== Lêker ===
{{lêker|de}}
# [[kirêt kirin]], [[pîs kirin]], [[gemarî kirin]]
6lhbng3zxs6y8lgltsbcnv32yr2xgiy
6531703
6531677
2026-09-15T18:29:06Z
Balyozbot
7867
[[Bikarhêner:Wikihez/skrîpt/py/dengbot.py|Bot]]: Formata parametreya "dever" hat guhartin
6531703
wikitext
text/x-wiki
== {{ziman|de}} ==
=== Bilêvkirin ===
* {{IPA|de|[bəˈʃmʊt͡sn̩]}}
* {{deng|de|De-beschmutzen2.ogg|dever=Berlîn, Almanya}}
* {{deng|de|De-beschmutzen.ogg}}
* {{kîte|de|be|schmut|zen}}
=== Lêker ===
{{lêker|de}}
# [[kirêt kirin]], [[pîs kirin]], [[gemarî kirin]]
ot2d0y3vfs92o0fmntxvb3k3a6kqu8e
MediaWiki:Gadgets-definition
8
118586
6531726
6514524
2026-09-15T20:15:25Z
Wikihez
7344
Add TreeDescs (imported from en.wiktionary): Dûnde lists drawn as a tree, default on in entries and Jinûvesazî
6531726
wikitext
text/x-wiki
Binêre [[Taybet:Gadgets]] ji bo belgekirina amûrên li jêr.
* LegacyScripts[ResourceLoader|default|dependencies=site,mediawiki.util,mediawiki.cookie,ext.gadget.LegacyScriptsNewNode,ext.gadget.StorageUtils|type=general]|legacy.js
==Rû==
* LiveClock[ResourceLoader|dependencies=mediawiki.util]|LiveClock.js
* charinsert-styles[ResourceLoader|hidden|type=styles]|charinsert-styles.css
* LanguagesAndScripts[ResourceLoader|default]|LanguagesAndScripts.css
* jeder[ResourceLoader|dependencies=ext.gadget.VisibilityToggles|default]|jeder.css|jeder.js
* CodeLinks[ResourceLoader|default|dependencies=site,mediawiki.util]|CodeLinks.js
* Tabset[ResourceLoader|default]|Tabset.js|Tabset.css
* kuTewandin[ResourceLoader|default]|kuTewandin.js
* wikt.showids[ResourceLoader|package|namespaces=0,2,4,108|actions=view|dependencies=mediawiki.util]|wikt.showids.js|wikt.showids.css
* EtytreeZoom[ResourceLoader|default|dependencies=mediawiki.util]|EtytreeZoom.js
* PagePreviews[ResourceLoader|default|dependencies=site,mediawiki.api,mediawiki.util,mediawiki.Title|skins=vector,vector-2022,monobook,timeless,modern,cologneblue]|PagePreviews.js
* AutoRedirect[ResourceLoader|default|dependencies=user,mediawiki.util|actions=view|namespaces=0]|AutoRedirect.js
* TreeDescs[ResourceLoader|default|namespaces=0,108|peers=TreeDescs-pagestyles]|TreeDescs.js
==AmûrênGuhartinê==
* wikEd[ResourceLoader]|wikEd.js
* charinsert[ResourceLoader|default|peers=charinsert-styles]|charinsert.js
* translation_editor[ResourceLoader|default|dependencies=mediawiki.notification|type=general]|silent_fail_storage.js|editor.js|translation_editor.js|translation_editor.css
* QQ[ResourceLoader|dependencies=mediawiki.util,oojs-ui-core,oojs-ui-widgets|type=general]|QQ.js|QQ.css
* WiktAccFormCreation[ResourceLoader|dependencies=mediawiki.util]|WiktAccFormCreation.js
* AjaxEdit[ResourceLoader|dependencies=mediawiki.util,mediawiki.user,mediawiki.api]|AjaxEdit.js
* EditHere[ResourceLoader|dependencies=mediawiki.api,oojs-ui-core,oojs-ui-widgets]|EditHere.js
* clearer-edit-summary[ResourceLoader|default|actions=view]|clearer-edit-summary.js
* DeluxeHistory[ResourceLoader|dependencies=user,mediawiki.util]|DeluxeHistory.js|DeluxeHistory.css
* JumpToError[ResourceLoader|dependencies=mediawiki.notification]|JumpToError.js
* aWa[ResourceLoader|rights=autoconfirmed|dependencies=mediawiki.util,mediawiki.api,mediawiki.user,user.options,mediawiki.Title|type=general]|aWa.js|aWa.css
* purgetab[ResourceLoader|dependencies=mediawiki.api,mediawiki.util]|purgetab.js
* start[ResourceLoader|default|package|namespaces=0,2,4,108|dependencies=mediawiki.util,ext.gadget.wikt.core]|start.js
* wikt.dsb[ResourceLoader|default|package|namespaces=0,2,4,108|actions=view|dependencies=mediawiki.util,ext.gadget.wikt.core]|wikt.dsb.js|wikt.dsb.css
* linkify-toolbar[ResourceLoader|default|actions=edit,submit|dependencies=ext.gadget.linkify,jquery.textSelection,ext.wikiEditor]|linkify-toolbar.js
==Kategorî==
* HotCat[ResourceLoader]|HotCat.js
* Cat-a-lot[ResourceLoader|rights=autoconfirmed]|Cat-a-lot.css|Cat-a-lot.js
==AmûrênVeşartî==
* LegacyScriptsNewNode[ResourceLoader|hidden|targets=desktop,mobile]|LegacyScriptsNewNode.js
* StorageUtils[ResourceLoader|hidden|dependencies=mediawiki.cookie,mediawiki.storage]|StorageUtils.js
* charinsert-core[ResourceLoader|hidden|dependencies=jquery.textSelection,user,mediawiki.storage]|charinsert-core.js
* VisibilityToggles[ResourceLoader|hidden|targets=desktop,mobile|dependencies=user,mediawiki.cookie]|VisibilityToggles.js
* defaultVisibilityToggles[ResourceLoader|hidden|default|targets=desktop,mobile|dependencies=ext.gadget.VisibilityToggles]|defaultVisibilityToggles.js
* Site[ResourceLoader|hidden|default]|Site.css
* catfix[ResourceLoader|hidden|default|dependencies=mediawiki.Title|categories=Pages using catfix]|catfix.js
* Palette[ResourceLoader|hidden|default]|Palette.css
* test[ResourceLoader|rights=autoconfirmed]|test.js
* citoid-helper[ResourceLoader|hidden|dependencies=mediawiki.util]|citoid-helper.js
* wikt.core[ResourceLoader|hidden|package|dependencies=mediawiki.util,mediawiki.storage]|wikt.core.js|wikt.core/sections.js|wikt.core/prefs.js|wikt.core/data-sections.json|wikt.core.css
* wikt.edit[ResourceLoader|hidden|package|dependencies=mediawiki.util,mediawiki.api,ext.gadget.wikt.core]|wikt.edit.js|wikt.edit/util.js|wikt.edit/wikitext.js|wikt.edit/langs.js|wikt.edit/labels.js|wikt.edit/posparams.js|wikt.edit/data-main.json|wikt.edit/ziman-core.json|wikt.edit.css
* wikt.cite[ResourceLoader|hidden|package|dependencies=mediawiki.api,oojs-ui-core,oojs-ui-widgets,oojs-ui-windows,ext.gadget.citoid-helper,ext.gadget.wikt.edit]|wikt.cite.js|wikt.cite.css
* wikt.dsb-panels[ResourceLoader|hidden|package|dependencies=mediawiki.util,mediawiki.api,ext.gadget.wikt.core,ext.gadget.wikt.edit,ext.gadget.wikt.cite,ext.gadget.wikt.add-examples,oojs-ui-core,oojs-ui-widgets,oojs-ui-windows]|wikt.dsb-panels.js|wikt.dsb-panels.css
* wikt.start-wizard[ResourceLoader|hidden|package|dependencies=mediawiki.util,mediawiki.api,ext.gadget.wikt.core,ext.gadget.wikt.edit,ext.gadget.wikt.cite,ext.gadget.StorageUtils,ext.gadget.citoid-helper,oojs-ui-core,oojs-ui-widgets,oojs-ui-windows]|wikt.start-wizard.js|wikt.start-wizard.css
* wikt.add-examples[ResourceLoader|hidden|package|dependencies=oojs,oojs-ui,ext.gadget.citoid-helper,ext.gadget.StorageUtils,ext.gadget.wikt.core,ext.gadget.wikt.edit,ext.gadget.wikt.cite]|wikt.add-examples.js|wikt.add-examples.css
* linkify[ResourceLoader|hidden|dependencies=mediawiki.api]|linkify.js
* TreeDescs-pagestyles[hidden|type=styles]|TreeDescs-pagestyles.css
loj7b0vuepl69ubhzyw8xol2puftaj6
fötr
0
137083
6531640
4611338
2026-09-15T16:55:55Z
Wikihez
7344
Tirkî/Navdêr: beşa «Herwiha binêre (h-bnr)» hate zêdekirin, mane hate guhartin, beşa «Etîmolojî» hate zêdekirin (DefSideBoxes)
6531640
wikitext
text/x-wiki
{{h-bnr|fotr}}
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[kulav|Kulavê]] [[tenik|tenik]] û [[nerm|nerm]] ku pê [[xeml|xemlên]] [[dora|dora]] [[şewqe|şewqe]], [[çente|çente]] û [[tişt|tiştên]] [[wisa|wisa]] tê çêkirin.
==== Etîmolojî ====
Ji
{{deyn|fr|tr}}.
o8swnlh1mq1bgjzr3d4ilawdwixts2p
6531686
6531640
2026-09-15T18:12:09Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir, --Valahiyên nehewce.)
6531686
wikitext
text/x-wiki
{{h-bnr|fotr}}
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[kulav|Kulavê]] [[tenik]] û [[nerm]] ku pê [[xeml]]ên [[dora]] [[şewqe]], [[çente]] û [[tişt]]ên [[wisa]] tê çêkirin.
==== Etîmolojî ====
Ji
{{deyn|fr|tr}}.
03pzqsesoly9njxp6hd4aecnp4jszfm
parsel
0
158502
6531707
6313999
2026-09-15T18:32:21Z
Balyozbot
7867
sererastkirina nimûneyên edebiyatê (bi destî) [[Wîkîpediya:AutoWikiBrowser|AWB]]
6531707
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[lat]], [[bestek]], parçe-[[zevî]]
#* {{jêder|ku|jêgirtin=bi mebesta avakirina medreseyekê li gundê Aqtepeyê 52 '''parsel''' erd daye wî|Nûbihar hj. 131 r. 11, jêrenot}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Parzelle|guman=erê}}
* {{Z|tr}}: {{W+|tr|parsel|guman=erê}}
{{werger-bin}}
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[bisam]], [[tej]], [[parsêl]]
# [[lat]], [[bestek]], [[taxe]] (ji bo bexçeyê sebze û kulîlkan)
9nvecf494fomck88got6eo6lk7og8zr
ceme
0
178625
6531634
6357932
2026-09-15T14:40:29Z
Wikihez
7344
/* Jê */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531634
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Mane ===
{{cure?|ku}}
# [[caw]]
==== Jê ====
{{kol3|ku
|cemedan<crz:navdêr>
}}
==== Anagram ====
* {{anagram|ku|a=ceem|Ecem|ecem}}
e3arxl75wcp21ovaae825qlm5j0gd81
ablöschen
0
180600
6531789
4097752
2026-09-16T10:40:35Z
Jeuwre
4694
/* Bilêvkirin */
6531789
wikitext
text/x-wiki
== {{ziman|de}} ==
=== Bilêvkirin ===
* {{IPA|de|[ˈapˌlœʃn̩]}}
* {{deng|de|De-ablöschen2.ogg|dever=Berlîn, Almanya}}
* {{deng|de|De-ablöschen.ogg|Deng}}
* {{kîte|de|ab|lö|schen}}
=== Lêker ===
{{lêker|de}}
# [[vemirandin]], [[kujandin]]
mn9kxuxw51j6q11e7t74i18g6hlxbol
6531790
6531789
2026-09-16T11:29:07Z
Balyozbot
7867
[[Bikarhêner:Wikihez/skrîpt/py/dengbot.py|Bot]]: Formata parametreya "dever" hat guhartin
6531790
wikitext
text/x-wiki
== {{ziman|de}} ==
=== Bilêvkirin ===
* {{IPA|de|[ˈapˌlœʃn̩]}}
* {{deng|de|De-ablöschen2.ogg|dever=Berlîn, Almanya}}
* {{deng|de|De-ablöschen.ogg}}
* {{kîte|de|ab|lö|schen}}
=== Lêker ===
{{lêker|de}}
# [[vemirandin]], [[kujandin]]
0jd7fz8gd31wqgxcsugy9dtxuab0eha
-logie
0
247383
6531645
6048853
2026-09-15T17:03:16Z
Wikihez
7344
/* Fransî */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:-logie|en.wikt]]
6531645
wikitext
text/x-wiki
== {{ziman|de}} ==
=== Bilêvkirin ===
* {{deng|de|LL-Q188 (deu)-Frank C. Müller--logie.wav|Deng|dever=Frankfurt, Almanya}}
=== Paşgir ===
{{paşgir|de}}
# [[-lojî]]
== {{ziman|cs}} ==
=== Paşgir ===
{{paşgir|cs}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|cs|-lojî}}
== {{ziman|gsw}} ==
=== Paşgir ===
{{paşgir|gsw}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|gsw|-lojî}}
== {{ziman|fr}} ==
=== Paşgir ===
{{paşgir|fr}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
==== Etîmolojî ====
{{etîmon|fr|id=beşa lêkolînê|:deynh|la:-logia<id:lêkolîn>|metn=++|dar=1}}
== {{ziman|nl}} ==
=== Paşgir ===
{{paşgir|nl}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|nl|-lojî}}
== {{ziman|lb}} ==
=== Paşgir ===
{{paşgir|lb}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|lb|-lojî}}
== {{ziman|nds}} ==
=== Paşgir ===
{{paşgir|nds}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|nds|-lojî}}
== {{ziman|ro}} ==
=== Paşgir ===
{{paşgir|ro}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|ro|-lojî}}
bthugraadv2vd4k8iw26pohdb13g1gf
detest
0
274163
6531699
6090406
2026-09-15T18:24:30Z
Wikihez
7344
Îngilîzî/Lêker: beşa «Binêre herwiha» hate nûkirin (DefSideBoxes)
6531699
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{IPA|en|/dɪˈtɛst/}}
* {{deng|en|en-us-detest.ogg|dever=DYA}}
* {{kîte|en|de|test}}
=== Lêker ===
{{en-lêker}}
# [[kerixîn]], [[bîz kirin]]
==== Têkildar ====
* {{g|en|detestable}}
* {{g|en|detestation}}
==== Binêre herwiha ====
{{kol3|en|cure=Binêre herwiha
|abhor
|despise
|disdain
|dislike
|hate
|loathe
}}
kb7ym5tgvj8afvm7xuwdm8xx3fe7teb
6531716
6531699
2026-09-15T18:41:16Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531716
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{IPA|en|/dɪˈtɛst/}}
* {{deng|en|en-us-detest.ogg|dever=DYA}}
* {{kîte|en|de|test}}
=== Lêker ===
{{en-lêker}}
# [[kerixîn]], [[bîz kirin]]
==== Têkildar ====
* {{g|en|detestable}}
* {{g|en|detestation}}
==== Herwiha binêre ====
{{kol3|en|cure=Binêre herwiha
|abhor
|despise
|disdain
|dislike
|hate
|loathe
}}
ruiqk74h70fr6bvp8zm3lf6pqbgrka6
rapparee
0
314796
6531756
6051113
2026-09-15T21:35:21Z
Wikihez
7344
Îngilîzî/Navdêr: maneyek hate rakirin, maneyek hate rakirin (DefSideBoxes)
6531756
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|LL-Q1860 (eng)-Wodencafe-rapparee.wav|Deng|dever=Teksas, DYA}}
=== Navdêr ===
{{navdêr|en}}
# de gerîlayê îrlandî
r8uau0utrnzkwx97jq5v3iloa1ktpss
6531757
6531756
2026-09-15T21:36:10Z
Wikihez
7344
Guhartoya [[Special:Diff/6531756|6531756]] yê [[Special:Contributions/Wikihez|Wikihez]] ([[User talk:Wikihez|gotûbêj]]) şûnde kir
6531757
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|LL-Q1860 (eng)-Wodencafe-rapparee.wav|Deng|dever=Teksas, DYA}}
=== Navdêr ===
{{navdêr|en}}
# [[rêgir]], [[rêbir]], [[keleş]], [[eşqiya]], [[korsan]]
7rpry9mtx2u7nkjnkadsodvz8r68by4
Jinûvesazî:proto-hindûewropî/déḱm̥
108
836944
6531731
4899298
2026-09-15T20:25:48Z
Wikihez
7344
/* Dûndeh */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531731
wikitext
text/x-wiki
{{jinûvesazkirî|ine-pro}}
== {{ziman|ine-pro}} ==
=== Hejmar ===
{{hejmar|ine-pro}}
# [[deh]], [[10]]
==== Têkilî ====
*[[sḗm|*sḗm]]
*[[dwóh₁|*dwóh₁]]
*[[tréyes|*tréyes]]
*[[kʷetwóres|*kʷetwóres]]
*[[pénkʷe|*pénkʷe]]
*[[swéḱs|*swéḱs]]
*[[septḿ̥|*septḿ̥]]
*[[oḱtṓw|*oḱtṓw]]
*[[h₁néwn̥|*h₁néwn̥]]
*[[ḱm̥tóm|*ḱm̥tóm]]
==== Dûnde ====
* zimanên [[baltî]]
** {{z|lv}}: [[desmit]]
** {{z|lt}}: [[dešimt|dešimt]]
** {{z|prg}}: [[dessimts]]
* zimanên ermenî:
** {{z|xcl}}: [[տասն]] (tasn)
*** {{z|hy}}: [[տասը]] (tasë)
* zimanên germenî: *''[[tehun]]''
** {{z|got}}: [[𐍄𐌰𐌹𐌷𐌿𐌽]] (taíhun)
** {{z|ang}}: [[tien|tīen]]
*** {{z|en}}: [[ten]]
** {{z|goh}}: [[zehan]]
*** {{z|de}}: [[zehn]]
*** {{z|nl}}: [[tien]]
** {{z|non}}: [[tíu]], [[tío]]
*** {{z|da}}: [[ti]]
*** {{z|is}}: [[tíu]]
*** {{z|no}}: [[tie]]
*** {{z|sv}}: [[tio]]
* zimanên hindûîranî:
** zimanên hindûarî:
*** {{z|sa}}: [[दश]] (daśa)
**** {{z|si}}: [[දහය]] (dhahaya)
**** {{z|gu}}: [[દશ]] (daśa)
**** {{z|hi}}: [[दस]] (das)
**** {{z|sd}}: [[ڏَهَه]] (daha)
**** {{z|ur}}: [[دس]] (das)
**** {{z|pa|}}: [[ਦਸ]] (das)
** zimanên îranî:
*** {{z|ku}}: [[deh]]
*** {{z|fa}}: [[ده]] (deh)
* zimanên îtalî:
** {{z|la}}: [[decem]]
*** zimanên latînî:
**** {{z|rup}}: [[date]]
**** {{z|ca}}: [[deu]]
**** {{z|co}}: [[desi]]
**** {{z|es}}: [[diez]]
**** {{z|fr}}: [[dix]]
**** {{z|gl}}: [[dez]]
**** {{z|it}}: [[dieci]]
**** {{z|oc}}: [[dètz]]
**** {{z|pt}}: [[dez]]
**** {{z|rm}}: [[diesch]]
**** {{z|ro}}: [[zece]]
* zimanên keltî:
** {{z|br}}: [[dek]]
** {{z|kw}}: [[dec]]
** {{z|cy}}: [[deg]]
** {{z|sga}}: [[deich]]
*** {{z|ga}}: [[deich]]
*** {{z|gd}}: [[deich]]
*** {{z|gv}}: [[jeih]]
* zimanên slavî: *[[desętъ]]
** {{z|cu}}:
*** cyrillique: [[дєсѧть]], [[дєсѧтъ]]
*** glagolitique: [[ⰄⰅⰔⰤⰕⰟ]]
** {{z|bg}}: [[десет]]
** {{z|csb}}: [[dzesãc]]
** {{z|mk}}: [[десет]]
** {{z|pl}}: [[dziesięć]]
** {{z|ru}}: [[десять]] (désjat’)
** {{z|sh}}:
*** cyrillique: [[десет]]
*** latin: [[dȅset]]
** {{z|sl}}: [[deset]]
** {{z|cs}}: [[deset]]
* zimanên toxarî:
** {{z|xto}}: [[śäk]]
** {{z|txb}}: [[śak]]
* zimanên yûnanî:
** {{z|grc}}: [[δέκα]] (deka)
*** {{z|el}}: [[δέκα]] (deka)
ln9axjvsza4ym3qgpupdey6uao7k82s
Jinûvesazî:proto-hindûewropî/septḿ̥
108
837147
6531728
4756379
2026-09-15T20:21:34Z
Wikihez
7344
/* Dûndeh */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531728
wikitext
text/x-wiki
== {{ziman|ine-pro}} ==
=== Hejmar ===
{{hejmar|ine-pro}}
# [[heft]], [[7]]
==== Dûnde ====
* Zimanên baltî:
** {{z|lv}}: [[septiņi]]
** {{z|lt}}: [[septyni]]
* Zimanên keltî:
** {{z|br}}: [[seizh]]
** {{z|kw}}: [[seyth]]
** {{z|cy}}: [[saith]]
** {{z|sga}}: [[secht]]
*** {{z|ga}}: [[seacht]]
*** {{z|gv}}: [[shiaght]]
*** {{z|gd}}: [[seachd]]
* Zimanên ermenî:
** ermeniya kevn: [[եաւթն]] (eawtʿn)
*** {{z|hy}}: [[յոթ]] (yot’)
* Zimanên germenî:
** {{z|ang}}: [[seofon]]
*** {{z|en}}: [[seven]]
** {{z|da}}: [[syv]]
** {{z|goh}}: [[sibun]]
*** {{z|de}}: [[sieben]]
** {{z|got}}: [[𐍃𐌹𐌱𐌿𐌽]] (siƀun)
* Zimanên îranî:
** {{z|ae}}: [[𐬵𐬀𐬞𐬙𐬀]] (hapta, /hepte/)
** {{z|ku}} [[heft]]
** {{z|fa}}: [[هفت]] (heft)
** Zimanên hindûarî:
*** {{z|sa}}: [[सप्तन्]] (saptán)
* Zimanên îtalîkî:
** {{z|la}}: [[septem]]
*** {{z|ca}}: [[set]]
*** {{z|es}}: [[siete]]
*** {{z|fr}}: [[sept]]
*** {{z|it}}: [[sette]]
*** {{z|pt}}: [[sete]]
*** {{z|ro}}: [[șapte]]
* Zimanên slavî:
** {{z|cu}}: [[сєдмь]] (sedmĭ)
** {{z|ru}}: [[семь]] (sem’)
* Zimanên yûnanî:
** {{z|grc}}: [[ἑπτά]] (heptá)
6b4ij0tkrvhynowlmgmln1ja1v36ych
Jinûvesazî:proto-hindûewropî/h₃rḗǵs
108
964910
6531729
4923595
2026-09-15T20:22:45Z
Wikihez
7344
Proto-hindûewropî/Navdêr: mane hate guhartin (DefSideBoxes)
6531729
wikitext
text/x-wiki
{{jinûvesazkirî|ine-pro}}
== {{ziman|ine-pro}} ==
=== Navdêr ===
{{navdêr|ine-pro}}
# [[qiral]], [[hikûmdar]]
==== Jê ====
* {{g|ine-pro||*h₃rḗǵ-ō}}
** {{dû|iir-pro|*Hrā́ȷ́ā}}
*** {{dû|inc-pro|*Hrā́ȷ́ā}}
==== Dûnde ====
* {{dû|cel-pro|*rīxs}}
* {{dû|iir-pro|*Hrā́ćš}}
* {{dû|itc-pro|*rēks}}
89rqg04kfi49n31aevtt8a4tqhevcba
długi
0
1012022
6531642
5244934
2026-09-15T16:58:11Z
~2026-49765-60
15374
6531642
wikitext
text/x-wiki
== {{ziman|pl}} ==
=== Bilêvkirin ===
* {{deng|pl|Pl-{{PAGENAME}}.ogg|Deng}}
=== Rengdêr ===
{{rengdêr|pl}}
# [[dirêj#ku|dirêj]]
7v9fn83ccup1iahpx0ftufc06sx2nne
6531667
6531642
2026-09-15T17:29:13Z
Balyozbot
7867
[[Bikarhêner:Wikihez/skrîpt/py/dengbot.py|Bot]]: Formata parametreya "dever" hat guhartin
6531667
wikitext
text/x-wiki
== {{ziman|pl}} ==
=== Bilêvkirin ===
* {{deng|pl|Pl-{{PAGENAME}}.ogg}}
=== Rengdêr ===
{{rengdêr|pl}}
# [[dirêj#ku|dirêj]]
7r5lobcgps38rt7cer5a33qnyboncl5
fotr
0
1016483
6531641
5251212
2026-09-15T16:56:21Z
Wikihez
7344
Çekî/Navdêr: beşa «Herwiha binêre (h-bnr)» hate zêdekirin (DefSideBoxes)
6531641
wikitext
text/x-wiki
{{h-bnr|fötr}}
== {{ziman|cs}} ==
=== Navdêr ===
{{navdêr|cs|z=n}}
# {{f|cs|malbat}} [[bav#ku|bav]], [[bab#ku|bab]]
7ba6lsh6evc1l4sa694mcpo3yr4w77p
λόγος
0
1051761
6531650
5298666
2026-09-15T17:09:06Z
Wikihez
7344
/* Yûnaniya kevn */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:λόγος|en.wikt]]
6531650
wikitext
text/x-wiki
== {{ziman|grc}} ==
=== Navdêr ===
{{navdêr|grc|z=n}}
# {{g|ku|peyv}}, {{g|ku|bêje}}, {{g|ku|kelîme}}, {{g|ku|gotin}}
==== Etîmolojî ====
{{etîmon|grc|id=îzahkirin|:mîr|grk-pro:*lógos<etimon:mîr<ine-pro:<alt:*lóǵos><etimon:qf<*leǵ-<id:berhev kirin>><*-os<id:tematîk>>>>>|dar=1|metn=++}}
== {{ziman|el}} ==
=== Navdêr ===
{{navdêr|el|z=n}}
# {{g|ku|gotin}}, {{g|ku|axiftin}}, {{g|ku|peyivîn}}
=== Çavkanî ===
{{rûpela wergerê|el|gotin}}
e5cmim1mwkwu44pwcg77lzbxas8x6a3
шофёр
0
1164551
6531738
6498918
2026-09-15T20:36:07Z
Wikihez
7344
/* Rusî */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:шофёр|en.wikt]]
6531738
wikitext
text/x-wiki
== {{ziman|ky}} ==
=== Navdêr ===
{{navdêr|ky}}
# {{mijar|ky|pîşe}} {{g|ku|şofêr}}, {{g|ku|ajovan}}
=== Çavkanî ===
{{rûpela wergerê|ky|şofêr}}
== {{ziman|ru}} ==
=== Navdêr ===
{{navdêr|ru|z=n}}
# {{mijar|ru|pîşe}} {{g|ku|şofêr}}, {{g|ku|ajovan}}
==== Dûnde ====
* {{dû|hy|շոֆեր|alts=1|deyn=1}}
* {{dû|az|şofer|alts=1|شوفر|deyn=1}}
* {{dû|ug|shopur|alts=1|شوپۇر|deyn=1}}
== {{ziman|tg}} ==
=== Navdêr ===
{{navdêr|tg}}
# {{mijar|tg|pîşe}} {{g|ku|şofêr}}, {{g|ku|ajovan}}
=== Çavkanî ===
{{rûpela wergerê|tg|şofêr}}
g92vv1srouxcse8ui17nzthkd6u0ele
-logia
0
1207383
6531646
5538477
2026-09-15T17:04:16Z
Wikihez
7344
/* Latînî */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:-logia|en.wikt]]
6531646
wikitext
text/x-wiki
== {{ziman|fi}} ==
=== Paşgir ===
{{paşgir|fi}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|fi|-lojî}}
== {{ziman|ia}} ==
=== Paşgir ===
{{paşgir|ia}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|ia|-lojî}}
== {{ziman|it}} ==
=== Paşgir ===
{{paşgir|it}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|it|-lojî}}
== {{ziman|ca}} ==
=== Paşgir ===
{{paşgir|ca}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|ca|-lojî}}
== {{ziman|la}} ==
=== Paşgir ===
{{paşgir|la}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
==== Etîmolojî ====
{{etîmon|la|id=lêkolîn|:deyn|grc:-λογῐ́ᾱ<id:lêkolîn>|dar=1|metn=++}}
== {{ziman|oc}} ==
=== Paşgir ===
{{paşgir|oc}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|oc|-lojî}}
== {{ziman|pl}} ==
=== Paşgir ===
{{paşgir|pl}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|pl|-lojî}}
== {{ziman|pt}} ==
=== Paşgir ===
{{paşgir|pt}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|pt|-lojî}}
== {{ziman|sc}} ==
=== Paşgir ===
{{paşgir|sc}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|sc|-lojî}}
28626gi6yw9ic65l93vaj2bo1zvd9tn
-λογία
0
1207477
6531648
5538492
2026-09-15T17:05:56Z
Wikihez
7344
/* Yûnaniya kevn */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:-λογία|en.wikt]]
6531648
wikitext
text/x-wiki
== {{ziman|grc}} ==
=== Paşgir ===
{{paşgir|grc}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
==== Etîmolojî ====
{{etîmon|grc|id=lêkolîn|:af|λόγος<id:îzahkirin>|-ῐ́ᾱ<crz:paşgira îsmî ya abstrakt>|metn=++|dar=1}}
== {{ziman|el}} ==
=== Paşgir ===
{{paşgir|el}}
# {{g|ku|-lojî}}, [[-nasî]], [[-zanî]]
=== Çavkanî ===
{{rûpela wergerê|el|-lojî}}
m496p628ddmihhullca06res44l2kd1
MediaWiki:Gadget-wikt.add-examples.js
8
1272598
6531676
6529966
2026-09-15T17:59:27Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none; fill button in the wizard's pill style
6531676
javascript
text/javascript
// <nowiki>
// dependencies (in MediaWiki:Gadgets-definition): oojs, oojs-ui, ext.gadget.citoid-helper,
// ext.gadget.StorageUtils, ext.gadget.wikt.core, ext.gadget.wikt.edit, ext.gadget.wikt.cite
//
// A ResourceLoader package module: it require()s wikt.cite, so it cannot be loaded as a plain
// script. Load it through its gadget definition or the test loader.
//
// Two copies at once would fight over wikt.addExamples, so the second to arrive does nothing.
window.wikt = window.wikt || {};
wikt.addExamples = wikt.addExamples || {};
var alreadyLoaded = wikt.addExamples.loaded;
wikt.addExamples.loaded = true;
// Formats a string using {0}, {1}, ... placeholders.
if (!String.prototype.format) {
String.prototype.format = function(var_args) {
var args = arguments;
return this.replace(/{(\d+)}/g, function(match, number) {
return typeof args[number] !== "undefined" ? args[number] : match;
});
};
}
$(function() {
"use strict";
if (alreadyLoaded) return;
// Entries, reconstructions and sandboxes -- the same set DefSideBoxes and the wizard run on, from
// wikt.core, so a page one of them works on is never a page this one quietly skips.
if (!require("ext.gadget.wikt.core").sections.isEntryPage()) return;
if (mw.config.get("wgPageName") === "Destpêk" || mw.config.get("wgArticleId") === 0) return;
var NAME = "[[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]]";
// This gadget puts nothing on the page of its own. DefSideBoxes opens the form from its own
// "Nimûne/jêgirtin" item, through openFor()/toggleFor() below, and takes the save over.
window.wikt = window.wikt || {};
wikt.addExamples = wikt.addExamples || {};
// Lets another gadget take the save over: "Qeyd bike" becomes msg.add and the edit joins
// that gadget's queue. Pass null to hand publishing back. Handler gets {transform, summary, done, fail}.
wikt.addExamples.setSubmitHandler = function (fn) {
wikt.addExamples.submitHandler = typeof fn === "function" ? fn : null;
};
// Re-clicking the caller's own trigger closes the form again.
wikt.addExamples.toggleFor = function (el) {
var toggle = $(el).data("wiktAddExampleToggle");
if (!toggle) {
return false;
}
toggle();
return true;
};
wikt.addExamples.openFor = function (el) {
var open = $(el).data("wiktAddExampleOpen");
if (!open) {
return false;
}
open();
return true;
};
// Opens the form on one example already under the sense el: itemEl is its <dd>/<li> on the page.
wikt.addExamples.editFor = function (el, itemEl) {
var editItem = $(el).data("wiktAddExampleEdit");
return !!editItem && editItem(itemEl);
};
var VERSION = "2.0";
// Wiki markup for bold, both the button's label and what it inserts.
var BOLD_MARKS = "'''";
// Persists form drafts (localStorage, cookie fallback) so unsaved text survives a failed submit.
var draftStorage = new ObjectStorage("add_examples_draft", 1);
// Citations are built by wikt.cite, the same module the wizard uses; Citoid lives in the
// citoid-helper gadget. "citoid" as a _currentJederKey is a sentinel, not a real jederDefs key.
var cite = require("ext.gadget.wikt.cite");
var core = require("ext.gadget.wikt.core");
var sections = core.sections;
var msg = core.msg;
var edit = require("ext.gadget.wikt.edit");
var jederDefs = cite.jederDefs;
var nlToSpace = cite.nlToSpace;
var boldenPageTitle = cite.boldenPageTitle;
var CATEGORY_TEMPLATE_KEY = cite.CATEGORY_KEY;
var RAW_TEMPLATE_KEY = cite.RAW_KEY;
// Params the form already has boxes for; skipped when building a Kategorî:Çavkanî template's fields.
var CATEGORY_TEMPLATE_COVERED_PARAMS = cite.COVERED_PARAMS;
// A param labeled exactly this is filled from the language dropdown instead of shown as a field,
// and only when the template's own TemplateData declares one.
var CATEGORY_TEMPLATE_LANG_LABEL = cite.LANG_LABEL;
// "span" is the field's width in the 12-column grid the fields render in (see .add-example-jeder-fields
// in the CSS and _setSourceType below) -- field ORDER here is also the grid's reading order.
// Heading/definition-list helpers and the recognized POS heading names, shared with the wizard and
// DefSideBoxes so all three read the same page structure.
var headingOuter = sections.headingOuter;
var languageCodeForOl = sections.languageCodeForOl;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var eachLeafDefinition = sections.eachLeafDefinition;
// A heading's section key, the wizard's unclassified "=== Mane ===" placeholder included: it holds
// real definitions, so examples belong in it, even though it is not a part of speech.
var sectionKeyOf = sections.sectionKeyOf;
// Locating a section and a definition inside it is wikt.edit's job, shared with DefSideBoxes.
var wt = edit.wt;
// Template names that count as real examples, so an annotation template is not mistaken for one.
var exampleTemplateNames = ["jêder"].concat(cite.USEX_NAMES, Object.keys(jederDefs));
// The "no example yet" placeholder, with or without a language parameter.
var EMPTY_EXAMPLE_MARKER = cite.EMPTY_EXAMPLE_MARKER;
// The template name starting a "#:"/"#*" line, or null when the line does not start with one.
function getLineTemplateName(line) {
var m = /^#[:*]\s*\{\{\s*([^|}]+)/.exec(line);
return m ? m[1].trim() : null;
}
// Where a template block starting at startIndex closes its braces, so multi-line calls stay whole.
// The "#" run a definition line carries, so a sense's own annotations can be told from a sub-sense's:
// "#:" belongs to "#", "##:" to "##".
function senseDepth(definitionLine) {
var m = /^#+/.exec(definitionLine || "");
return m ? m[0].length : 1;
}
// Any annotation of that sense -- "#:" an example, "#*" a citation -- at its own depth and no other.
function annotationRegex(definitionLine) {
return new RegExp("^#{" + senseDepth(definitionLine) + "}[:*]");
}
function citationRegex(definitionLine) {
return new RegExp("^#{" + senseDepth(definitionLine) + "}\\*");
}
function findBlockEnd(lines, startIndex) {
var depth = 0;
var i = startIndex;
for (; i < lines.length; i++) {
var opens = (lines[i].match(/\{\{/g) || []).length;
var closes = (lines[i].match(/\}\}/g) || []).length;
depth += opens - closes;
if (depth <= 0) {
break;
}
}
return i;
}
// URL of a template's own wiki page -- lets the user check a Kategorî:Çavkanî search result
// before picking it (see showResults below).
// Escapes str for interpolation into a RegExp: page titles carry regex metacharacters.
var escapeRegExp = edit.util.escapeRegExp;
// The readable half of an mw.Api rejection.
var apiErrorMessage = edit.util.apiErrorMessage;
// Same marker as the wizard's, positioned inside OOUI's own wrapper rather than a new one.
var highlightLinebreak = edit.util.makeLinebreakHighlighter({
prefix: "add-example",
wrap: false,
dataKey: "addExampleLinebreakBackdrop",
title: "Xeta n\u00fb d\u00ea wek <br> b\u00ea niv\u00eesandin"
});
// Linebreak marking on the quote text and its translation/transcription only, not the
// bibliographic fields.
function watchForLinebreaks(widget) {
widget.on("change", function() {
highlightLinebreak(widget.$input[0]);
});
}
// The shared grip from wikt.edit, kept as a sibling inside OOUI's own wrapper and without the saved
// height: these fields are plain MultilineTextInputWidgets with no class to key one by.
function makeManualResizable(textarea) {
edit.util.makeManualResizable(textarea, {
wrap: false,
persist: false,
gripClass: "add-example-resize-grip",
doneKey: "addExampleResizableDone"
});
}
// Applies makeManualResizable to every textarea in `context` (itself or its descendants).
function initManualResize(context) {
var $context = $(context);
var $textareas = $context.is("textarea") ? $context : $context.find("textarea");
$textareas.each(function() {
makeManualResizable(this);
});
}
var api = new mw.Api();
// Kategorî:Çavkanî's templates and their redirects, each redirect its own entry pointing at the
// Language display name, read from the matching section's .besazimani link text.
function languageNameForCode(code) {
var $mark = $(".besazimani[id='" + code + "']").first();
return $mark.length ? $mark.text().trim() : null;
}
// Whether the page title itself (accented Kurdish Latin letters included) is written in Latin script.
var pageTitleIsLatin = !/[^\x00-\x7FÀ-ɏḀ-ỿ‘’]/.test(mw.config.get("wgTitle") || "");
// What an example looks like once rendered: Şablon:jêder's old marker, Modul:usex's containers and
// Modul:quote's wrapper.
var EXAMPLE_SELECTOR = ".example, .benimune, .h-usage-example, .h-quotation, .citation-whole";
// The words drawn for one example: the example or quotation itself, never its translation or source.
function exampleTextOf($item) {
var $own = $item.find(".e-example, .e-quotation").first();
if ($own.length) {
return $own.text();
}
var $italic = $item.children("i").first();
return ($italic.length ? $italic : $item.clone().children("dl, ul, .dsb-example-toolbar").remove().end()).text();
}
// A definition's own citation group, and the last one when it has several: a new citation is
// written after every "#:"/"#*" line already there.
function exampleGroupOf($li) {
return $li.children("ul").filter(function () {
return $(this).children("li").children(EXAMPLE_SELECTOR).length > 0;
}).last();
}
// Every recognized part-of-speech section, down to each of its definitions. Section, language and
// nesting all come from the page's own structure, never from an example that happens to sit there.
// "===" on most entries, "====" where the senses hang off numbered etymologies.
$("h3, h4").each(function() {
var sectionType = sectionKeyOf($(this));
if (!sectionType) {
return; // not a recognized word-sense section (Bilêvkirin, Etîmolojî, Çavkanî, ...)
}
var sectionOrdinal = sectionOrdinalWithinLanguage($(this));
// Forward-walk siblings to the definitions <ol>, skipping over a declension table/TemplateStyles/the
// headword line; stop (nothing to attach to in this section) if another heading comes first instead.
var $ol = null;
for (var $cursor = headingOuter($(this)).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("ol")) {
$ol = $cursor;
break;
}
if (sections.headingDepth($cursor)) {
break;
}
}
if (!$ol) {
return;
}
var language = languageCodeForOl($ol);
if (!language) {
console.warn("Gadget-wikt.add-examples: nikaribe koda zimanê vê beşê bibîne.", $ol);
return;
}
eachLeafDefinition($ol, function($li) {
var definitionLevel = [];
var $definitionItem = $li;
while ($definitionItem.prop("tagName") === "LI") {
// Skip MediaWiki's empty <li class="mw-empty-elt"> so indices match the "#" lines in wikitext.
definitionLevel.splice(0, 0, $definitionItem.prevAll("li:not(.mw-empty-elt)").length);
$definitionItem = $definitionItem.parent().parent();
}
definitionLevel.splice(0, 0, sectionType + "|" + sectionOrdinal);
// Where a submitted example lands: after the group's last example, or after a hidden placeholder
// when the definition has no group.
var $group = exampleGroupOf($li);
var $placeholder = $group.length ? $group.children("li").last()
: $("<li style='display:none'></li>");
var $formItem = $("<li>");
// Never in the DOM: Form toggles it against the title, so it stays as the "is a form built
// for this sense" handle. Nothing on the page opens this gadget -- DefSideBoxes does.
var $button = $("<span>").hide();
// Says which sense and language the open form is for; the row exists only while it is open.
var $title = $("<span class='add-example-form-title'></span>").hide();
$formItem.append($title);
// The group the button lives in: the one on the page, or a new one. A "ul" under a definition
// "li" starts hidden, so a new one needs the explicit display override.
var $ul = $group.length ? $group
: $("<ul>").css("display", "block").append($placeholder);
// A citation already carrying the site's own small edit link does not need a second one.
$ul.find("span.example > span.stubedit").remove();
// The panel opens after $ul, not inside it: full width, clear of the list's hanging indent.
// edit, when given, is {index, text}: the form opens on that example instead of on a new one.
function setFormVisible(visible, edit) {
if (visible) {
// A sense with no example list of its own gets one on first open, not on page load:
// an empty list on every sense of every entry is margin nobody asked for.
// Editing shows its title inside the form, so the list row is only for a new example.
if (!edit && !$ul.parent().length) {
$li.append($ul);
}
if (!edit && !$formItem.parent().length) {
$ul.append($formItem);
}
if (edit) {
$formItem.detach();
}
if (!$button.form) {
$title.text("Nimûneyeke bi " + (languageNameForCode(language) || "zimanekî nenas").toLowerCase() + " lê zêde dikî.");
var form = new Form($placeholder, $button, $title, language, definitionLevel);
form.onHide = function () {
$formItem.detach();
};
$ul.after(form.$element);
}
// Right under the example being edited; after the whole list when adding a new one.
(edit ? $(edit.item) : $ul).after($button.form.$element);
if (edit) {
$button.form.beginEdit(edit.index, edit.text);
} else {
$button.form.endEdit();
$button.form.setVisible(true);
}
} else if ($button.form) {
$button.form.setVisible(false);
$formItem.detach();
}
return false;
}
function openForm() { return setFormVisible(true); }
function toggleForm() { return setFormVisible(!($button.form && $button.form.isVisible())); }
$li.data("wiktAddExampleOpen", openForm);
$li.data("wiktAddExampleToggle", toggleForm);
// Its place among the sense's examples finds the line; the words drawn for it confirm it.
$li.data("wiktAddExampleEdit", function (itemEl) {
var $item = $(itemEl);
var index = sections.exampleItemsOf($li).index($item);
if (index === -1) {
return false;
}
setFormVisible(true, { index: index, text: exampleTextOf($item), item: itemEl });
return true;
});
});
});
// Pulls the form panel out to the page's own edges, undoing the nested list's indent.
function alignFormToPage($form) {
var $container = $form.closest(".mw-parser-output");
if (!$container.length) {
return;
}
// Measure unshifted: this runs on every re-open, and a margin left from last time would otherwise
// count as part of the indent and cancel the alignment out.
$form.css("marginLeft", 0);
var pad = parseFloat(window.getComputedStyle($container[0]).paddingLeft) || 0;
var containerLeft = $container.offset().left + pad;
var indent = $form.offset().left - containerLeft;
$form.css({
marginLeft: indent > 0 ? -indent : 0,
width: $container.width() + "px"
});
}
/**
* language is the section the sense sits in: what the new example is tagged with, and what the
* wikitext lookup searches for. Never the language of an example that happens to be there.
*/
function Form($lastExample, $button, $title, language, definitionLevel) {
var self = this;
this._language = language;
this._definitionLevel = definitionLevel;
this._$lastExample = $lastExample;
this._$button = $button;
this._$button.form = this;
this._$title = $title;
// Where the title lives when adding; while editing it moves into the form itself.
this._$titleHome = $title.parent();
this._currentJederKey = "";
this._jederFieldWidgets = {};
// Set once a template is picked in the Kategorî:Çavkanî mode (see _selectCategoryTemplate); the
// canonical template title (without the "Şablon:" prefix), used as `sablon` in _doSubmit.
this._categoryTemplateName = null;
// Set by _renderCategoryTemplateFields if the picked template's TemplateData has a "koda zimanî" param;
// that param's own key, used to emit "<key>=<language>" in _doSubmit -- null if no such param exists.
this._categoryTemplateLangParamKey = null;
// Set while an example already on the page is being edited (see beginEdit): {index, original,
// example, key}, where key is the source type the line was read into.
this._editing = null;
this._addTitle = null;
// The same prompt the wizard's own example box carries, so the two ask for an example in one
// wording and the language it is to be written in is on screen here too.
this._textInput = new OO.ui.MultilineTextInputWidget({
rows: 4,
placeholder: edit.util.examplePrompt(languageNameForCode(language), false)
});
var textInputLayout = new OO.ui.FieldLayout(this._textInput, {
label: "Metna nimûneyê",
align: "top",
});
// Under the text box: what will happen to the entry's own name on save (see boldenPageTitle), and
// the one markup button worth having beside it. Purely informational, never blocks submitting.
this._titleBoldHint = $("<span class='add-example-title-bold-hint'></span>");
this._updateTitleBoldHint = function() {
var text = self._textInput.getValue().trim();
var title = mw.config.get("wgTitle");
var missing = !!text && !boldenPageTitle(text, title).bolded;
self._titleBoldHint
.toggleClass("add-example-title-bold-warn", missing)
.html(missing
? "Sermade (" + mw.html.escape(title) + ") nehat dîtin; dê neyê <b>stûr</b>kirin."
: "Sermade otomatîk tê <b>stûr</b>kirin.");
// Only where it is needed: when the name is bolded on its own there is nothing to reach for.
self._boldButton.toggle(missing);
};
// Shown only while the name cannot be bolded on its own -- a form the matcher does not recognise,
// or another word the writer wants bolded.
this._boldButton = $("<button type='button' class='add-example-bold-btn'></button>").hide()
.text(BOLD_MARKS).attr("title", "Nivîsa hilbijartî stûr bike");
this._boldButton.on("click", function(e) {
e.preventDefault();
var input = self._textInput.$input[0];
var value = input.value;
var start = input.selectionStart;
var end = input.selectionEnd;
var next, caret;
if (start !== end) {
next = value.slice(0, start) + BOLD_MARKS + value.slice(start, end) + BOLD_MARKS + value.slice(end);
caret = end + BOLD_MARKS.length * 2;
} else {
// Nothing selected: just the marks, where the caret is. Typing carries on after them.
next = value.slice(0, start) + BOLD_MARKS + value.slice(start);
caret = start + BOLD_MARKS.length;
}
self._textInput.setValue(next);
input.focus();
input.setSelectionRange(caret, caret);
});
this._textTools = $("<div class='add-example-text-tools'></div>")
.append(this._boldButton, this._titleBoldHint);
this._updateTitleBoldHint();
// A pasted citation switches to raw mode rather than being wrapped in a second {{nimûne}}: a whole
// line with its marker and a bare template call both count. Only while the type is still citoid.
this._maybeSwitchToRaw = function(value) {
if (self._currentJederKey !== "citoid" ||
!(cite.looksLikeCitationLine(value) || cite.looksLikeTemplate(value))) {
return;
}
var menu = self._sourceTypeDropdown.getMenu();
var item = menu.findItemFromData(RAW_TEMPLATE_KEY);
if (item) {
menu.chooseItem(item); // calls _setSourceType() itself
}
};
this._textInput.on("change", function(value) {
self._applyButton.setDisabled(!value);
self._maybeSwitchToRaw(value);
self._updateReadyHint();
self._updateTitleBoldHint();
});
watchForLinebreaks(this._textInput);
// Source type: free text, a jêder-* template, or "citoid" -- a sentinel meaning "not yet detected",
// switched automatically once a Citoid fetch succeeds.
var sourceTypeMenuItems = [
new OO.ui.MenuOptionWidget({ data: "citoid", label: msg.citoidAuto }),
new OO.ui.MenuOptionWidget({ data: "", label: msg.sourceByHand }),
];
for (var jederKey in jederDefs) {
if (jederDefs.hasOwnProperty(jederKey)) {
sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({
data: jederKey,
label: jederDefs[jederKey].label,
}));
}
}
sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({
data: CATEGORY_TEMPLATE_KEY,
label: msg.sourceFromCategory,
}));
// Paste a finished citation into the example box and have it saved exactly as typed -- the quickest
// way to reuse one jêgirtin across several entries.
sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({
data: RAW_TEMPLATE_KEY,
label: msg.sourceRaw,
}));
// Shown only in raw mode, where there is nothing else to fill in.
this._rawTemplateHint = new OO.ui.MessageWidget({
type: "notice",
inline: true,
label: "Şablona temam li qada \"Metna nimûneyê\" binivîse \u2014 wek xwe tê qeydkirin. "
+ "Tenê navê maddeyê yê di nav jêgirtin= de tê stûrkirin.",
classes: ["add-example-raw-hint"],
});
this._rawTemplateHint.toggle(false);
this._sourceTypeDropdown = new OO.ui.DropdownWidget({
menu: { items: sourceTypeMenuItems },
label: msg.citoidAuto,
});
var sourceTypeLayout = new OO.ui.FieldLayout(this._sourceTypeDropdown, {
label: "Şablona çavkaniyê",
align: "top",
});
this._sourceTypeDropdown.getMenu().on("choose", function(menuOption) {
self._setSourceType(menuOption.getData());
});
this._sourceInput = new OO.ui.MultilineTextInputWidget();
this._sourceInput.on("change", function() {
self._updateReadyHint();
});
this._sourceInputLayout = new OO.ui.FieldLayout(this._sourceInput, {
label: "Çavkaniya nimûneyê",
align: "top",
});
// Visible in every mode: submit() reads it as "url=" regardless. The Citoid button sits beside it.
this._sourceURLInput = new OO.ui.TextInputWidget({
icon: "link",
placeholder: "https://… an jî DOI/ISBN",
});
this._citoidButton = new OO.ui.ButtonWidget({
label: msg.citoidFill,
icon: "download",
flags: ["progressive"],
title: "Li gorî URL, DOI an ISBNya jorîn, malûmatan ji Citoid tîne û qadên şablona jêder-* otomatîk tijî dike.",
classes: ["add-example-citoid-fetch"],
});
this._citoidButton.on("click", this._fillFromCitoid.bind(this));
this._sourceURLInput.on("change", function() {
self._updateReadyHint();
self._autofillVideoTimestamp();
});
// URL input and its auto-fill button as one flex row; a bare OO.ui.Widget used as the container.
this._urlRowWidget = new OO.ui.Widget({ classes: ["add-example-url-row"] });
this._urlRowWidget.$element.append(this._sourceURLInput.$element, this._citoidButton.$element);
this._sourceURLInputLayout = new OO.ui.FieldLayout(this._urlRowWidget, {
label: "Lînk, DOI an ISBNya çavkaniyê",
align: "top",
help: "Li qutiyê URL, DOI an ISBN binivîse û pê lê bike butona '" + msg.citoidFill + "' da ku otomatîk çavkanî were dagirtin.",
helpInline: true,
});
// Same wording as above, so the initial _setSourceType("citoid") rebuilds nothing.
this._urlFieldIsFreeTextWording = false;
// Shown right after a successful Citoid fetch (see _fillFromCitoid); hidden again on clear.
this._citoidSuccessMsg = $("<span class='add-example-citoid-success'></span>").text(msg.citoidFilled).hide();
// Clears the URL field and any filled jêder-* fields without touching the example text/translation.
this._citoidClearButton = new OO.ui.ButtonWidget({
label: "Vala bike",
icon: "trash",
framed: false,
title: "Lînkê û qadên jêder-* yên vê beşê vala bike",
classes: ["add-example-citoid-clear"],
});
this._citoidClearButton.on("click", function() {
self._sourceURLInput.setValue("");
self._citoidSuccessMsg.hide();
Object.keys(self._jederFieldWidgets).forEach(function(key) {
self._jederFieldWidgets[key].setValue("");
});
});
// Plain div (not OO.ui.FieldsetLayout) so the grid CSS class applies directly to the field items.
this._jederFieldsFieldset = $("<div class='add-example-jeder-fields'>");
// Header and clear button above the fields grid. Only meaningful once a real jêder-* template is
// active, so the whole section toggles together in _setSourceType().
this._jederSection = $("<div class='add-example-jeder-section'>").append(
$("<div class='add-example-jeder-header'>")
.append($("<span class='add-example-jeder-header-label'>Malûmatên çavkaniyê</span>"))
.append(this._citoidClearButton.$element),
this._jederFieldsFieldset
);
// wikt.cite's picker, the one the wizard and DefSideBoxes show. Sits under the source-type
// dropdown, not in the fields section, and is built once so switching modes keeps its handlers.
var categoryPicker = cite.buildCategoryTemplateSearch({
describe: true,
onPick: function (entry) {
self._selectCategoryTemplate(entry);
}
});
this._categoryTemplateInput = categoryPicker.$input;
this._categoryTemplateStatus = categoryPicker.$status;
this._categoryTemplateSearchRow = categoryPicker.$element
.addClass("add-example-cat-tpl-search-row").hide();
this._translationInput = new OO.ui.MultilineTextInputWidget({ rows: 4 });
var translationInputLayout = new OO.ui.FieldLayout(this._translationInput, {
// A Kurmancî example gets an explanation rather than a translation.
label: language === "ku" ? "Îzaheke kurmancî ya nimûneyê" : "Tercimeya kurmancî ya nimûneyê",
align: "top",
});
watchForLinebreaks(this._translationInput);
this._transcriptionInput = new OO.ui.MultilineTextInputWidget({ rows: 4 });
var transcriptionInputLayout = new OO.ui.FieldLayout(this._transcriptionInput, {
label: "Translîterasyona nimûneyê",
align: "top",
help: "Tenê lê zêde bike eger metna di nimûneyê de bi alfabeya latînî nehatibe nivîsandin.",
helpInline: true,
});
watchForLinebreaks(this._transcriptionInput);
this._applyButton = new OO.ui.ButtonWidget({
label: "Qeyd bike",
icon: "check",
title: "Nimûneyê ji bo vê tarîfê nimûneyê lê zêde bike",
flags: ["progressive", "primary"],
disabled: true,
});
this._applyButton.on("click", this.submit.bind(this));
// Reactive "ready to submit" hint next to Qeyd bike: no text yet, text but no source (still submittable
// as a bare {{nimûne}}), or fully ready -- kept in sync by _updateReadyHint(), wired to every field below.
this._readyHint = $("<span class='add-example-ready-hint'></span>").text("Metna nimûneyê lazim e.");
// True while a Citoid-eligible URL is typed but unfetched and no jêder field has been filled by
// hand -- the "looks done but isn't" case.
this._citoidPending = function() {
return self._currentJederKey !== "" &&
self._currentJederKey !== CATEGORY_TEMPLATE_KEY &&
self._currentJederKey !== RAW_TEMPLATE_KEY &&
!!self._sourceURLInput.getValue().trim() &&
!self._citoidSuccessMsg.is(":visible") &&
!Object.keys(self._jederFieldWidgets).some(function(key) {
return self._jederFieldWidgets[key].getValue().trim();
});
};
// A Kurmancî jêgirtin takes no translation or explanation, only a {{nimûne}} does. An edited line
// that already has one keeps its box.
this._syncTranslationBox = function() {
if (self._language !== "ku") {
return;
}
var key = self._currentJederKey;
var isQuote = key === RAW_TEMPLATE_KEY || key === CATEGORY_TEMPLATE_KEY || !!jederDefs[key] ||
!!self._sourceInput.getValue().trim() || !!self._sourceURLInput.getValue().trim();
translationInputLayout.toggle(!isQuote ||
!!(self._editing && self._translationInput.getValue().trim()));
};
this._updateReadyHint = function() {
self._syncTranslationBox();
if (!self._textInput.getValue()) {
self._readyHint.text("Metna nimûneyê lazim e.");
return;
}
if (self._citoidPending()) {
self._readyHint.text("Pêşî '" + msg.citoidFill + "' bitikîne.");
return;
}
// Raw mode carries its own source inside the pasted template, so there is nothing else to ask for.
if (self._currentJederKey === RAW_TEMPLATE_KEY) {
self._readyHint.text("Hazir e.");
return;
}
var hasSource = self._sourceInput.getValue().trim() || self._sourceURLInput.getValue().trim() ||
Object.keys(self._jederFieldWidgets).some(function(key) {
return self._jederFieldWidgets[key].getValue().trim();
});
self._readyHint.text(hasSource ? "Hazir e." : "Çavkanî nehatiye dayîn.");
};
// Prefills Dem from the URL's own "t=" once jêder-vîdeo is active; never overwrites typed text.
this._autofillVideoTimestamp = function() {
var demWidget = self._jederFieldWidgets.dem;
if (self._currentJederKey !== "jêder-vîdeo" || !demWidget || demWidget.getValue().trim()) {
return;
}
var dem = wikt.citoid.extractVideoTimestamp(self._sourceURLInput.getValue().trim());
if (dem && !demWidget.getValue().trim()) {
demWidget.setValue(dem);
}
};
// framed:false (+ the "destructive" flag for the red text/hover) matches the mockup's borderless
// "Betal bike" -- a plain colored link rather than a boxed button, distinguishing it from Paqij bike.
this._cancelButton = new OO.ui.ButtonWidget({
label: "Betal bike",
icon: "close",
title: "Formê bigire",
flags: ["destructive"],
framed: false,
});
this._cancelButton.on("click", function() {
self.setVisible(false);
});
this._clearButton = new OO.ui.ButtonWidget({
label: "Paqij bike",
icon: "trash",
title: "Hemû qadan vala bike û pêşnûyisên hilanîbûyî jê bibe",
});
this._clearButton.on("click", function() {
self.clear();
self._clearDraft();
});
// Only while an example on the page is being edited.
this._deleteButton = new OO.ui.ButtonWidget({
label: "Jê bibe",
icon: "trash",
flags: ["destructive"],
});
this._deleteButton.toggle(false);
this._deleteButton.on("click", function() {
OO.ui.confirm("Ev nimûne dê were rakirin. Dewam bike?").done(function(ok) {
if (ok) {
self._submitDelete();
}
});
});
// A CSS spinner rather than a Commons GIF: no request, and it follows dark/light mode.
this._loadingImage = new OO.ui.Widget({
classes: ["add-example-loading"],
});
// The ring alone says nothing to a reader who has asked for less motion: MediaWiki's own
// reduced-motion reset cuts every animation to 0.01ms, so it never turns for them. The words
// beside it carry the state either way.
this._loadingImage.$element.append(
$('<span class="add-example-spinner" aria-hidden="true"></span>'),
$('<span class="add-example-loading-label"></span>').text(msg.loading)
);
this._loadingImage.toggle(false);
// Two-column top: example text (with translation/transliteration for non-ku) on the left, source
// type and link on the right. Plain divs, so the CSS can lay them out as a grid.
// The translation box is there for Kurmancî too: {{nimûne}} takes one for every language.
var $topLeft = $("<div class='add-example-top-col'>").append(textInputLayout.$element, this._textTools,
translationInputLayout.$element);
if (!pageTitleIsLatin) {
$topLeft.append(transcriptionInputLayout.$element);
}
// Kept for _fill, which adds either box when an edited line already carries one.
this._$topLeft = $topLeft;
this._translationInputLayout = translationInputLayout;
this._transcriptionInputLayout = transcriptionInputLayout;
var $topRight = $("<div class='add-example-top-col'>").append(
sourceTypeLayout.$element,
this._categoryTemplateSearchRow,
this._rawTemplateHint.$element,
this._sourceInputLayout.$element,
this._sourceURLInputLayout.$element,
this._citoidSuccessMsg
);
var $topGrid = $("<div class='add-example-top-grid'>").append($topLeft, $topRight);
// Saved on the instance (not just a local var) so _setSourceType() can swap the URL field's label/help
// wording in and out of it when switching to/from free text (see the FieldLayout replaceWith() there).
this._fieldsLayout = $("<div class='add-example-fieldset'>").append($topGrid, this._jederSection);
// Bottom action row: Qeyd bike (+ spinner and hint), a spacer, then Paqij bike and Betal bike. A
// flex div rather than an OOUI layout, so the hint and spacer can sit among the widgets.
var $buttonsRow = $("<div class='add-example-main-buttons'>").append(
this._applyButton.$element,
this._loadingImage.$element,
this._readyHint,
$("<span class='add-example-buttons-spacer'></span>"),
this._deleteButton.$element,
this._clearButton.$element,
this._cancelButton.$element
);
this._frame = new OO.ui.PanelLayout({
id: "add-example-definition-{0}-form".format(this._definitionLevel.join("-")),
classes: ["add-example-form"],
expanded: false,
});
this._frame.$element.append(this._fieldsLayout, $buttonsRow);
// selectItemByData() does not fire "choose", so the initial mode is set up by hand.
this._sourceTypeDropdown.getMenu().selectItemByData("citoid");
this._setSourceType("citoid");
initManualResize(this._frame.$element);
}
Form.prototype = {
// Returns the jQuery object for this form.
get $element() {
return this._frame.$element;
},
// Switches the source section between free text, Citoid auto-detect and a chosen jêder-* template,
// rebuilding the field list. The URL field stays visible in every mode.
_setSourceType: function(jederKey) {
var self = this;
this._currentJederKey = jederKey;
this._jederFieldsFieldset.empty();
this._jederFieldWidgets = {};
this._categoryTemplateName = null;
this._categoryTemplateLangParamKey = null;
var isRealJederKey = jederKey && jederDefs[jederKey];
var isCategoryTemplateKey = jederKey === CATEGORY_TEMPLATE_KEY;
var isRawTemplate = jederKey === RAW_TEMPLATE_KEY;
if (isRealJederKey) {
var def = jederDefs[jederKey];
def.fields.forEach(function(field) {
var widget = field.multiline
? new OO.ui.MultilineTextInputWidget({ rows: 2, autosize: false })
: new OO.ui.TextInputWidget();
self._jederFieldWidgets[field.key] = widget;
widget.on("change", function(value) {
// sal and tarîx say the same thing twice and Modul:quote refuses both; filling one
// clears the other rather than letting the citation break on save.
var partner = value && cite.exclusivePartner(field.key);
if (partner && self._jederFieldWidgets[partner]) {
self._jederFieldWidgets[partner].setValue("");
}
self._updateReadyHint();
});
var layout = new OO.ui.FieldLayout(widget, {
label: field.label,
align: "top",
classes: ["add-example-field-span-" + (field.span || 4)],
});
self._jederFieldsFieldset.append(layout.$element);
});
}
// The box lives in $topRight always; here it is only shown for this mode, and reset on the way out
// so a stale query does not linger.
this._categoryTemplateSearchRow.toggle(isCategoryTemplateKey);
// Raw mode has no source fields of its own: the citation is already written out in full.
this._rawTemplateHint.toggle(isRawTemplate);
if (!isCategoryTemplateKey) {
this._categoryTemplateInput.val("");
}
// The fields grid is for a real jêder-* template. The Kategorî:Çavkanî picker shows it only once a
// template is picked (see _selectCategoryTemplate).
this._jederSection.toggle(!!isRealJederKey);
// Free text textarea only for "" (free text); hidden for "citoid" and any real jêder-* key.
this._sourceInputLayout.toggle(jederKey === "");
// No Citoid button for free text (nothing to fill) or the Kategorî:Çavkanî picker (no mapping for
// an arbitrary template); shown for auto-detect and any jêder-* key.
this._citoidButton.toggle(jederKey !== "" && !isCategoryTemplateKey && !isRawTemplate);
// Plain "attach a link" wording for free text, Citoid wording otherwise. Rebuilt only when crossing
// that boundary.
var isFreeText = jederKey === "";
if (this._urlFieldIsFreeTextWording !== isFreeText) {
this._urlFieldIsFreeTextWording = isFreeText;
// On the <input> itself: TextInputWidget has no setter for its placeholder.
this._sourceURLInput.$input.attr("placeholder", isFreeText ? "URLyeke wekî https://…" : "URLyek wekî https://… an jî DOI/ISBN");
var $oldLayout = this._sourceURLInputLayout.$element;
// Re-wraps the SAME _urlRowWidget (still holding _sourceURLInput + _citoidButton) in a new
// FieldLayout with the new label/help; the widget's $element moves along with it.
this._sourceURLInputLayout = new OO.ui.FieldLayout(this._urlRowWidget, isFreeText ? {
label: "Adresa webê ya nimûneyê",
align: "top",
help: "Tenê binivîse eger lînk jixwe di referansa çavkaniyê de tinebe.",
helpInline: true,
} : {
label: "Lînk, DOI an ISBNya çavkaniyê",
align: "top",
help: "Ev dibe lînka çavkaniyê (url=). Ji bo şablonên jêder-*, dikare bi Citoid an bi destan jî bên dagirtin.",
helpInline: true,
});
$oldLayout.replaceWith(this._sourceURLInputLayout.$element);
}
// The URL/Citoid row is normally always visible (see the constructor comment), but Citoid has no
// mapping for an arbitrary Kategorî:Çavkanî template, so it'd just be dead UI in that one mode.
this._sourceURLInputLayout.toggle(!isCategoryTemplateKey && !isRawTemplate);
this._autofillVideoTimestamp();
this._updateReadyHint();
initManualResize(this._jederFieldsFieldset); // covers a freshly-rebuilt "sernav" field (jêder-*); the Kategorî:Çavkanî picker's own fields are resized separately once picked, see _renderCategoryTemplateFields
},
// Wires the search box's handlers and starts the template fetch. Called once: the box lives in
// $topRight and is only shown or hidden by _setSourceType.
// Records the picked template's canonical name -- a redirect resolves to its target -- and shows
// its parameter fields.
_selectCategoryTemplate: function(entry) {
// Left as the plain label (not "label → target") -- putting the arrow in the input's own value
// would get re-used as the search query if the box is focused again to pick something else.
this._categoryTemplateInput.val(entry.label);
this._categoryTemplateName = entry.target;
this._jederSection.toggle(true);
return this._renderCategoryTemplateFields(entry.target);
},
// One text input per TemplateData param of `name`, into the shared "Malûmatên çavkaniyê" grid, or a
// lone "Rûpel" box when the template has no TemplateData.
_renderCategoryTemplateFields: function(name) {
var self = this;
this._jederFieldsFieldset.empty();
this._jederFieldWidgets = {};
this._categoryTemplateLangParamKey = null;
return cite.templateParams(name).then(function(params) {
// A param labeled "koda zimanî" is filled from the language dropdown: found before filtering, so
// it is left out of the fields and emitted by _doSubmit.
(params || []).forEach(function(p) {
if ((p.label || "").trim().toLowerCase() === CATEGORY_TEMPLATE_LANG_LABEL) {
self._categoryTemplateLangParamKey = p.key;
}
});
var filtered = (params || []).filter(function(p) {
return CATEGORY_TEMPLATE_COVERED_PARAMS.indexOf(p.key) === -1 && p.key !== self._categoryTemplateLangParamKey;
});
var list = filtered.length ? filtered : [{ key: "rûpel", label: "Rûpel", required: false }];
list.forEach(function(p) {
// Reuse the jêder-* field's own width and multiline when the key matches one (cite.fieldSize).
var sizeHint = cite.fieldSize(p.key);
var widget = (sizeHint && sizeHint.multiline)
? new OO.ui.MultilineTextInputWidget({ rows: 2, autosize: false })
: new OO.ui.TextInputWidget();
self._jederFieldWidgets[p.key] = widget;
widget.on("change", function() {
self._updateReadyHint();
});
var layout = new OO.ui.FieldLayout(widget, {
label: p.label,
align: "top",
classes: ["add-example-field-span-" + (sizeHint ? sizeHint.span : 4)],
});
self._jederFieldsFieldset.append(layout.$element);
});
self._updateReadyHint();
initManualResize(self._jederFieldsFieldset); // matters now that a matched key like "sernav" can render multiline
});
},
// Fetches data via wikt.citoid, picks the matching jêder-* template, and fills its empty fields.
_fillFromCitoid: function() {
var self = this;
var query = this._sourceURLInput.getValue().trim();
if (!query) {
mw.notify(msg.citoidNeedsUrl);
return;
}
this._citoidButton.setDisabled(true);
this._loadingImage.toggle(true);
cite.fetchFromCitoid(query).then(function(result) {
// Citoid may pick the type only while none is chosen; a hand-picked one is kept.
var wasAutoDetect = self._currentJederKey === "citoid";
if (wasAutoDetect) {
var menu = self._sourceTypeDropdown.getMenu();
var item = menu.findItemFromData(result.jederKey);
if (item) {
menu.chooseItem(item); // this calls _setSourceType(jederKey) automatically
}
}
var targetJederKey = self._currentJederKey;
// Citoid shaped its fields for the type it detected; re-derive them when the active template
// differs.
var fields = targetJederKey === result.jederKey ? result.fields
: cite.citoidFieldsFor(targetJederKey, result.raw, query);
Object.keys(fields).forEach(function(key) {
var widget = self._jederFieldWidgets[key];
if (widget && !widget.getValue()) {
widget.setValue(fields[key]);
}
});
self._citoidSuccessMsg.show();
self._updateReadyHint();
mw.notify("Malûmat ji Citoid hatin barkirin.");
}, function(errorMsg) {
// Citoid cannot fetch some sites (x.com), so fall back to jêder-websîte with a guessed name.
var fallbackSite = wikt.citoid.websiteNameForUrl(query);
if (fallbackSite) {
if (self._currentJederKey !== "jêder-websîte") {
var menu = self._sourceTypeDropdown.getMenu();
var item = menu.findItemFromData("jêder-websîte");
if (item) {
menu.chooseItem(item); // this calls _setSourceType("jêder-websîte") automatically
}
}
var websiteWidget = self._jederFieldWidgets["websîte"];
if (websiteWidget && !websiteWidget.getValue()) {
websiteWidget.setValue(fallbackSite);
}
mw.notify("Citoid nikare vê malperê bixwîne; şablona Websîte hate hilbijartin, ji kerema xwe mayî bi destan tijî bike.");
return;
}
mw.notify(typeof errorMsg === "string" ? errorMsg : "Nikare malûmatan ji Citoid bistîne. Kontrol bike ka URL/DOI/ISBN rast e.");
}).always(function() {
self._citoidButton.setDisabled(false);
self._loadingImage.toggle(false);
});
},
// Shows or hides the form and restores any saved draft. The title takes the button's place while
// open; the button stays put for when it closes.
isVisible: function() {
return this._frame.isVisible();
},
setVisible: function(visible) {
this._$button.toggle(!visible);
this._$title.toggle(visible);
this._frame.toggle(visible);
// Closing from the form's own "Betal bike" has to take the row away too, or the empty <li>
// it lives in is left behind as a bullet with nothing after it.
if (!visible && typeof this.onHide === "function") {
this.onHide();
}
if (visible) {
// Says what the button will actually do: publish now, or join whoever has taken the save
// over. Set here rather than at build time, since the owner may install itself later.
this._applyButton.setLabel(wikt.addExamples.submitHandler ? msg.add : "Qeyd bike");
alignFormToPage(this._frame.$element);
// A draft belongs to a new example; an edited one opens with what its line says.
var draft = this._editing ? {} : (draftStorage.Get() || {});
if (!this._textInput.getValue() && draft.text) {
this._textInput.setValue(draft.text);
}
if (!this._sourceInput.getValue() && draft.source) {
this._sourceInput.setValue(draft.source);
}
if (!this._sourceURLInput.getValue() && draft.sourceURL) {
this._sourceURLInput.setValue(draft.sourceURL);
}
if (!this._translationInput.getValue() && draft.translation) {
this._translationInput.setValue(draft.translation);
}
if (!this._transcriptionInput.getValue() && draft.transcription) {
this._transcriptionInput.setValue(draft.transcription);
}
}
},
// Opens the form on the index-th example of this sense. drawnText is what the page shows for it: the
// line found at that place has to say the same before anything from it is put in the form.
beginEdit: function(index, drawnText) {
var self = this;
this.clear();
this._editing = { index: index, original: null, example: null, key: null };
if (this._addTitle === null) {
this._addTitle = this._$title.text();
}
this._$title.text("Vê nimûneyê diguherînî.");
this._frame.$element.prepend(this._$title);
this._deleteButton.toggle(true);
this.setVisible(true);
this._applyButton.setDisabled(true);
this._deleteButton.setDisabled(true);
this._loadingImage.toggle(true);
var sectionKeyParts = this._definitionLevel[0].split("|");
api.get({
action: "query",
prop: "revisions",
titles: mw.config.get("wgPageName"),
rvprop: "content",
rvslots: "main",
formatversion: 2
}).then(function(data) {
var editing = self._editing;
if (!editing || editing.index !== index || editing.original !== null) {
return; // closed, or opened on another example, while this was loading
}
var content = data.query.pages[0].revisions[0].slots.main.content;
// The page as it will read once the queued changes are saved, so an example already edited
// in the queue is found as it is drawn.
if (typeof wikt.addExamples.pendingContent === "function") {
content = wikt.addExamples.pendingContent(content);
}
var loc = wt.locateSection(content, self._language, sectionKeyParts[0], parseInt(sectionKeyParts[1], 10));
var def = wt.findDefinitionLine(loc.lines, loc.posLineIndex, self._definitionLevel);
var block = cite.exampleBlocks(loc.lines, def).filter(function(b) {
return b.example;
})[index];
if (!block || !cite.exampleMatches(drawnText, block.example)) {
throw new edit.util.SubmitAbort("Ev nimûne di wîkîtextê de nehat dîtin — rûpelê nû bike û dîsa biceribîne.");
}
editing.original = block.text;
editing.example = block.example;
return self._fill(block.example).then(function(aliasMap) {
editing.aliases = aliasMap;
editing.key = self._currentJederKey;
self._syncTranslationBox();
self._deleteButton.setDisabled(false);
self._loadingImage.toggle(false);
});
}).catch(function(err) {
self._loadingImage.toggle(false);
mw.notify(err instanceof edit.util.SubmitAbort ? err.message : "Nikare muhtewaya heyî bixwîne.", { type: "error" });
self.setVisible(false);
self.endEdit();
});
},
// Leaves edit mode: the next time the form opens, it adds a new example again.
endEdit: function() {
if (!this._editing) {
return;
}
this._editing = null;
this._deleteButton.toggle(false);
if (this._addTitle !== null) {
this._$title.text(this._addTitle);
}
this._$titleHome.append(this._$title);
this.clear();
},
// Puts a line read by cite.parseCitationLine() into the form. A plain italic line and every
// {{nimûne}} redirect read as {{nimûne}}, so saving writes them as that. Resolves once the fields
// that depend on the template's own parameters -- a Kategorî:Çavkanî template's boxes, an alias
// such as "r=" for "rûpel=" -- are drawn and filled.
_fill: function(example) {
var self = this;
var key = example.mode === "jeder" ? example.template
: example.mode === "category" ? CATEGORY_TEMPLATE_KEY
: example.mode === "raw" ? RAW_TEMPLATE_KEY
: "";
this._sourceTypeDropdown.getMenu().selectItemByData(key);
this._setSourceType(key);
this._sourceInput.setValue(example.source);
this._sourceURLInput.setValue(example.sourceURL);
this._transcriptionInput.setValue(example.transcription);
var drawn = example.mode === "category"
? this._selectCategoryTemplate({ label: example.template, target: example.template })
: $.Deferred().resolve().promise();
var aliasTemplate = example.mode === "jeder" || example.mode === "category" ? example.template
: example.mode === "nimune" ? "nimûne"
: example.mode === "source" ? "jêder"
: null;
var aliases = aliasTemplate ? cite.templateAliases(aliasTemplate) : $.Deferred().resolve({}).promise();
return $.when(drawn, aliases).then(function(_, aliasMap) {
cite.applyAliases(example, aliasMap || {}, Object.keys(self._jederFieldWidgets));
// buildCitation() writes the language parameter itself.
var langKey = self._categoryTemplateLangParamKey;
if (langKey) {
example.extra = example.extra.filter(function(param) {
return $.trim(param.split("=")[0]) !== langKey;
});
}
self._textInput.setValue(example.text);
self._translationInput.setValue(example.translation);
Object.keys(example.fields).forEach(function(field) {
self._jederFieldWidgets[field].setValue(example.fields[field]);
});
// A box the form leaves out for this language still opens when the line already has something in it.
[[example.translation, self._translationInputLayout], [example.transcription, self._transcriptionInputLayout]]
.forEach(function(pair) {
if (pair[0] && !$.contains(self._$topLeft[0], pair[1].$element[0])) {
self._$topLeft.append(pair[1].$element);
}
});
// Kept for the save, which puts the rebuilt line's parameters back in the page's order.
return aliasMap || {};
});
},
// Removes the example being edited, through the same hand-over as a save.
_submitDelete: function() {
var self = this;
function settle() {
self._deleteButton.setDisabled(false);
self._applyButton.setDisabled(!self._textInput.getValue());
}
this._applyButton.setDisabled(true);
this._deleteButton.setDisabled(true);
this._sendReplacement(null, "−nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION), settle, function(message) {
mw.notify(message || msg.saveFailed, { type: "error" });
settle();
});
},
// Replaces the example being edited with code, or removes it when code is null: queued with
// DefSideBoxes when it has taken the save over, published at once otherwise.
_sendReplacement: function(code, summary, reenable, error) {
var self = this;
var editing = this._editing;
var parts = this._definitionLevel[0].split("|");
var language = this._language;
var definitionLevel = this._definitionLevel;
function transform(revision) {
return {
text: cite.replaceExample(revision.content, language, parts[0], parseInt(parts[1], 10),
definitionLevel, editing.index, editing.original, code),
summary: summary
};
}
var handOver = wikt.addExamples.submitHandler;
if (typeof handOver === "function") {
handOver({
transform: transform,
summary: summary,
// No key of its own: an example edited twice is two entries, applied in turn, the
// second reading the line as the first left it.
done: function() {
self.setVisible(false);
self.endEdit();
reenable();
},
fail: function(message) {
error(message || "Nikare li rêza guhartinan zêde bike.");
}
});
return;
}
api.edit(mw.config.get("wgPageName"), transform).then(function() {
location.reload();
}, function(codeOrAbort, result) {
error(codeOrAbort instanceof edit.util.SubmitAbort ? codeOrAbort.message
: msg.saveFailed + apiErrorMessage(codeOrAbort, result));
});
},
// Drops the saved draft so it cannot resurface on another page.
_clearDraft: function() {
draftStorage.Set({});
},
// Clears all fields in this form.
clear: function() {
this._textInput.setValue("");
this._sourceInput.setValue("");
this._sourceURLInput.setValue("");
this._translationInput.setValue("");
this._transcriptionInput.setValue("");
this._citoidSuccessMsg.hide();
this._sourceTypeDropdown.getMenu().selectItemByData("citoid");
this._setSourceType("citoid");
},
// Warns when a Citoid URL was typed but never fetched, then defers to _doSubmit. A name that
// cannot be bolded is reported under the text box instead, never blocking.
submit: function() {
if (this._citoidPending() && !window.confirm("Te lînk, DOI an ISBN nivîsand lê hîn jê malûmat nehatine standin. Bêyî wan berdewam bikî?")) {
return;
}
this._doSubmit();
},
// Generates and submits the wikicode, then inserts the resulting HTML element if no errors occurred.
_doSubmit: function() {
var self = this;
self._textInput.setDisabled(true);
self._sourceInput.setDisabled(true);
self._sourceURLInput.setDisabled(true);
self._translationInput.setDisabled(true);
self._transcriptionInput.setDisabled(true);
self._sourceTypeDropdown.setDisabled(true);
Object.keys(self._jederFieldWidgets).forEach(function(key) {
self._jederFieldWidgets[key].setDisabled(true);
});
// self._disableTranslationChk.setDisabled(true);
this._applyButton.setDisabled(true);
this._clearButton.setDisabled(true);
this._loadingImage.toggle(true);
var source = this._sourceInput.getValue().trim();
var sourceURL = this._sourceURLInput.getValue().trim();
var text = this._textInput.getValue().trim();
// The filled-in fields of the selected template. buildCitation() adds the language parameter and
// the "Ç:" flag itself.
var jederFields = {};
var usingCategoryTemplate = this._currentJederKey === CATEGORY_TEMPLATE_KEY && !!this._categoryTemplateName;
if (usingCategoryTemplate) {
Object.keys(self._jederFieldWidgets).forEach(function(key) {
// A raw linebreak would break the single-line "#*" wikitext just like in the example text,
// but these are plain param values (not display text), so collapse to a space, not <br>.
var value = nlToSpace(self._jederFieldWidgets[key].getValue().trim());
if (value) {
jederFields[key] = value;
}
});
} else if (this._currentJederKey && jederDefs[this._currentJederKey]) {
jederDefs[this._currentJederKey].fields.forEach(function(field) {
var widget = self._jederFieldWidgets[field.key];
var value = widget ? nlToSpace(widget.getValue().trim()) : "";
if (value) {
jederFields[field.key] = value;
}
});
cite.withLinkAsUrl(this._currentJederKey, jederFields, sourceURL);
}
// A Kategorî:Çavkanî template is used as soon as it's picked, even with every field left blank
// (still submittable as a bare jêgirtin=); a jêder-* type only counts once a field is actually filled.
var usingJederTemplate = usingCategoryTemplate || (this._currentJederKey && Object.keys(jederFields).length > 0);
var usingRawTemplate = this._currentJederKey === RAW_TEMPLATE_KEY;
var minakAnNa, sablon;
if (usingRawTemplate) {
// A citation, so "#*" like the others -- the line itself is built below, not assembled here.
minakAnNa = "*";
sablon = "";
} else if (usingJederTemplate) {
// The jêder-*/Kategorî:Çavkanî template itself is used as the template, not as the "2=" content.
minakAnNa = "*";
sablon = usingCategoryTemplate ? this._categoryTemplateName : this._currentJederKey;
} else if (source || sourceURL) {
minakAnNa = "*";
sablon = "jêder";
} else {
minakAnNa = ":";
sablon = "nimûne";
}
var listMarker = "".padStart(this._definitionLevel.length - 1, "#") + minakAnNa;
var title = mw.config.get('wgTitle');
// One builder for every citation line here and in the wizard. Bolding the entry name, the language
// parameter, the "Ç:" flag and <br> conversion all belong to it.
var code = cite.buildCitation({
mode: usingRawTemplate ? "raw"
: usingCategoryTemplate ? "category"
: usingJederTemplate ? "jeder"
: "plain",
marker: listMarker,
language: this._language,
title: title,
template: usingCategoryTemplate ? this._categoryTemplateName : this._currentJederKey,
fields: jederFields,
langParamKey: this._categoryTemplateLangParamKey,
quote: text,
source: source,
sourceURL: sourceURL,
translation: this._translationInput.getValue().trim(),
transcription: this._transcriptionInput.getValue().trim(),
// An edited line keeps its translation, and every parameter the form has no box for as long
// as it is still the same kind of line.
keepTranslation: !!this._editing,
extra: this._editing && this._editing.key === this._currentJederKey ? this._editing.example.extra : []
});
// definitionLevel[0] is "type|ordinal": the section's position among same-type headings within its
// language, resolved client-side, never MediaWiki's page-wide DOM id number.
var sectionKeyParts = this._definitionLevel[0].split("|");
var sectionType = sectionKeyParts[0];
var sectionOrdinal = parseInt(sectionKeyParts[1], 10);
if (this._editing) {
// A citation written out by hand is saved exactly as typed, its list marker included.
// Otherwise the rebuilt line keeps the page's own parameter order.
var replacement = usingRawTemplate
? (/^#/.test(text) ? text : this._editing.original.match(/^#+[:*]/)[0] + " " + text)
: cite.keepOrder(code, this._editing.original, this._editing.aliases);
this._sendReplacement(replacement, "±nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION), reenable, error);
return;
}
// Read by the success handler below, which is a sibling of the transform, not nested in it.
var emptyTemplate = false;
// Thrown from inside an edit() transform to bail out with a specific message.
var SubmitAbort = edit.util.SubmitAbort;
// api.edit() fetches the revision it will conflict-check against and hands it here, so the
// insertion runs on exactly that content. Named, so another gadget can run it instead.
function buildEdit(revision) {
var pageContent = revision.content;
// Said here rather than left to the locator: which sections this tool does not work in is
// this tool's own business, and the locator only knows that the key names no section.
if (!sections.headingNamesFor(sectionType)) {
throw new SubmitAbort('Navê beşê xelet e. Di beşên "Ji wêjêyê" û "Ji wêjêya klasîk" ev amûr nayê bikaranîn.');
}
// loc.lines covers the rest of the page from this language's heading on: the saved page is rebuilt
// from it below (see "text:").
var loc = wt.locateSection(pageContent, self._language, sectionType, sectionOrdinal);
var langSectionIndex = loc.langSectionIndex;
var lines = loc.lines;
var targetLineIndex = wt.findDefinitionLine(lines, loc.posLineIndex, self._definitionLevel);
// Where to insert inside this definition's "#:"/"#*" block. findBlockEnd() keeps multi-line
// templates whole; exampleTemplateNames tells a real example from an annotation.
var insertionFound = false;
var lastRelevantBlockEnd = -1;
var isJederFamily = (sablon === "jêder" || Object.prototype.hasOwnProperty.call(jederDefs, sablon));
var scanIndex = targetLineIndex + 1;
// Read off the definition line, so a sub-sense's examples are matched at its own depth and
// a deeper "#" line ends the block instead of being skipped past.
var annotation = annotationRegex(lines[targetLineIndex]);
var citation = citationRegex(lines[targetLineIndex]);
for (; scanIndex < lines.length; scanIndex++) {
var scanLine = lines[scanIndex];
if (!annotation.test(scanLine)) {
// Left this definition's own block (a sub-sense, a new definition, a new section).
break;
}
var blockEnd = findBlockEnd(lines, scanIndex);
var templateName = getLineTemplateName(scanLine);
var isExampleLine = templateName !== null &&
exampleTemplateNames.indexOf(templateName) !== -1;
if (isJederFamily) {
// A jêder-family example goes after every "#:"/"#*" line of this definition.
lastRelevantBlockEnd = blockEnd;
insertionFound = true;
} else if (sablon === "nimûne") {
// "nimûne" examples go before any "#*" line, but after "#:" lines.
if (citation.test(scanLine)) {
break;
}
lastRelevantBlockEnd = blockEnd;
insertionFound = true;
}
scanIndex = blockEnd; // Skip past the whole (possibly multi-line) block
}
var insertAt = lastRelevantBlockEnd >= 0 ? lastRelevantBlockEnd + 1 : scanIndex;
// After the relevant block, or straight after the definition when it had no "#:"/"#*" lines.
lines.splice(insertAt, 0, code);
// The "no example yet" marker goes once a real one lands. The whole "#:"/"#*" block is swept, not
// just its last line: the marker is often followed by other annotations.
var sweepFrom = targetLineIndex + 1;
for (var sweep = sweepFrom; sweep < lines.length; sweep++) {
if (!annotation.test(lines[sweep])) {
break;
}
if (EMPTY_EXAMPLE_MARKER.test(lines[sweep])) {
lines.splice(sweep, 1);
emptyTemplate = true; // tells the DOM update below to replace the marker's <li>
sweep--;
}
}
return {
text: pageContent.slice(0, langSectionIndex) + lines.join("\n"),
summary: "+nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION),
};
}
// DefSideBoxes takes the save over when its panel opened this form: the edit joins that gadget's
// queue instead of publishing now.
var handOver = wikt.addExamples.submitHandler;
if (typeof handOver === "function") {
handOver({
transform: buildEdit,
summary: "+nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION),
done: function() {
self.setVisible(false);
self.clear();
self._clearDraft();
reenable();
},
fail: function(message) {
error(message || "Nikare li rêza guhartinan zêde bike.");
}
});
return;
}
api.edit(mw.config.get("wgPageName"), buildEdit).then(function() {
api.parse(code).done(function(data) {
var $renderedExample = $(data).find("ul > li").html();
var $item;
// Insert rendered example into page
if (emptyTemplate) {
self._$lastExample.html($renderedExample);
$item = self._$lastExample;
} else {
self._$lastExample.after($item = $("<li>").append($renderedExample));
}
$item.css("background-color", "lightgreen");
setTimeout(function() {
$item.css("background-color", "inherit");
}, 1000);
self._$lastExample = $item;
});
self.setVisible(false);
self.clear();
self._clearDraft();
reenable();
}, function(codeOrAbort, result) {
if (codeOrAbort instanceof SubmitAbort) {
error(codeOrAbort.message);
} else {
error(msg.saveFailed + apiErrorMessage(codeOrAbort, result));
}
});
function error(errorMessage) {
mw.notify(errorMessage || "Nimûne nehat weşandin, îhtimalen rûpel hate guhartin. Ji kerema xwe re rûpelê dîsa bar bike û dîsa biceribîne.");
// A draft is kept for a new example only: an edited one would come back on the next new one.
if (!self._editing) {
draftStorage.Set({
text: self._textInput.getValue(),
source: self._sourceInput.getValue(),
sourceURL: self._sourceURLInput.getValue(),
translation: self._translationInput.getValue(),
transcription: self._transcriptionInput.getValue(),
});
}
reenable();
}
function reenable() {
self._textInput.setDisabled(false);
self._sourceInput.setDisabled(false);
self._sourceURLInput.setDisabled(false);
self._translationInput.setDisabled(false);
self._transcriptionInput.setDisabled(false);
self._sourceTypeDropdown.setDisabled(false);
Object.keys(self._jederFieldWidgets).forEach(function(key) {
self._jederFieldWidgets[key].setDisabled(false);
});
// self._disableTranslationChk.setDisabled(false);
self._applyButton.setDisabled(false);
self._cancelButton.setDisabled(false);
self._clearButton.setDisabled(false);
self._loadingImage.toggle(false);
}
},
};
});
// </nowiki>
9tx4jr1vgartpb50c1tgks88gpasawj
Modul:parameters/data
828
1324875
6531698
6369002
2026-09-15T18:18:13Z
Wikihez
7344
etymon: crz as the Kurdish name of pos, Kurdish pos values (reh, kelîme, pêşgir, paşgir, navgir), root/word categories in Kurdish (rehê ... yê, kelîmeya ... ya); tested on the ceribandin copy
6531698
Scribunto
text/plain
local list_to_set = require("Module:table").listToSet
local function process_set_param(param)
param.set = list_to_set(param.set)
param.converted_set = true
return param
end
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 m = {}
-- [[Module:anchors]]
m["anchor"] = {
[1] = {required = true, list = true, disallow_holes = true},
}
m["senseid"] = {
[1] = required_lang_default_und,
[2] = required_default_,
tag = process_set_param{set = {"li", "p"}, default = "li"},
}
m["etymid"] = {
[1] = required_lang_default_und,
[2] = required_default_
}
-- [[Module:etymon]]
m["etymon"] = {
[1] = required_lang_default_und,
[2] = {list = true, disallow_holes = true},
id = true,
title = true,
tree = boolean,
text = true,
-- Kurdish names for the same two parameters.
dar = {alias_of = "tree"},
metn = {alias_of = "text"},
exnihilo = boolean,
etydate = true,
doublet = {sublist = "comma without whitespace"},
-- Kurdish name for the same parameter, cf. Şablon:cotreh.
cotreh = {alias_of = "doublet"},
rfe = true,
etystub = true,
nl = boolean,
pos = true,
-- Kurdish name for the same parameter; Modul:etymon maps its Kurdish values (reh, kelîme, ...).
crz = {alias_of = "pos"},
notree = {default = false, type = "boolean"},
nocat = {default = false, type = "boolean"},
nodot = {default = false, type = "boolean"},
dot = true,
json = {default = false, type = "boolean"},
}
-- [[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,
}
m["t-needed"] = {
[1] = required_lang_default_und,
[2] = process_set_param{set = {"usex", "quote"}},
["nocat"] = boolean,
["sort"] = true,
}
m["trans-top"] = {
[1] = true,
["id"] = true,
["column-width"] = true,
}
m["trans-top-also"] = {
[1] = required,
[2] = list,
["id"] = list_allow_holes_separate_no_index,
["column-width"] = true,
}
m["checktrans-top"] = {
[1] = true,
["column-width"] = true,
}
m["trans-bottom"] = empty_list
m["trans-see"] = {
[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
698tqojgwgm6vmgyss5ua5qdnh9630p
Modul:etîmolojî/templates/descendant
828
1325876
6531715
6104752
2026-09-15T18:36:10Z
Wikihez
7344
descendant: hrf as the Kurdish name of lit, like w for t
6531715
Scribunto
text/plain
local export = {}
local concat = table.concat
local insert = table.insert
local listToSet = require("Module:table").listToSet
local b = require("Module:bingeh")
local rsplit = mw.text.split
local descendants_tree_module = "Module:descendants tree"
local labels_module = "Module:labels"
local languages_module = "Module:languages"
local links_module = "Module:links"
local parse_utilities_module = "Module:parse utilities"
local scripts_module = "Module:scripts"
local table_module = "Module:table"
local error_on_no_descendants = false
local function qualifier(content)
if content then
return '<span class="ib-brac qualifier-brac">(</span><span class="ib-content qualifier-content">' .. content .. '</span><span class="ib-brac qualifier-brac">)</span>'
end
end
local function track(page)
return require("Module:debug/track")("descendant/" .. page)
end
local function ine(arg)
if arg == "" then
return nil
else
return arg
end
end
local function add_tooltip(text, tooltip)
return '<span class="desc-arr" title="' .. tooltip .. '">' .. text .. '</span>'
end
local function split_on_comma(term)
if term:find(",%s") then
return require(parse_utilities_module).split_on_comma(term)
else
return rsplit(term, ",")
end
end
-- Params that modify a descendant term (as also supported by {{l}}, {{m}}). Doesn't include gloss=, which we
-- handle specially.
local param_term_mods = {"alt", "g", "id", "lit", "pos", "t", "tr", "ts"}
local param_term_mod_set = listToSet(param_term_mods)
-- Boolean params indicating whether a descendant term (or all terms) are particular sorts of borrowings.
local bortypes = {"mîr", "deyn", "lbor", "slb", "obor", "translît", "ji", "clq", "pclq", "sml", "unc"}
local bortype_set = listToSet(bortypes)
-- Aliases of clq=.
local calque_aliases = {"cal", "calq", "calque", "yekser"}
local calque_alias_set = listToSet(calque_aliases)
-- Aliases of pclq=.
local partial_calque_aliases = {"pcal", "pcalq", "pcalque", "qyekser"}
local partial_calque_alias_set = listToSet(partial_calque_aliases)
-- Miscellaneous list params.
local misc_list_params = {"q", "qq", "lb"}
local misc_list_param_set = listToSet(misc_list_params)
-- Add a "regular" list param such as g=, gloss=, lit=, etc. "Regular" here means that `param` and `param1` are
-- the same thing. `type` if given is the param type (e.g. "boolean") and `alias_of` is used for params that are
-- aliases of other params.
local function add_regular_list_param(params, param, type, alias_of)
local spec = {type = type, list = true}
if alias_of == nil then
spec.allow_holes = true
else
spec.alias_of = alias_of
end
params[param] = spec
end
-- Add an index-separated list param such as bor=, calq=, qq=, etc. "Index-separated" means that `param` and
-- `param1` are different. Non-numbered `param` is accessible as `args.param` while numbered `param1`, `param2`,
-- etc. are accessible as `args.partparam[1]`, `args.partparam[2]`, etc. `type` if given is the param type (e.g.
-- "boolean") and `alias_of` is used for params that are aliases of other params.
local function add_index_separated_list_param(params, param, type, alias_of)
params[param] = {alias_of = alias_of, type = type}
local spec = {type = type, list = param, require_index = true}
if alias_of == nil then
spec.allow_holes = true
else
spec.alias_of = "part" .. alias_of
end
params["part" .. param] = spec
end
-- Convert a raw lb= param (or nil) to a list of label info objects of the format described in get_label_info() in
-- [[Module:labels]]). Unrecognized labels will end up with an unchanged display form. Return nil if nil passed in.
local function split_and_process_raw_labels(raw_lb, lang)
if not raw_lb then
return nil
end
return require(labels_module).split_and_process_raw_labels { labels = raw_lb, lang = lang, nocat = true }
end
-- Return a function of one argument `arg` (a param name), which fetches args[`arg`] if index == 0, else
-- args["part" .. `arg`][index].
local function get_val(args, index)
return function(arg)
if index == 0 then
return args[arg]
else
return args["part" .. arg][index]
end
end
end
-- Return the arrow text for the `index`th term, or the overall arrow text if index == 0.
local function get_arrow(args, index)
local val = get_val(args, index)
local arrow
if val("deyn") then
arrow = add_tooltip("→", "deynkirî")
elseif val("lbor") then
arrow = add_tooltip("→", "learned borrowing")
elseif val("slb") then
arrow = add_tooltip("→", "semi-learned borrowing")
elseif val("obor") then
arrow = add_tooltip("→", "orthographic borrowing")
elseif val("translît") then
arrow = add_tooltip("→", "translîterasyon")
elseif val("clq") then
arrow = add_tooltip("→", "tercimeya yekser")
elseif val("pclq") then
arrow = add_tooltip("→", "qismî tercimeya yekser")
elseif val("sml") then
arrow = add_tooltip("→", "semantic loan")
elseif val("mîr") or (val("unc") and not val("ji")) then
arrow = add_tooltip(">", "mîrasmayî")
else
arrow = ""
end
-- allow der=1 in conjunction with bor=1 to indicate e.g. English "pars recta"
-- derived and borrowed from Latin "pars".
if val("ji") then
arrow = arrow .. add_tooltip("⇒", "reshaped by analogy or addition of morphemes")
end
if val("unc") then
arrow = arrow .. add_tooltip("?", "uncertain")
end
if arrow ~= "" then
arrow = arrow .. " "
end
return arrow
end
-- Return the pre-qualifier text for the `index`th term, or the overall pre-qualifier text if index == 0.
local function get_pre_qualifiers(args, index, lang)
local val = get_val(args, index)
local quals
if index > 0 then
local labels = split_and_process_raw_labels(val("lb"), lang)
if labels then
labels = require(labels_module).format_processed_labels {
labels = labels, lang = lang, no_ib_content = true
}
if labels ~= "" then -- not sure labels can be an empty string but it seems possible in some circumstances
quals = {labels}
end
end
end
if val("q") then
quals = quals or {}
insert(quals, val("q"))
end
if quals then
return require("Module:qualifier").format_qualifier(quals) .. " "
else
return ""
end
end
-- Return the post-qualifier text for the `index`th term, or the overall post-qualifier text if index == 0.
local function get_post_qualifiers(args, index, lang)
local val = get_val(args, index)
local postqs = {}
if val("mîr") then
insert(postqs, qualifier("mîrasmayî"))
end
if val("lbor") then
insert(postqs, qualifier("learned"))
end
if val("slb") then
insert(postqs, qualifier("semi-learned"))
end
if val("translît") then
insert(postqs, qualifier("translîterasyon"))
end
if val("clq") then
insert(postqs, qualifier("tercimeya yekser"))
end
if val("pclq") then
insert(postqs, qualifier("qismî tercimeya yekser"))
end
if val("sml") then
insert(postqs, qualifier("semantic loan"))
end
if val("qq") then
insert(postqs, require("Module:qualifier").format_qualifier(val("qq")))
end
if index == 0 then
local labels = split_and_process_raw_labels(val("lb"), lang)
if labels then
labels = require(labels_module).format_processed_labels {
labels = labels, lang = lang
}
if labels ~= "" then
insert(postqs, "— " .. labels)
end
end
end
if #postqs > 0 then
return " " .. concat(postqs, " ")
end
return ""
end
local function desc_or_desc_tree(frame, desc_tree)
local params
local boolean = {type = "boolean"}
if desc_tree then
params = {
[1] = {required = true, type = "language", family = true, default = "gem-pro"},
[2] = {required = true, list = true, allow_holes = true, default = "*fuhsaz"},
["notext"] = boolean,
["noalts"] = boolean,
["noparent"] = boolean,
}
else
params = {
[1] = {required = true, type = "language", family = true, default = "en"},
[2] = {list = true, allow_holes = true},
["alts"] = boolean
}
-- If template namespace.
if mw.title.getCurrentTitle().namespace == 10 then
params[2].default = "peyv"
end
end
for _, term_mod in ipairs(param_term_mods) do
add_regular_list_param(params, term_mod)
end
-- Handle gloss= specially because it's an alias.
add_regular_list_param(params, "gloss", nil, "t")
add_regular_list_param(params, "tercime", nil, "t")
add_regular_list_param(params, "wate", nil, "t")
add_regular_list_param(params, "w", nil, "t")
--cinsiyet
add_regular_list_param(params, "z", nil, "g")
add_regular_list_param(params, "c", nil, "g")
add_regular_list_param(params, "cuda", nil, "alt")
add_regular_list_param(params, "hrf", nil, "lit")
-- Handle sc= specially because the type is "script".
add_regular_list_param(params, "sc", "script")
for _, bortype in ipairs(bortypes) do
add_index_separated_list_param(params, bortype, "boolean")
end
for _, calque_alias in ipairs(calque_aliases) do
add_index_separated_list_param(params, calque_alias, "boolean", "clq")
end
for _, partial_calque_alias in ipairs(partial_calque_aliases) do
add_index_separated_list_param(params, partial_calque_alias, "boolean", "pclq")
end
for _, misc_list_param in ipairs(misc_list_params) do
add_index_separated_list_param(params, misc_list_param)
end
-- Add other single params.
params.sclang = boolean
params.sclb = {type = "boolean", alias_of = "sclang"}
params.nolang = boolean
params.nolb = {type = "boolean", alias_of = "nolang"}
local namespace = mw.title.getCurrentTitle().nsText
local parent_args
if frame.args[1] then
parent_args = frame.args
else
parent_args = frame:getParent().args
end
-- FIXME: Temporary error message.
for arg, _ in pairs(parent_args) do
if type(arg) == "string" and arg:find("^tag[0-9]*$") then
local lbarg = arg:gsub("^tag", "lb")
error(("Use %s= instead of %s="):format(lbarg, arg))
end
end
-- Error to catch most uses of old-style parameters.
if ine(parent_args[4]) and not ine(parent_args[3]) and not ine(parent_args.tr2) and not ine(parent_args.ts2)
and not ine(parent_args.t2) and not ine(parent_args.gloss2) and not ine(parent_args.g2)
and not ine(parent_args.alt2) then
error("You specified a term in 4= and not one in 3=. You probably meant to use t= to specify a gloss instead. "
.. "If you intended to specify two terms, put the second term in 3=.")
end
if not ine(parent_args[3]) and not ine(parent_args.alt2) and not ine(parent_args.tr2) and not ine(parent_args.ts2)
and ine(parent_args.g2) then
error("You specified a gender in g2= but no term in 3=. You were probably trying to specify two genders for "
.. "a single term. To do that, put both genders in g=, comma-separated.")
end
local args = require("Module:parameters").process(parent_args, params)
local lang = args[1]
local terms = args[2]
local alts = args.alt
if (namespace == "" or namespace == "Reconstruction") and (lang:hasType("appendix-constructed") and not lang:hasType("regular")) then
error("Terms in appendix-only constructed languages may not be given as descendants.")
end
local fetch_alt_forms = desc_tree and not args.noalts or not desc_tree and args.alts
local m_desctree
if desc_tree or fetch_alt_forms then
m_desctree = require(descendants_tree_module)
end
if lang:getCode() ~= lang:getFullCode() then
-- [[Special:WhatLinksHere/Wiktionary:Tracking/descendant/etymological]]
track("etymological")
track("etymological/" .. lang:getCode())
end
local is_family = lang:hasType("family")
local proxy_lang
if is_family then
-- [[Special:WhatLinksHere/Wiktionary:Tracking/descendant/family]]
track("family")
track("family/" .. lang:getCode())
proxy_lang = require(languages_module).getByCode("und")
else
proxy_lang = lang
end
local languageName
if is_family then
-- The display form for families includes the word "languages", which we probably don't want to
-- display.
languageName = lang:getCanonicalName()
else
languageName = lang:getCanonicalName()
end
languageName = b.ucfirst(languageName)
local langtag
if args.sclang then
local sc = args.sc[1]
if sc then
langtag = sc:getCategoryName()
else
local term, alt = terms[1], alts[1]
local best_sc
if is_family then
best_sc = require(scripts_module).findBestScriptWithoutLang(term or alt, "none is last resort")
else
best_sc = lang:findBestScript(term or alt)
end
langtag = best_sc:getCategoryName()
end
else
langtag = languageName
end
-- Find the maximum index among any of the list parameters.
local maxmaxindex = terms.maxindex
for k, v in pairs(args) do
if type(v) == "table" and v.maxindex and v.maxindex > maxmaxindex then
maxmaxindex = v.maxindex
end
end
local parts = {}
local terms_for_descendant_trees = {}
-- Keep track of descendants whose descendant tree we fetch. Don't fetch the same descendant tree twice (which
-- can happen especially with Arabic-script terms with the same unvocalized spelling but differing vocalization).
-- This happens e.g. with Ottoman Turkish [[پورتقال]], which has {{desctree|fa-cls|پُرْتُقَال|پُرْتِقَال|bor=1}}, with
-- two terms that have the same unvocalized spelling.
local terms_and_ids_fetched = {}
local descendant_terms_seen = {}
local use_semicolon = false
local ind = 0
for i = 1, maxmaxindex do
local term = terms[i]
if term ~= ";" then
ind = ind + 1
local alt = args.alt[ind]
local id = args.id[ind]
local sc = args.sc[ind]
local tr = args.tr[ind]
local ts = args.ts[ind]
local gloss = args.t[ind]
local pos = args.pos[ind]
local lit = args.lit[ind]
local g = args.g[ind] and rsplit(args.g[ind], "%s*,%s*") or {}
local link
local terms_for_alt_forms = {}
local termobj = {
lang = proxy_lang,
}
-- Initialize `termobj` with indexed modifier params such as t1, t2, etc. and alt1, alt2, etc. Inline
-- modifiers specified using the <...> notation override these.
local function reinit_termobj(term)
termobj.term = term
termobj.sc = sc
termobj.track_sc = true
termobj.term = term
termobj.alt = alt
termobj.id = id
termobj.tr = tr
termobj.ts = ts
termobj.genders = g
termobj.gloss = gloss
termobj.pos = pos
termobj.lit = lit
end
-- Construct a link out of `termobj`. Also add the term to the list of descendant trees and/or alternative
-- forms to fetch, if the page+ID combination hasn't already been seen.
local function get_link()
local link = ""
-- If an individual term has a literal comma in it, use semicolons for all joiners. Otherwise we use
-- semicolon only if the user specified a literal semicolon as a term.
if termobj.term and termobj.term:find(",") then
use_semicolon = true
end
if termobj.term ~= "-" then -- including term == nil
link = require("Module:links").full_link(termobj, nil, true)
if termobj.term and (desc_tree or fetch_alt_forms) then
local entry_name = require(links_module).get_link_page(termobj.term, lang, sc)
-- NOTE: We use the term and ID as the key, but not the language. This is OK currently because
-- all terms have the same language; but if we ever add support for a term-specific language,
-- we need to fix this.
local term_and_id = termobj.id and entry_name .. "!!!" .. termobj.id or entry_name
if not terms_and_ids_fetched[term_and_id] then
terms_and_ids_fetched[term_and_id] = true
local term_for_fetching = {
lang = lang, entry_name = entry_name, id = termobj.id
}
if desc_tree then
if is_family then
error("No support currently (and probably ever) for fetching a descendant tree when a family code instead of language code is given")
end
if error_on_no_descendants then
require(table_module).insertIfNot(descendant_terms_seen,
{ term = termobj.term, id = termobj.id })
end
table.insert(terms_for_descendant_trees, term_for_fetching)
end
if fetch_alt_forms then
if is_family then
error("No support currently (and probably ever) for fetching alternative forms when a family code instead of language code is given")
end
-- [[Special:WhatLinksHere/Wiktionary:Tracking/descendant/alts]]
track("alts")
table.insert(terms_for_alt_forms, term_for_fetching)
end
end
end
elseif termobj.ts or termobj.gloss or #termobj.genders > 0 then
-- [[Special:WhatLinksHere/Wiktionary:Tracking/descendant/no term]]
track("no term")
termobj.term = nil
link = require("Module:links").full_link(termobj, nil, true)
link = link
:gsub("<small>%[Peyv%?%]</small> ", "")
:gsub("<small>%[Peyv%?%]</small> ", "")
:gsub("%[%[Category:Peyvên ku skrîpt hewce ye ([^%[%]]+)%]%]", "")
else -- display no link at all
-- [[Special:WhatLinksHere/Wiktionary:Tracking/descendant/no term or annotations]]
track("no term or annotations")
end
return link
end
-- Check for inline modifier, e.g. מרים<tr:Miryem>. But exclude HTML entry with <span ...>, <i ...>,
-- <br/> or similar in it, caused by wrapping an argument in {{l|...}}, {{af|...}} or similar.
if term and term:find("<") and not require(parse_utilities_module).term_contains_top_level_html(term) then
local run = require(parse_utilities_module).parse_balanced_segment_run(term, "<", ">")
-- Split the non-modifier parts of an alternating run on comma, but not on comma+whitespace.
local comma_separated_runs = require(parse_utilities_module).split_alternating_runs_on_comma(run)
local sub_links = {}
local function parse_err(msg)
local parts = {}
for _, run in ipairs(comma_separated_runs) do
insert(parts, concat(run))
end
error(msg .. ": " .. (i + 1) .. "=" .. concat(parts, ","))
end
for j, run in ipairs(comma_separated_runs) do
reinit_termobj(run[1])
local seen_mods = {}
for k = 2, #run - 1, 2 do
if run[k + 1] ~= "" then
parse_err("Extraneous text '" .. run[k + 1] .. "' after modifier")
end
local modtext = run[k]:match("^<(.*)>$")
if not modtext then
parse_err("Internal error: Modifier '" .. modtext .. "' isn't surrounded by angle brackets")
end
local prefix, arg = modtext:match("^(%l+):(.*)$")
if prefix then
if seen_mods[prefix] then
parse_err("Modifier '" .. prefix .. "' occurs twice, second occurrence " .. run[k])
end
seen_mods[prefix] = true
if prefix == "t" or prefix == "gloss" then
termobj.gloss = arg
elseif prefix == "g" then
termobj.genders = rsplit(arg, "%s*,%s*")
elseif prefix == "sc" then
termobj.sc = arg
elseif param_term_mod_set[prefix] then
termobj[prefix] = arg
elseif misc_list_param_set[prefix] then
if j < #comma_separated_runs then
parse_err("Modifier " .. run[k] .. " should come after the last term")
end
args["part" .. prefix][ind] = arg
elseif prefix == "tag" then
-- FIXME: Remove support for <tag:...> in favor of <lb:...>
error("Use <lb:...> instead of <tag:...>")
else
parse_err("Unrecognized prefix '" .. prefix .. "' in modifier " .. run[k])
end
elseif j < #comma_separated_runs then
parse_err("Modifier " .. run[k] .. " should come after the last term")
else
if seen_mods[modtext] then
parse_err("Modifier '" .. modtext .. "' occurs twice")
end
seen_mods[modtext] = true
if bortype_set[modtext] then
args["part" .. modtext][ind] = true
elseif calque_alias_set[modtext] then
args.partclq[ind] = true
elseif partial_calque_alias_set[modtext] then
args.partpclq[ind] = true
else
parse_err("Unrecognized modifier '" .. modtext .. "'")
end
end
end
local sub_link = get_link()
if sub_link ~= "" then
insert(sub_links, sub_link)
end
end
link = concat(sub_links, "/")
elseif term and term:find(",") then
local sub_terms = split_on_comma(term)
local sub_links = {}
for _, sub_term in ipairs(sub_terms) do
reinit_termobj(sub_term)
local sub_link = get_link()
if sub_link ~= "" then
insert(sub_links, sub_link)
end
end
link = concat(sub_links, "/")
else
reinit_termobj(term)
link = get_link()
end
local arrow = get_arrow(args, ind)
local preqs = get_pre_qualifiers(args, ind, proxy_lang)
local postqs = get_post_qualifiers(args, ind, proxy_lang)
insert(parts, {
arrow = arrow, preqs = preqs, link = link, terms_for_alt_forms = terms_for_alt_forms, postqs = postqs,
use_semicolon = terms[i - 1] == ";"
})
end
end
local descendant_trees = {}
for _, descterm in ipairs(terms_for_descendant_trees) do
-- When I ([[User:Benwing2]]) first implemented this in Nov 2020, I had `maxmaxindex > 1` as the last argument.
-- Since then, [[User:Fytcha]] changed the last param to `true`.
local descendant_tree = m_desctree.get_descendants(descterm.lang, descterm.entry_name, descterm.id, true)
if descendant_tree and descendant_tree ~= "" then
insert(descendant_trees, descendant_tree)
end
end
if error_on_no_descendants and desc_tree and not descendant_trees[1] then
local function format_term_seen(term_seen)
if term_seen.id then
return ("[[%s]] with ID '%s'"):format(term_seen.term, term_seen.id)
else
return ("[[%s]]"):format(term_seen.term)
end
end
if #descendant_terms_seen == 0 then
error("[[Template:desctree]] invoked but no terms to retrieve descendants from")
elseif #descendant_terms_seen == 1 then
error(("No Descendants section was found in the entry %s under the header for %s"):format(
format_term_seen(descendant_terms_seen[1]), lang:getFullName()))
else
for i, term_seen in ipairs(descendant_terms_seen) do
descendant_terms_seen[i] = format_term_seen(term_seen)
end
error(("No Descendants section was found in any of the entries %s under the header for %s"):format(
concat(descendant_terms_seen, ", "), lang:getFullName()))
end
end
local descendants = concat(descendant_trees)
if args.noparent then
return descendants
end
local initial_arrow = get_arrow(args, 0)
local initial_preqs = get_pre_qualifiers(args, 0, proxy_lang)
local final_postqs = get_post_qualifiers(args, 0, proxy_lang)
-- Now format each part. We wait to do this because we may not know the separator (semicolon or comma) till now.
for i, part in ipairs(parts) do
local partparts = {}
local function ins(text)
insert(partparts, text)
end
if not args.notext then
ins(part.arrow)
end
ins(part.preqs)
ins(part.link)
for _, altterm in ipairs(part.terms_for_alt_forms) do
local altform = m_desctree.get_alternative_forms(altterm.lang, altterm.entry_name, altterm.id,
use_semicolon and "; " or ", ")
if altform ~= "" then
ins(use_semicolon and "; " or ", ")
ins(altform)
end
end
ins(part.postqs)
local parttext = concat(partparts)
if i > 1 and parttext ~= "" then
parttext = ((use_semicolon or part.use_semicolon) and "; " or ", ") .. parttext
end
parts[i] = parttext
end
local all_linktext = initial_preqs .. concat(parts) .. final_postqs .. descendants
if args.notext then
return all_linktext
elseif args.nolang then
return initial_arrow .. all_linktext
else
return concat { initial_arrow, langtag, ":", all_linktext ~= "" and " " or "", all_linktext }
end
end
function export.descendant(frame)
return desc_or_desc_tree(frame, false) .. require("Module:TemplateStyles")("Module:etîmolojî/stîl.css")
end
function export.descendants_tree(frame)
return desc_or_desc_tree(frame, true)
end
return export
dqh5vqamur64t4x4r1bklcze0m4w2e6
Modul:etymon
828
1326097
6531695
6433605
2026-09-15T18:17:33Z
Wikihez
7344
etymon: crz as the Kurdish name of pos, Kurdish pos values (reh, kelîme, pêşgir, paşgir, navgir), root/word categories in Kurdish (rehê ... yê, kelîmeya ... ya); tested on the ceribandin copy
6531695
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
- [[Module:etymon/tracking]]: tracking
]=]
local export = {}
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,
linked_page_partial_etymology_sections = false,
partial_etymology_targets = {},
skip_partial_etymology_category = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
id_stats = nil,
warnings = {},
}
local function reset_invocation_state()
__state.current_page_has_inline_etymology = false
__state.current_page_has_redundant_etymology = false
__state.used_idless_etymon = false
__state.toplevel_has_inline_etymology = false
__state.toplevel_redundant_etymology = false
__state.toplevel_idless_etymon = false
__state.has_mismatched_id = false
__state.linked_page_multiple_etymons_idless = false
__state.linked_page_partial_etymology_sections = false
__state.max_depth_reached = 0
__state.total_nodes = 0
__state.language_count = {}
__state.toplevel_keyword_stats = {}
__state.warnings = {}
end
local M = require("Module:module loader").init({
require = {
data = "Module:etymon/data",
tree = "Module:etymon/tree",
text = "Module:etymon/text",
categories = "Module:etymon/categories",
tracking = "Module:etymon/tracking",
descendants = "Module:etymon/descendants",
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",
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",
},
})
-- Template names that carry etymon data, and the heading that opens an etymology section.
-- Pages are scraped for these, so both must match what ku.wiktionary actually uses:
-- the section is "Etîmolojî", and the new template is "etîmon".
-- Only the new template: pages still using Şablon:etymon carry the old ">"-separated syntax, which
-- this module would misparse into garbage node labels. Add "etymon" here only once
-- Şablon:etymon itself invokes this module.
local ETYMON_TEMPLATES = {
["etîmon"] = true,
["etymon/nû"] = true, -- redirect to Şablon:etîmon; still recognised if written out
-- Do NOT add "etymon" here while Şablon:etymon still uses the old ">"-separated syntax:
-- this module would read those calls and parse the bare keyword ("inh") as a term.
}
local ETYMOLOGY_HEADING = "^Etîmolojî"
-- ku's reconstruction namespace is "Jinûvesazî" (id 108), where en.wiktionary uses
-- "Reconstruction" (id 118). A page here is a reconstructed term, so its title needs the
-- leading "*" that Modul:links requires for reconstructed languages.
local RECONSTRUCTION_NS_NAME = "Jinûvesazî"
local RECONSTRUCTION_NS_ID = 108
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
-- Match a term language against a text=:lang stop target (supports etymology-only codes).
function Util.lang_matches_stop_code(term_lang, stop_code)
if not term_lang or not stop_code or stop_code == "" then
return false
end
local stop_lang = Util.get_lang(stop_code, true)
if not stop_lang then
return false
end
if term_lang:getCode() == stop_lang:getCode() then
return true
end
if stop_lang:getFullCode() == stop_lang:getCode() then
return term_lang:getFullCode() == stop_lang:getCode()
end
return false
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
function Util.resolve_context_lang(lang, node_args)
if type(node_args) ~= "table" then return lang end
if node_args.status == M.data.STATUS.INLINE then return lang end
if not (lang.hasType and lang:hasType("etymology-only")) then return lang end
local full = lang.getFull and lang:getFull()
if not full or full:getCode() == lang:getCode() then return lang end
if full.hasAncestor and full:hasAncestor(lang) then return lang end
return full
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
local REQUEST_TEMPLATE_PARAM_MODS = {
rfe = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
},
etystub = {
nocat = { type = "boolean" },
sort = {},
nocap = { type = "boolean" },
nodot = { type = "boolean" },
},
}
function Util.expand_request_template(frame, template_name, param_value, lang_code)
local param_mods = REQUEST_TEMPLATE_PARAM_MODS[template_name]
local with_defaults = Util.add_boolean_defaults(param_value, param_mods)
local parsed = M.parse_utilities.parse_inline_modifiers(with_defaults, {
param_mods = param_mods,
generate_obj = function(text)
if M.yesno(text, false) then
return { is_boolean = true }
end
return { text = text }
end,
})
local template_args = { [1] = lang_code }
for name in pairs(param_mods) do
template_args[name] = parsed[name]
end
if not parsed.is_boolean then
template_args[2] = parsed.text
end
return " " .. frame:expandTemplate({
title = template_name,
args = template_args,
})
end
-- Kurdish values accepted for |pos= (|crz=) and <postype:>, mapped to the English values the rest of
-- the module compares against.
local POS_ALIASES = {
["reh"] = "root",
["kelîme"] = "word",
["pêşgir"] = "prefix",
["paşgir"] = "suffix",
["navgir"] = "interfix",
}
function Util.normalize_pos(value)
if type(value) ~= "string" then
return value
end
value = mw.text.trim(value)
return POS_ALIASES[value] or value
end
-- {{etîmon}} arguments read straight from another page skip Module:parameters, so `crz` (the Kurdish
-- name of `pos`) and the Kurdish values are resolved here.
function Util.normalize_raw_etymon_args(args)
if args.pos == nil and args.crz ~= nil then
args.pos = args.crz
end
args.pos = Util.normalize_pos(args.pos)
return args
end
-- Centralized term formatting: handles suppress_term (-), unknown_term (empty/+), 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
-- An explicit <t:...> wins; otherwise fall back to the etymon id, which on ku is usually a
-- Kurdish gloss in its own right ("berik", "hêştir"). An id that is a key rather than a
-- translation ("H2O", "paşgira meslekî") can be overridden with a <t:...>.
local t = (term.t and term.t ~= "") and term.t or nil
local id_gloss = (term.id and term.id ~= "") and term.id or nil
local gloss = t or id_gloss
-- gloss = "separate" leaves the gloss out of the link and returns it as a second value, so
-- the tree can put it on its own line.
if opts.gloss ~= "suppress" and opts.gloss ~= "separate" then
link_params.gloss = gloss
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
local link = M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
if opts.gloss == "separate" then
return link, gloss
end
return link
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
local function is_affix_group_keyword(keyword)
local config = keyword and M.data.keywords[keyword]
return config and config.affix_categories or false
end
local function reject_removed_surf_keyword(param)
local base = get_keyword_base(param)
if base == "surf" then
error("The `:surf` keyword has been removed. Use `<surf>` on a formation keyword instead (e.g. `:af<surf>`, `:bor<surf>`).")
end
end
local function copy_keyword_info(source)
local copy = {}
for k, v in pairs(source) do
copy[k] = v
end
return copy
end
local function lowercase_glossary_display(text)
return text:gsub("(%[%[Appendix:Glossary#[^|]+|)([^%]])([^%]]*)%]%]", function(prefix, first, rest)
return prefix .. mw.ustring.lower(first) .. rest .. "]]"
end)
end
local function surf_should_keep_formation_phrase(base)
if not base.phrase then
return false
end
if base.glossary then
return true
end
return not (base.phrase == "from" and (base.text == "From" or base.text == "from"))
end
-- Runtime overrides when <surf> is present on a keyword.
local function get_effective_keyword_info(keyword, modifiers)
local base = M.data.keywords[keyword]
if not base or not modifiers or not modifiers.surf then
return base
end
local effective = copy_keyword_info(base)
local surf_text = "By [[Appendix:Glossary#surface_analysis|surface analysis]],"
local surf_phrase = "by surface analysis,"
effective.new_sentence = true
effective.invisible = "tree"
if surf_should_keep_formation_phrase(base) then
effective.phrase = surf_phrase .. " " .. base.phrase
if base.text then
effective.text = surf_text .. " " .. lowercase_glossary_display(base.text)
else
effective.text = surf_text .. " " .. base.phrase
end
else
effective.text = surf_text
effective.phrase = surf_phrase
end
return effective
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:find("-", 1, true) then
-- ku combines a gender with a number by hyphenating them ("m-p"), where en.wikt runs the
-- letters together ("mp"); build the label from the parts. Splitting two-letter codes the
-- en.wikt way would mislabel ku codes that are not combinations at all, such as "np"
-- (nepersonel).
local parts, all_known = {}, true
for part in code:gmatch("[^-]+") do
local part_adj = codes[part]
if part_adj then
table.insert(parts, part_adj)
else
all_known = false
end
end
if all_known and parts[1] then
adj = table.concat(parts, " ")
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 = { affix_group = true } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
surf = { type = "boolean" },
senseid = { restrict = { keywords = { "semantic loan" } } },
-- Kurdish names, resolved by resolve_modifier_aliases() below.
maneid = { alias_of = "senseid", restrict = { keywords = { "semantic loan" } } },
}
-- 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 = { affix_group = true } },
postype = {},
bor = { type = "boolean", restrict = { affix_group = true } },
slbor = { type = "boolean", restrict = { affix_group = true } },
lbor = { type = "boolean", restrict = { affix_group = true } },
-- Kurdish names for the same modifiers, resolved by resolve_modifier_aliases() below.
-- cuda/t/w/crz are the names ku's own templates already accept; the affix-group
-- borrowing flags reuse the Kurdish keyword names.
cuda = { alias_of = "alt" },
-- ASCII only: Modul:parse utilities matches a modifier prefix with "^([a-zA-Z0-9+_-]+):",
-- so a name containing "î" can never be recognised as a prefix.
etimon = { alias_of = "ety" },
w = { alias_of = "t" },
crz = { alias_of = "pos" },
nediyar = { type = "boolean", alias_of = "unc" },
deyn = { type = "boolean", alias_of = "bor", restrict = { affix_group = true } },
deynh = { type = "boolean", alias_of = "lbor", restrict = { affix_group = true } },
deynhq = { type = "boolean", alias_of = "slbor", restrict = { affix_group = true } },
}
--[==[
Move any Kurdish modifier onto the canonical name it aliases, so the rest of the module only ever
sees the canonical set. Called right after the inline modifiers are parsed.
]==]
function EtymonParser.resolve_modifier_aliases(obj, param_mods)
for name, def in pairs(param_mods) do
if def.alias_of and obj[name] ~= nil then
if obj[def.alias_of] ~= nil then
error("`<" .. name .. ":>` and `<" .. def.alias_of .. ":>` are the same modifier; use one.")
end
obj[def.alias_of] = obj[name]
obj[name] = nil
end
end
return obj
end
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" and key ~= "alias_of" 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 then
if mod_def.restrict.affix_group then
if not is_affix_group_keyword(current_keyword) then
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for affix-group keywords (e.g. `:af`, `:blend`, `:univ`).")
end
elseif 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
end
local TERM_RULE_DISALLOW = {
suppress = { field = "suppress_term", label = "suppressed" },
unknown = { field = "unknown_term", label = "unknown" },
family = { field = "is_family", label = "family" },
}
function EtymonParser.check_etymon_limits(count, limits, label, opts)
if not limits then
return
end
opts = opts or {}
local min_etymons = limits.min_etymons
if min_etymons == nil and not opts.skip_default_min then
min_etymons = 1
end
if min_etymons and count < min_etymons then
if min_etymons > 1 then
error("Detected " .. label .. " group with fewer than " .. min_etymons .. " etymons.")
else
error("Detected " .. label .. " with no etymons.")
end
end
if limits.max_etymons and count > limits.max_etymons then
local unit = (limits.max_etymons == 1) and "etymon" or "etymons"
error("Detected " .. label .. " with more than " .. limits.max_etymons .. " " .. unit .. ".")
end
end
function EtymonParser.check_term_rules(etymon_data, entry_lang, rules, label)
label = label or "term"
if rules and rules.disallow then
local disallowed = {}
for _, typ in ipairs(rules.disallow) do
local spec = TERM_RULE_DISALLOW[typ]
if spec and etymon_data[spec.field] then
table.insert(disallowed, spec.label)
end
end
if #disallowed > 0 then
error(label .. " does not support " ..
mw.text.listToText(disallowed, "or") .. " etymons.")
end
end
if etymon_data.is_family then
if rules and rules.family == "disallowed" then
error(label .. " does not support family codes" .. (rules.family_suffix or "."))
elseif not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
end
if rules then
if rules.require_term and (not etymon_data.term or etymon_data.term == "") then
error(label .. " requires a term for each listed form.")
end
if rules.entry_lang then
if Util.get_norm_lang(etymon_data.lang):getFullCode() ~=
Util.get_norm_lang(entry_lang):getFullCode() then
error(label .. " terms must be in the entry language (" ..
entry_lang:getFullCode() .. "), got '" .. etymon_data.lang:getFullCode() .. "'.")
end
end
if rules.ancestor_check then
M.etymology.check_ancestor(entry_lang, etymon_data.lang)
end
elseif etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
end
function EtymonParser.check_keyword_term(etymon_data, entry_lang, keyword)
local config = M.data.keywords[keyword]
EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "`:" .. keyword .. "`")
end
function EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type)
local config = M.data.supplements[supplement_type]
EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "|" .. supplement_type .. "=")
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 })
EtymonParser.resolve_modifier_aliases(parsed, EtymonParser.keyword_param_mods)
local modifiers = {
unc = parsed.unc or false,
ref = parsed.ref,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
surf = parsed.surf or false,
senseid = parsed.senseid,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
local function normalize_keyword_param(keyword_with_mods)
local trimmed = M.string_utilities.trim(keyword_with_mods)
reject_removed_surf_keyword(trimmed:match("^:") and trimmed or (":" .. trimmed))
local base = get_keyword_base(trimmed)
if not base or not M.data.keywords[base] then
error("Invalid keyword '" .. trimmed .. "' in inline etymology")
end
local canonical_base = get_canonical_keyword(base)
local without_colon = trimmed:gsub("^:", "")
local mods_part = without_colon:sub(#base + 1)
local kw_param = normalize_keyword(canonical_base .. mods_part)
EtymonParser.parse_keyword_modifiers(kw_param)
return kw_param
end
local function get_keyword_mod_names()
local names = {}
for mod_name in pairs(EtymonParser.keyword_param_mods) do
names[mod_name] = true
end
return names
end
local function parse_inline_ety_run(ety_string)
local body = ety_string or ""
if body == "" then
error("Empty inline etymology")
end
local keyword_mod_names = get_keyword_mod_names()
local pos = 1
local len = #body
local function parse_err(msg)
error(msg .. " in inline etymology: '" .. body .. "'")
end
local function peek_double()
return body:sub(pos, pos + 1) == "<<"
end
local function mod_name_from_unwrapped(unwrapped)
return unwrapped:match("^<([^:>]+)")
end
local function is_keyword_mod(unwrapped)
local name = mod_name_from_unwrapped(unwrapped)
return name and keyword_mod_names[name] or false
end
local function read_double_bracket()
if not peek_double() then
return nil
end
local start = pos
pos = pos + 2
while pos <= len - 1 do
if body:sub(pos, pos + 1) == ">>" then
local token = body:sub(start, pos + 1)
pos = pos + 2
return token, token:sub(2, -2)
end
pos = pos + 1
end
parse_err("Unmatched <<")
end
local function read_angle_cell()
if body:sub(pos, pos) ~= "<" or peek_double() then
return nil
end
local open = pos
pos = pos + 1
local depth = 1
local i = pos
while i <= len do
local ch = body:sub(i, i)
if ch == "<" then
depth = depth + 1
elseif ch == ">" then
depth = depth - 1
if depth == 0 then
local inner = body:sub(open + 1, i - 1)
pos = i + 1
return inner
end
end
i = i + 1
end
parse_err("Unmatched <")
end
local function read_bare_run()
local start = pos
while pos <= len and body:sub(pos, pos) ~= "<" do
pos = pos + 1
end
return body:sub(start, pos - 1)
end
local function absorb_double_keyword_mods(keyword_str)
while peek_double() do
local saved = pos
local _, unwrapped = read_double_bracket()
if is_keyword_mod(unwrapped) then
keyword_str = keyword_str .. unwrapped
else
pos = saved
break
end
end
return keyword_str
end
local kw_start = pos
while pos <= len and body:sub(pos, pos) ~= "<" do
pos = pos + 1
end
local keyword = body:sub(kw_start, pos - 1)
if keyword:match("^%s*$") then
parse_err("Missing keyword")
end
keyword = absorb_double_keyword_mods(keyword)
local cells = {}
while pos <= len do
if peek_double() then
local _, unwrapped = read_double_bracket()
if is_keyword_mod(unwrapped) then
parse_err("Unexpected keyword modifier " .. unwrapped .. " outside of a keyword")
end
table.insert(cells, "+" .. unwrapped)
elseif body:sub(pos, pos) == "<" then
local inner = read_angle_cell()
if inner ~= "" then
table.insert(cells, inner)
end
else
local bare = read_bare_run()
if bare ~= "" then
if bare:sub(1, 1) ~= ":" then
parse_err("Unexpected bare text '" .. bare .. "' (use :keyword for nested keywords in inline etymology)")
end
if not is_keyword(bare, true) then
parse_err("Invalid keyword '" .. bare .. "' in inline etymology")
end
table.insert(cells, absorb_double_keyword_mods(bare))
end
end
end
return {
keyword = keyword,
cells = cells,
}
end
function EtymonParser.inline_ety_to_pipe(ety_string)
local run = parse_inline_ety_run(ety_string)
if not run.keyword or run.keyword:match("^%s*$") then
return "|"
end
local pipe_parts = { normalize_keyword_param(M.string_utilities.trim(run.keyword)) }
for _, segment in ipairs(run.cells) do
if is_keyword(segment, true) then
table.insert(pipe_parts, normalize_keyword_param(segment))
else
table.insert(pipe_parts, segment)
end
end
return "|" .. table.concat(pipe_parts, "|") .. "|"
end
function EtymonParser.pipe_to_inline_ety(pipe_string)
local cells = {}
for cell in pipe_string:gmatch("([^|]+)") do
if cell ~= "" then
table.insert(cells, cell)
end
end
if #cells == 0 then
return ""
end
local inline_parts = {}
for index, cell in ipairs(cells) do
local base = get_keyword_base(cell)
if base and M.data.keywords[base] then
local without_colon = cell:gsub("^:", "")
local kw_base, mods = without_colon:match("^([^<]+)(.*)$")
local inline_kw = (kw_base or without_colon) .. (mods or ""):gsub("<([^>]+)>", "<<%1>>")
if index > 1 then
inline_kw = ":" .. inline_kw
end
table.insert(inline_parts, inline_kw)
elseif cell:sub(1, 1) == "+" then
local mod = cell:sub(2)
if mod:match("^<.->$") then
mod = mod:sub(2, -2)
end
table.insert(inline_parts, "<<" .. mod .. ">>")
else
table.insert(inline_parts, "<" .. cell .. ">")
end
end
return table.concat(inline_parts, "")
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local run = parse_inline_ety_run(ety_string)
local keyword = M.string_utilities.trim(run.keyword)
reject_removed_surf_keyword(":" .. keyword)
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword_param(keyword) }
for _, segment in ipairs(run.cells) do
if is_keyword(segment, true) then
table.insert(args, normalize_keyword_param(segment))
else
table.insert(args, segment)
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
M.tracking.track_term(rest)
if rest == "" or 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 == "" or 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 })
EtymonParser.resolve_modifier_aliases(parsed_obj, EtymonParser.etymon_param_mods)
parsed_obj.postype = Util.normalize_pos(parsed_obj.postype)
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), ", ")
.. " (Kurdish: " .. table.concat(require("Module:table").keysToList(POS_ALIASES), ", ") .. ").")
end
local current_keyword = "from"
local current_keyword_explicit = false
local keyword_etymons = {}
local keywords = M.data.keywords
local function checkKeyword()
local config = keywords[current_keyword]
if current_keyword == "from" and not current_keyword_explicit and #keyword_etymons == 0 then
keyword_etymons = {}
return
end
EtymonParser.check_etymon_limits(#keyword_etymons, config, "`:" .. current_keyword .. "`")
keyword_etymons = {}
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
reject_removed_surf_keyword(param)
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkKeyword()
current_keyword = get_canonical_keyword(get_keyword(param))
current_keyword_explicit = true
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(keyword_etymons, param)
EtymonParser.check_keyword_term(etymon_data, lang, current_keyword)
-- 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\" (Kurdish \"reh\" or \"kelîme\").")
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(keyword_etymons, param)
end
end
end
checkKeyword()
end
local DataRetriever = {}
local function format_etymon_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 function etymon_target_page_link(page, norm_lang)
-- `page` is an already-resolved page name ("Jinûvesazî:proto-îranî/hižwáH"), so link to it
-- directly. Passing it back through full_link() with a reconstructed language raises an error,
-- because the resolved name no longer carries the leading "*" those languages require -- and
-- this link is built even when the warning it belongs to is later discarded outside preview.
return "[[" .. page .. "]]"
end
-- Summarize {{etymon}} id slots on a linked page for preview warnings.
local function summarize_available_etymon_ids(ids)
local id_list = {}
local all_idless = true
local target_has_idless = false
local any_pos = false
for i, id_data in ipairs(ids) do
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
all_idless = false
else
target_has_idless = true
end
if pos and pos ~= "" then
any_pos = true
end
table.insert(id_list, format_etymon_id_hint(id_data, i))
end
return {
id_list = id_list,
all_idless = all_idless,
target_has_idless = target_has_idless,
any_pos = any_pos,
count = #ids,
options_text = mw.text.listToText(id_list),
}
end
local function ambiguous_etymon_suggestion(page_link, summary)
if summary.all_idless then
if summary.any_pos then
return " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. summary.options_text .. "."
end
return " 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
return " Specify which one with `<id:identifier>` after the term. Options: " .. summary.options_text .. "."
end
local function warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel)
local page_link = etymon_target_page_link(page, norm_lang)
local summary = summarize_available_etymon_ids(ids)
if is_toplevel and summary.target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local lang_name = norm_lang:getCanonicalName()
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. summary.count
.. " {{etymon}} templates for " .. lang_name .. ")."
Util.add_warning(lead .. ambiguous_etymon_suggestion(page_link, summary), true)
end
local function is_mismatched_explicit_id(base_key, cached_args, parent_etymon)
return cached_args == M.data.STATUS.MISSING and not parent_etymon
and #(__state.available_etymon_ids[base_key] or {}) > 0
end
local function maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel)
if not is_toplevel or __state.skip_partial_etymology_category then
return
end
if not __state.partial_etymology_targets[base_key] then
return
end
if etymon_data.id and type(cached_args) == "table" then
return
end
__state.linked_page_partial_etymology_sections = true
end
local function is_nonlemma_etymon_template(template_args)
return template_args and M.yesno(template_args.nl, false)
end
local function warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_id)
local page_link = etymon_target_page_link(page, norm_lang)
local summary = summarize_available_etymon_ids(__state.available_etymon_ids[base_key] or {})
local lang_name = norm_lang:getCanonicalName()
local lead = "Etymology link to " .. page_link .. " uses `<id:" .. etymon_id
.. ">`, but no {{etymon}} on that page has `|id=" .. etymon_id .. "|` for " .. lang_name .. "."
Util.add_warning(lead .. " Valid IDs: " .. summary.options_text .. ".", true)
end
-- 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
-- ku.wiktionary writes L2 headings as "== {{ziman|CODE}} ==" rather than the language
-- name, and this scrape runs on raw wikitext, so the template form has to be tried too.
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
or M.pages.get_section(content, "{{ziman|" .. etymon_lang:getFullCode() .. "}}", 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
local etymology_section_count = 0
local etymology_sections_with_etymon = 0
local current_etymology_has_etymon = false
local current_etymology_has_nonlemma = false
local function finalize_current_etymology_section()
if etymology_section_count == 0 then
return
end
if current_etymology_has_etymon or current_etymology_has_nonlemma then
etymology_sections_with_etymon = etymology_sections_with_etymon + 1
end
current_etymology_has_etymon = false
current_etymology_has_nonlemma = false
end
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_HEADING) then
finalize_current_etymology_section()
etymology_section_count = etymology_section_count + 1
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if ETYMON_TEMPLATES[template_name] then
local template_args = Util.normalize_raw_etymon_args(node:get_arguments())
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
if is_nonlemma_etymon_template(template_args) then
if etymology_section_count > 0 then
current_etymology_has_nonlemma = true
end
else
if etymology_section_count > 0 then
current_etymology_has_etymon = true
end
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
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
finalize_current_etymology_section()
if lang_section_for_descendants
and etymology_section_count > 1
and etymology_sections_with_etymon > 0
and etymology_sections_with_etymon < etymology_section_count
then
__state.partial_etymology_targets[lang_page_key] = true
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
local function has_linkable_term(etymon_data)
if etymon_data.is_family or etymon_data.suppress_term or etymon_data.unknown_term then
return false
end
local term = etymon_data.term
if term == nil or term == "" then
return false
end
return M.string_utilities.trim(term) ~= ""
end
local function record_term_id_tracking(etymon_data)
if not has_linkable_term(etymon_data) then
return
end
local term_page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
M.tracking.record_term_id_usage(__state.id_stats, etymon_data, term_page)
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)
if not has_linkable_term(etymon_data) then
return M.data.STATUS.MISSING, nil, nil, nil
end
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
local mismatched_id = is_mismatched_explicit_id(base_key, cached_args, parent_etymon)
if mismatched_id and is_toplevel then
__state.has_mismatched_id = true
M.tracking.record_mismatched_id_usage(__state.id_stats, norm_lang, page, etymon_data.id)
warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_data.id)
end
maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel)
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
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "postype")
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
local matched_args = __state.cached_etymon_args[matched_key]
maybe_flag_partial_etymology_reference(base_key, etymon_data, matched_args, is_toplevel)
return matched_args, __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 "*"
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "single")
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
local single_args = __state.single_etymons[base_key]
maybe_flag_partial_etymology_reference(base_key, etymon_data, single_args, is_toplevel)
return single_args, __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "ambiguous")
warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel)
maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.AMBIGUOUS, is_toplevel)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "missing")
maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.MISSING, is_toplevel)
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local function keyword_invisible_in_tree(keyword_info)
if not keyword_info then
return false
end
local inv = keyword_info.invisible
return inv == "all" or inv == true or inv == "tree"
end
-- True when the node has at least one top-level child container visible in the tree.
local function node_has_visible_tree_children(node)
for _, container in ipairs(node.children or {}) do
if not keyword_invisible_in_tree(container.keyword_info) then
return true
end
end
return false
end
-- Count visible term nodes in the tree.
local function get_visible_tree_depth(node, skip_child_rendering)
local max_depth = 1
if skip_child_rendering or not node then
return max_depth
end
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if not keyword_invisible_in_tree(keyword_info) then
local skip_grandchildren = keyword_info and keyword_info.no_child_categories
for _, term in ipairs(container.terms or {}) do
if term.is_duplicate then
if term.original_has_children then
max_depth = math.max(max_depth, 2)
end
else
max_depth = math.max(max_depth, 1 + get_visible_tree_depth(term, skip_grandchildren))
end
end
end
end
return max_depth
end
local function as_param_list(val)
if val == nil then
return {}
end
if type(val) == "table" then
return val
end
if type(val) == "string" and val ~= "" then
return { val }
end
return {}
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
if node.supplements then
for _, supplement in ipairs(node.supplements) do
if supplement.terms then
for _, term in ipairs(supplement.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
-- Copy parsed etymon modifiers onto a tree/supplement term node.
function TreeBuilder.apply_etymon_fields(term, etymon_data)
-- A reference without <id:> still resolves when the target page has a single etymon, and the
-- node already carries that etymon's own id. Keep it instead of clearing it, so the tree shows
-- the target's id as the gloss without every reference having to repeat it. An explicit
-- <id:...> still wins.
term.id = etymon_data.id or term.id
term.t = etymon_data.t
term.tr = etymon_data.tr
term.ts = etymon_data.ts
term.alt = etymon_data.alt
term.g = etymon_data.g
term.pos = etymon_data.pos
term.ng = etymon_data.ng
term.ref = etymon_data.ref
term.is_uncertain = etymon_data.unc
term.lit = etymon_data.lit
term.q = etymon_data.q
term.qq = etymon_data.qq
term.l = etymon_data.l
term.ll = etymon_data.ll
term.suppress_term = etymon_data.suppress_term
term.unknown_term = etymon_data.unknown_term
term.is_family = etymon_data.is_family
term.override = etymon_data.override
term.aftype = etymon_data.aftype
term.postype = etymon_data.postype
term.bor = etymon_data.bor
term.lbor = etymon_data.lbor
term.slbor = etymon_data.slbor
end
function TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type)
EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type)
local term = {
lang = etymon_data.lang,
title = etymon_data.term,
children = {},
status = M.data.STATUS.OK,
}
TreeBuilder.apply_etymon_fields(term, etymon_data)
return term
end
function TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value)
local terms = {}
for _, term_param in ipairs(as_param_list(param_value)) do
if type(term_param) == "string" and term_param ~= "" then
local etymon_data = EtymonParser.parse_etymon(term_param, entry_lang)
if etymon_data then
table.insert(terms, TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type))
end
end
end
return terms
end
-- Attach a |param= supplement defined in etymon_data.supplements (e.g. doublet=).
function TreeBuilder.append_term_supplement(data_tree, entry_lang, supplement_type, param_value)
local config = M.data.supplements[supplement_type]
if not config then
error("Unknown supplement '" .. tostring(supplement_type) .. "'.")
end
local terms = TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value)
if #terms == 0 then
return
end
data_tree.supplements = data_tree.supplements or {}
table.insert(data_tree.supplements, {
type = supplement_type,
config = config,
terms = terms,
})
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, supplement_type, entry_lang, entry_lang, true)
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
local _, kw_modifiers = EtymonParser.parse_keyword_modifiers(param:sub(1, 1) == ":" and param or (":" .. param))
if not keyword_invisible_in_tree(get_effective_keyword_info(keyword_base, kw_modifiers)) 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
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
local keyword_info = get_effective_keyword_info(current_keyword, current_keyword_modifiers)
current_container = {
keyword = current_keyword,
keyword_info = keyword_info,
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 = copy_keyword_info(keyword_info)
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
return current_container
end
local parse_context_lang = Util.resolve_context_lang(lang, args)
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
if not keyword then
error("Invalid keyword '" .. param .. "'.")
end
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
reject_removed_surf_keyword(param)
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, parse_context_lang)
if etymon_data then
-- Track keyword usage at top level
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, current_keyword, lang, etymon_data.lang, is_toplevel)
local term_node = {}
local container
-- Handle suppress_term (-) and unknown_term (empty or +) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
container = 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
TreeBuilder.apply_etymon_fields(term_node, etymon_data)
else
-- Regular term: fetch arguments from page
record_term_id_tracking(etymon_data)
local etymon_args, page_of, resolved_etymon_id, descendants_check =
DataRetriever.get_etymon_args(etymon_data, is_toplevel)
-- 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
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, inline_keyword, etymon_data.lang, inline_etymon.lang, is_toplevel)
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
container = ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = 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.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.page_of = page_of
TreeBuilder.apply_etymon_fields(term_node, etymon_data)
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(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
-- 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()
__state.id_stats = M.tracking.new_id_stats()
__state.skip_partial_etymology_category = options.skip_partial_etymology_category == true
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 == RECONSTRUCTION_NS_ID 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)
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
__state.id_stats = M.tracking.new_id_stats()
-- 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:makeEntryName(M.links.remove_links(tree_title)) ~= lang:makeEntryName(M.links.remove_links(title)) then
M.tracking.track_title_pagename_mismatch(lang)
end
reset_invocation_state()
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, {
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
gloss = "separate",
})
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)
args.pos = Util.normalize_pos(args.pos)
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 doublet = args.doublet
local rfe = args.rfe
local etystub = args.etystub
local is_nonlemma = M.yesno(args.nl, false)
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == RECONSTRUCTION_NS_NAME then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == RECONSTRUCTION_NS_NAME then
entry_pagename = "*" .. entry_pagename
end
if lang:makeEntryName(M.links.remove_links(title)) ~= lang:makeEntryName(M.links.remove_links(entry_pagename)) then
M.tracking.track_title_pagename_mismatch(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
reset_invocation_state()
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
skip_partial_etymology_category = is_nonlemma,
})
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.supplements = ety_data_tree.supplements or {}
table.insert(ety_data_tree.supplements, {
type = "etydate",
etydate_text = M.etydate.format_etydate(etydate_args, { omit_refs = true }),
etydate_refs = (parsed_etydate.ref and #parsed_etydate.ref > 0) and parsed_etydate.ref or nil,
})
end
TreeBuilder.append_term_supplement(ety_data_tree, lang, "doublet", doublet)
if ety_data_tree.supplements then
parse_tree_references(ety_data_tree)
end
local has_visible_children = node_has_visible_tree_children(ety_data_tree)
-- Suppress trees for multiword entries and one-step chains
local visible_tree_depth = get_visible_tree_depth(ety_data_tree)
local is_trivial_tree = visible_tree_depth <= 2
local is_multiword = title:find("%s") ~= nil or title:find("_") ~= nil
if tree and (is_multiword or is_trivial_tree) then
tree = false
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, {
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
gloss = "separate",
})
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)
local text_stop_lang_missing = nil
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
local text_output, text_render_meta = M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
lang_matches_stop_code = Util.lang_matches_stop_code,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
dot = args.dot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
})
table.insert(output, text_output)
if stop_at_lang and text_render_meta and not text_render_meta.stop_lang_reached then
M.tracking.track_text_stop_lang_missing(lang, stop_at_lang)
text_stop_lang_missing = stop_at_lang
end
end
if rfe then
table.insert(output, Util.expand_request_template(frame, "rfe", rfe, lang:getCode()))
end
if etystub then
table.insert(output, Util.expand_request_template(frame, "etystub", etystub, lang:getCode()))
end
if is_nonlemma then
table.insert(output, " " .. frame:expandTemplate({
title = "nonlemma",
args = {},
}))
end
local categories = {}
if Util.is_content_page() then
M.tracking.track_tree_metrics({
max_depth_reached = __state.max_depth_reached,
total_nodes = __state.total_nodes,
language_count = __state.language_count,
lang = lang,
})
categories = M.categories.build({
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,
suppress_categories = lang_exc and lang_exc.suppress_categories,
nocat = args.nocat,
tree = tree,
text = text,
exnihilo = args.exnihilo,
toplevel_has_inline_etymology = __state.toplevel_has_inline_etymology,
toplevel_redundant_etymology = __state.toplevel_redundant_etymology,
toplevel_idless_etymon = __state.toplevel_idless_etymon,
has_mismatched_id = __state.has_mismatched_id,
linked_page_multiple_etymons_idless = __state.linked_page_multiple_etymons_idless,
linked_page_partial_etymology_sections = __state.linked_page_partial_etymology_sections,
text_stop_lang_missing = text_stop_lang_missing,
})
M.tracking.track_keywords(__state.toplevel_keyword_stats, lang)
M.tracking.track_page_id(lang, id)
M.tracking.track_ids(__state.id_stats, lang)
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
6gtzlztmc4biw3paomkfhssswinvx6m
Şablon:Ç:txb:DTB
10
1326244
6531683
6113711
2026-09-15T18:11:45Z
Wikihez
7344
Wikihezî/ê navê [[Şablon:R:txb:DTB]] weke [[Şablon:Ç:txb:DTB]] guhart
6113711
wikitext
text/x-wiki
{{cite-book<!--
-->|last=Adams<!--
-->|first=Douglas Q.<!--
-->|authorlink=Douglas Q. Adams<!--
-->|title=A Dictionary of Tocharian B: Revised and Greatly Enlarged<!--
-->|isbn=9042036710<!--
-->|entry={{{head|{{{1|}}}}}}<!--
-->|location=Amsterdam, New York<!--
-->|publisher=Rodopi<!--
-->|year=2013<!--
-->|page={{{page|{{{2|}}}}}}<!--
-->|pages={{{pages|}}}<!--
-->|passage={{{passage|{{{3|}}}}}}<!--
-->|series=Leiden Studies in Indo-European<!--
-->|seriesvolume=10<!--
-->|pageurl=https://books.google.com/books?id=fXPiAAAAQBAJ&pg=PA{{#invoke:ugly hacks|match|{{{page|{{{2|{{{pages|}}}}}}}}}|[0-9]+|}}<!--
-->}}<noinclude>{{refcat|xto|txb|ine-toc-pro}}</noinclude>
bngbu9d9izwc1w0jb1nqgfc33zwwziw
6531687
6531683
2026-09-15T18:12:19Z
Wikihez
7344
6531687
wikitext
text/x-wiki
{{cite-book|en<!--
-->|last=Adams<!--
-->|first=Douglas Q.<!--
-->|authorlink=Douglas Q. Adams<!--
-->|title=A Dictionary of Tocharian B: Revised and Greatly Enlarged<!--
-->|isbn=9042036710<!--
-->|entry={{{head|{{{1|}}}}}}<!--
-->|location=Amsterdam, New York<!--
-->|publisher=Rodopi<!--
-->|year=2013<!--
-->|page={{{page|{{{2|}}}}}}<!--
-->|pages={{{pages|}}}<!--
-->|passage={{{passage|{{{3|}}}}}}<!--
-->|series=Leiden Studies in Indo-European<!--
-->|seriesvolume=10<!--
-->|pageurl=https://books.google.com/books?id=fXPiAAAAQBAJ&pg=PA{{#invoke:ugly hacks|match|{{{page|{{{2|{{{pages|}}}}}}}}}|[0-9]+|}}<!--
-->}}<noinclude>{{refcat|xto|txb|ine-toc-pro}}</noinclude>
rf67w7sz8lyibmokpkljabc56nml4f1
6531688
6531687
2026-09-15T18:14:04Z
Wikihez
7344
6531688
wikitext
text/x-wiki
{{cite-book|en<!--
-->|last=Adams<!--
-->|first=Douglas Q.<!--
-->|authorlink=en:Douglas Q. Adams<!--
-->|title=en:A Dictionary of Tocharian B: Revised and Greatly Enlarged<t:Ferhenga Toxarî B: Versiyona revîzekirî û gelek firehkirî><!--
-->|isbn=9042036710<!--
-->|entry={{{head|{{{1|}}}}}}<!--
-->|location=Amsterdam, New York<!--
-->|publisher=Rodopi<!--
-->|year=2013<!--
-->|page={{{page|{{{2|{{{r|{{{rûpel|}}}}}}}}}}}}<!--
-->|pages={{{pages|}}}<!--
-->|passage={{{passage|{{{3|{{{jêgirtin|{{{jg|}}}}}}}}}}}}<!--
-->|series=Leiden Studies in Indo-European<!--
-->|seriesvolume=10<!--
-->|pageurl=https://books.google.com/books?id=fXPiAAAAQBAJ&pg=PA{{#invoke:ugly hacks|match|{{{page|{{{2|{{{pages|{{{r|{{{rûpel|}}}}}}}}}}}}}}}|[0-9]+|}}<!--
-->}}<noinclude>{{refcat|xto|txb|ine-toc-pro}}</noinclude>
g07ct2iz46581wsvhunl0903xxv84vh
6531689
6531688
2026-09-15T18:14:19Z
Wikihez
7344
6531689
wikitext
text/x-wiki
{{cite-book|en<!--
-->|last=Adams<!--
-->|first=Douglas Q.<!--
-->|authorlink=en:Douglas Q. Adams<!--
-->|title=en:A Dictionary of Tocharian B: Revised and Greatly Enlarged<t:Ferhenga Toxarî B: Versiyona revîzekirî û gelek firehkirî><!--
-->|isbn=9042036710<!--
-->|entry={{{head|{{{1|}}}}}}<!--
-->|location=Amsterdam, New York<!--
-->|publisher=Rodopi<!--
-->|year=2013<!--
-->|page={{{page|{{{2|{{{r|{{{rûpel|}}}}}}}}}}}}<!--
-->|pages={{{pages|}}}<!--
-->|passage={{{passage|{{{3|{{{jêgirtin|{{{jg|}}}}}}}}}}}}<!--
-->|series=en:Leiden Studies in Indo-European<!--
-->|seriesvolume=10<!--
-->|pageurl=https://books.google.com/books?id=fXPiAAAAQBAJ&pg=PA{{#invoke:ugly hacks|match|{{{page|{{{2|{{{pages|{{{r|{{{rûpel|}}}}}}}}}}}}}}}|[0-9]+|}}<!--
-->}}<noinclude>{{refcat|xto|txb|ine-toc-pro}}</noinclude>
tahwo7sb8ucgmc4zvkr0ol2a5wd2ilw
Modul:etymon/categories
828
1357628
6531696
6393015
2026-09-15T18:17:53Z
Wikihez
7344
etymon: crz as the Kurdish name of pos, Kurdish pos values (reh, kelîme, pêşgir, paşgir, navgir), root/word categories in Kurdish (rehê ... yê, kelîmeya ... ya); tested on the ceribandin copy
6531696
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,
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
local function lang_is_source(page_lang, source)
return page_lang:getCode() == source:getCode() or page_lang:hasParent(source)
end
local function is_borrowing_keyword_config(config)
return config and (config.borrowing_type or config.specialized_borrowing)
end
local function add_reborrow_category(categories, page_lang)
local lang_name = page_lang:getFullName()
add_category(categories, lang_name .. " terms borrowed back into " .. lang_name)
end
local function borrow_returns_to_page_lang(page_lang, source, in_foreign_branch)
if not in_foreign_branch then
return false
end
if source:getFullCode() == page_lang:getFullCode() then
return true
end
return page_lang:hasParent(source)
end
local function node_borrows_from_lang(node, page_lang, visited, in_foreign_branch)
visited = visited or {}
if not node or visited[node] then
return false
end
visited[node] = true
if node.is_duplicate then
if node.duplicate_of then
return node_borrows_from_lang(node.duplicate_of, page_lang, visited, in_foreign_branch)
end
return false
end
local node_is_foreign = in_foreign_branch
or (node.lang and node.lang:getFullCode() ~= page_lang:getFullCode())
for _, container in ipairs(node.children or {}) do
if is_borrowing_keyword_config(container.keyword_info) then
for _, child_term in ipairs(container.terms or {}) do
if borrow_returns_to_page_lang(page_lang, child_term.lang, node_is_foreign) then
return true
end
end
end
for _, child_term in ipairs(container.terms or {}) do
if child_term.status == M.data.STATUS.OK or child_term.status == M.data.STATUS.INLINE then
if node_borrows_from_lang(child_term, page_lang, visited, node_is_foreign) then
return true
end
end
end
end
return false
end
local function should_add_reborrow_category(page_lang, term)
if page_lang:getCode() == term.lang:getCode() then
return false
end
if term.status ~= M.data.STATUS.OK and term.status ~= M.data.STATUS.INLINE then
return false
end
return node_borrows_from_lang(term, page_lang, {}, false)
end
-- Add borrowing-related categories (top-level only)
local function collect_borrowing_categories(categories, page_lang, term, config, check_reborrow_path)
if check_reborrow_path and should_add_reborrow_category(page_lang, term) then
add_reborrow_category(categories, page_lang)
end
if config.borrowing_type == "borrowed" and not lang_is_source(page_lang, term.lang) 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 and not lang_is_source(page_lang, term.lang) 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,
-- insert_source_cat_get_display() composes "Peyvên <lang> <this> ji <source>", so this
-- has to be the Kurdish fragment. Matches what Modul:etîmolojî's format_inherited()
-- produces for {{mîrasmayî}}.
borrowing_type = "yên mîrasmayî",
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)
-- The word in the izafe form that joins it to the term ("rehê *leǵ-", "kelîmeya *X"), and the
-- connector after the term, which follows the word's gender: reh is nêr (yê), kelîme mê (ya).
local pos_types = {
root = { word = "rehê", connector = "yê" },
word = { word = "kelîmeya", connector = "ya" },
}
-- 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 = "Peyvên " .. lang_name .. " yên ji " .. pos_type.word .. " " .. entry_name .. " " .. pos_type.connector .. " " .. etymon_lang_name .. " dariştî"
else
cat_name = "Peyvên " .. lang_name .. " yên aîdî " .. pos_type.word .. " " .. 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, "Peyvên " .. lang_name .. " " .. cat.cat, cat.sort_key, cat.sort_base)
end
if node.supplements then
for _, supplement in ipairs(node.supplements) do
local config = supplement.config
if config and config.toplevel_category then
add_category(categories, config.toplevel_category .. " bi " .. lang_name)
end
end
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, "Madeyên bi referansa etîmoneke neheyî bi " .. lang_name)
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, config.toplevel_category .. " bi " .. lang_name)
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, true)
end
-- Borrowing categories from <bor>, <lbor>, or <slbor> modifiers on affix-group terms
local kw_config = M.data.keywords[keyword]
if kw_config and kw_config.affix_categories then
if term.bor then
local bor_config = { borrowing_type = "borrowed" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
elseif term.lbor then
local bor_config = { specialized_borrowing = "learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
elseif term.slbor then
local bor_config = { specialized_borrowing = "semi-learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
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.build(opts)
opts = opts or {}
local categories = {}
if not opts.suppress_categories and not opts.nocat then
categories = export.render({
data_tree = opts.data_tree,
page_lang = opts.page_lang,
available_etymon_ids = opts.available_etymon_ids,
senseid_parent_etymon = opts.senseid_parent_etymon,
get_norm_lang_func = opts.get_norm_lang_func,
lang_exc = opts.lang_exc,
})
end
local page_lang = opts.page_lang
if not page_lang then
return categories
end
local lang_name = page_lang:getCanonicalName()
table.insert(categories, "Rûpelên bi etîmonê")
table.insert(categories, "Madeyên bi etîmonê bi " .. lang_name )
if opts.tree then
table.insert(categories, "Madeyên bi darên etîmolojiyê")
table.insert(categories, "Madeyên bi darên etîmolojiyê bi " .. lang_name)
end
if opts.text then
table.insert(categories, "Madeyên bi metna etîmolojiyê bi ".. lang_name)
end
if opts.exnihilo then
table.insert(categories, lang_name .. " terms coined ex nihilo")
end
if opts.toplevel_has_inline_etymology then
table.insert(categories, "Pages with inline etymon for redlinks")
end
if opts.toplevel_redundant_etymology then
table.insert(categories, "Pages with redundant inline etymon")
end
if opts.toplevel_idless_etymon then
table.insert(categories, "Rûpelên etîmoneke bê ID bi kar tînin")
end
if opts.has_mismatched_id then
table.insert(categories, lang_name .. " entries referencing etymons with mismatched IDs")
end
if opts.linked_page_multiple_etymons_idless then
table.insert(categories,
lang_name .. " entries referencing pages with multiple etymons missing IDs")
end
if opts.linked_page_partial_etymology_sections then
table.insert(categories,
lang_name .. " entries referencing pages with etymology sections missing etymons")
end
if opts.text_stop_lang_missing then
table.insert(categories, "Pages with etymology text stop language not in chain")
table.insert(categories, lang_name .. " entries with etymology text stop language not in chain")
end
return categories
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
5xahkq024dmw2dbzuww0rp97cw74zz2
Bikarhêner:Wikihez/Gadget-wikt.add-examples.js
2
1357893
6531664
6514258
2026-09-15T17:27:21Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none
6531664
javascript
text/javascript
// <nowiki>
// dependencies (in MediaWiki:Gadgets-definition): oojs, oojs-ui, ext.gadget.citoid-helper,
// ext.gadget.StorageUtils, ext.gadget.wikt.core, ext.gadget.wikt.edit, ext.gadget.wikt.cite
//
// A ResourceLoader package module: it require()s wikt.cite, so it cannot be loaded as a plain
// script. Load it through its gadget definition or the test loader.
//
// Two copies at once would fight over wikt.addExamples, so the second to arrive does nothing.
window.wikt = window.wikt || {};
wikt.addExamples = wikt.addExamples || {};
var alreadyLoaded = wikt.addExamples.loaded;
wikt.addExamples.loaded = true;
// Formats a string using {0}, {1}, ... placeholders.
if (!String.prototype.format) {
String.prototype.format = function(var_args) {
var args = arguments;
return this.replace(/{(\d+)}/g, function(match, number) {
return typeof args[number] !== "undefined" ? args[number] : match;
});
};
}
$(function() {
"use strict";
if (alreadyLoaded) return;
// Entries, reconstructions and sandboxes -- the same set DefSideBoxes and the wizard run on, from
// wikt.core, so a page one of them works on is never a page this one quietly skips.
if (!require("ext.gadget.wikt.core").sections.isEntryPage()) return;
if (mw.config.get("wgPageName") === "Destpêk" || mw.config.get("wgArticleId") === 0) return;
var NAME = "[[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]]";
// This gadget puts nothing on the page of its own. DefSideBoxes opens the form from its own
// "Nimûne/jêgirtin" item, through openFor()/toggleFor() below, and takes the save over.
window.wikt = window.wikt || {};
wikt.addExamples = wikt.addExamples || {};
// Lets another gadget take the save over: "Qeyd bike" becomes msg.add and the edit joins
// that gadget's queue. Pass null to hand publishing back. Handler gets {transform, summary, done, fail}.
wikt.addExamples.setSubmitHandler = function (fn) {
wikt.addExamples.submitHandler = typeof fn === "function" ? fn : null;
};
// Re-clicking the caller's own trigger closes the form again.
wikt.addExamples.toggleFor = function (el) {
var toggle = $(el).data("wiktAddExampleToggle");
if (!toggle) {
return false;
}
toggle();
return true;
};
wikt.addExamples.openFor = function (el) {
var open = $(el).data("wiktAddExampleOpen");
if (!open) {
return false;
}
open();
return true;
};
// Opens the form on one example already under the sense el: itemEl is its <dd>/<li> on the page.
wikt.addExamples.editFor = function (el, itemEl) {
var editItem = $(el).data("wiktAddExampleEdit");
return !!editItem && editItem(itemEl);
};
var VERSION = "2.0";
// Wiki markup for bold, both the button's label and what it inserts.
var BOLD_MARKS = "'''";
// Persists form drafts (localStorage, cookie fallback) so unsaved text survives a failed submit.
var draftStorage = new ObjectStorage("add_examples_draft", 1);
// Citations are built by wikt.cite, the same module the wizard uses; Citoid lives in the
// citoid-helper gadget. "citoid" as a _currentJederKey is a sentinel, not a real jederDefs key.
var cite = require("ext.gadget.wikt.cite");
var core = require("ext.gadget.wikt.core");
var sections = core.sections;
var msg = core.msg;
var edit = require("ext.gadget.wikt.edit");
var jederDefs = cite.jederDefs;
var nlToSpace = cite.nlToSpace;
var boldenPageTitle = cite.boldenPageTitle;
var CATEGORY_TEMPLATE_KEY = cite.CATEGORY_KEY;
var RAW_TEMPLATE_KEY = cite.RAW_KEY;
// Params the form already has boxes for; skipped when building a Kategorî:Çavkanî template's fields.
var CATEGORY_TEMPLATE_COVERED_PARAMS = cite.COVERED_PARAMS;
// A param labeled exactly this is filled from the language dropdown instead of shown as a field,
// and only when the template's own TemplateData declares one.
var CATEGORY_TEMPLATE_LANG_LABEL = cite.LANG_LABEL;
// "span" is the field's width in the 12-column grid the fields render in (see .add-example-jeder-fields
// in the CSS and _setSourceType below) -- field ORDER here is also the grid's reading order.
// Heading/definition-list helpers and the recognized POS heading names, shared with the wizard and
// DefSideBoxes so all three read the same page structure.
var headingOuter = sections.headingOuter;
var languageCodeForOl = sections.languageCodeForOl;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var eachLeafDefinition = sections.eachLeafDefinition;
// A heading's section key, the wizard's unclassified "=== Mane ===" placeholder included: it holds
// real definitions, so examples belong in it, even though it is not a part of speech.
var sectionKeyOf = sections.sectionKeyOf;
// Locating a section and a definition inside it is wikt.edit's job, shared with DefSideBoxes.
var wt = edit.wt;
// Template names that count as real examples, so an annotation template is not mistaken for one.
var exampleTemplateNames = ["jêder"].concat(cite.USEX_NAMES, Object.keys(jederDefs));
// The "no example yet" placeholder, with or without a language parameter.
var EMPTY_EXAMPLE_MARKER = cite.EMPTY_EXAMPLE_MARKER;
// The template name starting a "#:"/"#*" line, or null when the line does not start with one.
function getLineTemplateName(line) {
var m = /^#[:*]\s*\{\{\s*([^|}]+)/.exec(line);
return m ? m[1].trim() : null;
}
// Where a template block starting at startIndex closes its braces, so multi-line calls stay whole.
// The "#" run a definition line carries, so a sense's own annotations can be told from a sub-sense's:
// "#:" belongs to "#", "##:" to "##".
function senseDepth(definitionLine) {
var m = /^#+/.exec(definitionLine || "");
return m ? m[0].length : 1;
}
// Any annotation of that sense -- "#:" an example, "#*" a citation -- at its own depth and no other.
function annotationRegex(definitionLine) {
return new RegExp("^#{" + senseDepth(definitionLine) + "}[:*]");
}
function citationRegex(definitionLine) {
return new RegExp("^#{" + senseDepth(definitionLine) + "}\\*");
}
function findBlockEnd(lines, startIndex) {
var depth = 0;
var i = startIndex;
for (; i < lines.length; i++) {
var opens = (lines[i].match(/\{\{/g) || []).length;
var closes = (lines[i].match(/\}\}/g) || []).length;
depth += opens - closes;
if (depth <= 0) {
break;
}
}
return i;
}
// URL of a template's own wiki page -- lets the user check a Kategorî:Çavkanî search result
// before picking it (see showResults below).
// Escapes str for interpolation into a RegExp: page titles carry regex metacharacters.
var escapeRegExp = edit.util.escapeRegExp;
// The readable half of an mw.Api rejection.
var apiErrorMessage = edit.util.apiErrorMessage;
// Same marker as the wizard's, positioned inside OOUI's own wrapper rather than a new one.
var highlightLinebreak = edit.util.makeLinebreakHighlighter({
prefix: "add-example",
wrap: false,
dataKey: "addExampleLinebreakBackdrop",
title: "Xeta n\u00fb d\u00ea wek <br> b\u00ea niv\u00eesandin"
});
// Linebreak marking on the quote text and its translation/transcription only, not the
// bibliographic fields.
function watchForLinebreaks(widget) {
widget.on("change", function() {
highlightLinebreak(widget.$input[0]);
});
}
// The shared grip from wikt.edit, kept as a sibling inside OOUI's own wrapper and without the saved
// height: these fields are plain MultilineTextInputWidgets with no class to key one by.
function makeManualResizable(textarea) {
edit.util.makeManualResizable(textarea, {
wrap: false,
persist: false,
gripClass: "add-example-resize-grip",
doneKey: "addExampleResizableDone"
});
}
// Applies makeManualResizable to every textarea in `context` (itself or its descendants).
function initManualResize(context) {
var $context = $(context);
var $textareas = $context.is("textarea") ? $context : $context.find("textarea");
$textareas.each(function() {
makeManualResizable(this);
});
}
var api = new mw.Api();
// Kategorî:Çavkanî's templates and their redirects, each redirect its own entry pointing at the
// Language display name, read from the matching section's .besazimani link text.
function languageNameForCode(code) {
var $mark = $(".besazimani[id='" + code + "']").first();
return $mark.length ? $mark.text().trim() : null;
}
// Whether the page title itself (accented Kurdish Latin letters included) is written in Latin script.
var pageTitleIsLatin = !/[^\x00-\x7FÀ-ɏḀ-ỿ‘’]/.test(mw.config.get("wgTitle") || "");
// What an example looks like once rendered: Şablon:jêder's old marker, Modul:usex's containers and
// Modul:quote's wrapper.
var EXAMPLE_SELECTOR = ".example, .benimune, .h-usage-example, .h-quotation, .citation-whole";
// The words drawn for one example: the example or quotation itself, never its translation or source.
function exampleTextOf($item) {
var $own = $item.find(".e-example, .e-quotation").first();
if ($own.length) {
return $own.text();
}
var $italic = $item.children("i").first();
return ($italic.length ? $italic : $item.clone().children("dl, ul, .dsb-example-toolbar").remove().end()).text();
}
// A definition's own citation group, and the last one when it has several: a new citation is
// written after every "#:"/"#*" line already there.
function exampleGroupOf($li) {
return $li.children("ul").filter(function () {
return $(this).children("li").children(EXAMPLE_SELECTOR).length > 0;
}).last();
}
// Every recognized part-of-speech section, down to each of its definitions. Section, language and
// nesting all come from the page's own structure, never from an example that happens to sit there.
// "===" on most entries, "====" where the senses hang off numbered etymologies.
$("h3, h4").each(function() {
var sectionType = sectionKeyOf($(this));
if (!sectionType) {
return; // not a recognized word-sense section (Bilêvkirin, Etîmolojî, Çavkanî, ...)
}
var sectionOrdinal = sectionOrdinalWithinLanguage($(this));
// Forward-walk siblings to the definitions <ol>, skipping over a declension table/TemplateStyles/the
// headword line; stop (nothing to attach to in this section) if another heading comes first instead.
var $ol = null;
for (var $cursor = headingOuter($(this)).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("ol")) {
$ol = $cursor;
break;
}
if (sections.headingDepth($cursor)) {
break;
}
}
if (!$ol) {
return;
}
var language = languageCodeForOl($ol);
if (!language) {
console.warn("Gadget-wikt.add-examples: nikaribe koda zimanê vê beşê bibîne.", $ol);
return;
}
eachLeafDefinition($ol, function($li) {
var definitionLevel = [];
var $definitionItem = $li;
while ($definitionItem.prop("tagName") === "LI") {
// Skip MediaWiki's empty <li class="mw-empty-elt"> so indices match the "#" lines in wikitext.
definitionLevel.splice(0, 0, $definitionItem.prevAll("li:not(.mw-empty-elt)").length);
$definitionItem = $definitionItem.parent().parent();
}
definitionLevel.splice(0, 0, sectionType + "|" + sectionOrdinal);
// Where a submitted example lands: after the group's last example, or after a hidden placeholder
// when the definition has no group.
var $group = exampleGroupOf($li);
var $placeholder = $group.length ? $group.children("li").last()
: $("<li style='display:none'></li>");
var $formItem = $("<li>");
// Never in the DOM: Form toggles it against the title, so it stays as the "is a form built
// for this sense" handle. Nothing on the page opens this gadget -- DefSideBoxes does.
var $button = $("<span>").hide();
// Says which sense and language the open form is for; the row exists only while it is open.
var $title = $("<span class='add-example-form-title'></span>").hide();
$formItem.append($title);
// The group the button lives in: the one on the page, or a new one. A "ul" under a definition
// "li" starts hidden, so a new one needs the explicit display override.
var $ul = $group.length ? $group
: $("<ul>").css("display", "block").append($placeholder);
// A citation already carrying the site's own small edit link does not need a second one.
$ul.find("span.example > span.stubedit").remove();
// The panel opens after $ul, not inside it: full width, clear of the list's hanging indent.
// edit, when given, is {index, text}: the form opens on that example instead of on a new one.
function setFormVisible(visible, edit) {
if (visible) {
// A sense with no example list of its own gets one on first open, not on page load:
// an empty list on every sense of every entry is margin nobody asked for.
// Editing shows its title inside the form, so the list row is only for a new example.
if (!edit && !$ul.parent().length) {
$li.append($ul);
}
if (!edit && !$formItem.parent().length) {
$ul.append($formItem);
}
if (edit) {
$formItem.detach();
}
if (!$button.form) {
$title.text("Nimûneyeke bi " + (languageNameForCode(language) || "zimanekî nenas").toLowerCase() + " lê zêde dikî.");
var form = new Form($placeholder, $button, $title, language, definitionLevel);
form.onHide = function () {
$formItem.detach();
};
$ul.after(form.$element);
}
// Right under the example being edited; after the whole list when adding a new one.
(edit ? $(edit.item) : $ul).after($button.form.$element);
if (edit) {
$button.form.beginEdit(edit.index, edit.text);
} else {
$button.form.endEdit();
$button.form.setVisible(true);
}
} else if ($button.form) {
$button.form.setVisible(false);
$formItem.detach();
}
return false;
}
function openForm() { return setFormVisible(true); }
function toggleForm() { return setFormVisible(!($button.form && $button.form.isVisible())); }
$li.data("wiktAddExampleOpen", openForm);
$li.data("wiktAddExampleToggle", toggleForm);
// Its place among the sense's examples finds the line; the words drawn for it confirm it.
$li.data("wiktAddExampleEdit", function (itemEl) {
var $item = $(itemEl);
var index = sections.exampleItemsOf($li).index($item);
if (index === -1) {
return false;
}
setFormVisible(true, { index: index, text: exampleTextOf($item), item: itemEl });
return true;
});
});
});
// Pulls the form panel out to the page's own edges, undoing the nested list's indent.
function alignFormToPage($form) {
var $container = $form.closest(".mw-parser-output");
if (!$container.length) {
return;
}
// Measure unshifted: this runs on every re-open, and a margin left from last time would otherwise
// count as part of the indent and cancel the alignment out.
$form.css("marginLeft", 0);
var pad = parseFloat(window.getComputedStyle($container[0]).paddingLeft) || 0;
var containerLeft = $container.offset().left + pad;
var indent = $form.offset().left - containerLeft;
$form.css({
marginLeft: indent > 0 ? -indent : 0,
width: $container.width() + "px"
});
}
/**
* language is the section the sense sits in: what the new example is tagged with, and what the
* wikitext lookup searches for. Never the language of an example that happens to be there.
*/
function Form($lastExample, $button, $title, language, definitionLevel) {
var self = this;
this._language = language;
this._definitionLevel = definitionLevel;
this._$lastExample = $lastExample;
this._$button = $button;
this._$button.form = this;
this._$title = $title;
// Where the title lives when adding; while editing it moves into the form itself.
this._$titleHome = $title.parent();
this._currentJederKey = "";
this._jederFieldWidgets = {};
// Set once a template is picked in the Kategorî:Çavkanî mode (see _selectCategoryTemplate); the
// canonical template title (without the "Şablon:" prefix), used as `sablon` in _doSubmit.
this._categoryTemplateName = null;
// Set by _renderCategoryTemplateFields if the picked template's TemplateData has a "koda zimanî" param;
// that param's own key, used to emit "<key>=<language>" in _doSubmit -- null if no such param exists.
this._categoryTemplateLangParamKey = null;
// Set while an example already on the page is being edited (see beginEdit): {index, original,
// example, key}, where key is the source type the line was read into.
this._editing = null;
this._addTitle = null;
// The same prompt the wizard's own example box carries, so the two ask for an example in one
// wording and the language it is to be written in is on screen here too.
this._textInput = new OO.ui.MultilineTextInputWidget({
rows: 4,
placeholder: edit.util.examplePrompt(languageNameForCode(language), false)
});
var textInputLayout = new OO.ui.FieldLayout(this._textInput, {
label: "Metna nimûneyê",
align: "top",
});
// Under the text box: what will happen to the entry's own name on save (see boldenPageTitle), and
// the one markup button worth having beside it. Purely informational, never blocks submitting.
this._titleBoldHint = $("<span class='add-example-title-bold-hint'></span>");
this._updateTitleBoldHint = function() {
var text = self._textInput.getValue().trim();
var title = mw.config.get("wgTitle");
var missing = !!text && !boldenPageTitle(text, title).bolded;
self._titleBoldHint
.toggleClass("add-example-title-bold-warn", missing)
.html(missing
? "Sermade (" + mw.html.escape(title) + ") nehat dîtin; dê neyê <b>stûr</b>kirin."
: "Sermade otomatîk tê <b>stûr</b>kirin.");
// Only where it is needed: when the name is bolded on its own there is nothing to reach for.
self._boldButton.toggle(missing);
};
// Shown only while the name cannot be bolded on its own -- a form the matcher does not recognise,
// or another word the writer wants bolded.
this._boldButton = $("<button type='button' class='add-example-bold-btn'></button>").hide()
.text(BOLD_MARKS).attr("title", "Nivîsa hilbijartî stûr bike");
this._boldButton.on("click", function(e) {
e.preventDefault();
var input = self._textInput.$input[0];
var value = input.value;
var start = input.selectionStart;
var end = input.selectionEnd;
var next, caret;
if (start !== end) {
next = value.slice(0, start) + BOLD_MARKS + value.slice(start, end) + BOLD_MARKS + value.slice(end);
caret = end + BOLD_MARKS.length * 2;
} else {
// Nothing selected: just the marks, where the caret is. Typing carries on after them.
next = value.slice(0, start) + BOLD_MARKS + value.slice(start);
caret = start + BOLD_MARKS.length;
}
self._textInput.setValue(next);
input.focus();
input.setSelectionRange(caret, caret);
});
this._textTools = $("<div class='add-example-text-tools'></div>")
.append(this._boldButton, this._titleBoldHint);
this._updateTitleBoldHint();
// A pasted citation switches to raw mode rather than being wrapped in a second {{nimûne}}: a whole
// line with its marker and a bare template call both count. Only while the type is still citoid.
this._maybeSwitchToRaw = function(value) {
if (self._currentJederKey !== "citoid" ||
!(cite.looksLikeCitationLine(value) || cite.looksLikeTemplate(value))) {
return;
}
var menu = self._sourceTypeDropdown.getMenu();
var item = menu.findItemFromData(RAW_TEMPLATE_KEY);
if (item) {
menu.chooseItem(item); // calls _setSourceType() itself
}
};
this._textInput.on("change", function(value) {
self._applyButton.setDisabled(!value);
self._maybeSwitchToRaw(value);
self._updateReadyHint();
self._updateTitleBoldHint();
});
watchForLinebreaks(this._textInput);
// Source type: free text, a jêder-* template, or "citoid" -- a sentinel meaning "not yet detected",
// switched automatically once a Citoid fetch succeeds.
var sourceTypeMenuItems = [
new OO.ui.MenuOptionWidget({ data: "citoid", label: msg.citoidAuto }),
new OO.ui.MenuOptionWidget({ data: "", label: msg.sourceByHand }),
];
for (var jederKey in jederDefs) {
if (jederDefs.hasOwnProperty(jederKey)) {
sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({
data: jederKey,
label: jederDefs[jederKey].label,
}));
}
}
sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({
data: CATEGORY_TEMPLATE_KEY,
label: msg.sourceFromCategory,
}));
// Paste a finished citation into the example box and have it saved exactly as typed -- the quickest
// way to reuse one jêgirtin across several entries.
sourceTypeMenuItems.push(new OO.ui.MenuOptionWidget({
data: RAW_TEMPLATE_KEY,
label: msg.sourceRaw,
}));
// Shown only in raw mode, where there is nothing else to fill in.
this._rawTemplateHint = new OO.ui.MessageWidget({
type: "notice",
inline: true,
label: "Şablona temam li qada \"Metna nimûneyê\" binivîse \u2014 wek xwe tê qeydkirin. "
+ "Tenê navê maddeyê yê di nav jêgirtin= de tê stûrkirin.",
classes: ["add-example-raw-hint"],
});
this._rawTemplateHint.toggle(false);
this._sourceTypeDropdown = new OO.ui.DropdownWidget({
menu: { items: sourceTypeMenuItems },
label: msg.citoidAuto,
});
var sourceTypeLayout = new OO.ui.FieldLayout(this._sourceTypeDropdown, {
label: "Şablona çavkaniyê",
align: "top",
});
this._sourceTypeDropdown.getMenu().on("choose", function(menuOption) {
self._setSourceType(menuOption.getData());
});
this._sourceInput = new OO.ui.MultilineTextInputWidget();
this._sourceInput.on("change", function() {
self._updateReadyHint();
});
this._sourceInputLayout = new OO.ui.FieldLayout(this._sourceInput, {
label: "Çavkaniya nimûneyê",
align: "top",
});
// Visible in every mode: submit() reads it as "url=" regardless. The Citoid button sits beside it.
this._sourceURLInput = new OO.ui.TextInputWidget({
icon: "link",
placeholder: "https://… an jî DOI/ISBN",
});
this._citoidButton = new OO.ui.ButtonWidget({
label: msg.citoidFill,
icon: "download",
flags: ["progressive"],
title: "Li gorî URL, DOI an ISBNya jorîn, malûmatan ji Citoid tîne û qadên şablona jêder-* otomatîk tijî dike.",
classes: ["add-example-citoid-fetch"],
});
this._citoidButton.on("click", this._fillFromCitoid.bind(this));
this._sourceURLInput.on("change", function() {
self._updateReadyHint();
self._autofillVideoTimestamp();
});
// URL input and its auto-fill button as one flex row; a bare OO.ui.Widget used as the container.
this._urlRowWidget = new OO.ui.Widget({ classes: ["add-example-url-row"] });
this._urlRowWidget.$element.append(this._sourceURLInput.$element, this._citoidButton.$element);
this._sourceURLInputLayout = new OO.ui.FieldLayout(this._urlRowWidget, {
label: "Lînk, DOI an ISBNya çavkaniyê",
align: "top",
help: "Li qutiyê URL, DOI an ISBN binivîse û pê lê bike butona '" + msg.citoidFill + "' da ku otomatîk çavkanî were dagirtin.",
helpInline: true,
});
// Same wording as above, so the initial _setSourceType("citoid") rebuilds nothing.
this._urlFieldIsFreeTextWording = false;
// Shown right after a successful Citoid fetch (see _fillFromCitoid); hidden again on clear.
this._citoidSuccessMsg = $("<span class='add-example-citoid-success'></span>").text(msg.citoidFilled).hide();
// Clears the URL field and any filled jêder-* fields without touching the example text/translation.
this._citoidClearButton = new OO.ui.ButtonWidget({
label: "Vala bike",
icon: "trash",
framed: false,
title: "Lînkê û qadên jêder-* yên vê beşê vala bike",
classes: ["add-example-citoid-clear"],
});
this._citoidClearButton.on("click", function() {
self._sourceURLInput.setValue("");
self._citoidSuccessMsg.hide();
Object.keys(self._jederFieldWidgets).forEach(function(key) {
self._jederFieldWidgets[key].setValue("");
});
});
// Plain div (not OO.ui.FieldsetLayout) so the grid CSS class applies directly to the field items.
this._jederFieldsFieldset = $("<div class='add-example-jeder-fields'>");
// Header and clear button above the fields grid. Only meaningful once a real jêder-* template is
// active, so the whole section toggles together in _setSourceType().
this._jederSection = $("<div class='add-example-jeder-section'>").append(
$("<div class='add-example-jeder-header'>")
.append($("<span class='add-example-jeder-header-label'>Malûmatên çavkaniyê</span>"))
.append(this._citoidClearButton.$element),
this._jederFieldsFieldset
);
// wikt.cite's picker, the one the wizard and DefSideBoxes show. Sits under the source-type
// dropdown, not in the fields section, and is built once so switching modes keeps its handlers.
var categoryPicker = cite.buildCategoryTemplateSearch({
describe: true,
onPick: function (entry) {
self._selectCategoryTemplate(entry);
}
});
this._categoryTemplateInput = categoryPicker.$input;
this._categoryTemplateStatus = categoryPicker.$status;
this._categoryTemplateSearchRow = categoryPicker.$element
.addClass("add-example-cat-tpl-search-row").hide();
this._translationInput = new OO.ui.MultilineTextInputWidget({ rows: 4 });
var translationInputLayout = new OO.ui.FieldLayout(this._translationInput, {
// A Kurmancî example gets an explanation rather than a translation.
label: language === "ku" ? "Îzaheke kurmancî ya nimûneyê" : "Tercimeya kurmancî ya nimûneyê",
align: "top",
});
watchForLinebreaks(this._translationInput);
this._transcriptionInput = new OO.ui.MultilineTextInputWidget({ rows: 4 });
var transcriptionInputLayout = new OO.ui.FieldLayout(this._transcriptionInput, {
label: "Translîterasyona nimûneyê",
align: "top",
help: "Tenê lê zêde bike eger metna di nimûneyê de bi alfabeya latînî nehatibe nivîsandin.",
helpInline: true,
});
watchForLinebreaks(this._transcriptionInput);
this._applyButton = new OO.ui.ButtonWidget({
label: "Qeyd bike",
icon: "check",
title: "Nimûneyê ji bo vê tarîfê nimûneyê lê zêde bike",
flags: ["progressive", "primary"],
disabled: true,
});
this._applyButton.on("click", this.submit.bind(this));
// Reactive "ready to submit" hint next to Qeyd bike: no text yet, text but no source (still submittable
// as a bare {{nimûne}}), or fully ready -- kept in sync by _updateReadyHint(), wired to every field below.
this._readyHint = $("<span class='add-example-ready-hint'></span>").text("Metna nimûneyê lazim e.");
// True while a Citoid-eligible URL is typed but unfetched and no jêder field has been filled by
// hand -- the "looks done but isn't" case.
this._citoidPending = function() {
return self._currentJederKey !== "" &&
self._currentJederKey !== CATEGORY_TEMPLATE_KEY &&
self._currentJederKey !== RAW_TEMPLATE_KEY &&
!!self._sourceURLInput.getValue().trim() &&
!self._citoidSuccessMsg.is(":visible") &&
!Object.keys(self._jederFieldWidgets).some(function(key) {
return self._jederFieldWidgets[key].getValue().trim();
});
};
// A Kurmancî jêgirtin takes no translation or explanation, only a {{nimûne}} does. An edited line
// that already has one keeps its box.
this._syncTranslationBox = function() {
if (self._language !== "ku") {
return;
}
var key = self._currentJederKey;
var isQuote = key === RAW_TEMPLATE_KEY || key === CATEGORY_TEMPLATE_KEY || !!jederDefs[key] ||
!!self._sourceInput.getValue().trim() || !!self._sourceURLInput.getValue().trim();
translationInputLayout.toggle(!isQuote ||
!!(self._editing && self._translationInput.getValue().trim()));
};
this._updateReadyHint = function() {
self._syncTranslationBox();
if (!self._textInput.getValue()) {
self._readyHint.text("Metna nimûneyê lazim e.");
return;
}
if (self._citoidPending()) {
self._readyHint.text("Pêşî '" + msg.citoidFill + "' bitikîne.");
return;
}
// Raw mode carries its own source inside the pasted template, so there is nothing else to ask for.
if (self._currentJederKey === RAW_TEMPLATE_KEY) {
self._readyHint.text("Hazir e.");
return;
}
var hasSource = self._sourceInput.getValue().trim() || self._sourceURLInput.getValue().trim() ||
Object.keys(self._jederFieldWidgets).some(function(key) {
return self._jederFieldWidgets[key].getValue().trim();
});
self._readyHint.text(hasSource ? "Hazir e." : "Çavkanî nehatiye dayîn.");
};
// Prefills Dem from the URL's own "t=" once jêder-vîdeo is active; never overwrites typed text.
this._autofillVideoTimestamp = function() {
var demWidget = self._jederFieldWidgets.dem;
if (self._currentJederKey !== "jêder-vîdeo" || !demWidget || demWidget.getValue().trim()) {
return;
}
var dem = wikt.citoid.extractVideoTimestamp(self._sourceURLInput.getValue().trim());
if (dem && !demWidget.getValue().trim()) {
demWidget.setValue(dem);
}
};
// framed:false (+ the "destructive" flag for the red text/hover) matches the mockup's borderless
// "Betal bike" -- a plain colored link rather than a boxed button, distinguishing it from Paqij bike.
this._cancelButton = new OO.ui.ButtonWidget({
label: "Betal bike",
icon: "close",
title: "Formê bigire",
flags: ["destructive"],
framed: false,
});
this._cancelButton.on("click", function() {
self.setVisible(false);
});
this._clearButton = new OO.ui.ButtonWidget({
label: "Paqij bike",
icon: "trash",
title: "Hemû qadan vala bike û pêşnûyisên hilanîbûyî jê bibe",
});
this._clearButton.on("click", function() {
self.clear();
self._clearDraft();
});
// Only while an example on the page is being edited.
this._deleteButton = new OO.ui.ButtonWidget({
label: "Jê bibe",
icon: "trash",
flags: ["destructive"],
});
this._deleteButton.toggle(false);
this._deleteButton.on("click", function() {
OO.ui.confirm("Ev nimûne dê were rakirin. Dewam bike?").done(function(ok) {
if (ok) {
self._submitDelete();
}
});
});
// A CSS spinner rather than a Commons GIF: no request, and it follows dark/light mode.
this._loadingImage = new OO.ui.Widget({
classes: ["add-example-loading"],
});
// The ring alone says nothing to a reader who has asked for less motion: MediaWiki's own
// reduced-motion reset cuts every animation to 0.01ms, so it never turns for them. The words
// beside it carry the state either way.
this._loadingImage.$element.append(
$('<span class="add-example-spinner" aria-hidden="true"></span>'),
$('<span class="add-example-loading-label"></span>').text(msg.loading)
);
this._loadingImage.toggle(false);
// Two-column top: example text (with translation/transliteration for non-ku) on the left, source
// type and link on the right. Plain divs, so the CSS can lay them out as a grid.
// The translation box is there for Kurmancî too: {{nimûne}} takes one for every language.
var $topLeft = $("<div class='add-example-top-col'>").append(textInputLayout.$element, this._textTools,
translationInputLayout.$element);
if (!pageTitleIsLatin) {
$topLeft.append(transcriptionInputLayout.$element);
}
// Kept for _fill, which adds either box when an edited line already carries one.
this._$topLeft = $topLeft;
this._translationInputLayout = translationInputLayout;
this._transcriptionInputLayout = transcriptionInputLayout;
var $topRight = $("<div class='add-example-top-col'>").append(
sourceTypeLayout.$element,
this._categoryTemplateSearchRow,
this._rawTemplateHint.$element,
this._sourceInputLayout.$element,
this._sourceURLInputLayout.$element,
this._citoidSuccessMsg
);
var $topGrid = $("<div class='add-example-top-grid'>").append($topLeft, $topRight);
// Saved on the instance (not just a local var) so _setSourceType() can swap the URL field's label/help
// wording in and out of it when switching to/from free text (see the FieldLayout replaceWith() there).
this._fieldsLayout = $("<div class='add-example-fieldset'>").append($topGrid, this._jederSection);
// Bottom action row: Qeyd bike (+ spinner and hint), a spacer, then Paqij bike and Betal bike. A
// flex div rather than an OOUI layout, so the hint and spacer can sit among the widgets.
var $buttonsRow = $("<div class='add-example-main-buttons'>").append(
this._applyButton.$element,
this._loadingImage.$element,
this._readyHint,
$("<span class='add-example-buttons-spacer'></span>"),
this._deleteButton.$element,
this._clearButton.$element,
this._cancelButton.$element
);
this._frame = new OO.ui.PanelLayout({
id: "add-example-definition-{0}-form".format(this._definitionLevel.join("-")),
classes: ["add-example-form"],
expanded: false,
});
this._frame.$element.append(this._fieldsLayout, $buttonsRow);
// selectItemByData() does not fire "choose", so the initial mode is set up by hand.
this._sourceTypeDropdown.getMenu().selectItemByData("citoid");
this._setSourceType("citoid");
initManualResize(this._frame.$element);
}
Form.prototype = {
// Returns the jQuery object for this form.
get $element() {
return this._frame.$element;
},
// Switches the source section between free text, Citoid auto-detect and a chosen jêder-* template,
// rebuilding the field list. The URL field stays visible in every mode.
_setSourceType: function(jederKey) {
var self = this;
this._currentJederKey = jederKey;
this._jederFieldsFieldset.empty();
this._jederFieldWidgets = {};
this._categoryTemplateName = null;
this._categoryTemplateLangParamKey = null;
var isRealJederKey = jederKey && jederDefs[jederKey];
var isCategoryTemplateKey = jederKey === CATEGORY_TEMPLATE_KEY;
var isRawTemplate = jederKey === RAW_TEMPLATE_KEY;
if (isRealJederKey) {
var def = jederDefs[jederKey];
def.fields.forEach(function(field) {
var widget = field.multiline
? new OO.ui.MultilineTextInputWidget({ rows: 2, autosize: false })
: new OO.ui.TextInputWidget();
self._jederFieldWidgets[field.key] = widget;
widget.on("change", function(value) {
// sal and tarîx say the same thing twice and Modul:quote refuses both; filling one
// clears the other rather than letting the citation break on save.
var partner = value && cite.exclusivePartner(field.key);
if (partner && self._jederFieldWidgets[partner]) {
self._jederFieldWidgets[partner].setValue("");
}
self._updateReadyHint();
});
var layout = new OO.ui.FieldLayout(widget, {
label: field.label,
align: "top",
classes: ["add-example-field-span-" + (field.span || 4)],
});
self._jederFieldsFieldset.append(layout.$element);
});
}
// The box lives in $topRight always; here it is only shown for this mode, and reset on the way out
// so a stale query does not linger.
this._categoryTemplateSearchRow.toggle(isCategoryTemplateKey);
// Raw mode has no source fields of its own: the citation is already written out in full.
this._rawTemplateHint.toggle(isRawTemplate);
if (!isCategoryTemplateKey) {
this._categoryTemplateInput.val("");
}
// The fields grid is for a real jêder-* template. The Kategorî:Çavkanî picker shows it only once a
// template is picked (see _selectCategoryTemplate).
this._jederSection.toggle(!!isRealJederKey);
// Free text textarea only for "" (free text); hidden for "citoid" and any real jêder-* key.
this._sourceInputLayout.toggle(jederKey === "");
// No Citoid button for free text (nothing to fill) or the Kategorî:Çavkanî picker (no mapping for
// an arbitrary template); shown for auto-detect and any jêder-* key.
this._citoidButton.toggle(jederKey !== "" && !isCategoryTemplateKey && !isRawTemplate);
// Plain "attach a link" wording for free text, Citoid wording otherwise. Rebuilt only when crossing
// that boundary.
var isFreeText = jederKey === "";
if (this._urlFieldIsFreeTextWording !== isFreeText) {
this._urlFieldIsFreeTextWording = isFreeText;
// On the <input> itself: TextInputWidget has no setter for its placeholder.
this._sourceURLInput.$input.attr("placeholder", isFreeText ? "URLyeke wekî https://…" : "URLyek wekî https://… an jî DOI/ISBN");
var $oldLayout = this._sourceURLInputLayout.$element;
// Re-wraps the SAME _urlRowWidget (still holding _sourceURLInput + _citoidButton) in a new
// FieldLayout with the new label/help; the widget's $element moves along with it.
this._sourceURLInputLayout = new OO.ui.FieldLayout(this._urlRowWidget, isFreeText ? {
label: "Adresa webê ya nimûneyê",
align: "top",
help: "Tenê binivîse eger lînk jixwe di referansa çavkaniyê de tinebe.",
helpInline: true,
} : {
label: "Lînk, DOI an ISBNya çavkaniyê",
align: "top",
help: "Ev dibe lînka çavkaniyê (url=). Ji bo şablonên jêder-*, dikare bi Citoid an bi destan jî bên dagirtin.",
helpInline: true,
});
$oldLayout.replaceWith(this._sourceURLInputLayout.$element);
}
// The URL/Citoid row is normally always visible (see the constructor comment), but Citoid has no
// mapping for an arbitrary Kategorî:Çavkanî template, so it'd just be dead UI in that one mode.
this._sourceURLInputLayout.toggle(!isCategoryTemplateKey && !isRawTemplate);
this._autofillVideoTimestamp();
this._updateReadyHint();
initManualResize(this._jederFieldsFieldset); // covers a freshly-rebuilt "sernav" field (jêder-*); the Kategorî:Çavkanî picker's own fields are resized separately once picked, see _renderCategoryTemplateFields
},
// Wires the search box's handlers and starts the template fetch. Called once: the box lives in
// $topRight and is only shown or hidden by _setSourceType.
// Records the picked template's canonical name -- a redirect resolves to its target -- and shows
// its parameter fields.
_selectCategoryTemplate: function(entry) {
// Left as the plain label (not "label → target") -- putting the arrow in the input's own value
// would get re-used as the search query if the box is focused again to pick something else.
this._categoryTemplateInput.val(entry.label);
this._categoryTemplateName = entry.target;
this._jederSection.toggle(true);
return this._renderCategoryTemplateFields(entry.target);
},
// One text input per TemplateData param of `name`, into the shared "Malûmatên çavkaniyê" grid, or a
// lone "Rûpel" box when the template has no TemplateData.
_renderCategoryTemplateFields: function(name) {
var self = this;
this._jederFieldsFieldset.empty();
this._jederFieldWidgets = {};
this._categoryTemplateLangParamKey = null;
return cite.templateParams(name).then(function(params) {
// A param labeled "koda zimanî" is filled from the language dropdown: found before filtering, so
// it is left out of the fields and emitted by _doSubmit.
(params || []).forEach(function(p) {
if ((p.label || "").trim().toLowerCase() === CATEGORY_TEMPLATE_LANG_LABEL) {
self._categoryTemplateLangParamKey = p.key;
}
});
var filtered = (params || []).filter(function(p) {
return CATEGORY_TEMPLATE_COVERED_PARAMS.indexOf(p.key) === -1 && p.key !== self._categoryTemplateLangParamKey;
});
var list = filtered.length ? filtered : [{ key: "rûpel", label: "Rûpel", required: false }];
list.forEach(function(p) {
// Reuse the jêder-* field's own width and multiline when the key matches one (cite.fieldSize).
var sizeHint = cite.fieldSize(p.key);
var widget = (sizeHint && sizeHint.multiline)
? new OO.ui.MultilineTextInputWidget({ rows: 2, autosize: false })
: new OO.ui.TextInputWidget();
self._jederFieldWidgets[p.key] = widget;
widget.on("change", function() {
self._updateReadyHint();
});
var layout = new OO.ui.FieldLayout(widget, {
label: p.label,
align: "top",
classes: ["add-example-field-span-" + (sizeHint ? sizeHint.span : 4)],
});
self._jederFieldsFieldset.append(layout.$element);
});
self._updateReadyHint();
initManualResize(self._jederFieldsFieldset); // matters now that a matched key like "sernav" can render multiline
});
},
// Fetches data via wikt.citoid, picks the matching jêder-* template, and fills its empty fields.
_fillFromCitoid: function() {
var self = this;
var query = this._sourceURLInput.getValue().trim();
if (!query) {
mw.notify(msg.citoidNeedsUrl);
return;
}
this._citoidButton.setDisabled(true);
this._loadingImage.toggle(true);
cite.fetchFromCitoid(query).then(function(result) {
// Citoid may pick the type only while none is chosen; a hand-picked one is kept.
var wasAutoDetect = self._currentJederKey === "citoid";
if (wasAutoDetect) {
var menu = self._sourceTypeDropdown.getMenu();
var item = menu.findItemFromData(result.jederKey);
if (item) {
menu.chooseItem(item); // this calls _setSourceType(jederKey) automatically
}
}
var targetJederKey = self._currentJederKey;
// Citoid shaped its fields for the type it detected; re-derive them when the active template
// differs.
var fields = targetJederKey === result.jederKey ? result.fields
: cite.citoidFieldsFor(targetJederKey, result.raw, query);
Object.keys(fields).forEach(function(key) {
var widget = self._jederFieldWidgets[key];
if (widget && !widget.getValue()) {
widget.setValue(fields[key]);
}
});
self._citoidSuccessMsg.show();
self._updateReadyHint();
mw.notify("Malûmat ji Citoid hatin barkirin.");
}, function(errorMsg) {
// Citoid cannot fetch some sites (x.com), so fall back to jêder-websîte with a guessed name.
var fallbackSite = wikt.citoid.websiteNameForUrl(query);
if (fallbackSite) {
if (self._currentJederKey !== "jêder-websîte") {
var menu = self._sourceTypeDropdown.getMenu();
var item = menu.findItemFromData("jêder-websîte");
if (item) {
menu.chooseItem(item); // this calls _setSourceType("jêder-websîte") automatically
}
}
var websiteWidget = self._jederFieldWidgets["websîte"];
if (websiteWidget && !websiteWidget.getValue()) {
websiteWidget.setValue(fallbackSite);
}
mw.notify("Citoid nikare vê malperê bixwîne; şablona Websîte hate hilbijartin, ji kerema xwe mayî bi destan tijî bike.");
return;
}
mw.notify(typeof errorMsg === "string" ? errorMsg : "Nikare malûmatan ji Citoid bistîne. Kontrol bike ka URL/DOI/ISBN rast e.");
}).always(function() {
self._citoidButton.setDisabled(false);
self._loadingImage.toggle(false);
});
},
// Shows or hides the form and restores any saved draft. The title takes the button's place while
// open; the button stays put for when it closes.
isVisible: function() {
return this._frame.isVisible();
},
setVisible: function(visible) {
this._$button.toggle(!visible);
this._$title.toggle(visible);
this._frame.toggle(visible);
// Closing from the form's own "Betal bike" has to take the row away too, or the empty <li>
// it lives in is left behind as a bullet with nothing after it.
if (!visible && typeof this.onHide === "function") {
this.onHide();
}
if (visible) {
// Says what the button will actually do: publish now, or join whoever has taken the save
// over. Set here rather than at build time, since the owner may install itself later.
this._applyButton.setLabel(wikt.addExamples.submitHandler ? msg.add : "Qeyd bike");
alignFormToPage(this._frame.$element);
// A draft belongs to a new example; an edited one opens with what its line says.
var draft = this._editing ? {} : (draftStorage.Get() || {});
if (!this._textInput.getValue() && draft.text) {
this._textInput.setValue(draft.text);
}
if (!this._sourceInput.getValue() && draft.source) {
this._sourceInput.setValue(draft.source);
}
if (!this._sourceURLInput.getValue() && draft.sourceURL) {
this._sourceURLInput.setValue(draft.sourceURL);
}
if (!this._translationInput.getValue() && draft.translation) {
this._translationInput.setValue(draft.translation);
}
if (!this._transcriptionInput.getValue() && draft.transcription) {
this._transcriptionInput.setValue(draft.transcription);
}
}
},
// Opens the form on the index-th example of this sense. drawnText is what the page shows for it: the
// line found at that place has to say the same before anything from it is put in the form.
beginEdit: function(index, drawnText) {
var self = this;
this.clear();
this._editing = { index: index, original: null, example: null, key: null };
if (this._addTitle === null) {
this._addTitle = this._$title.text();
}
this._$title.text("Vê nimûneyê diguherînî.");
this._frame.$element.prepend(this._$title);
this._deleteButton.toggle(true);
this.setVisible(true);
this._applyButton.setDisabled(true);
this._deleteButton.setDisabled(true);
this._loadingImage.toggle(true);
var sectionKeyParts = this._definitionLevel[0].split("|");
api.get({
action: "query",
prop: "revisions",
titles: mw.config.get("wgPageName"),
rvprop: "content",
rvslots: "main",
formatversion: 2
}).then(function(data) {
var editing = self._editing;
if (!editing || editing.index !== index || editing.original !== null) {
return; // closed, or opened on another example, while this was loading
}
var content = data.query.pages[0].revisions[0].slots.main.content;
// The page as it will read once the queued changes are saved, so an example already edited
// in the queue is found as it is drawn.
if (typeof wikt.addExamples.pendingContent === "function") {
content = wikt.addExamples.pendingContent(content);
}
var loc = wt.locateSection(content, self._language, sectionKeyParts[0], parseInt(sectionKeyParts[1], 10));
var def = wt.findDefinitionLine(loc.lines, loc.posLineIndex, self._definitionLevel);
var block = cite.exampleBlocks(loc.lines, def).filter(function(b) {
return b.example;
})[index];
if (!block || !cite.exampleMatches(drawnText, block.example)) {
throw new edit.util.SubmitAbort("Ev nimûne di wîkîtextê de nehat dîtin — rûpelê nû bike û dîsa biceribîne.");
}
editing.original = block.text;
editing.example = block.example;
return self._fill(block.example).then(function(aliasMap) {
editing.aliases = aliasMap;
editing.key = self._currentJederKey;
self._syncTranslationBox();
self._deleteButton.setDisabled(false);
self._loadingImage.toggle(false);
});
}).catch(function(err) {
self._loadingImage.toggle(false);
mw.notify(err instanceof edit.util.SubmitAbort ? err.message : "Nikare muhtewaya heyî bixwîne.", { type: "error" });
self.setVisible(false);
self.endEdit();
});
},
// Leaves edit mode: the next time the form opens, it adds a new example again.
endEdit: function() {
if (!this._editing) {
return;
}
this._editing = null;
this._deleteButton.toggle(false);
if (this._addTitle !== null) {
this._$title.text(this._addTitle);
}
this._$titleHome.append(this._$title);
this.clear();
},
// Puts a line read by cite.parseCitationLine() into the form. A plain italic line and every
// {{nimûne}} redirect read as {{nimûne}}, so saving writes them as that. Resolves once the fields
// that depend on the template's own parameters -- a Kategorî:Çavkanî template's boxes, an alias
// such as "r=" for "rûpel=" -- are drawn and filled.
_fill: function(example) {
var self = this;
var key = example.mode === "jeder" ? example.template
: example.mode === "category" ? CATEGORY_TEMPLATE_KEY
: example.mode === "raw" ? RAW_TEMPLATE_KEY
: "";
this._sourceTypeDropdown.getMenu().selectItemByData(key);
this._setSourceType(key);
this._sourceInput.setValue(example.source);
this._sourceURLInput.setValue(example.sourceURL);
this._transcriptionInput.setValue(example.transcription);
var drawn = example.mode === "category"
? this._selectCategoryTemplate({ label: example.template, target: example.template })
: $.Deferred().resolve().promise();
var aliasTemplate = example.mode === "jeder" || example.mode === "category" ? example.template
: example.mode === "nimune" ? "nimûne"
: example.mode === "source" ? "jêder"
: null;
var aliases = aliasTemplate ? cite.templateAliases(aliasTemplate) : $.Deferred().resolve({}).promise();
return $.when(drawn, aliases).then(function(_, aliasMap) {
cite.applyAliases(example, aliasMap || {}, Object.keys(self._jederFieldWidgets));
// buildCitation() writes the language parameter itself.
var langKey = self._categoryTemplateLangParamKey;
if (langKey) {
example.extra = example.extra.filter(function(param) {
return $.trim(param.split("=")[0]) !== langKey;
});
}
self._textInput.setValue(example.text);
self._translationInput.setValue(example.translation);
Object.keys(example.fields).forEach(function(field) {
self._jederFieldWidgets[field].setValue(example.fields[field]);
});
// A box the form leaves out for this language still opens when the line already has something in it.
[[example.translation, self._translationInputLayout], [example.transcription, self._transcriptionInputLayout]]
.forEach(function(pair) {
if (pair[0] && !$.contains(self._$topLeft[0], pair[1].$element[0])) {
self._$topLeft.append(pair[1].$element);
}
});
// Kept for the save, which puts the rebuilt line's parameters back in the page's order.
return aliasMap || {};
});
},
// Removes the example being edited, through the same hand-over as a save.
_submitDelete: function() {
var self = this;
function settle() {
self._deleteButton.setDisabled(false);
self._applyButton.setDisabled(!self._textInput.getValue());
}
this._applyButton.setDisabled(true);
this._deleteButton.setDisabled(true);
this._sendReplacement(null, "−nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION), settle, function(message) {
mw.notify(message || msg.saveFailed, { type: "error" });
settle();
});
},
// Replaces the example being edited with code, or removes it when code is null: queued with
// DefSideBoxes when it has taken the save over, published at once otherwise.
_sendReplacement: function(code, summary, reenable, error) {
var self = this;
var editing = this._editing;
var parts = this._definitionLevel[0].split("|");
var language = this._language;
var definitionLevel = this._definitionLevel;
function transform(revision) {
return {
text: cite.replaceExample(revision.content, language, parts[0], parseInt(parts[1], 10),
definitionLevel, editing.index, editing.original, code),
summary: summary
};
}
var handOver = wikt.addExamples.submitHandler;
if (typeof handOver === "function") {
handOver({
transform: transform,
summary: summary,
// No key of its own: an example edited twice is two entries, applied in turn, the
// second reading the line as the first left it.
done: function() {
self.setVisible(false);
self.endEdit();
reenable();
},
fail: function(message) {
error(message || "Nikare li rêza guhartinan zêde bike.");
}
});
return;
}
api.edit(mw.config.get("wgPageName"), transform).then(function() {
location.reload();
}, function(codeOrAbort, result) {
error(codeOrAbort instanceof edit.util.SubmitAbort ? codeOrAbort.message
: msg.saveFailed + apiErrorMessage(codeOrAbort, result));
});
},
// Drops the saved draft so it cannot resurface on another page.
_clearDraft: function() {
draftStorage.Set({});
},
// Clears all fields in this form.
clear: function() {
this._textInput.setValue("");
this._sourceInput.setValue("");
this._sourceURLInput.setValue("");
this._translationInput.setValue("");
this._transcriptionInput.setValue("");
this._citoidSuccessMsg.hide();
this._sourceTypeDropdown.getMenu().selectItemByData("citoid");
this._setSourceType("citoid");
},
// Warns when a Citoid URL was typed but never fetched, then defers to _doSubmit. A name that
// cannot be bolded is reported under the text box instead, never blocking.
submit: function() {
if (this._citoidPending() && !window.confirm("Te lînk, DOI an ISBN nivîsand lê hîn jê malûmat nehatine standin. Bêyî wan berdewam bikî?")) {
return;
}
this._doSubmit();
},
// Generates and submits the wikicode, then inserts the resulting HTML element if no errors occurred.
_doSubmit: function() {
var self = this;
self._textInput.setDisabled(true);
self._sourceInput.setDisabled(true);
self._sourceURLInput.setDisabled(true);
self._translationInput.setDisabled(true);
self._transcriptionInput.setDisabled(true);
self._sourceTypeDropdown.setDisabled(true);
Object.keys(self._jederFieldWidgets).forEach(function(key) {
self._jederFieldWidgets[key].setDisabled(true);
});
// self._disableTranslationChk.setDisabled(true);
this._applyButton.setDisabled(true);
this._clearButton.setDisabled(true);
this._loadingImage.toggle(true);
var source = this._sourceInput.getValue().trim();
var sourceURL = this._sourceURLInput.getValue().trim();
var text = this._textInput.getValue().trim();
// The filled-in fields of the selected template. buildCitation() adds the language parameter and
// the "Ç:" flag itself.
var jederFields = {};
var usingCategoryTemplate = this._currentJederKey === CATEGORY_TEMPLATE_KEY && !!this._categoryTemplateName;
if (usingCategoryTemplate) {
Object.keys(self._jederFieldWidgets).forEach(function(key) {
// A raw linebreak would break the single-line "#*" wikitext just like in the example text,
// but these are plain param values (not display text), so collapse to a space, not <br>.
var value = nlToSpace(self._jederFieldWidgets[key].getValue().trim());
if (value) {
jederFields[key] = value;
}
});
} else if (this._currentJederKey && jederDefs[this._currentJederKey]) {
jederDefs[this._currentJederKey].fields.forEach(function(field) {
var widget = self._jederFieldWidgets[field.key];
var value = widget ? nlToSpace(widget.getValue().trim()) : "";
if (value) {
jederFields[field.key] = value;
}
});
cite.withLinkAsUrl(this._currentJederKey, jederFields, sourceURL);
}
// A Kategorî:Çavkanî template is used as soon as it's picked, even with every field left blank
// (still submittable as a bare jêgirtin=); a jêder-* type only counts once a field is actually filled.
var usingJederTemplate = usingCategoryTemplate || (this._currentJederKey && Object.keys(jederFields).length > 0);
var usingRawTemplate = this._currentJederKey === RAW_TEMPLATE_KEY;
var minakAnNa, sablon;
if (usingRawTemplate) {
// A citation, so "#*" like the others -- the line itself is built below, not assembled here.
minakAnNa = "*";
sablon = "";
} else if (usingJederTemplate) {
// The jêder-*/Kategorî:Çavkanî template itself is used as the template, not as the "2=" content.
minakAnNa = "*";
sablon = usingCategoryTemplate ? this._categoryTemplateName : this._currentJederKey;
} else if (source || sourceURL) {
minakAnNa = "*";
sablon = "jêder";
} else {
minakAnNa = ":";
sablon = "nimûne";
}
var listMarker = "".padStart(this._definitionLevel.length - 1, "#") + minakAnNa;
var title = mw.config.get('wgTitle');
// One builder for every citation line here and in the wizard. Bolding the entry name, the language
// parameter, the "Ç:" flag and <br> conversion all belong to it.
var code = cite.buildCitation({
mode: usingRawTemplate ? "raw"
: usingCategoryTemplate ? "category"
: usingJederTemplate ? "jeder"
: "plain",
marker: listMarker,
language: this._language,
title: title,
template: usingCategoryTemplate ? this._categoryTemplateName : this._currentJederKey,
fields: jederFields,
langParamKey: this._categoryTemplateLangParamKey,
quote: text,
source: source,
sourceURL: sourceURL,
translation: this._translationInput.getValue().trim(),
transcription: this._transcriptionInput.getValue().trim(),
// An edited line keeps its translation, and every parameter the form has no box for as long
// as it is still the same kind of line.
keepTranslation: !!this._editing,
extra: this._editing && this._editing.key === this._currentJederKey ? this._editing.example.extra : []
});
// definitionLevel[0] is "type|ordinal": the section's position among same-type headings within its
// language, resolved client-side, never MediaWiki's page-wide DOM id number.
var sectionKeyParts = this._definitionLevel[0].split("|");
var sectionType = sectionKeyParts[0];
var sectionOrdinal = parseInt(sectionKeyParts[1], 10);
if (this._editing) {
// A citation written out by hand is saved exactly as typed, its list marker included.
// Otherwise the rebuilt line keeps the page's own parameter order.
var replacement = usingRawTemplate
? (/^#/.test(text) ? text : this._editing.original.match(/^#+[:*]/)[0] + " " + text)
: cite.keepOrder(code, this._editing.original, this._editing.aliases);
this._sendReplacement(replacement, "±nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION), reenable, error);
return;
}
// Read by the success handler below, which is a sibling of the transform, not nested in it.
var emptyTemplate = false;
// Thrown from inside an edit() transform to bail out with a specific message.
var SubmitAbort = edit.util.SubmitAbort;
// api.edit() fetches the revision it will conflict-check against and hands it here, so the
// insertion runs on exactly that content. Named, so another gadget can run it instead.
function buildEdit(revision) {
var pageContent = revision.content;
// Said here rather than left to the locator: which sections this tool does not work in is
// this tool's own business, and the locator only knows that the key names no section.
if (!sections.headingNamesFor(sectionType)) {
throw new SubmitAbort('Navê beşê xelet e. Di beşên "Ji wêjêyê" û "Ji wêjêya klasîk" ev amûr nayê bikaranîn.');
}
// loc.lines covers the rest of the page from this language's heading on: the saved page is rebuilt
// from it below (see "text:").
var loc = wt.locateSection(pageContent, self._language, sectionType, sectionOrdinal);
var langSectionIndex = loc.langSectionIndex;
var lines = loc.lines;
var targetLineIndex = wt.findDefinitionLine(lines, loc.posLineIndex, self._definitionLevel);
// Where to insert inside this definition's "#:"/"#*" block. findBlockEnd() keeps multi-line
// templates whole; exampleTemplateNames tells a real example from an annotation.
var insertionFound = false;
var lastRelevantBlockEnd = -1;
var isJederFamily = (sablon === "jêder" || Object.prototype.hasOwnProperty.call(jederDefs, sablon));
var scanIndex = targetLineIndex + 1;
// Read off the definition line, so a sub-sense's examples are matched at its own depth and
// a deeper "#" line ends the block instead of being skipped past.
var annotation = annotationRegex(lines[targetLineIndex]);
var citation = citationRegex(lines[targetLineIndex]);
for (; scanIndex < lines.length; scanIndex++) {
var scanLine = lines[scanIndex];
if (!annotation.test(scanLine)) {
// Left this definition's own block (a sub-sense, a new definition, a new section).
break;
}
var blockEnd = findBlockEnd(lines, scanIndex);
var templateName = getLineTemplateName(scanLine);
var isExampleLine = templateName !== null &&
exampleTemplateNames.indexOf(templateName) !== -1;
if (isJederFamily) {
// A jêder-family example goes after every "#:"/"#*" line of this definition.
lastRelevantBlockEnd = blockEnd;
insertionFound = true;
} else if (sablon === "nimûne") {
// "nimûne" examples go before any "#*" line, but after "#:" lines.
if (citation.test(scanLine)) {
break;
}
lastRelevantBlockEnd = blockEnd;
insertionFound = true;
}
scanIndex = blockEnd; // Skip past the whole (possibly multi-line) block
}
var insertAt = lastRelevantBlockEnd >= 0 ? lastRelevantBlockEnd + 1 : scanIndex;
// After the relevant block, or straight after the definition when it had no "#:"/"#*" lines.
lines.splice(insertAt, 0, code);
// The "no example yet" marker goes once a real one lands. The whole "#:"/"#*" block is swept, not
// just its last line: the marker is often followed by other annotations.
var sweepFrom = targetLineIndex + 1;
for (var sweep = sweepFrom; sweep < lines.length; sweep++) {
if (!annotation.test(lines[sweep])) {
break;
}
if (EMPTY_EXAMPLE_MARKER.test(lines[sweep])) {
lines.splice(sweep, 1);
emptyTemplate = true; // tells the DOM update below to replace the marker's <li>
sweep--;
}
}
return {
text: pageContent.slice(0, langSectionIndex) + lines.join("\n"),
summary: "+nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION),
};
}
// DefSideBoxes takes the save over when its panel opened this form: the edit joins that gadget's
// queue instead of publishing now.
var handOver = wikt.addExamples.submitHandler;
if (typeof handOver === "function") {
handOver({
transform: buildEdit,
summary: "+nimûneya bikaranînê bi amûra « {0} » (v{1}).".format(NAME, VERSION),
done: function() {
self.setVisible(false);
self.clear();
self._clearDraft();
reenable();
},
fail: function(message) {
error(message || "Nikare li rêza guhartinan zêde bike.");
}
});
return;
}
api.edit(mw.config.get("wgPageName"), buildEdit).then(function() {
api.parse(code).done(function(data) {
var $renderedExample = $(data).find("ul > li").html();
var $item;
// Insert rendered example into page
if (emptyTemplate) {
self._$lastExample.html($renderedExample);
$item = self._$lastExample;
} else {
self._$lastExample.after($item = $("<li>").append($renderedExample));
}
$item.css("background-color", "lightgreen");
setTimeout(function() {
$item.css("background-color", "inherit");
}, 1000);
self._$lastExample = $item;
});
self.setVisible(false);
self.clear();
self._clearDraft();
reenable();
}, function(codeOrAbort, result) {
if (codeOrAbort instanceof SubmitAbort) {
error(codeOrAbort.message);
} else {
error(msg.saveFailed + apiErrorMessage(codeOrAbort, result));
}
});
function error(errorMessage) {
mw.notify(errorMessage || "Nimûne nehat weşandin, îhtimalen rûpel hate guhartin. Ji kerema xwe re rûpelê dîsa bar bike û dîsa biceribîne.");
// A draft is kept for a new example only: an edited one would come back on the next new one.
if (!self._editing) {
draftStorage.Set({
text: self._textInput.getValue(),
source: self._sourceInput.getValue(),
sourceURL: self._sourceURLInput.getValue(),
translation: self._translationInput.getValue(),
transcription: self._transcriptionInput.getValue(),
});
}
reenable();
}
function reenable() {
self._textInput.setDisabled(false);
self._sourceInput.setDisabled(false);
self._sourceURLInput.setDisabled(false);
self._translationInput.setDisabled(false);
self._transcriptionInput.setDisabled(false);
self._sourceTypeDropdown.setDisabled(false);
Object.keys(self._jederFieldWidgets).forEach(function(key) {
self._jederFieldWidgets[key].setDisabled(false);
});
// self._disableTranslationChk.setDisabled(false);
self._applyButton.setDisabled(false);
self._cancelButton.setDisabled(false);
self._clearButton.setDisabled(false);
self._loadingImage.toggle(false);
}
},
};
});
// </nowiki>
9tx4jr1vgartpb50c1tgks88gpasawj
Bikarhêner:Wikihez/Gadget-test.css
2
1358394
6531661
6424926
2026-09-15T17:26:50Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none
6531661
css
text/css
/* <nowiki> */
/*
* Page: [[MediaWiki:Gadget-wikt.start-wizard.css]]
*/
/* ===================================================================
#editform while the wizard is active
=================================================================== */
/* Keeps #editform genuinely laid out instead of display:none*/
.wiktku-offscreen {
position: absolute !important;
left: -9999px !important;
top: 0 !important;
width: 100%;
max-width: 100%;
visibility: hidden;
}
/* ===================================================================
Base scale
=================================================================== */
#wiktku-start-form {
font-size: 0.92em;
line-height: 1.35;
}
#wiktku-start-form * {
box-sizing: border-box;
}
.wiktku-addlang-notice {
margin: 0 0 0.8em;
padding: 0.6em 0.8em;
background: var(--wikt-palette-paleblue);
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
}
/* ===================================================================
Step cards
a compact card with a left accent bar, so each step reads as
a distinct unit
=================================================================== */
.wiktku-start-step {
padding: 0.7em 0.9em;
margin-bottom: 0.6em;
border: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
border-left: 4px solid var(--wikt-palette-blue-9, #3366cc);
border-radius: 6px;
background: var(--wikt-palette-white);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: border-left-color 0.15s ease, box-shadow 0.15s ease;
}
.wiktku-start-step:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.wiktku-start-step:focus-within {
border-left-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
/* Fuses the posparams card onto the bottom of the pos card (shared left
accent bar, no border/gap) since posparams always renders directly
after #wiktku-start-pos once a pos is chosen. */
#wiktku-start-pos {
border-radius: 6px 6px 0 0;
margin-bottom: 0;
border-bottom: none;
}
#wiktku-start-posparams {
border-radius: 0 0 6px 6px;
border-top: none;
padding-top: 0.3em;
}
#wiktku-start-posparams .wiktku-start-step-name {
display: none;
}
/* Scoped and !important: this is a real <h3> tag (see start.js), so the
skin's default heading styles would otherwise override a plain class
rule. Coloured to match the card's accent bar; .wiktku-start-step-question
below stays lighter/muted so the two don't compete. */
#wiktku-start-form .wiktku-start-step-name {
font-size: 1.1em !important;
font-weight: 700;
letter-spacing: 0.03em;
margin: 0 0 0.3em;
line-height: 1.2;
color: var(--wikt-palette-blue-9, #3366cc);
border: 0;
padding: 0;
}
/* Sized close to the heading so the card reads consistently instead of
the text below it looking like fine print. */
.wiktku-start-step-question {
font-size: 1em;
font-weight: 400;
color: var(--wikt-palette-grey-9, #555);
margin: 0 0 0.5em;
line-height: 1.45;
}
.wiktku-start-step label {
padding-right: 1.2em;
font-size: 1em;
}
/* Muted hint text next to a field, styled to read as a hint rather than
another question competing with .wiktku-start-step-question. */
.wiktku-step-hint {
font-size: 0.88em;
font-style: italic;
color: var(--wikt-palette-grey-8, #72777d);
margin: 0 0 0.5em;
line-height: 1.4;
}
/* Same specificity as the rule just above, so it must come after to
actually win: the name label isn't a field label needing right
padding or the form's base label size, it's a heading. */
#wiktku-start-form .wiktku-start-step-name label {
padding-right: 0;
font-size: inherit !important;
color: inherit;
}
input[type="text"].wiktku-start-step-field {
width: 100%;
padding: 5px 8px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 4px;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
input[type="text"].wiktku-start-step-field:focus {
border-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: inset 0 0 0 1px var(--wikt-palette-blue-9, #3366cc);
outline: 0;
}
input[type="text"].wiktku-start-step-field.wiktku-start-narrow {
width: 20em;
}
/* Only used by selects that opt out via .wiktku-plain-select; other
selects get their width from the searchable picker instead (see
input.wiktku-ac-input). */
select.wiktku-start-narrow,
input.wiktku-dfn-tpl-title {
width: 20em;
max-width: 100%;
box-sizing: border-box;
}
#wiktku-start-spinner {
float: right;
margin-left: 1em;
}
#wiktku-start-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5em;
padding: 0.6em 0.2em;
position: sticky;
bottom: 0;
background: var(--wikt-palette-white);
border-top: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
z-index: 5;
}
/* Quiet, non-competing text — sits next to the cache buttons so the
person can tell at a glance whether their latest edits are backed
up, without it fighting the buttons for attention. */
#wiktku-last-saved {
font-size: 0.85em;
color: var(--wikt-palette-grey-9, #767676);
white-space: nowrap;
}
/* ── Tercîh (settings) icon button — sits in the footer next to the
cache buttons. Uses the Codex "settings" (color-subtle) icon from
Commons directly rather than an OOUI icon-pack module, so it renders
the exact requested glyph without pulling in an extra dependency. */
/* Icon-only, sized to sit level with the OOUI buttons beside it in the footer: same pill, same height,
just square. The label lives in title/aria-label. */
.wiktku-prefs-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 32px;
height: 32px;
padding: 0;
border: 1px solid #a2a9b1;
border-radius: var(--wikt-r-pill, 999px);
background-color: #f8f9fa;
background-image: url(//upload.wikimedia.org/wikipedia/commons/e/e0/Codex_icon_settings_color-subtle.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 14px 14px;
cursor: pointer;
transition: background-color 0.12s ease, border-color 0.12s ease;
}
.wiktku-prefs-btn:hover {
background-color: var(--wikt-card, #fffdf8);
border-color: var(--wikt-accent, #2a5db0);
}
.wiktku-prefs-btn:active {
background-color: var(--wikt-accent-tint, #eef2fa);
}
.wiktku-prefs-btn:focus-visible {
outline: 0;
border-color: var(--wikt-accent, #2a5db0);
box-shadow: 0 0 0 3px var(--wikt-accent-tint, #eef2fa);
}
/* Tercîh dialog body — OOUI's bare ProcessDialog body has no inset of its own, so text and fields
otherwise sit flush against the window edges. */
.wiktku-prefs-dialog-body {
padding: 18px 20px 22px;
background: var(--wikt-card, #fffdf8);
}
/* ── Tercîh dialog ──
OOUI's ProcessDialog chrome is the plain Vector grey; these rules put it on the wizard's own
palette and give the head/body/foot bars a shared rhythm. Scoped to .wiktku-prefs-dialog (set in
initialize) so no other OOUI window on the page is touched. */
.wiktku-prefs-dialog .oo-ui-window-frame {
border-radius: var(--wikt-r-card, 14px);
overflow: hidden;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
}
.wiktku-prefs-dialog .oo-ui-processDialog-navigation {
background: var(--wikt-card-sunken, #fbf8f1);
border-bottom: 1px solid var(--wikt-rule, #ded6c8);
}
.wiktku-prefs-dialog .oo-ui-processDialog-title {
font-family: var(--wikt-serif, "Spectral", "Iowan Old Style", Georgia, "Times New Roman", serif);
font-size: 17px;
font-weight: 600;
color: var(--wikt-ink, #1c1a17);
}
/* Head and foot actions read as the same pills as the wizard's own buttons. OOUI gives the three
action groups no shared class, so each is named. */
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
border-radius: var(--wikt-r-pill, 999px) !important;
font-family: var(--wikt-sans, "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif);
font-size: 14px;
font-weight: 500;
padding: 7px 18px;
min-height: 34px;
box-shadow: none !important;
}
/* OOUI stretches the two head groups edge to edge and corner to corner, which pushed the pills flush
against the rounded frame. Inset them and let them size to the button. */
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary {
top: 50%;
bottom: auto;
height: auto;
transform: translateY(-50%);
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe {
left: 10px;
right: auto;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary {
right: 10px;
left: auto;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button {
background: transparent;
border-color: var(--wikt-field-edge, #cfc7b8);
color: var(--wikt-ink-2, #3d3830);
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button {
background: var(--wikt-accent, #2a5db0) !important;
border-color: var(--wikt-accent, #2a5db0) !important;
color: var(--wikt-on-accent, #fff) !important;
}
/* The destructive action is the only thing in the foot, so it sits at the trailing edge instead of
stretching along a bare full-width bar. */
.wiktku-prefs-dialog .oo-ui-window-foot {
background: var(--wikt-card-sunken, #fbf8f1);
border-top: 1px solid var(--wikt-rule, #ded6c8);
padding: 10px 14px;
text-align: right;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other {
display: inline-block;
float: none;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
background: var(--wikt-card, #fffdf8) !important;
border: 1px solid var(--wikt-danger, #b32424) !important;
color: var(--wikt-danger, #b32424) !important;
}
/* ── Groups ── */
.wiktku-prefs-dialog .oo-ui-fieldsetLayout + .oo-ui-fieldsetLayout {
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid var(--wikt-rule-soft, #efe8db);
}
.wiktku-prefs-dialog .oo-ui-fieldsetLayout-header > .oo-ui-labelElement-label {
font-family: var(--wikt-serif, "Spectral", "Iowan Old Style", Georgia, "Times New Roman", serif);
font-size: 15px;
font-weight: 600;
color: var(--wikt-ink, #1c1a17);
margin-bottom: 10px;
padding: 0;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout {
margin-top: 14px;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout:first-child {
margin-top: 0;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
font-size: 14px;
font-weight: 500;
color: var(--wikt-ink-2, #3d3830);
padding-bottom: 6px;
}
.wiktku-prefs-dialog .oo-ui-inline-help {
font-size: 12.5px;
line-height: 1.5;
color: var(--wikt-muted, #8a8378);
margin-top: 5px;
}
/* ── Fields ── */
.wiktku-prefs-dialog .oo-ui-textInputWidget input,
.wiktku-prefs-dialog .oo-ui-dropdownWidget-handle {
border: 1px solid var(--wikt-field-edge, #cfc7b8);
border-radius: var(--wikt-r-field, 10px);
background: var(--wikt-field-bg, #fff);
font-family: var(--wikt-sans, "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif);
font-size: 14px;
color: var(--wikt-ink, #1c1a17);
min-height: 36px;
padding: 6px 10px;
box-shadow: none;
}
.wiktku-prefs-dialog .oo-ui-textInputWidget input:focus,
.wiktku-prefs-dialog .oo-ui-dropdownWidget.oo-ui-widget-enabled:hover .oo-ui-dropdownWidget-handle {
border-color: var(--wikt-accent, #2a5db0);
box-shadow: none;
outline: 0;
}
.wiktku-prefs-dialog .oo-ui-textInputWidget input::placeholder {
color: var(--wikt-placeholder, #b3ab9e);
}
/* Checkbox rows: the label carries the row, so the text beside the box is the target, and the whole
row lifts on hover to say so. */
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline {
padding: 8px 10px;
border-radius: var(--wikt-r-field, 10px);
}
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline:hover {
background: var(--wikt-card-sunken, #fbf8f1);
}
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
padding-bottom: 0;
color: var(--wikt-ink, #1c1a17);
cursor: pointer;
}
/* Narrow screens: the dialog is full-bleed, so the corners it would round are off-screen anyway. */
@media (max-width: 720px) {
.wiktku-prefs-dialog-body {
padding: 14px 14px 18px;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
font-size: 13px;
padding: 8px 14px;
}
}
/* ── "Bişîne" click feedback ── */
@keyframes wiktku-btn-flash-pulse {
0% { box-shadow: 0 0 0 0 rgba(51, 102, 204, 0.55); }
70% { box-shadow: 0 0 0 9px rgba(51, 102, 204, 0); }
100% { box-shadow: 0 0 0 0 rgba(51, 102, 204, 0); }
}
#wiktku-unhide-editform.wiktku-btn-flash {
animation: wiktku-btn-flash-pulse 0.5s ease-out;
}
/* Persistent state while the wikitext is being built/submitted, set
alongside OOUI's own setDisabled(true) so it reads clearly even if
the skin's default disabled style is subtle. */
#wiktku-unhide-editform.wiktku-btn-sending .oo-ui-buttonElement-button {
background-color: var(--wikt-palette-grey-9, #767676) !important;
border-color: var(--wikt-palette-grey-9, #767676) !important;
cursor: progress;
opacity: 1 !important;
}
/* ── "Beşên din" picker ── */
.wiktku-besendin-picker {
margin: 0.6em 0 1em;
}
.wiktku-besendin-picker .wiktku-besendin-select {
width: 100%;
max-width: 24em;
}
#wiktku-start-form textarea.wiktku-start-step-field,
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield,
#wiktku-start-form textarea.wiktku-word-input {
resize: vertical !important;
overflow: auto !important;
box-sizing: border-box !important;
vertical-align: top;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
transition: border-color 0.1s ease, box-shadow 0.1s ease;
}
/* Modern focus state to match the searchable-picker inputs. */
#wiktku-start-form textarea.wiktku-start-step-field:focus,
#wiktku-start-form textarea.wiktku-start-narrow:focus,
#wiktku-start-form textarea.wiktku-dfn-subfield:focus,
#wiktku-start-form textarea.wiktku-word-input:focus {
border-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: inset 0 0 0 1px var(--wikt-palette-blue-9, #3366cc);
outline: 0;
}
/* Standard: every textarea in the form is full width by default */
#wiktku-start-form textarea:not(.wiktku-start-narrow) {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield.wiktku-start-narrow {
width: 20em;
max-width: 100%;
min-width: 8em;
height: calc(1.7em + 10px);
min-height: calc(1.7em + 10px);
padding-top: 4px !important;
padding-bottom: 4px !important;
line-height: 1.35 !important;
resize: none !important;
}
/* Exception: "Sernav" / "Sernavê orjînal" citation-source fields (kitêb,
kovar, malper, …) — long enough to be worth 2 rows and manual resizing. */
#wiktku-start-form textarea.wiktku-dfn-quote-src-input.wiktku-dfn-quote-src-input--title {
height: calc(2 * 1.35em + 10px);
min-height: calc(2 * 1.35em + 10px);
}
/* Image rows (util.js's buildImageListField): each image is its own block
with its own name, so two sets of the same three boxes never run together. */
#wiktku-start-form .wiktku-resim-list {
margin: 0;
padding-left: 0;
}
#wiktku-start-form li.wiktku-resim-item {
list-style: none;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--wikt-rule-soft);
}
#wiktku-start-form li.wiktku-resim-item:first-child {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
#wiktku-start-form .wiktku-resim-index {
margin-bottom: 6px;
font-size: 12px;
font-weight: 600;
color: var(--wikt-ink-2);
}
/* ── Manual drag-to-resize handle ── */
#wiktku-start-form textarea:not(.wiktku-start-narrow) {
resize: none !important;
}
/* Inline modifier button (⊕) */
/* Each word is always a plain box (never collapses into a pill shape); the
box + its ⊕/× buttons sit on one line, and the attached-modifier chips,
if any, on their own line right under it. */
.wiktku-word-li {
display: block;
}
/* ── ⊕-modifier word lists (Hevmane/Dijmane/Formên alternatîv/Termên
koordîne/Jornav/Jêrnav — both the top-level steps and the per-sense
sub-groups): every word is its own always-visible, always-editable box,
one per line — no packing/wrapping of multiple words onto one row. ── */
#wiktku-start-form .wiktku-dfn-sublist > li.wiktku-word-li {
margin-bottom: 12px;
}
/* Modifier chip row: sits under the word's box, indented and closer to it
than the next word is -- that gap difference is what says which chips
belong to which box. */
#wiktku-start-form .wiktku-word-mods {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 3px;
margin-left: 10px;
}
#wiktku-start-form .wiktku-word-mod-chip {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--wikt-card);
color: var(--wikt-ink-2);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
font-size: 11.5px;
padding: 2px 4px 2px 8px;
cursor: pointer;
}
#wiktku-start-form .wiktku-word-mod-chip-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 12px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-word-mod-chip-remove:hover {
color: var(--wikt-danger);
}
/* Word box + its ⊕ (add modifier) / × (remove word) buttons, side by side. */
#wiktku-start-form .wiktku-word-box-row {
display: flex;
align-items: center;
gap: 4px;
}
#wiktku-start-form .wiktku-word-box-row .wiktku-word-input {
flex: 1 1 auto;
}
/* Offered only while the box holds a comma that separates two words. */
#wiktku-start-form .wiktku-word-split-row {
margin-top: 4px;
}
#wiktku-start-form .wiktku-word-split {
flex: 0 0 auto;
height: 1.6em;
padding: 0 8px;
line-height: 1;
font-size: 0.85em;
border: 1px solid var(--wikt-accent-edge);
border-radius: 6px;
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
cursor: pointer;
}
#wiktku-start-form .wiktku-word-split:hover {
border-color: var(--wikt-accent);
}
#wiktku-start-form .wiktku-word-remove {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 6px;
background: var(--wikt-palette-white);
color: var(--wikt-muted);
font-size: 1em;
cursor: pointer;
}
#wiktku-start-form .wiktku-word-remove:hover {
border-color: var(--wikt-danger);
color: var(--wikt-danger);
}
.wiktku-mod-btn {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 2px;
background: var(--wikt-palette-white);
color: var(--wikt-palette-blue-9, #3366cc);
font-size: 1em;
cursor: pointer;
}
.wiktku-mod-btn:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-paleblue, #eaf3ff);
}
.wiktku-mod-picker {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
width: 100%;
margin-top: 0.15em;
padding: 0.35em;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
background: var(--wikt-palette-white);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.wiktku-mod-pill {
padding: 2px 8px;
border: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
border-radius: 12px;
background: var(--wikt-palette-paleblue, #eaf3ff);
font-size: 0.85em;
white-space: nowrap;
cursor: pointer;
}
.wiktku-mod-pill:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
}
.wiktku-mod-pill:disabled,
.wiktku-mod-pill.wiktku-mod-pill-used {
border-color: var(--wikt-palette-grey-3, #e0e0e0);
background: var(--wikt-palette-lightgrey);
color: var(--wikt-palette-grey-3, #999);
cursor: not-allowed;
opacity: 0.6;
}
.wiktku-mod-pill:disabled:hover,
.wiktku-mod-pill.wiktku-mod-pill-used:hover {
border-color: var(--wikt-palette-grey-3, #e0e0e0);
background: var(--wikt-palette-lightgrey);
color: var(--wikt-palette-grey-3, #999);
}
.wiktku-mod-val-wrap {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
width: 100%;
margin-top: 0.15em;
}
/* The key's full name -- the chip itself shows only "crz:", so this is where
it is spelled out. */
.wiktku-mod-val-label {
flex: 0 0 auto;
font-size: 11.5px;
color: var(--wikt-ink-2, #54595d);
white-space: nowrap;
}
.wiktku-mod-val-input {
flex: 0 1 14em;
max-width: 14em;
padding: 3px 6px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
}
.wiktku-mod-confirm,
.wiktku-mod-cancel {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 6px;
background: var(--wikt-palette-white);
cursor: pointer;
}
.wiktku-mod-confirm {
color: #178a17;
border-color: #178a17;
}
.wiktku-mod-cancel {
color: var(--wikt-palette-red-9, #d33);
border-color: var(--wikt-palette-red-9, #d33);
}
/* .wiktku-resize-wrap, .wiktku-resize-grip and .wiktku-linebreak-wrap are util.js's own, so they live in
wikt-edit.css with the file that builds them -- DefSideBoxes uses the same grip and does not load this
stylesheet. */
/* Inline preview + confirm panel shown before inserting an auto-content
pronunciation template, part of the form layout rather than a browser
confirm()/prompt() dialog. */
.wiktku-pron-preview {
margin: 6px 0;
padding: 8px 10px;
border: 1px solid var(--wikt-palette-blue-9, #3366cc);
border-radius: 6px;
background: var(--wikt-palette-paleblue, #eaf3ff);
}
.wiktku-pron-preview-label {
font-size: 0.88em;
font-weight: 600;
color: var(--wikt-palette-grey-9, #555);
margin-bottom: 3px;
}
.wiktku-pron-preview-body {
font-family: monospace;
font-size: 1em;
white-space: pre-wrap;
word-break: break-word;
margin-bottom: 6px;
}
.wiktku-pron-preview-actions {
display: flex;
gap: 6px;
}
.wiktku-pron-preview-confirm,
.wiktku-pron-preview-cancel {
font-size: 0.88em;
padding: 3px 10px;
border-radius: 4px;
cursor: pointer;
border: 1px solid var(--wikt-palette-lightgrey, #a2a9b1);
background: var(--wikt-palette-white, #fff);
}
.wiktku-pron-preview-confirm {
border-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-blue-9, #3366cc);
font-weight: 600;
}
.wiktku-pron-preview-confirm:disabled {
opacity: 0.5;
cursor: wait;
}
.wiktku-pron-preview-cancel:hover {
background: var(--wikt-palette-grey-0, #f8f9fa);
}
/* Template pickers fill their container since the search box, not the
<select>, is what the user sees. */
.wiktku-pron-wrap .wiktku-native-select-wrap,
.wiktku-etym-wrap .wiktku-native-select-wrap,
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
width: 100%;
}
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 0.88em;
}
.wiktku-cavkani-wrap .wiktku-native-select-wrap,
.wiktku-cavkani-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
width: 100%;
}
/* Template selector inside a sense block. */
.wiktku-dfn-tpl-row .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 0.88em;
}
/* Language/dialect rows: picker must line up with the word field beside it. */
.wiktku-lang-row .wiktku-native-select-wrap {
flex: 0 0 auto;
}
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t,
#wiktku-start-form textarea.wiktku-dfn-quote-src-manual,
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text,
#wiktku-start-form .wiktku-tewandin-wrap textarea.wiktku-tewandin-text,
#wiktku-start-form textarea.wiktku-cavkani-text,
#wiktku-start-form textarea.wiktku-dfn-izah {
--wiktku-ta-rows: 2;
height: calc(var(--wiktku-ta-rows) * 1.35em + 6px);
min-height: calc(var(--wiktku-ta-rows) * 1.35em + 6px);
}
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text {
--wiktku-ta-rows: 3;
}
#wiktku-start-form textarea.wiktku-cavkani-text {
--wiktku-ta-rows: 4;
}
/* Mane, Nimûne + Jêgirtin (citation) boxes, and their tr/t sub-fields: at
least 4 rows so there's room to actually write a definition/example/quote.
Manual resize (see util.js) still lets the person grow or shrink any of
these afterward — this only sets where they start. */
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t,
#wiktku-start-form textarea.wiktku-dfn-quote-src-manual {
--wiktku-ta-rows: 4;
}
/* Nudges the border when the box contains a typed/pasted linebreak, so
the user notices before it's silently turned into <br> on output; the
break itself is marked via a mirrored backdrop layer (see below). */
#wiktku-start-form textarea.wiktku-has-linebreak {
border-color: var(--wikt-palette-yellow6, #ac6600) !important;
}
/* Sits behind the textarea showing the same text, with a highlighted
marker at each line followed by a break; font/padding/line-height are
copied from the textarea via computed style, not shared CSS classes. */
.wiktku-linebreak-backdrop {
position: absolute;
inset: 0;
z-index: 0;
margin: 0;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
color: transparent;
background: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
resize: none !important;
pointer-events: none;
}
/* Raise the real textarea above its backdrop so typing/selection/caret
behave completely normally; the backdrop's mark shows through only
where the real textarea's own background is transparent. */
#wiktku-start-form textarea.wiktku-has-linebreak-source {
position: relative;
z-index: 1;
background-color: transparent;
}
/* Zero width, so the backdrop's text wraps exactly where the textarea's does; the marker itself is
painted out of flow from here, into the empty space the line break leaves. */
.wiktku-linebreak-anchor {
position: relative;
display: inline-block;
width: 0;
}
.wiktku-linebreak-mark {
position: absolute;
top: 0;
left: 0;
background-color: var(--wikt-palette-yellow6, #ac6600);
color: var(--wikt-palette-white, #fff);
border-radius: 2px;
}
/* ── Îzaha maneyî ──
Opens under the mane box; the two are one gloss, so it sits tight against it rather than reading as
a separate field. */
.wiktku-dfn-izah-wrap {
display: flex;
flex-direction: column;
gap: 3px;
margin-top: 4px;
}
.wiktku-dfn-nimune-sub,
.wiktku-dfn-quote-sub {
margin-top: 3px;
margin-left: 1em;
padding-left: 0.6em;
border-left: 2px solid var(--wikt-palette-lightgrey);
display: flex;
flex-direction: column;
gap: 3px;
}
.wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
.wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t {
min-width: 8em;
padding-top: 2px !important;
padding-bottom: 2px !important;
line-height: 1.35 !important;
font-size: 0.88em;
}
/* Sense list (Mane step) — one sense visible at a time, switched via
.wiktku-dfn-sense-tabs. */
ol.wiktku-dfn-list {
list-style: none;
margin: 0;
padding: 0;
}
ol.wiktku-dfn-list > li.wiktku-dfn-sense {
margin-bottom: 0.9em;
}
/* Template selector row inside a sense block */
.wiktku-dfn-tpl-row {
margin-bottom: 3px;
}
/* ── Sub-field titles (Etîket, Kategorî, Hevmane, Dijmane, Nimûne,
Jêgirtin, …): each group reads as a small labelled field — the term
stands out, the hint stays quiet. ── */
.wiktku-dfn-subgroup {
margin-top: 0.5em;
padding-top: 0.4em;
border-top: 1px dashed var(--wikt-palette-grey-3, #e0e0e0);
}
.wiktku-dfn-subgroup:first-of-type {
margin-top: 0.3em;
padding-top: 0;
border-top: none;
}
.wiktku-dfn-sublabel {
display: block;
margin-bottom: 3px;
font-size: 0.88em;
}
.wiktku-dfn-sublabel-term {
font-weight: 600;
color: var(--wikt-palette-blue-9, #3366cc);
letter-spacing: 0.03em;
font-size: 0.92em;
}
.wiktku-dfn-sublabel-hint {
color: var(--wikt-palette-grey-9, #767676);
font-style: italic;
}
.wiktku-pron-wrap,
.wiktku-etym-wrap,
.wiktku-tewandin-wrap,
.wiktku-cavkani-wrap {
display: flex;
flex-direction: column;
gap: 3px;
}
/* ── POS section tabs — rounded pill style ── */
#wiktku-section-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.6em;
align-items: center;
}
#wiktku-section-tabs .wiktku-section-tab {
padding: 3px 12px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 999px;
background: var(--wikt-palette-dullcyan);
cursor: pointer;
font-size: 0.86em;
user-select: none;
transition: background 0.12s ease, border-color 0.12s ease;
}
#wiktku-section-tabs .wiktku-section-tab:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
}
#wiktku-section-tabs .wiktku-section-tab.wiktku-tab-active {
background: var(--wikt-palette-blue-9, #3366cc);
border-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
font-weight: 600;
}
/* ── Step-group tabs (Mane / Bilêvkirin / Beşên din — see STEP_GROUPS in
start.js) ── Sits directly under the pill-style section tabs above;
styled as an underlined secondary nav so the two bars stay visually
distinct. */
#wiktku-group-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.8em;
border-bottom: 2px solid var(--wikt-palette-lightgrey);
}
#wiktku-group-tabs .wiktku-group-tab {
position: relative;
padding: 7px 14px 8px;
margin-bottom: -2px;
border-bottom: 3px solid transparent;
border-radius: 3px 3px 0 0;
cursor: pointer;
font-size: 0.95em;
font-weight: 600;
color: var(--wikt-palette-grey-9, #333);
background: var(--wikt-palette-grey-0, #f8f9fa);
user-select: none;
transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
#wiktku-group-tabs .wiktku-group-tab:hover {
color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-paleblue, #eaf3ff);
}
#wiktku-group-tabs .wiktku-group-tab.wiktku-tab-active {
border-bottom-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
background: var(--wikt-palette-blue-9, #3366cc);
font-weight: 700;
}
/* Small dot marking a tab that still has an unfilled required field
hidden inside it (see updateGroupTabIndicators in start.js). Cleared
automatically once that tab becomes the active one. */
#wiktku-group-tabs .wiktku-group-tab.wiktku-tab-missing::after {
content: "";
position: absolute;
top: 4px;
right: -1px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--wikt-palette-red-9, #d33);
}
.wiktku-lang-row-list {
margin: 0;
padding: 0;
list-style: none;
}
.wiktku-lang-row {
display: flex;
align-items: flex-start;
gap: 6px;
margin-bottom: 3px;
flex-wrap: wrap;
}
.wiktku-lang-row-words {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 3px;
}
/* Dûnde/Werger/Zarava rows: align the picker's height with the adjacent
word textarea (see .wiktku-start-narrow above). */
.wiktku-lang-row .wiktku-native-select-wrap input.wiktku-ac-input {
height: calc(1.7em + 10px);
min-height: calc(1.7em + 10px);
padding-top: 4px;
padding-bottom: 4px;
}
/* ── Werger / Bi zaravayên din groups: one per gloss, shared spacing ── */
#wiktku-start-trans .wiktku-trans-group,
#wiktku-start-zarava .wiktku-zarava-group {
margin-bottom: 0.6em;
}
#wiktku-start-trans .wiktku-trans-group:last-child,
#wiktku-start-zarava .wiktku-zarava-group:last-child {
margin-bottom: 0;
}
#wiktku-start-trans .wiktku-trans-group-label,
#wiktku-start-zarava .wiktku-zarava-group-label {
font-weight: 600;
font-size: 0.88em;
margin-bottom: 3px;
}
.wiktku-dfn-jeder-group {
margin-top: 3px;
}
/* Numbers each citation ("Jêgirtin 1:", "Jêgirtin 2:"…) and separates entries with a rule so multiple ones don't blur together. */
.wiktku-dfn-jeder-list {
counter-reset: wiktku-jeder-num;
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li {
counter-increment: wiktku-jeder-num;
padding-top: 8px;
border-top: 1px solid var(--wikt-palette-lightgrey, #eaecf0);
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li:first-child {
padding-top: 0;
border-top: none;
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li::before {
content: "Jêgirtin " counter(wiktku-jeder-num) ":";
display: block;
margin-bottom: 3px;
font-weight: 600;
font-size: 0.82em;
color: var(--wikt-palette-blue-9, #3366cc);
letter-spacing: 0.03em;
}
.wiktku-dfn-quote-src-sel {
font-size: 0.88em;
margin-top: 3px;
display: block;
/* A native select with no width rule sizes to fit its longest <option>
(here, the longest çavkanî template name) rather than its container,
which overflows the card on a phone — cap it like every other field. */
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* Wide/narrow/medium layout, same idea as MediaWiki:Gadget-wikt.add-
examples.js: rows wrap freely, each sized by its own width tier
(.wiktku-dfn-quote-src-row--wide/narrow/medium) instead of always
stacking one field per line. */
.wiktku-dfn-quote-src-fields {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 6px 10px;
margin-top: 3px;
}
/* Fills the flex row so the manual-source textarea's resize wrapper doesn't shrink to the browser's default width. */
.wiktku-dfn-quote-src-fields > .wiktku-resize-wrap {
flex: 1 1 100%;
width: 100%;
}
/* The Kategorî:Çavkanî picker is not one of the parameter boxes -- it decides which template's boxes
these are -- so it takes a line of its own above them. Left as a plain flex item it shrank to its
content and the first narrow field wrapped up beside it, which read as a nameless box paired with
someone else's label. */
.wiktku-dfn-quote-src-fields > .wiktku-cat-tpl-picker {
flex: 1 1 100%;
width: 100%;
}
/* A labelled source box -- "Şablona çavkaniyê", the link -- in the add-examples form's stacked shape:
label, box, then the button and hints under it at their natural size. */
.wiktku-dfn-quote-field {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.4em;
margin-top: 8px;
}
.wiktku-dfn-quote-url {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.wiktku-dfn-quote-citoid-btn,
.wiktku-dfn-quote-citoid-status {
align-self: flex-start;
}
#wiktku-start-form .wiktku-dfn-quote-url-hint {
margin: 0;
}
/* "Malûmatên çavkaniyê" and its "Vala bike", over a jêder-* template's boxes. */
.wiktku-dfn-quote-src-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--wikt-palette-lightgrey, #eaecf0);
}
.wiktku-dfn-quote-src-head-label {
font-size: 11px;
letter-spacing: 0.12em;
color: var(--wikt-muted, #72777d);
}
.wiktku-dfn-quote-src-clear {
font-size: 12px;
}
.wiktku-dfn-quote-src-cat-status {
display: block;
font-size: 0.82em;
color: var(--wikt-palette-lightgrey, #767676);
margin-top: 2px;
}
.wiktku-dfn-quote-src-row {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1 1 12em;
}
.wiktku-dfn-quote-src-row--wide {
flex-basis: 100%;
}
.wiktku-dfn-quote-src-row--narrow {
flex: 0 1 7em;
}
.wiktku-dfn-quote-src-row--medium {
flex: 1 1 12em;
}
.wiktku-dfn-quote-src-row label {
font-size: 0.82em;
white-space: nowrap;
}
.wiktku-dfn-quote-src-input {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
@media (max-width: 720px) {
.wiktku-lang-row {
flex-direction: column;
}
/* Re-asserts against the skin's own mobile-breakpoint heading styles,
which load after ours in the cascade. */
#wiktku-start-form .wiktku-start-step-name {
font-size: 1.05em !important;
margin: 0 0 0.2em !important;
line-height: 1.2 !important;
border: 0 !important;
padding: 0 !important;
}
/* Single column: search box, list, and narrow textareas (Etîket,
Hevmane, Dûnde/Jê/Têkildar/Binêre herwiha word boxes, citation
source fields, …) all use the full width. */
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield.wiktku-start-narrow {
width: 100%;
max-width: 100%;
}
.wiktku-mod-val-input {
flex: 1 1 auto;
max-width: 100%;
}
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text {
--wiktku-ta-rows: 3;
font-size: 1em;
}
#wiktku-start-form .wiktku-tewandin-wrap textarea.wiktku-tewandin-text,
#wiktku-start-form textarea.wiktku-cavkani-text {
--wiktku-ta-rows: 4;
font-size: 1em;
}
/* Mane, Nimûne + Jêgirtin and their tr/t sub-fields: keep at least 4 rows
on mobile too, rather than shrinking further than the desktop size. */
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t {
--wiktku-ta-rows: 4;
font-size: 1em;
}
.wiktku-dfn-nimune-sub,
.wiktku-dfn-quote-sub {
margin-left: 0.5em;
padding-left: 0.4em;
}
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 1em;
min-height: 2.2em;
}
/* Every field stacks full-width on mobile, regardless of its wide/
narrow/medium tier — there's no room to wrap several per line. */
#wiktku-start-form .wiktku-dfn-quote-src-row {
flex-basis: 100%;
}
.wiktku-start-step {
padding: 0.5em 0.6em;
}
}
/* ===================================================================
Night mode
Palette.css already flips --wikt-palette-* vars for night mode; this
hook only covers real colours (panel border) that aren't a palette var,
since a drop shadow is invisible against an equally dark page.
=================================================================== */
@media screen {
html.skin-theme-clientpref-night .wiktku-start-step,
html.skin-theme-clientpref-night .wiktku-start-step:hover,
html.skin-theme-clientpref-night .wiktku-start-step:focus-within {
box-shadow: none;
}
}
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os .wiktku-start-step,
html.skin-theme-clientpref-os .wiktku-start-step:hover,
html.skin-theme-clientpref-os .wiktku-start-step:focus-within {
box-shadow: none;
}
}
/* ── posparams step (live wikitext + helper widgets) ── */
.wiktku-posparams-wrap {
display: flex;
flex-direction: column;
gap: 6px;
}
.wiktku-posparams-helpers {
display: flex;
flex-direction: column;
gap: 4px;
}
.wiktku-posparams-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.wiktku-posparams-row label {
font-size: 1em;
min-width: 10em;
padding-right: 0;
}
.wiktku-posparams-subheading {
font-weight: 600;
font-size: 0.88em;
margin-top: 6px;
}
.wiktku-posparams-pronoun-hint {
font-size: 0.9em;
color: var(--wikt-ink-2);
font-style: italic;
}
/* Keeps a pronoun hint and its box on one line, immediately next to each
other, even when the row around them stacks label-over-field (see the
desktop grid layout for .wiktku-posparams-helpers below). */
.wiktku-posparams-field-line {
display: flex;
align-items: center;
gap: 6px;
}
.wiktku-posparams-text {
width: 100%;
box-sizing: border-box;
font-family: monospace;
font-size: 0.9em;
}
.wiktku-posparams-opt-toggle {
font-size: 0.85em;
margin-top: 2px;
display: inline-block;
}
.wiktku-posparams-opt-panel {
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px 0 2px 1em;
}
.wiktku-posparams-opt-row {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.85em;
}
/* ── resim step (one or more images) ── */
.wiktku-resim-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.wiktku-resim-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.wiktku-resim-item .wiktku-posparams-row {
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.wiktku-resim-item .wiktku-posparams-row label {
min-width: 0;
font-size: 0.9em;
}
/* ── dfn: which sub-field is on screen ──
Every sub-group (Nimûne, Jêgirtin, Hevmane, …) is built hidden and shown
only once it is picked from the chip row or the "Hevmane û Bêhtir" menu — see the
PICK_DEFS block in start.js. */
#wiktku-start-form .wiktku-dfn-subgroup-hidden {
display: none;
}
/* A şablon sense ({{guharto}}, {{şaşnivîs}}…) has no gloss of its own, so
the word lists make no sense for it — both the boxes and the menu entries
that would ask for them go away. */
#wiktku-start-form li.wiktku-sense-is-tpl .wiktku-dfn-wordgroup,
#wiktku-start-form li.wiktku-sense-is-tpl .wiktku-dfn-pick-word {
display: none;
}
/* × on a revealed sub-group's label, to put it away again. Dim until the
group is hovered, so it is never the loudest thing in the row. */
#wiktku-start-form .wiktku-dfn-subgroup-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 14px;
line-height: 1;
padding: 0 4px;
margin-left: 4px;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-dfn-subgroup:hover .wiktku-dfn-subgroup-remove,
#wiktku-start-form .wiktku-dfn-subgroup-remove:focus {
opacity: 1;
}
#wiktku-start-form .wiktku-dfn-subgroup-remove:hover {
color: var(--wikt-danger);
opacity: 1;
}
/* ── "Hevmane û Bêhtir" menu ──
An overlay anchored to its own toggle, so opening it never pushes the
boxes below it down the page. */
#wiktku-start-form .wiktku-dfn-more-wrap {
position: relative;
display: inline-block;
}
#wiktku-start-form .wiktku-dfn-more-arrow {
font-size: 9px;
vertical-align: 1px;
}
/* Same menu DefSideBoxes draws (.dsb-more-menu): a plain vertical list of links, sized to its widest
entry, floating over the page rather than pushing it down. */
#wiktku-start-form .wiktku-dfn-more-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 30;
display: flex;
flex-direction: column;
align-items: flex-start;
width: max-content;
max-width: min(320px, calc(100vw - 40px));
gap: 6px;
margin: 2px 0 0 4px;
padding: 8px 12px;
background: var(--wikt-card);
border: 1px solid var(--wikt-rule);
border-radius: var(--wikt-r-field);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}
#wiktku-start-form .wiktku-dfn-more-item {
align-self: flex-start;
font-size: 12px;
color: var(--wikt-accent);
white-space: nowrap;
cursor: pointer;
text-decoration: none;
/* The form's own link style underlines every <a> with a border; on a block-level menu entry that
reads as a rule between the items. */
border-bottom: 0;
}
#wiktku-start-form .wiktku-dfn-more-item:hover,
#wiktku-start-form .wiktku-dfn-more-item:focus {
color: var(--wikt-accent-deep);
text-decoration: underline;
border-bottom: 0;
}
/* Rules the menu's three sections apart: label fields, word lists, and "Maneyeke din zêde bike". */
#wiktku-start-form .wiktku-dfn-more-group-start {
margin-top: 7px;
padding-top: 7px;
border-top: 1px solid var(--wikt-rule-soft);
}
/* ===================================================================
Desktop text bump
Modifier pills/buttons and posparams helper text read fine at their
small mobile sizes but look too small on desktop, so bump them up
past the 720px breakpoint only.
=================================================================== */
@media (min-width: 721px) {
.wiktku-mod-btn,
.wiktku-mod-confirm,
.wiktku-mod-cancel {
width: 1.8em;
height: 1.8em;
font-size: 1.1em;
}
.wiktku-mod-pill {
font-size: 0.95em;
padding: 3px 10px;
}
.wiktku-mod-val-input {
font-size: 0.95em;
}
.wiktku-posparams-row label {
font-size: 1em;
}
.wiktku-posparams-text {
font-size: 1em;
}
.wiktku-posparams-opt-toggle {
font-size: 0.95em;
}
.wiktku-posparams-opt-row {
font-size: 0.95em;
}
/* Multi-column card layout for posparams helper fields (transliteration,
gendered forms, comparative/superlative, hanja, verb-tense roots, …),
desktop-only. Rows are shown/hidden via jQuery inline display; the
grid only rearranges visible rows, not which ones are visible. */
.wiktku-posparams-helpers {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px 16px;
align-items: start;
}
.wiktku-posparams-helpers > .wiktku-posparams-row {
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
gap: 3px;
}
.wiktku-posparams-helpers > .wiktku-posparams-row label {
min-width: 0;
}
.wiktku-posparams-helpers > .wiktku-posparams-row .wiktku-posparams-field,
.wiktku-posparams-helpers > .wiktku-posparams-row select.wiktku-posparams-cinsiyet-sel,
.wiktku-posparams-helpers > .wiktku-posparams-row select.wiktku-posparams-lektur-sel {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* The "Jimar" row lives here too — just another .wiktku-posparams-row,
no extra CSS needed. */
/* These two aren't per-field rows — span the full card width so the
collapsed toggle and its checkbox list don't get squeezed into one
narrow column. */
.wiktku-posparams-helpers > .wiktku-posparams-opt-toggle,
.wiktku-posparams-helpers > .wiktku-posparams-opt-panel,
.wiktku-posparams-helpers > .wiktku-posparams-subheading {
grid-column: 1 / -1;
}
}
/* ===================================================================
SÊWIRANA NÛ — design system layer
===================================================================
Purely the visual skin (colour, type, weight, radius, spacing), layered
after the layout rules above so it wins the cascade.
No @import of Spectral / IBM Plex from Google Fonts: Wikimedia's CSP
blocks third-party font hosts, so the stacks below name the spec's
faces first (used if the wiki installs them locally) and fall back to
faces with the same feel.
Accent colour is reserved for actionable elements (focus, links, the
active tab, the primary button, chips); section headings are near-black.
=================================================================== */
#wiktku-start-form {
font-family: var(--wikt-sans);
font-size: 15px;
line-height: 1.55;
color: var(--wikt-ink);
background: var(--wikt-page);
padding: 24px;
border-radius: var(--wikt-r-card);
}
/* ── Cards ───────────────────────────────────────────────────────────
Flat cream panel held by a single hairline border, no shadow. */
#wiktku-start-form .wiktku-start-step {
background: var(--wikt-card);
border: 1px solid var(--wikt-card-edge);
border-left: 1px solid var(--wikt-card-edge);
border-radius: var(--wikt-r-card);
box-shadow: none;
padding: 24px 24px 20px;
margin-bottom: 16px;
transition: border-color 0.12s ease;
}
#wiktku-start-form .wiktku-start-step:hover,
#wiktku-start-form .wiktku-start-step:focus-within {
box-shadow: none;
border-color: var(--wikt-rule);
}
/* The fused pos + posparams pair keeps reading as one card. margin-bottom
has to be reset here too — .wiktku-start-step's own 16px otherwise wins
(higher specificity than the older #wiktku-start-pos-only rule this was
meant to override), leaving a visible gap between the two "halves". */
#wiktku-start-form #wiktku-start-pos {
border-radius: var(--wikt-r-card) var(--wikt-r-card) 0 0;
border-bottom: none;
padding-bottom: 8px;
margin-bottom: 0;
}
#wiktku-start-form #wiktku-start-posparams {
border-radius: 0 0 var(--wikt-r-card) var(--wikt-r-card);
border-top: none;
padding-top: 4px;
}
/* ── Type ────────────────────────────────────────────────────────────
Step headings: serif, regular weight, near-black. 20px matches what the
narrow breakpoint already resolves to, so the two read the same; at 24px
every step title was a third taller than the answer beneath it and a form
with a dozen sections spent most of a desktop screen on headings. */
#wiktku-start-form .wiktku-start-step-name {
font-family: var(--wikt-serif);
font-size: 20px !important;
font-weight: 400;
letter-spacing: -0.01em;
line-height: 1.15;
color: var(--wikt-ink);
margin: 0 0 8px;
}
/* display:block matters: this is a <label>, so it was sharing a line with
the control it introduces wherever start.js didn't emit a <br>. */
#wiktku-start-form .wiktku-start-step-question {
display: block;
font-size: 15px;
font-weight: 400;
color: var(--wikt-ink-3);
margin: 0 0 12px;
line-height: 1.6;
max-width: 62ch;
}
/* Collapses the label entirely when empty (posparams' "Cins/zayend" title
can be blank) instead of leaving its margin behind as a gap; a no-op
for every other step, whose question text is never empty. */
#wiktku-start-form .wiktku-start-step-question:empty {
display: none;
margin: 0;
}
/* Same reason — a field label belongs above its field, not beside it. */
#wiktku-start-form .wiktku-posparams-row label,
#wiktku-start-form .wiktku-dfn-sublabel {
display: block;
margin-bottom: 6px;
padding-right: 0;
}
/* Field labels: small, medium weight, full-strength ink — they label a
control, so they should not read as body copy. */
#wiktku-start-form .wiktku-start-step label,
#wiktku-start-form .wiktku-besendin-picker label,
#wiktku-start-form .wiktku-posparams-row label,
#wiktku-start-form .wiktku-dfn-sublabel {
font-size: 13px;
font-weight: 500;
color: var(--wikt-ink);
letter-spacing: 0.01em;
}
/* The parenthetical/example half of a label is supporting text. */
#wiktku-start-form .wiktku-dfn-sublabel-hint,
#wiktku-start-form .wiktku-step-hint {
font-weight: 400;
font-style: italic;
color: var(--wikt-muted);
font-size: 12.5px;
}
/* ── Fields ──────────────────────────────────────────────────────────
One shape for every control: 1px edge, 10px radius, 44px tall. */
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form input.wiktku-ac-input,
#wiktku-start-form textarea,
#wiktku-start-form select {
font-family: var(--wikt-sans);
font-size: 14px;
color: var(--wikt-ink);
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
padding: 6px 10px;
box-shadow: none;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* --wikt-tap is the 44px touch minimum, which a mouse does not need: on a pointer-precise screen it made
every dropdown a third taller than its own text. The touch size is put back under the coarse-pointer
and narrow rules below. */
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form select {
min-height: 34px;
}
#wiktku-start-form textarea {
line-height: 1.6;
}
/* Wikitext and anything template-shaped reads as code. The monospace face carries that on its own;
these are fields you type into, so they keep the same white fill and edge as every other field. */
#wiktku-start-form .wiktku-posparams-text,
#wiktku-start-form .wiktku-pron-text,
#wiktku-start-form .wiktku-etym-text,
#wiktku-start-form .wiktku-cavkani-text,
#wiktku-start-form .wiktku-tewandin-text {
font-family: var(--wikt-mono);
font-size: 13.5px;
background: var(--wikt-field-bg);
}
/* The pronunciation preview is generated output, not an input, so it stays tinted. */
#wiktku-start-form .wiktku-pron-preview-body {
font-family: var(--wikt-mono);
font-size: 13.5px;
background: var(--wikt-code-bg);
border-color: var(--wikt-card-edge);
}
#wiktku-start-form ::placeholder {
color: var(--wikt-placeholder);
opacity: 1;
}
/* Focus: accent edge + 3px tint halo. The only "elevation" in the design. */
#wiktku-start-form input[type="text"]:focus,
#wiktku-start-form input[type="search"]:focus,
#wiktku-start-form textarea:focus,
#wiktku-start-form select:focus,
#wiktku-start-form input.wiktku-ac-input:focus {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
outline: 0;
}
/* The searchable picker's visible input is the field, so it takes the
field shape too (it otherwise inherits an older, boxier rule). */
#wiktku-start-form input.wiktku-ac-input {
min-height: 34px;
border-radius: var(--wikt-r-field);
border-color: var(--wikt-field-edge);
font-size: 14px;
}
#wiktku-start-form .wiktku-ac-results {
border: 1px solid var(--wikt-rule);
border-radius: var(--wikt-r-field);
background: var(--wikt-card);
box-shadow: none;
}
#wiktku-start-form .wiktku-ac-results li.wiktku-ac-active,
#wiktku-start-form .wiktku-ac-results li:hover {
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
}
/* ── Tabs → the accent's main job ────────────────────────────────────
Active tab: tint fill + 3px accent underline, per the spec's
"xala hûrgulî" note. Inactive tabs are plain text. */
#wiktku-start-form #wiktku-group-tabs,
#wiktku-start-form #wiktku-section-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
border-bottom: 1px solid var(--wikt-rule);
margin-bottom: 16px;
padding-bottom: 0;
}
/* Both bars are direct children of #wiktku-start-form, so a sticky child
stays stuck for as long as any part of the form is in view. Background
is opaque so content scrolling underneath doesn't show through.
Section-tabs pins below --wiktku-siteheader-h — an offset for Vector
2022's own sticky header, which otherwise sits on top of it at top:0
(Minerva has no such header, so mobile needs no offset); group-tabs
pins below that plus --wiktku-stabs-h (section-tabs' own height). Both
custom properties are set by syncStickyTabsOffset() in start.js and
fall back to 0 on skins without a Vector sticky header. */
#wiktku-start-form #wiktku-section-tabs {
position: sticky;
top: var(--wiktku-siteheader-h, 0px);
z-index: 20;
background: var(--wikt-card);
}
#wiktku-start-form #wiktku-group-tabs {
position: sticky;
top: calc(var(--wiktku-siteheader-h, 0px) + var(--wiktku-stabs-h, 0px));
z-index: 19;
background: var(--wikt-card);
}
#wiktku-start-form .wiktku-group-tab,
#wiktku-start-form .wiktku-section-tab {
font-size: 13.5px;
font-weight: 500;
color: var(--wikt-ink-3);
background: transparent;
border: 0;
border-bottom: 3px solid transparent;
border-radius: var(--wikt-r-pill) var(--wikt-r-pill) 0 0;
padding: 9px 14px 11px;
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-group-tab:hover,
#wiktku-start-form .wiktku-section-tab:hover {
background: var(--wikt-card-sunken);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-group-tab.wiktku-tab-active,
#wiktku-start-form .wiktku-section-tab.wiktku-tab-active {
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom-color: var(--wikt-accent);
}
/* ── Progressive-disclosure toggles ──────────────────────────────────
Shared by "Tercîhên pêşketî" (posparams) and "Malûmatên pêşketî"
(Mane): a rotating chevron plus a count of what's hidden. */
#wiktku-start-form .wiktku-adv-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin: 4px 0 12px;
padding-top: 16px;
border-top: 1px solid var(--wikt-rule-soft);
}
#wiktku-start-form .wiktku-adv-toggle {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 500;
color: var(--wikt-accent);
text-decoration: none;
cursor: pointer;
}
#wiktku-start-form .wiktku-adv-toggle:hover .wiktku-adv-toggle-text {
text-decoration: underline;
}
#wiktku-start-form .wiktku-adv-toggle-chevron {
width: 22px;
height: 22px;
flex: 0 0 22px;
display: grid;
place-items: center;
font-size: 11px;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
border-radius: 6px;
transform: rotate(-90deg);
transition: transform 0.15s ease;
}
/* Open state: dfn flips the class on the step, posparams on the link. */
#wiktku-start-form .wiktku-adv-toggle.wiktku-adv-open .wiktku-adv-toggle-chevron,
#wiktku-start-dfn:not(.wiktku-dfn-subs-hidden) .wiktku-adv-toggle-chevron {
transform: rotate(0deg);
}
#wiktku-start-form .wiktku-posparams-adv-panel {
border: 1px solid var(--wikt-rule-soft);
border-radius: 12px;
background: var(--wikt-card-sunken);
padding: 20px 18px;
margin-bottom: 4px;
}
/* Vertical rhythm between stacked field rows (e.g. "Tewandina lêkerê"),
wider than a row's own label-to-field gap so rows read as distinct
groups instead of bleeding together. */
#wiktku-start-form .wiktku-posparams-helpers {
display: flex;
flex-direction: column;
gap: 14px;
}
/* ── posparams grid ──────────────────────────────────────────────────
"Tor, ne rêz" — two columns, nothing narrower than 220px, so the six
gender/number fields stop being a six-across scanline. */
@media (min-width: 721px) {
#wiktku-start-form .wiktku-posparams-helpers,
#wiktku-start-form .wiktku-posparams-adv-panel {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 18px 22px;
align-items: start;
}
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-row,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-row {
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
gap: 7px;
}
/* The row's own gap already separates the label from its field — the
label's margin-bottom (set for the stacked mobile layout) would
otherwise double that spacing here. */
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-row label,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-row label {
margin-bottom: 0;
}
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-adv-toggle-row,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-adv-panel,
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-opt-panel {
grid-column: 1 / -1;
}
}
#wiktku-start-form .wiktku-posparams-opt-panel {
border-top: 1px solid var(--wikt-rule-soft);
padding-top: 14px;
margin-top: 4px;
}
#wiktku-start-form .wiktku-posparams-opt-row {
font-size: 13px;
color: var(--wikt-ink-3);
}
/* ── Sense blocks ────────────────────────────────────────────────────
Each sense is a sunken panel inside the Mane card, with its number as
an accent counter — so "how many meanings am I writing" is legible at
a glance. */
#wiktku-start-form .wiktku-dfn-subgroup {
margin: 14px 0 0;
padding-top: 14px;
border-top: 1px solid var(--wikt-rule-soft);
}
/* No divider above the first item — it would be a rule against the
sense's own text. The class is put on the first *shown* group by
updateFirstVisible() in start.js: :first-of-type would keep picking a
group that is still hidden behind the chip row, leaving the first
visible one with a stray rule above it. */
#wiktku-start-form .wiktku-dfn-subgroup-first {
margin-top: 8px;
padding-top: 0;
border-top: none;
}
#wiktku-start-form .wiktku-dfn-sublabel-term {
color: var(--wikt-ink);
}
/* Plain white field like any other, not a tinted fill — a tint would
read as "disabled" next to the white Mane box above it. */
#wiktku-start-form .wiktku-dfn-subfield,
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form .wiktku-word-input {
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
}
/* No "1." "2." markers on the sub-lists: numbered rows would read as a
form to fill top-down, when each row is really "another one if you
have one" — and the marker would land on its own line above the
block-level textarea, looking like a stray character. */
#wiktku-start-form .wiktku-dfn-sublist {
margin: 0;
padding-left: 0;
list-style: none;
}
#wiktku-start-form .wiktku-dfn-sublist > li {
margin-bottom: 6px;
}
/* ── Chips: tags, modifiers, template pills ──────────────────────────── */
#wiktku-start-form .wiktku-mod-pill,
#wiktku-start-form .wiktku-besendin-select + .wiktku-chip {
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
font-size: 13px;
padding: 6px 12px;
}
#wiktku-start-form .wiktku-mod-btn,
#wiktku-start-form .wiktku-mod-confirm,
#wiktku-start-form .wiktku-mod-cancel {
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-radius: 8px;
}
/* ── Chip + toggle fields ("1c" design) — Dever/Şîwe, Etîket, Kategorî:
a small "+ …" toggle on the sense, not an always-visible field (see
buildChipField in start.js, used by all three). Chips sit inline before
the toggle; clicking it opens a compact panel with optional quick-pick
suggestions plus free text below. Each field's own wrap class (e.g.
.wiktku-dfn-dever-wrap) only scopes chip reads/writes in start.js — all
the visual styling here is shared via .wiktku-dfn-chipfield-*. The three
toggles live together in .wiktku-dfn-chipfield-row; their panels render
separately in .wiktku-dfn-chipfield-panels below it, so opening one
field's panel never pushes the other toggles around. ── */
#wiktku-start-form .wiktku-dfn-chipfield-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 8px 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-panels {
display: flex;
flex-direction: column;
gap: 8px;
}
#wiktku-start-form .wiktku-dfn-chipfield-wrap {
margin: 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-chips {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
#wiktku-start-form .wiktku-dfn-chipfield-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
font-size: 12px;
padding: 3px 6px 3px 10px;
}
#wiktku-start-form .wiktku-dfn-chipfield-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-dfn-chipfield-remove:hover {
color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-dfn-chipfield-toggle {
font-size: 12px;
color: var(--wikt-ink-2);
background: var(--wikt-card);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
padding: 3px 10px;
cursor: pointer;
}
#wiktku-start-form .wiktku-dfn-chipfield-toggle:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* Maneid: one value, so a labelled box in the chip row rather than a picker panel. */
#wiktku-start-form .wiktku-dfn-maneid-label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--wikt-ink-2);
}
#wiktku-start-form input.wiktku-dfn-maneid-input {
width: 10em;
padding: 3px 8px;
font-size: 12px;
color: var(--wikt-ink);
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
}
#wiktku-start-form input.wiktku-dfn-maneid-input:focus {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
outline: 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-panel {
margin-top: 8px;
border: 1px solid var(--wikt-field-edge);
background: var(--wikt-card);
border-radius: var(--wikt-r-card, 6px);
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
/* max-width bounds .wiktku-dfn-chipfield-suggest so its pills (e.g.
Dever/Şîwe's 11 suggestions) wrap onto multiple lines instead of
stretching the panel into one long unbroken row. */
min-width: 260px;
max-width: 340px;
}
#wiktku-start-form .wiktku-dfn-chipfield-hint {
font-size: 13px;
color: var(--wikt-ink-2);
}
#wiktku-start-form .wiktku-dfn-chipfield-suggest {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
cursor: pointer;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
padding: 3px 8px 3px 10px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill-remove:hover {
color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-dfn-chipfield-pill:hover {
background: var(--wikt-accent-tint);
border-color: var(--wikt-accent-edge);
color: var(--wikt-accent-deep);
}
#wiktku-start-form .wiktku-dfn-chipfield-input-row {
display: flex;
gap: 6px;
}
#wiktku-start-form .wiktku-dfn-chipfield-input {
flex: 1;
/* Without this, flexbox gives a text input an automatic minimum size
based on its default content width, which is wider than this panel
ever gets on a phone — the row can't shrink to fit and "Qeyd bike"
gets pushed out past the card's edge instead of wrapping/shrinking. */
min-width: 0;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
font-size: 13px;
padding: 6px 8px;
background: var(--wikt-card);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-dfn-chipfield-add {
flex-shrink: 0;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
background: var(--wikt-card);
color: var(--wikt-ink-2);
font-family: var(--wikt-sans);
font-size: 13px;
padding: 6px 14px;
cursor: pointer;
}
#wiktku-start-form .wiktku-dfn-chipfield-add:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* ── Footer + buttons ────────────────────────────────────────────────
One primary filled pill; destructive actions are outline-only in the
danger colour so they stay quiet next to it. */
#wiktku-start-form #wiktku-start-footer {
background: var(--wikt-card-sunken);
border-top: 1px solid var(--wikt-rule);
border-radius: 0 0 var(--wikt-r-card) var(--wikt-r-card);
padding: 12px 16px;
gap: 8px;
}
/* OOUI ships each button widget with its own margin-right, which lands on top of the flex gap and
costs an extra 8px per button when the row is deciding whether to wrap. The gap spaces them. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement {
margin-right: 0;
}
/* --wikt-tap is the 44px touch minimum, which a mouse does not need; at 14px/22px the bar read as
five oversized slabs. Touch sizing is restored under the narrow/coarse rules below. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
border-radius: var(--wikt-r-pill) !important;
font-family: var(--wikt-sans);
font-size: 13px;
font-weight: 500;
padding: 7px 16px;
box-shadow: none !important;
min-height: 32px;
}
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
background: var(--wikt-accent) !important;
border-color: var(--wikt-accent) !important;
color: var(--wikt-on-accent) !important;
}
/* Destructive actions: outline, not fill. */
#wiktku-start-form #wiktku-start-footer .oo-ui-flaggedElement-destructive .oo-ui-buttonElement-button {
background: var(--wikt-card) !important;
border-color: var(--wikt-danger) !important;
color: var(--wikt-danger) !important;
}
#wiktku-start-form #wiktku-last-saved {
font-family: var(--wikt-mono);
font-size: 12px;
color: var(--wikt-muted);
}
/* ── Beşên din picker ────────────────────────────────────────────────
This dropdown is now the entry point to eighteen sections, so it gets
room and a label rather than sitting flush under the tab bar. */
#wiktku-start-form .wiktku-besendin-picker {
margin: 0 0 20px;
padding: 16px 18px;
background: var(--wikt-card);
border: 1px solid var(--wikt-card-edge);
border-radius: 12px;
}
#wiktku-start-form .wiktku-besendin-picker .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-besendin-picker input.wiktku-ac-input {
width: 100%;
max-width: 28em;
}
/* ── Notices ─────────────────────────────────────────────────────────── */
#wiktku-start-form .wiktku-addlang-notice {
background: var(--wikt-accent-tint);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-field);
color: var(--wikt-ink-2);
padding: 12px 16px;
margin-bottom: 16px;
}
#wiktku-start-form .wiktku-pron-preview {
background: var(--wikt-accent-tint);
border-color: var(--wikt-accent-edge);
border-radius: 12px;
}
#wiktku-start-form .wiktku-pron-preview-confirm,
#wiktku-start-form .wiktku-pron-preview-cancel {
border-radius: var(--wikt-r-pill);
padding: 8px 16px;
min-height: 36px;
}
/* Links carry the accent and a soft underline, per the spec's `a` rule. */
#wiktku-start-form a {
color: var(--wikt-accent);
text-decoration: none;
border-bottom: 1px solid var(--wikt-link-under);
}
#wiktku-start-form a:hover {
color: var(--wikt-accent-deep);
}
#wiktku-start-form .wiktku-adv-toggle,
#wiktku-start-form .wiktku-group-tab,
#wiktku-start-form .wiktku-section-tab {
border-bottom-style: solid;
}
#wiktku-start-form .wiktku-adv-toggle {
border-bottom: 0;
}
/* ===================================================================
Dark mode
===================================================================
Every colour above is a token on #wiktku-start-form, so this is a
swap of that one block.
Only applies given an explicit dark signal (skin-theme-clientpref-night /
skin-nightmode, or skin-theme-clientpref-os plus prefers-color-scheme:
dark) — no bare prefers-color-scheme fallback, since legacy Vector sets
no theme class either and would otherwise go dark with nothing else on
the page to match.
Palette is warm, not neutral grey, to keep the cream design's character.
Accent lightens to #7ba7e8 since #2a5db0 on a dark card is ~2.4:1
contrast and reads as near-black.
=================================================================== */
/* A handful of rules still name literal light colours instead of tokens;
neutralise the ones that actually show through in night mode. */
html.skin-theme-clientpref-night #wiktku-start-form .wiktku-ac-results,
html.skin-nightmode #wiktku-start-form .wiktku-ac-results {
background: var(--wikt-card);
border-color: var(--wikt-rule);
color: var(--wikt-ink);
}
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os #wiktku-start-form .wiktku-ac-results {
background: var(--wikt-card);
border-color: var(--wikt-rule);
color: var(--wikt-ink);
}
}
/* ===================================================================
Autocomplete list: stop it escaping its card
===================================================================
A fixed width would run past the card's border on full-width pickers,
since the list is absolutely positioned at the wrapper's left edge.
width:100% ties it to the (shrink-to-fit) wrapper's input width instead;
min/max-width keep short pickers readable and stop viewport overflow. */
#wiktku-start-form ul.wiktku-ac-results {
width: 100%;
min-width: min(18em, 100%);
max-width: min(92vw, 34em);
box-sizing: border-box;
border-radius: var(--wikt-r-field);
box-shadow: none;
}
/* And the wrapper itself must not exceed the card it sits in. */
#wiktku-start-form .wiktku-native-select-wrap {
max-width: 100%;
}
#wiktku-start-form .wiktku-cavkani-wrap .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-cavkani-wrap input.wiktku-ac-input,
#wiktku-start-form .wiktku-pron-wrap .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-etym-wrap .wiktku-native-select-wrap {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* ===================================================================
Mobile footer
===================================================================
Below 720px the primary button keeps its full size and everything
else shrinks to a secondary scale, so the sticky footer doesn't
dominate a phone viewport.
=================================================================== */
@media (max-width: 720px) {
#wiktku-start-form {
padding: 12px;
font-size: 14px;
}
#wiktku-start-form .wiktku-start-step {
padding: 16px 14px 14px;
border-radius: 12px;
}
#wiktku-start-form .wiktku-start-step-name {
font-size: 20px !important;
}
#wiktku-start-form #wiktku-start-footer {
padding: 10px 10px;
gap: 6px;
row-gap: 8px;
}
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
font-size: 13px;
padding: 8px 14px;
min-height: 38px;
}
#wiktku-start-form .wiktku-prefs-btn {
width: 38px;
height: 38px;
background-size: 15px 15px;
}
/* Exactly two button rows, whatever the labels measure: send + editor, then the three small ones.
A flex container's ::after is itself a flex item, so this is the row break; `order` puts it
between the two groups. */
#wiktku-start-form #wiktku-start-footer::after {
content: "";
flex: 0 0 100%;
height: 0;
order: 2;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary {
order: 1;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-cache {
order: 3;
}
/* The gear rides with the primaries: those two leave room for it, the two cache labels do not.
Horizontal padding is trimmed just enough that all three still fit a 360px screen. */
#wiktku-start-form #wiktku-start-footer .wiktku-prefs-btn {
order: 1;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary .oo-ui-buttonElement-button {
padding-left: 12px;
padding-right: 12px;
}
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
padding-left: 14px;
padding-right: 14px;
}
}
/* Below the narrowest common phone the labels no longer fit two to a row at 13px, so the whole bar
steps down one size rather than spilling onto a third and fourth row. */
@media (max-width: 359px) {
#wiktku-start-form #wiktku-start-footer {
padding: 10px 8px;
column-gap: 4px;
}
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
font-size: 12px;
padding-left: 8px;
padding-right: 8px;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary .oo-ui-buttonElement-button,
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
padding-left: 8px;
padding-right: 8px;
}
/* The one you actually press stays thumb-sized. */
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
font-size: 14px;
padding: 10px 20px;
min-height: var(--wikt-tap);
}
/* Secondary actions share a row instead of each claiming a full one. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement {
flex: 0 1 auto;
}
#wiktku-start-form #wiktku-last-saved {
flex: 1 0 100%;
order: 10;
font-size: 11px;
white-space: normal;
}
#wiktku-start-form .wiktku-adv-toggle-row {
gap: 8px;
}
}
/* ===================================================================
IPA character palette (v3 spec)
===================================================================
Click-to-insert chart under Bilêvkirin, after fr.wiktionary's. Two
sub-tabs — a short Kurmancî set that opens by default, and the full
chart — because ~20 of the ~180 IPA symbols ever occur in Kurmancî
and showing all of them first is the same "wall of fields" problem
this redesign exists to fix.
=================================================================== */
/* Templates are not characters, so they sit apart from the chart —
pinned to the right of the sub-tab row. */
/* Full IPA chart: a dropdown picks one group (Bêdeng 1, Bêdeng 2, …)
instead of showing all ~180 symbols' rows at once. */
/* Ligatures like d͡ʒ and long vowels need a little more room than one
glyph; let those keys grow instead of clipping. */
/* A combining mark is shown on a dotted circle so it has something to
attach to; only the mark itself is inserted. */
@media (max-width: 720px) {
}
/* ── "+ Beş zêde bike" menu ──────────────────────────────────────────
Adding a part of speech and adding Bilêvkirin are different actions,
so the button opens a small menu instead of doing one of them. */
#wiktku-start-form .wiktku-addsection-menu {
position: absolute;
z-index: 60;
margin-top: 6px;
min-width: 15em;
background: var(--wikt-card);
border: 1px solid var(--wikt-rule);
border-radius: 10px;
padding: 4px;
display: flex;
flex-direction: column;
}
#wiktku-start-form .wiktku-addsection-item {
display: flex;
flex-direction: column;
gap: 1px;
padding: 8px 10px;
border-radius: 7px;
border-bottom: 0;
text-decoration: none;
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-addsection-item:hover {
background: var(--wikt-accent-tint);
}
#wiktku-start-form .wiktku-addsection-item-label {
font-size: 13.5px;
font-weight: 500;
}
#wiktku-start-form .wiktku-addsection-item-hint {
font-size: 11.5px;
color: var(--wikt-muted);
}
#wiktku-start-form #wiktku-section-tabs .oo-ui-buttonElement {
position: relative;
}
/* Bilêvkirin is a peer of the POS sections but not a part of speech —
a little extra space before it marks the boundary, no divider line. */
#wiktku-start-form .wiktku-section-tab-pron {
margin-left: 6px;
}
/* ===================================================================
Per-sense tabs
===================================================================
"Mane 1 / Mane 2 / + Mane zêde bike" switches which sense is shown;
only the active one is in the flow, so a multi-sense entry doesn't
turn into a long scroll. */
#wiktku-start-form .wiktku-dfn-sense-tabs {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
border-bottom: 1px solid var(--wikt-rule);
margin-bottom: 14px;
}
#wiktku-start-form .wiktku-dfn-sense-tab {
font-size: 13px;
font-weight: 500;
color: var(--wikt-ink-3);
background: transparent;
border: 0;
border-bottom: 3px solid transparent;
border-radius: var(--wikt-r-pill) var(--wikt-r-pill) 0 0;
padding: 7px 12px 9px;
cursor: pointer;
user-select: none;
transition: background 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-dfn-sense-tab:hover {
background: var(--wikt-card-sunken);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-dfn-sense-tab.wiktku-tab-active {
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom-color: var(--wikt-accent);
}
/* Delete button on the Mane / Beş tab bars — a real OOUI ButtonWidget
(icon: "trash", flags: ["destructive"], framed: false), so its glyph,
red destructive tint and hover/focus states all come from OOUI's own
WikimediaUI theme rather than being reimplemented here. This class
only places it: a little breathing room from the tab before it, and
vertically centered in the flex tab bar (OOUI's own icon-button box
is already close to the tabs' height, so no extra sizing is needed). */
#wiktku-start-form .wiktku-trash-btn {
margin-left: 4px;
}
#wiktku-start-form li.wiktku-dfn-sense {
border: 1px solid var(--wikt-card-edge);
border-radius: 12px;
background: var(--wikt-card);
margin-bottom: 10px;
padding: 14px;
list-style: none;
overflow: visible;
}
#wiktku-start-form li.wiktku-dfn-sense.wiktku-sense-collapsed {
display: none;
}
/* ===================================================================
Template rows in the pickers (v3 spec)
===================================================================
One row shape everywhere a template is chosen: the ⓘ explains terms
like "guharto/kevnbûyî/ku-Latn/şaşnivîs" without leaving the form,
and the 🔗 links out for when it doesn't.
=================================================================== */
#wiktku-start-form ul.wiktku-ac-results li.wiktku-ac-tplrow {
display: flex;
align-items: center;
gap: 8px;
min-height: 44px;
white-space: normal;
padding: 6px 10px;
}
#wiktku-start-form .wiktku-ac-tpltext {
flex: 1 1 auto;
min-width: 0;
font-family: var(--wikt-mono);
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Status, not category: green is the one to reach for, yellow means
"only if you specifically need it" (kevnbûyî, şaşnivîs, kêmpeyda). */
#wiktku-start-form .wiktku-tpl-dot {
flex: 0 0 8px;
width: 8px;
height: 8px;
border-radius: 999px;
}
#wiktku-start-form .wiktku-tpl-dot-ok { background: #3d8a55; }
#wiktku-start-form .wiktku-tpl-dot-special { background: #c9971f; }
#wiktku-start-form .wiktku-tpl-icons {
flex: 0 0 auto;
display: flex;
gap: 5px;
align-items: center;
}
#wiktku-start-form .wiktku-tpl-icon {
width: 26px;
height: 26px;
display: grid;
place-items: center;
font-size: 12px;
line-height: 1;
cursor: pointer;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom: 1px solid var(--wikt-accent-edge);
text-decoration: none;
padding: 0;
}
#wiktku-start-form .wiktku-tpl-icon-link { border-radius: 7px; }
#wiktku-start-form .wiktku-tpl-icon-info {
border-radius: 999px;
font-family: var(--wikt-serif);
font-style: italic;
font-weight: 600;
background: transparent;
border-color: var(--wikt-field-edge);
color: var(--wikt-muted);
}
#wiktku-start-form .wiktku-tpl-icon:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* ── The ⓘ panel ─────────────────────────────────────────────────────
Three parts, in the spec's order: what it does, its parameters, and a
way out to the template page. */
/* Floated over the page from the ⓘ that opened it, so no rule here hangs off the wizard's own id --
the panel is appended to <body> to stay clear of the form's scrolling and stacking. */
.wiktku-tpl-info {
position: fixed;
z-index: 1000;
max-height: 60vh;
overflow-y: auto;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
padding: 14px 16px;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
border-radius: 12px;
font-size: 13.5px;
line-height: 1.6;
color: var(--wikt-ink-2);
}
/* A tip above the dropdown it explains, pointing down at it -- the look of MediaWiki's own filter tips.
The skin's tokens give it the page's own light or dark colours. The arrow sits outside the box, so the
box no longer scrolls; a long parameter list scrolls on its own instead. */
.wiktku-tpl-info {
overflow: visible;
max-height: none;
background: var(--background-color-base, #fff);
color: var(--color-base, #202122);
border: 1px solid var(--border-color-base, #a2a9b1);
border-radius: 2px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
.wiktku-tpl-info::after {
content: "";
position: absolute;
left: var(--wiktku-arrow-x, 16px);
width: 12px;
height: 12px;
margin-left: -6px;
background: inherit;
border: inherit;
transform: rotate(45deg);
}
.wiktku-tpl-info-above::after {
bottom: -7px;
border-top: 0;
border-left: 0;
}
.wiktku-tpl-info-below::after {
top: -7px;
border-bottom: 0;
border-right: 0;
}
.wiktku-tpl-info-params ul {
max-height: 30vh;
overflow-y: auto;
}
.wiktku-tpl-info-name {
font-family: var(--wikt-mono);
font-size: 13px;
color: var(--wikt-accent-deep);
margin-bottom: 6px;
}
.wiktku-tpl-info-params {
margin-top: 8px;
}
.wiktku-tpl-info-params-title {
font-size: 12px;
color: var(--wikt-muted);
margin-bottom: 2px;
}
.wiktku-tpl-info-params ul {
margin: 0;
padding-left: 18px;
font-family: var(--wikt-mono);
font-size: 12.5px;
}
.wiktku-tpl-info-link {
display: inline-block;
margin-top: 10px;
font-size: 13px;
}
.wiktku-tpl-info-close {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 999px;
border: 0;
background: transparent;
color: var(--wikt-muted);
cursor: pointer;
font-size: 13px;
line-height: 1;
}
.wiktku-tpl-info-close:hover {
background: var(--wikt-card);
color: var(--wikt-ink);
}
/* ===================================================================
Footer actions
===================================================================
Everything sits on the bar: Bişîne, "Edîtora çavkaniyê veke", both
cache buttons, Tercîh and the saved-at note. The bar wraps rather
than hiding anything behind a menu.
=================================================================== */
#wiktku-start-form .wiktku-btn-danger .oo-ui-buttonElement-button,
#wiktku-start-form .wiktku-btn-danger.oo-ui-buttonElement .oo-ui-buttonElement-button {
color: var(--wikt-danger);
border-color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-cache-note {
flex: 1 1 100%;
order: 99;
font-size: 12px;
color: var(--wikt-muted);
}
#wiktku-start-form .wiktku-cache-note #wiktku-last-saved {
white-space: normal;
}
/* "+ Mane zêde bike" sits as the last item in .wiktku-dfn-sense-tabs. */
#wiktku-start-form .wiktku-add-sense {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 3px 0;
padding: 6px 14px;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
background: var(--wikt-card);
color: var(--wikt-ink-2);
font-family: var(--wikt-sans);
font-size: 13px;
cursor: pointer;
min-height: 34px;
}
#wiktku-start-form .wiktku-add-sense:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
#wiktku-start-form .wiktku-add-sense-plus {
color: var(--wikt-accent);
font-size: 15px;
line-height: 1;
}
/* "+ Beşeke din zêde bike" and "Bi otomatîk tijî bike" are OO.ui.ButtonWidgets
(see start.js), which otherwise render with OOUI's own framed chrome —
override the inner .oo-ui-buttonElement-button to match .wiktku-add-sense's
flat-pill shape above instead, same !important pattern already used for
#wiktku-start-footer's buttons. */
#wiktku-start-form .wiktku-add-sense-btn .oo-ui-buttonElement-button {
border: 1px solid var(--wikt-field-edge) !important;
border-radius: var(--wikt-r-pill) !important;
background: var(--wikt-card) !important;
color: var(--wikt-ink-2) !important;
font-family: var(--wikt-sans);
font-size: 13px;
font-weight: normal;
padding: 6px 14px;
min-height: 34px;
box-shadow: none !important;
}
#wiktku-start-form .wiktku-add-sense-btn .oo-ui-buttonElement-button:hover {
border-color: var(--wikt-accent) !important;
color: var(--wikt-accent) !important;
background: var(--wikt-accent-tint) !important;
}
/* ── Şablon picker, demoted to a link ────────────────────────────────
Picking a template replaces the definition rather than adding to it,
so it sits above the Mane box as a quiet alternative route, hidden
behind one line of text until it is actually wanted. */
#wiktku-start-form .wiktku-dfn-tpl-link {
display: inline-block;
margin-bottom: 8px;
font-size: 12.5px;
color: var(--wikt-muted);
border-bottom: 1px solid transparent;
}
#wiktku-start-form .wiktku-dfn-tpl-link:hover {
color: var(--wikt-accent);
border-bottom-color: var(--wikt-link-under);
}
#wiktku-start-form .wiktku-dfn-tpl-row {
margin-bottom: 10px;
padding-bottom: 12px;
border-bottom: 1px solid var(--wikt-rule-soft);
}
/* ── Template-row icons on touch screens ─────────────────────────────
26px is fine for a mouse and too small for a thumb, and these two sit
right next to each other at the end of a row you can also tap to
select. Give them real targets and some separation on small screens. */
@media (max-width: 720px), (pointer: coarse) {
#wiktku-start-form .wiktku-tpl-icon {
width: 34px;
height: 34px;
font-size: 14px;
}
#wiktku-start-form .wiktku-tpl-icons {
gap: 8px;
padding-left: 4px;
}
#wiktku-start-form ul.wiktku-ac-results li.wiktku-ac-tplrow {
min-height: 50px;
}
}
/* The rendered pronunciation preview is real parser output, so it needs
to sit as content rather than in the monospace wikitext treatment the
panel inherits. */
#wiktku-start-form .wiktku-pron-preview-body {
font-family: var(--wikt-sans);
font-size: 15px;
background: var(--wikt-card);
border: 1px solid var(--wikt-accent-edge);
border-radius: 8px;
padding: 10px 12px;
white-space: normal;
}
#wiktku-start-form .wiktku-pron-preview-body .IPA,
#wiktku-start-form .wiktku-pron-preview-body code,
#wiktku-start-form .wiktku-pron-preview-body tt {
font-family: var(--wikt-mono);
}
/* Parser output brings its own margins; strip the outer ones so the
preview box doesn't gain a blank first line. */
#wiktku-start-form .wiktku-pron-preview-body > *:first-child { margin-top: 0; }
#wiktku-start-form .wiktku-pron-preview-body > *:last-child { margin-bottom: 0; }
/* ===================================================================
Touch: the fields go back to thumb size
===================================================================
The base sizes above are tuned for a mouse. Anything coarse-pointered
or narrow gets the 44px minimum back, and >=16px text so iOS Safari
does not zoom the page when a field takes focus.
=================================================================== */
@media (max-width: 720px), (pointer: coarse) {
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form input.wiktku-ac-input,
#wiktku-start-form select {
min-height: var(--wikt-tap);
padding: 10px 14px;
font-size: 16px;
}
#wiktku-start-form textarea {
padding: 10px 14px;
font-size: 16px;
}
}
/* </nowiki> */
5xmlvnuqvcf1ovbsqv4yrg44cbdhq0v
6531670
6531661
2026-09-15T17:37:33Z
Wikihez
7344
Citoid fill button in the wizard's pill style; no link underline on frameless buttons
6531670
css
text/css
/* <nowiki> */
/*
* Page: [[MediaWiki:Gadget-wikt.start-wizard.css]]
*/
/* ===================================================================
#editform while the wizard is active
=================================================================== */
/* Keeps #editform genuinely laid out instead of display:none*/
.wiktku-offscreen {
position: absolute !important;
left: -9999px !important;
top: 0 !important;
width: 100%;
max-width: 100%;
visibility: hidden;
}
/* ===================================================================
Base scale
=================================================================== */
#wiktku-start-form {
font-size: 0.92em;
line-height: 1.35;
}
#wiktku-start-form * {
box-sizing: border-box;
}
.wiktku-addlang-notice {
margin: 0 0 0.8em;
padding: 0.6em 0.8em;
background: var(--wikt-palette-paleblue);
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
}
/* ===================================================================
Step cards
a compact card with a left accent bar, so each step reads as
a distinct unit
=================================================================== */
.wiktku-start-step {
padding: 0.7em 0.9em;
margin-bottom: 0.6em;
border: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
border-left: 4px solid var(--wikt-palette-blue-9, #3366cc);
border-radius: 6px;
background: var(--wikt-palette-white);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: border-left-color 0.15s ease, box-shadow 0.15s ease;
}
.wiktku-start-step:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.wiktku-start-step:focus-within {
border-left-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
/* Fuses the posparams card onto the bottom of the pos card (shared left
accent bar, no border/gap) since posparams always renders directly
after #wiktku-start-pos once a pos is chosen. */
#wiktku-start-pos {
border-radius: 6px 6px 0 0;
margin-bottom: 0;
border-bottom: none;
}
#wiktku-start-posparams {
border-radius: 0 0 6px 6px;
border-top: none;
padding-top: 0.3em;
}
#wiktku-start-posparams .wiktku-start-step-name {
display: none;
}
/* Scoped and !important: this is a real <h3> tag (see start.js), so the
skin's default heading styles would otherwise override a plain class
rule. Coloured to match the card's accent bar; .wiktku-start-step-question
below stays lighter/muted so the two don't compete. */
#wiktku-start-form .wiktku-start-step-name {
font-size: 1.1em !important;
font-weight: 700;
letter-spacing: 0.03em;
margin: 0 0 0.3em;
line-height: 1.2;
color: var(--wikt-palette-blue-9, #3366cc);
border: 0;
padding: 0;
}
/* Sized close to the heading so the card reads consistently instead of
the text below it looking like fine print. */
.wiktku-start-step-question {
font-size: 1em;
font-weight: 400;
color: var(--wikt-palette-grey-9, #555);
margin: 0 0 0.5em;
line-height: 1.45;
}
.wiktku-start-step label {
padding-right: 1.2em;
font-size: 1em;
}
/* Muted hint text next to a field, styled to read as a hint rather than
another question competing with .wiktku-start-step-question. */
.wiktku-step-hint {
font-size: 0.88em;
font-style: italic;
color: var(--wikt-palette-grey-8, #72777d);
margin: 0 0 0.5em;
line-height: 1.4;
}
/* Same specificity as the rule just above, so it must come after to
actually win: the name label isn't a field label needing right
padding or the form's base label size, it's a heading. */
#wiktku-start-form .wiktku-start-step-name label {
padding-right: 0;
font-size: inherit !important;
color: inherit;
}
input[type="text"].wiktku-start-step-field {
width: 100%;
padding: 5px 8px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 4px;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
input[type="text"].wiktku-start-step-field:focus {
border-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: inset 0 0 0 1px var(--wikt-palette-blue-9, #3366cc);
outline: 0;
}
input[type="text"].wiktku-start-step-field.wiktku-start-narrow {
width: 20em;
}
/* Only used by selects that opt out via .wiktku-plain-select; other
selects get their width from the searchable picker instead (see
input.wiktku-ac-input). */
select.wiktku-start-narrow,
input.wiktku-dfn-tpl-title {
width: 20em;
max-width: 100%;
box-sizing: border-box;
}
#wiktku-start-spinner {
float: right;
margin-left: 1em;
}
#wiktku-start-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5em;
padding: 0.6em 0.2em;
position: sticky;
bottom: 0;
background: var(--wikt-palette-white);
border-top: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
z-index: 5;
}
/* Quiet, non-competing text — sits next to the cache buttons so the
person can tell at a glance whether their latest edits are backed
up, without it fighting the buttons for attention. */
#wiktku-last-saved {
font-size: 0.85em;
color: var(--wikt-palette-grey-9, #767676);
white-space: nowrap;
}
/* ── Tercîh (settings) icon button — sits in the footer next to the
cache buttons. Uses the Codex "settings" (color-subtle) icon from
Commons directly rather than an OOUI icon-pack module, so it renders
the exact requested glyph without pulling in an extra dependency. */
/* Icon-only, sized to sit level with the OOUI buttons beside it in the footer: same pill, same height,
just square. The label lives in title/aria-label. */
.wiktku-prefs-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 32px;
height: 32px;
padding: 0;
border: 1px solid #a2a9b1;
border-radius: var(--wikt-r-pill, 999px);
background-color: #f8f9fa;
background-image: url(//upload.wikimedia.org/wikipedia/commons/e/e0/Codex_icon_settings_color-subtle.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 14px 14px;
cursor: pointer;
transition: background-color 0.12s ease, border-color 0.12s ease;
}
.wiktku-prefs-btn:hover {
background-color: var(--wikt-card, #fffdf8);
border-color: var(--wikt-accent, #2a5db0);
}
.wiktku-prefs-btn:active {
background-color: var(--wikt-accent-tint, #eef2fa);
}
.wiktku-prefs-btn:focus-visible {
outline: 0;
border-color: var(--wikt-accent, #2a5db0);
box-shadow: 0 0 0 3px var(--wikt-accent-tint, #eef2fa);
}
/* Tercîh dialog body — OOUI's bare ProcessDialog body has no inset of its own, so text and fields
otherwise sit flush against the window edges. */
.wiktku-prefs-dialog-body {
padding: 18px 20px 22px;
background: var(--wikt-card, #fffdf8);
}
/* ── Tercîh dialog ──
OOUI's ProcessDialog chrome is the plain Vector grey; these rules put it on the wizard's own
palette and give the head/body/foot bars a shared rhythm. Scoped to .wiktku-prefs-dialog (set in
initialize) so no other OOUI window on the page is touched. */
.wiktku-prefs-dialog .oo-ui-window-frame {
border-radius: var(--wikt-r-card, 14px);
overflow: hidden;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
}
.wiktku-prefs-dialog .oo-ui-processDialog-navigation {
background: var(--wikt-card-sunken, #fbf8f1);
border-bottom: 1px solid var(--wikt-rule, #ded6c8);
}
.wiktku-prefs-dialog .oo-ui-processDialog-title {
font-family: var(--wikt-serif, "Spectral", "Iowan Old Style", Georgia, "Times New Roman", serif);
font-size: 17px;
font-weight: 600;
color: var(--wikt-ink, #1c1a17);
}
/* Head and foot actions read as the same pills as the wizard's own buttons. OOUI gives the three
action groups no shared class, so each is named. */
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
border-radius: var(--wikt-r-pill, 999px) !important;
font-family: var(--wikt-sans, "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif);
font-size: 14px;
font-weight: 500;
padding: 7px 18px;
min-height: 34px;
box-shadow: none !important;
}
/* OOUI stretches the two head groups edge to edge and corner to corner, which pushed the pills flush
against the rounded frame. Inset them and let them size to the button. */
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary {
top: 50%;
bottom: auto;
height: auto;
transform: translateY(-50%);
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe {
left: 10px;
right: auto;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary {
right: 10px;
left: auto;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button {
background: transparent;
border-color: var(--wikt-field-edge, #cfc7b8);
color: var(--wikt-ink-2, #3d3830);
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button {
background: var(--wikt-accent, #2a5db0) !important;
border-color: var(--wikt-accent, #2a5db0) !important;
color: var(--wikt-on-accent, #fff) !important;
}
/* The destructive action is the only thing in the foot, so it sits at the trailing edge instead of
stretching along a bare full-width bar. */
.wiktku-prefs-dialog .oo-ui-window-foot {
background: var(--wikt-card-sunken, #fbf8f1);
border-top: 1px solid var(--wikt-rule, #ded6c8);
padding: 10px 14px;
text-align: right;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other {
display: inline-block;
float: none;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
background: var(--wikt-card, #fffdf8) !important;
border: 1px solid var(--wikt-danger, #b32424) !important;
color: var(--wikt-danger, #b32424) !important;
}
/* ── Groups ── */
.wiktku-prefs-dialog .oo-ui-fieldsetLayout + .oo-ui-fieldsetLayout {
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid var(--wikt-rule-soft, #efe8db);
}
.wiktku-prefs-dialog .oo-ui-fieldsetLayout-header > .oo-ui-labelElement-label {
font-family: var(--wikt-serif, "Spectral", "Iowan Old Style", Georgia, "Times New Roman", serif);
font-size: 15px;
font-weight: 600;
color: var(--wikt-ink, #1c1a17);
margin-bottom: 10px;
padding: 0;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout {
margin-top: 14px;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout:first-child {
margin-top: 0;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
font-size: 14px;
font-weight: 500;
color: var(--wikt-ink-2, #3d3830);
padding-bottom: 6px;
}
.wiktku-prefs-dialog .oo-ui-inline-help {
font-size: 12.5px;
line-height: 1.5;
color: var(--wikt-muted, #8a8378);
margin-top: 5px;
}
/* ── Fields ── */
.wiktku-prefs-dialog .oo-ui-textInputWidget input,
.wiktku-prefs-dialog .oo-ui-dropdownWidget-handle {
border: 1px solid var(--wikt-field-edge, #cfc7b8);
border-radius: var(--wikt-r-field, 10px);
background: var(--wikt-field-bg, #fff);
font-family: var(--wikt-sans, "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif);
font-size: 14px;
color: var(--wikt-ink, #1c1a17);
min-height: 36px;
padding: 6px 10px;
box-shadow: none;
}
.wiktku-prefs-dialog .oo-ui-textInputWidget input:focus,
.wiktku-prefs-dialog .oo-ui-dropdownWidget.oo-ui-widget-enabled:hover .oo-ui-dropdownWidget-handle {
border-color: var(--wikt-accent, #2a5db0);
box-shadow: none;
outline: 0;
}
.wiktku-prefs-dialog .oo-ui-textInputWidget input::placeholder {
color: var(--wikt-placeholder, #b3ab9e);
}
/* Checkbox rows: the label carries the row, so the text beside the box is the target, and the whole
row lifts on hover to say so. */
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline {
padding: 8px 10px;
border-radius: var(--wikt-r-field, 10px);
}
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline:hover {
background: var(--wikt-card-sunken, #fbf8f1);
}
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
padding-bottom: 0;
color: var(--wikt-ink, #1c1a17);
cursor: pointer;
}
/* Narrow screens: the dialog is full-bleed, so the corners it would round are off-screen anyway. */
@media (max-width: 720px) {
.wiktku-prefs-dialog-body {
padding: 14px 14px 18px;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
font-size: 13px;
padding: 8px 14px;
}
}
/* ── "Bişîne" click feedback ── */
@keyframes wiktku-btn-flash-pulse {
0% { box-shadow: 0 0 0 0 rgba(51, 102, 204, 0.55); }
70% { box-shadow: 0 0 0 9px rgba(51, 102, 204, 0); }
100% { box-shadow: 0 0 0 0 rgba(51, 102, 204, 0); }
}
#wiktku-unhide-editform.wiktku-btn-flash {
animation: wiktku-btn-flash-pulse 0.5s ease-out;
}
/* Persistent state while the wikitext is being built/submitted, set
alongside OOUI's own setDisabled(true) so it reads clearly even if
the skin's default disabled style is subtle. */
#wiktku-unhide-editform.wiktku-btn-sending .oo-ui-buttonElement-button {
background-color: var(--wikt-palette-grey-9, #767676) !important;
border-color: var(--wikt-palette-grey-9, #767676) !important;
cursor: progress;
opacity: 1 !important;
}
/* ── "Beşên din" picker ── */
.wiktku-besendin-picker {
margin: 0.6em 0 1em;
}
.wiktku-besendin-picker .wiktku-besendin-select {
width: 100%;
max-width: 24em;
}
#wiktku-start-form textarea.wiktku-start-step-field,
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield,
#wiktku-start-form textarea.wiktku-word-input {
resize: vertical !important;
overflow: auto !important;
box-sizing: border-box !important;
vertical-align: top;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
transition: border-color 0.1s ease, box-shadow 0.1s ease;
}
/* Modern focus state to match the searchable-picker inputs. */
#wiktku-start-form textarea.wiktku-start-step-field:focus,
#wiktku-start-form textarea.wiktku-start-narrow:focus,
#wiktku-start-form textarea.wiktku-dfn-subfield:focus,
#wiktku-start-form textarea.wiktku-word-input:focus {
border-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: inset 0 0 0 1px var(--wikt-palette-blue-9, #3366cc);
outline: 0;
}
/* Standard: every textarea in the form is full width by default */
#wiktku-start-form textarea:not(.wiktku-start-narrow) {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield.wiktku-start-narrow {
width: 20em;
max-width: 100%;
min-width: 8em;
height: calc(1.7em + 10px);
min-height: calc(1.7em + 10px);
padding-top: 4px !important;
padding-bottom: 4px !important;
line-height: 1.35 !important;
resize: none !important;
}
/* Exception: "Sernav" / "Sernavê orjînal" citation-source fields (kitêb,
kovar, malper, …) — long enough to be worth 2 rows and manual resizing. */
#wiktku-start-form textarea.wiktku-dfn-quote-src-input.wiktku-dfn-quote-src-input--title {
height: calc(2 * 1.35em + 10px);
min-height: calc(2 * 1.35em + 10px);
}
/* Image rows (util.js's buildImageListField): each image is its own block
with its own name, so two sets of the same three boxes never run together. */
#wiktku-start-form .wiktku-resim-list {
margin: 0;
padding-left: 0;
}
#wiktku-start-form li.wiktku-resim-item {
list-style: none;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--wikt-rule-soft);
}
#wiktku-start-form li.wiktku-resim-item:first-child {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
#wiktku-start-form .wiktku-resim-index {
margin-bottom: 6px;
font-size: 12px;
font-weight: 600;
color: var(--wikt-ink-2);
}
/* ── Manual drag-to-resize handle ── */
#wiktku-start-form textarea:not(.wiktku-start-narrow) {
resize: none !important;
}
/* Inline modifier button (⊕) */
/* Each word is always a plain box (never collapses into a pill shape); the
box + its ⊕/× buttons sit on one line, and the attached-modifier chips,
if any, on their own line right under it. */
.wiktku-word-li {
display: block;
}
/* ── ⊕-modifier word lists (Hevmane/Dijmane/Formên alternatîv/Termên
koordîne/Jornav/Jêrnav — both the top-level steps and the per-sense
sub-groups): every word is its own always-visible, always-editable box,
one per line — no packing/wrapping of multiple words onto one row. ── */
#wiktku-start-form .wiktku-dfn-sublist > li.wiktku-word-li {
margin-bottom: 12px;
}
/* Modifier chip row: sits under the word's box, indented and closer to it
than the next word is -- that gap difference is what says which chips
belong to which box. */
#wiktku-start-form .wiktku-word-mods {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 3px;
margin-left: 10px;
}
#wiktku-start-form .wiktku-word-mod-chip {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--wikt-card);
color: var(--wikt-ink-2);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
font-size: 11.5px;
padding: 2px 4px 2px 8px;
cursor: pointer;
}
#wiktku-start-form .wiktku-word-mod-chip-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 12px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-word-mod-chip-remove:hover {
color: var(--wikt-danger);
}
/* Word box + its ⊕ (add modifier) / × (remove word) buttons, side by side. */
#wiktku-start-form .wiktku-word-box-row {
display: flex;
align-items: center;
gap: 4px;
}
#wiktku-start-form .wiktku-word-box-row .wiktku-word-input {
flex: 1 1 auto;
}
/* Offered only while the box holds a comma that separates two words. */
#wiktku-start-form .wiktku-word-split-row {
margin-top: 4px;
}
#wiktku-start-form .wiktku-word-split {
flex: 0 0 auto;
height: 1.6em;
padding: 0 8px;
line-height: 1;
font-size: 0.85em;
border: 1px solid var(--wikt-accent-edge);
border-radius: 6px;
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
cursor: pointer;
}
#wiktku-start-form .wiktku-word-split:hover {
border-color: var(--wikt-accent);
}
#wiktku-start-form .wiktku-word-remove {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 6px;
background: var(--wikt-palette-white);
color: var(--wikt-muted);
font-size: 1em;
cursor: pointer;
}
#wiktku-start-form .wiktku-word-remove:hover {
border-color: var(--wikt-danger);
color: var(--wikt-danger);
}
.wiktku-mod-btn {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 2px;
background: var(--wikt-palette-white);
color: var(--wikt-palette-blue-9, #3366cc);
font-size: 1em;
cursor: pointer;
}
.wiktku-mod-btn:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-paleblue, #eaf3ff);
}
.wiktku-mod-picker {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
width: 100%;
margin-top: 0.15em;
padding: 0.35em;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
background: var(--wikt-palette-white);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.wiktku-mod-pill {
padding: 2px 8px;
border: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
border-radius: 12px;
background: var(--wikt-palette-paleblue, #eaf3ff);
font-size: 0.85em;
white-space: nowrap;
cursor: pointer;
}
.wiktku-mod-pill:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
}
.wiktku-mod-pill:disabled,
.wiktku-mod-pill.wiktku-mod-pill-used {
border-color: var(--wikt-palette-grey-3, #e0e0e0);
background: var(--wikt-palette-lightgrey);
color: var(--wikt-palette-grey-3, #999);
cursor: not-allowed;
opacity: 0.6;
}
.wiktku-mod-pill:disabled:hover,
.wiktku-mod-pill.wiktku-mod-pill-used:hover {
border-color: var(--wikt-palette-grey-3, #e0e0e0);
background: var(--wikt-palette-lightgrey);
color: var(--wikt-palette-grey-3, #999);
}
.wiktku-mod-val-wrap {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
width: 100%;
margin-top: 0.15em;
}
/* The key's full name -- the chip itself shows only "crz:", so this is where
it is spelled out. */
.wiktku-mod-val-label {
flex: 0 0 auto;
font-size: 11.5px;
color: var(--wikt-ink-2, #54595d);
white-space: nowrap;
}
.wiktku-mod-val-input {
flex: 0 1 14em;
max-width: 14em;
padding: 3px 6px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
}
.wiktku-mod-confirm,
.wiktku-mod-cancel {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 6px;
background: var(--wikt-palette-white);
cursor: pointer;
}
.wiktku-mod-confirm {
color: #178a17;
border-color: #178a17;
}
.wiktku-mod-cancel {
color: var(--wikt-palette-red-9, #d33);
border-color: var(--wikt-palette-red-9, #d33);
}
/* .wiktku-resize-wrap, .wiktku-resize-grip and .wiktku-linebreak-wrap are util.js's own, so they live in
wikt-edit.css with the file that builds them -- DefSideBoxes uses the same grip and does not load this
stylesheet. */
/* Inline preview + confirm panel shown before inserting an auto-content
pronunciation template, part of the form layout rather than a browser
confirm()/prompt() dialog. */
.wiktku-pron-preview {
margin: 6px 0;
padding: 8px 10px;
border: 1px solid var(--wikt-palette-blue-9, #3366cc);
border-radius: 6px;
background: var(--wikt-palette-paleblue, #eaf3ff);
}
.wiktku-pron-preview-label {
font-size: 0.88em;
font-weight: 600;
color: var(--wikt-palette-grey-9, #555);
margin-bottom: 3px;
}
.wiktku-pron-preview-body {
font-family: monospace;
font-size: 1em;
white-space: pre-wrap;
word-break: break-word;
margin-bottom: 6px;
}
.wiktku-pron-preview-actions {
display: flex;
gap: 6px;
}
.wiktku-pron-preview-confirm,
.wiktku-pron-preview-cancel {
font-size: 0.88em;
padding: 3px 10px;
border-radius: 4px;
cursor: pointer;
border: 1px solid var(--wikt-palette-lightgrey, #a2a9b1);
background: var(--wikt-palette-white, #fff);
}
.wiktku-pron-preview-confirm {
border-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-blue-9, #3366cc);
font-weight: 600;
}
.wiktku-pron-preview-confirm:disabled {
opacity: 0.5;
cursor: wait;
}
.wiktku-pron-preview-cancel:hover {
background: var(--wikt-palette-grey-0, #f8f9fa);
}
/* Template pickers fill their container since the search box, not the
<select>, is what the user sees. */
.wiktku-pron-wrap .wiktku-native-select-wrap,
.wiktku-etym-wrap .wiktku-native-select-wrap,
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
width: 100%;
}
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 0.88em;
}
.wiktku-cavkani-wrap .wiktku-native-select-wrap,
.wiktku-cavkani-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
width: 100%;
}
/* Template selector inside a sense block. */
.wiktku-dfn-tpl-row .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 0.88em;
}
/* Language/dialect rows: picker must line up with the word field beside it. */
.wiktku-lang-row .wiktku-native-select-wrap {
flex: 0 0 auto;
}
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t,
#wiktku-start-form textarea.wiktku-dfn-quote-src-manual,
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text,
#wiktku-start-form .wiktku-tewandin-wrap textarea.wiktku-tewandin-text,
#wiktku-start-form textarea.wiktku-cavkani-text,
#wiktku-start-form textarea.wiktku-dfn-izah {
--wiktku-ta-rows: 2;
height: calc(var(--wiktku-ta-rows) * 1.35em + 6px);
min-height: calc(var(--wiktku-ta-rows) * 1.35em + 6px);
}
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text {
--wiktku-ta-rows: 3;
}
#wiktku-start-form textarea.wiktku-cavkani-text {
--wiktku-ta-rows: 4;
}
/* Mane, Nimûne + Jêgirtin (citation) boxes, and their tr/t sub-fields: at
least 4 rows so there's room to actually write a definition/example/quote.
Manual resize (see util.js) still lets the person grow or shrink any of
these afterward — this only sets where they start. */
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t,
#wiktku-start-form textarea.wiktku-dfn-quote-src-manual {
--wiktku-ta-rows: 4;
}
/* Nudges the border when the box contains a typed/pasted linebreak, so
the user notices before it's silently turned into <br> on output; the
break itself is marked via a mirrored backdrop layer (see below). */
#wiktku-start-form textarea.wiktku-has-linebreak {
border-color: var(--wikt-palette-yellow6, #ac6600) !important;
}
/* Sits behind the textarea showing the same text, with a highlighted
marker at each line followed by a break; font/padding/line-height are
copied from the textarea via computed style, not shared CSS classes. */
.wiktku-linebreak-backdrop {
position: absolute;
inset: 0;
z-index: 0;
margin: 0;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
color: transparent;
background: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
resize: none !important;
pointer-events: none;
}
/* Raise the real textarea above its backdrop so typing/selection/caret
behave completely normally; the backdrop's mark shows through only
where the real textarea's own background is transparent. */
#wiktku-start-form textarea.wiktku-has-linebreak-source {
position: relative;
z-index: 1;
background-color: transparent;
}
/* Zero width, so the backdrop's text wraps exactly where the textarea's does; the marker itself is
painted out of flow from here, into the empty space the line break leaves. */
.wiktku-linebreak-anchor {
position: relative;
display: inline-block;
width: 0;
}
.wiktku-linebreak-mark {
position: absolute;
top: 0;
left: 0;
background-color: var(--wikt-palette-yellow6, #ac6600);
color: var(--wikt-palette-white, #fff);
border-radius: 2px;
}
/* ── Îzaha maneyî ──
Opens under the mane box; the two are one gloss, so it sits tight against it rather than reading as
a separate field. */
.wiktku-dfn-izah-wrap {
display: flex;
flex-direction: column;
gap: 3px;
margin-top: 4px;
}
.wiktku-dfn-nimune-sub,
.wiktku-dfn-quote-sub {
margin-top: 3px;
margin-left: 1em;
padding-left: 0.6em;
border-left: 2px solid var(--wikt-palette-lightgrey);
display: flex;
flex-direction: column;
gap: 3px;
}
.wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
.wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t {
min-width: 8em;
padding-top: 2px !important;
padding-bottom: 2px !important;
line-height: 1.35 !important;
font-size: 0.88em;
}
/* Sense list (Mane step) — one sense visible at a time, switched via
.wiktku-dfn-sense-tabs. */
ol.wiktku-dfn-list {
list-style: none;
margin: 0;
padding: 0;
}
ol.wiktku-dfn-list > li.wiktku-dfn-sense {
margin-bottom: 0.9em;
}
/* Template selector row inside a sense block */
.wiktku-dfn-tpl-row {
margin-bottom: 3px;
}
/* ── Sub-field titles (Etîket, Kategorî, Hevmane, Dijmane, Nimûne,
Jêgirtin, …): each group reads as a small labelled field — the term
stands out, the hint stays quiet. ── */
.wiktku-dfn-subgroup {
margin-top: 0.5em;
padding-top: 0.4em;
border-top: 1px dashed var(--wikt-palette-grey-3, #e0e0e0);
}
.wiktku-dfn-subgroup:first-of-type {
margin-top: 0.3em;
padding-top: 0;
border-top: none;
}
.wiktku-dfn-sublabel {
display: block;
margin-bottom: 3px;
font-size: 0.88em;
}
.wiktku-dfn-sublabel-term {
font-weight: 600;
color: var(--wikt-palette-blue-9, #3366cc);
letter-spacing: 0.03em;
font-size: 0.92em;
}
.wiktku-dfn-sublabel-hint {
color: var(--wikt-palette-grey-9, #767676);
font-style: italic;
}
.wiktku-pron-wrap,
.wiktku-etym-wrap,
.wiktku-tewandin-wrap,
.wiktku-cavkani-wrap {
display: flex;
flex-direction: column;
gap: 3px;
}
/* ── POS section tabs — rounded pill style ── */
#wiktku-section-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.6em;
align-items: center;
}
#wiktku-section-tabs .wiktku-section-tab {
padding: 3px 12px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 999px;
background: var(--wikt-palette-dullcyan);
cursor: pointer;
font-size: 0.86em;
user-select: none;
transition: background 0.12s ease, border-color 0.12s ease;
}
#wiktku-section-tabs .wiktku-section-tab:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
}
#wiktku-section-tabs .wiktku-section-tab.wiktku-tab-active {
background: var(--wikt-palette-blue-9, #3366cc);
border-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
font-weight: 600;
}
/* ── Step-group tabs (Mane / Bilêvkirin / Beşên din — see STEP_GROUPS in
start.js) ── Sits directly under the pill-style section tabs above;
styled as an underlined secondary nav so the two bars stay visually
distinct. */
#wiktku-group-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.8em;
border-bottom: 2px solid var(--wikt-palette-lightgrey);
}
#wiktku-group-tabs .wiktku-group-tab {
position: relative;
padding: 7px 14px 8px;
margin-bottom: -2px;
border-bottom: 3px solid transparent;
border-radius: 3px 3px 0 0;
cursor: pointer;
font-size: 0.95em;
font-weight: 600;
color: var(--wikt-palette-grey-9, #333);
background: var(--wikt-palette-grey-0, #f8f9fa);
user-select: none;
transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
#wiktku-group-tabs .wiktku-group-tab:hover {
color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-paleblue, #eaf3ff);
}
#wiktku-group-tabs .wiktku-group-tab.wiktku-tab-active {
border-bottom-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
background: var(--wikt-palette-blue-9, #3366cc);
font-weight: 700;
}
/* Small dot marking a tab that still has an unfilled required field
hidden inside it (see updateGroupTabIndicators in start.js). Cleared
automatically once that tab becomes the active one. */
#wiktku-group-tabs .wiktku-group-tab.wiktku-tab-missing::after {
content: "";
position: absolute;
top: 4px;
right: -1px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--wikt-palette-red-9, #d33);
}
.wiktku-lang-row-list {
margin: 0;
padding: 0;
list-style: none;
}
.wiktku-lang-row {
display: flex;
align-items: flex-start;
gap: 6px;
margin-bottom: 3px;
flex-wrap: wrap;
}
.wiktku-lang-row-words {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 3px;
}
/* Dûnde/Werger/Zarava rows: align the picker's height with the adjacent
word textarea (see .wiktku-start-narrow above). */
.wiktku-lang-row .wiktku-native-select-wrap input.wiktku-ac-input {
height: calc(1.7em + 10px);
min-height: calc(1.7em + 10px);
padding-top: 4px;
padding-bottom: 4px;
}
/* ── Werger / Bi zaravayên din groups: one per gloss, shared spacing ── */
#wiktku-start-trans .wiktku-trans-group,
#wiktku-start-zarava .wiktku-zarava-group {
margin-bottom: 0.6em;
}
#wiktku-start-trans .wiktku-trans-group:last-child,
#wiktku-start-zarava .wiktku-zarava-group:last-child {
margin-bottom: 0;
}
#wiktku-start-trans .wiktku-trans-group-label,
#wiktku-start-zarava .wiktku-zarava-group-label {
font-weight: 600;
font-size: 0.88em;
margin-bottom: 3px;
}
.wiktku-dfn-jeder-group {
margin-top: 3px;
}
/* Numbers each citation ("Jêgirtin 1:", "Jêgirtin 2:"…) and separates entries with a rule so multiple ones don't blur together. */
.wiktku-dfn-jeder-list {
counter-reset: wiktku-jeder-num;
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li {
counter-increment: wiktku-jeder-num;
padding-top: 8px;
border-top: 1px solid var(--wikt-palette-lightgrey, #eaecf0);
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li:first-child {
padding-top: 0;
border-top: none;
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li::before {
content: "Jêgirtin " counter(wiktku-jeder-num) ":";
display: block;
margin-bottom: 3px;
font-weight: 600;
font-size: 0.82em;
color: var(--wikt-palette-blue-9, #3366cc);
letter-spacing: 0.03em;
}
.wiktku-dfn-quote-src-sel {
font-size: 0.88em;
margin-top: 3px;
display: block;
/* A native select with no width rule sizes to fit its longest <option>
(here, the longest çavkanî template name) rather than its container,
which overflows the card on a phone — cap it like every other field. */
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* Wide/narrow/medium layout, same idea as MediaWiki:Gadget-wikt.add-
examples.js: rows wrap freely, each sized by its own width tier
(.wiktku-dfn-quote-src-row--wide/narrow/medium) instead of always
stacking one field per line. */
.wiktku-dfn-quote-src-fields {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 6px 10px;
margin-top: 3px;
}
/* Fills the flex row so the manual-source textarea's resize wrapper doesn't shrink to the browser's default width. */
.wiktku-dfn-quote-src-fields > .wiktku-resize-wrap {
flex: 1 1 100%;
width: 100%;
}
/* The Kategorî:Çavkanî picker is not one of the parameter boxes -- it decides which template's boxes
these are -- so it takes a line of its own above them. Left as a plain flex item it shrank to its
content and the first narrow field wrapped up beside it, which read as a nameless box paired with
someone else's label. */
.wiktku-dfn-quote-src-fields > .wiktku-cat-tpl-picker {
flex: 1 1 100%;
width: 100%;
}
/* A labelled source box -- "Şablona çavkaniyê", the link -- in the add-examples form's stacked shape:
label, box, then the button and hints under it at their natural size. */
.wiktku-dfn-quote-field {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.4em;
margin-top: 8px;
}
.wiktku-dfn-quote-url {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.wiktku-dfn-quote-citoid-btn,
.wiktku-dfn-quote-citoid-status {
align-self: flex-start;
}
#wiktku-start-form .wiktku-dfn-quote-url-hint {
margin: 0;
}
/* "Malûmatên çavkaniyê" and its "Vala bike", over a jêder-* template's boxes. */
.wiktku-dfn-quote-src-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--wikt-palette-lightgrey, #eaecf0);
}
.wiktku-dfn-quote-src-head-label {
font-size: 11px;
letter-spacing: 0.12em;
color: var(--wikt-muted, #72777d);
}
.wiktku-dfn-quote-src-clear {
font-size: 12px;
}
.wiktku-dfn-quote-src-cat-status {
display: block;
font-size: 0.82em;
color: var(--wikt-palette-lightgrey, #767676);
margin-top: 2px;
}
.wiktku-dfn-quote-src-row {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1 1 12em;
}
.wiktku-dfn-quote-src-row--wide {
flex-basis: 100%;
}
.wiktku-dfn-quote-src-row--narrow {
flex: 0 1 7em;
}
.wiktku-dfn-quote-src-row--medium {
flex: 1 1 12em;
}
.wiktku-dfn-quote-src-row label {
font-size: 0.82em;
white-space: nowrap;
}
.wiktku-dfn-quote-src-input {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
@media (max-width: 720px) {
.wiktku-lang-row {
flex-direction: column;
}
/* Re-asserts against the skin's own mobile-breakpoint heading styles,
which load after ours in the cascade. */
#wiktku-start-form .wiktku-start-step-name {
font-size: 1.05em !important;
margin: 0 0 0.2em !important;
line-height: 1.2 !important;
border: 0 !important;
padding: 0 !important;
}
/* Single column: search box, list, and narrow textareas (Etîket,
Hevmane, Dûnde/Jê/Têkildar/Binêre herwiha word boxes, citation
source fields, …) all use the full width. */
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield.wiktku-start-narrow {
width: 100%;
max-width: 100%;
}
.wiktku-mod-val-input {
flex: 1 1 auto;
max-width: 100%;
}
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text {
--wiktku-ta-rows: 3;
font-size: 1em;
}
#wiktku-start-form .wiktku-tewandin-wrap textarea.wiktku-tewandin-text,
#wiktku-start-form textarea.wiktku-cavkani-text {
--wiktku-ta-rows: 4;
font-size: 1em;
}
/* Mane, Nimûne + Jêgirtin and their tr/t sub-fields: keep at least 4 rows
on mobile too, rather than shrinking further than the desktop size. */
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t {
--wiktku-ta-rows: 4;
font-size: 1em;
}
.wiktku-dfn-nimune-sub,
.wiktku-dfn-quote-sub {
margin-left: 0.5em;
padding-left: 0.4em;
}
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 1em;
min-height: 2.2em;
}
/* Every field stacks full-width on mobile, regardless of its wide/
narrow/medium tier — there's no room to wrap several per line. */
#wiktku-start-form .wiktku-dfn-quote-src-row {
flex-basis: 100%;
}
.wiktku-start-step {
padding: 0.5em 0.6em;
}
}
/* ===================================================================
Night mode
Palette.css already flips --wikt-palette-* vars for night mode; this
hook only covers real colours (panel border) that aren't a palette var,
since a drop shadow is invisible against an equally dark page.
=================================================================== */
@media screen {
html.skin-theme-clientpref-night .wiktku-start-step,
html.skin-theme-clientpref-night .wiktku-start-step:hover,
html.skin-theme-clientpref-night .wiktku-start-step:focus-within {
box-shadow: none;
}
}
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os .wiktku-start-step,
html.skin-theme-clientpref-os .wiktku-start-step:hover,
html.skin-theme-clientpref-os .wiktku-start-step:focus-within {
box-shadow: none;
}
}
/* ── posparams step (live wikitext + helper widgets) ── */
.wiktku-posparams-wrap {
display: flex;
flex-direction: column;
gap: 6px;
}
.wiktku-posparams-helpers {
display: flex;
flex-direction: column;
gap: 4px;
}
.wiktku-posparams-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.wiktku-posparams-row label {
font-size: 1em;
min-width: 10em;
padding-right: 0;
}
.wiktku-posparams-subheading {
font-weight: 600;
font-size: 0.88em;
margin-top: 6px;
}
.wiktku-posparams-pronoun-hint {
font-size: 0.9em;
color: var(--wikt-ink-2);
font-style: italic;
}
/* Keeps a pronoun hint and its box on one line, immediately next to each
other, even when the row around them stacks label-over-field (see the
desktop grid layout for .wiktku-posparams-helpers below). */
.wiktku-posparams-field-line {
display: flex;
align-items: center;
gap: 6px;
}
.wiktku-posparams-text {
width: 100%;
box-sizing: border-box;
font-family: monospace;
font-size: 0.9em;
}
.wiktku-posparams-opt-toggle {
font-size: 0.85em;
margin-top: 2px;
display: inline-block;
}
.wiktku-posparams-opt-panel {
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px 0 2px 1em;
}
.wiktku-posparams-opt-row {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.85em;
}
/* ── resim step (one or more images) ── */
.wiktku-resim-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.wiktku-resim-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.wiktku-resim-item .wiktku-posparams-row {
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.wiktku-resim-item .wiktku-posparams-row label {
min-width: 0;
font-size: 0.9em;
}
/* ── dfn: which sub-field is on screen ──
Every sub-group (Nimûne, Jêgirtin, Hevmane, …) is built hidden and shown
only once it is picked from the chip row or the "Hevmane û Bêhtir" menu — see the
PICK_DEFS block in start.js. */
#wiktku-start-form .wiktku-dfn-subgroup-hidden {
display: none;
}
/* A şablon sense ({{guharto}}, {{şaşnivîs}}…) has no gloss of its own, so
the word lists make no sense for it — both the boxes and the menu entries
that would ask for them go away. */
#wiktku-start-form li.wiktku-sense-is-tpl .wiktku-dfn-wordgroup,
#wiktku-start-form li.wiktku-sense-is-tpl .wiktku-dfn-pick-word {
display: none;
}
/* × on a revealed sub-group's label, to put it away again. Dim until the
group is hovered, so it is never the loudest thing in the row. */
#wiktku-start-form .wiktku-dfn-subgroup-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 14px;
line-height: 1;
padding: 0 4px;
margin-left: 4px;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-dfn-subgroup:hover .wiktku-dfn-subgroup-remove,
#wiktku-start-form .wiktku-dfn-subgroup-remove:focus {
opacity: 1;
}
#wiktku-start-form .wiktku-dfn-subgroup-remove:hover {
color: var(--wikt-danger);
opacity: 1;
}
/* ── "Hevmane û Bêhtir" menu ──
An overlay anchored to its own toggle, so opening it never pushes the
boxes below it down the page. */
#wiktku-start-form .wiktku-dfn-more-wrap {
position: relative;
display: inline-block;
}
#wiktku-start-form .wiktku-dfn-more-arrow {
font-size: 9px;
vertical-align: 1px;
}
/* Same menu DefSideBoxes draws (.dsb-more-menu): a plain vertical list of links, sized to its widest
entry, floating over the page rather than pushing it down. */
#wiktku-start-form .wiktku-dfn-more-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 30;
display: flex;
flex-direction: column;
align-items: flex-start;
width: max-content;
max-width: min(320px, calc(100vw - 40px));
gap: 6px;
margin: 2px 0 0 4px;
padding: 8px 12px;
background: var(--wikt-card);
border: 1px solid var(--wikt-rule);
border-radius: var(--wikt-r-field);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}
#wiktku-start-form .wiktku-dfn-more-item {
align-self: flex-start;
font-size: 12px;
color: var(--wikt-accent);
white-space: nowrap;
cursor: pointer;
text-decoration: none;
/* The form's own link style underlines every <a> with a border; on a block-level menu entry that
reads as a rule between the items. */
border-bottom: 0;
}
#wiktku-start-form .wiktku-dfn-more-item:hover,
#wiktku-start-form .wiktku-dfn-more-item:focus {
color: var(--wikt-accent-deep);
text-decoration: underline;
border-bottom: 0;
}
/* Rules the menu's three sections apart: label fields, word lists, and "Maneyeke din zêde bike". */
#wiktku-start-form .wiktku-dfn-more-group-start {
margin-top: 7px;
padding-top: 7px;
border-top: 1px solid var(--wikt-rule-soft);
}
/* ===================================================================
Desktop text bump
Modifier pills/buttons and posparams helper text read fine at their
small mobile sizes but look too small on desktop, so bump them up
past the 720px breakpoint only.
=================================================================== */
@media (min-width: 721px) {
.wiktku-mod-btn,
.wiktku-mod-confirm,
.wiktku-mod-cancel {
width: 1.8em;
height: 1.8em;
font-size: 1.1em;
}
.wiktku-mod-pill {
font-size: 0.95em;
padding: 3px 10px;
}
.wiktku-mod-val-input {
font-size: 0.95em;
}
.wiktku-posparams-row label {
font-size: 1em;
}
.wiktku-posparams-text {
font-size: 1em;
}
.wiktku-posparams-opt-toggle {
font-size: 0.95em;
}
.wiktku-posparams-opt-row {
font-size: 0.95em;
}
/* Multi-column card layout for posparams helper fields (transliteration,
gendered forms, comparative/superlative, hanja, verb-tense roots, …),
desktop-only. Rows are shown/hidden via jQuery inline display; the
grid only rearranges visible rows, not which ones are visible. */
.wiktku-posparams-helpers {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px 16px;
align-items: start;
}
.wiktku-posparams-helpers > .wiktku-posparams-row {
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
gap: 3px;
}
.wiktku-posparams-helpers > .wiktku-posparams-row label {
min-width: 0;
}
.wiktku-posparams-helpers > .wiktku-posparams-row .wiktku-posparams-field,
.wiktku-posparams-helpers > .wiktku-posparams-row select.wiktku-posparams-cinsiyet-sel,
.wiktku-posparams-helpers > .wiktku-posparams-row select.wiktku-posparams-lektur-sel {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* The "Jimar" row lives here too — just another .wiktku-posparams-row,
no extra CSS needed. */
/* These two aren't per-field rows — span the full card width so the
collapsed toggle and its checkbox list don't get squeezed into one
narrow column. */
.wiktku-posparams-helpers > .wiktku-posparams-opt-toggle,
.wiktku-posparams-helpers > .wiktku-posparams-opt-panel,
.wiktku-posparams-helpers > .wiktku-posparams-subheading {
grid-column: 1 / -1;
}
}
/* ===================================================================
SÊWIRANA NÛ — design system layer
===================================================================
Purely the visual skin (colour, type, weight, radius, spacing), layered
after the layout rules above so it wins the cascade.
No @import of Spectral / IBM Plex from Google Fonts: Wikimedia's CSP
blocks third-party font hosts, so the stacks below name the spec's
faces first (used if the wiki installs them locally) and fall back to
faces with the same feel.
Accent colour is reserved for actionable elements (focus, links, the
active tab, the primary button, chips); section headings are near-black.
=================================================================== */
#wiktku-start-form {
font-family: var(--wikt-sans);
font-size: 15px;
line-height: 1.55;
color: var(--wikt-ink);
background: var(--wikt-page);
padding: 24px;
border-radius: var(--wikt-r-card);
}
/* ── Cards ───────────────────────────────────────────────────────────
Flat cream panel held by a single hairline border, no shadow. */
#wiktku-start-form .wiktku-start-step {
background: var(--wikt-card);
border: 1px solid var(--wikt-card-edge);
border-left: 1px solid var(--wikt-card-edge);
border-radius: var(--wikt-r-card);
box-shadow: none;
padding: 24px 24px 20px;
margin-bottom: 16px;
transition: border-color 0.12s ease;
}
#wiktku-start-form .wiktku-start-step:hover,
#wiktku-start-form .wiktku-start-step:focus-within {
box-shadow: none;
border-color: var(--wikt-rule);
}
/* The fused pos + posparams pair keeps reading as one card. margin-bottom
has to be reset here too — .wiktku-start-step's own 16px otherwise wins
(higher specificity than the older #wiktku-start-pos-only rule this was
meant to override), leaving a visible gap between the two "halves". */
#wiktku-start-form #wiktku-start-pos {
border-radius: var(--wikt-r-card) var(--wikt-r-card) 0 0;
border-bottom: none;
padding-bottom: 8px;
margin-bottom: 0;
}
#wiktku-start-form #wiktku-start-posparams {
border-radius: 0 0 var(--wikt-r-card) var(--wikt-r-card);
border-top: none;
padding-top: 4px;
}
/* ── Type ────────────────────────────────────────────────────────────
Step headings: serif, regular weight, near-black. 20px matches what the
narrow breakpoint already resolves to, so the two read the same; at 24px
every step title was a third taller than the answer beneath it and a form
with a dozen sections spent most of a desktop screen on headings. */
#wiktku-start-form .wiktku-start-step-name {
font-family: var(--wikt-serif);
font-size: 20px !important;
font-weight: 400;
letter-spacing: -0.01em;
line-height: 1.15;
color: var(--wikt-ink);
margin: 0 0 8px;
}
/* display:block matters: this is a <label>, so it was sharing a line with
the control it introduces wherever start.js didn't emit a <br>. */
#wiktku-start-form .wiktku-start-step-question {
display: block;
font-size: 15px;
font-weight: 400;
color: var(--wikt-ink-3);
margin: 0 0 12px;
line-height: 1.6;
max-width: 62ch;
}
/* Collapses the label entirely when empty (posparams' "Cins/zayend" title
can be blank) instead of leaving its margin behind as a gap; a no-op
for every other step, whose question text is never empty. */
#wiktku-start-form .wiktku-start-step-question:empty {
display: none;
margin: 0;
}
/* Same reason — a field label belongs above its field, not beside it. */
#wiktku-start-form .wiktku-posparams-row label,
#wiktku-start-form .wiktku-dfn-sublabel {
display: block;
margin-bottom: 6px;
padding-right: 0;
}
/* Field labels: small, medium weight, full-strength ink — they label a
control, so they should not read as body copy. */
#wiktku-start-form .wiktku-start-step label,
#wiktku-start-form .wiktku-besendin-picker label,
#wiktku-start-form .wiktku-posparams-row label,
#wiktku-start-form .wiktku-dfn-sublabel {
font-size: 13px;
font-weight: 500;
color: var(--wikt-ink);
letter-spacing: 0.01em;
}
/* The parenthetical/example half of a label is supporting text. */
#wiktku-start-form .wiktku-dfn-sublabel-hint,
#wiktku-start-form .wiktku-step-hint {
font-weight: 400;
font-style: italic;
color: var(--wikt-muted);
font-size: 12.5px;
}
/* ── Fields ──────────────────────────────────────────────────────────
One shape for every control: 1px edge, 10px radius, 44px tall. */
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form input.wiktku-ac-input,
#wiktku-start-form textarea,
#wiktku-start-form select {
font-family: var(--wikt-sans);
font-size: 14px;
color: var(--wikt-ink);
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
padding: 6px 10px;
box-shadow: none;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* --wikt-tap is the 44px touch minimum, which a mouse does not need: on a pointer-precise screen it made
every dropdown a third taller than its own text. The touch size is put back under the coarse-pointer
and narrow rules below. */
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form select {
min-height: 34px;
}
#wiktku-start-form textarea {
line-height: 1.6;
}
/* Wikitext and anything template-shaped reads as code. The monospace face carries that on its own;
these are fields you type into, so they keep the same white fill and edge as every other field. */
#wiktku-start-form .wiktku-posparams-text,
#wiktku-start-form .wiktku-pron-text,
#wiktku-start-form .wiktku-etym-text,
#wiktku-start-form .wiktku-cavkani-text,
#wiktku-start-form .wiktku-tewandin-text {
font-family: var(--wikt-mono);
font-size: 13.5px;
background: var(--wikt-field-bg);
}
/* The pronunciation preview is generated output, not an input, so it stays tinted. */
#wiktku-start-form .wiktku-pron-preview-body {
font-family: var(--wikt-mono);
font-size: 13.5px;
background: var(--wikt-code-bg);
border-color: var(--wikt-card-edge);
}
#wiktku-start-form ::placeholder {
color: var(--wikt-placeholder);
opacity: 1;
}
/* Focus: accent edge + 3px tint halo. The only "elevation" in the design. */
#wiktku-start-form input[type="text"]:focus,
#wiktku-start-form input[type="search"]:focus,
#wiktku-start-form textarea:focus,
#wiktku-start-form select:focus,
#wiktku-start-form input.wiktku-ac-input:focus {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
outline: 0;
}
/* The searchable picker's visible input is the field, so it takes the
field shape too (it otherwise inherits an older, boxier rule). */
#wiktku-start-form input.wiktku-ac-input {
min-height: 34px;
border-radius: var(--wikt-r-field);
border-color: var(--wikt-field-edge);
font-size: 14px;
}
#wiktku-start-form .wiktku-ac-results {
border: 1px solid var(--wikt-rule);
border-radius: var(--wikt-r-field);
background: var(--wikt-card);
box-shadow: none;
}
#wiktku-start-form .wiktku-ac-results li.wiktku-ac-active,
#wiktku-start-form .wiktku-ac-results li:hover {
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
}
/* ── Tabs → the accent's main job ────────────────────────────────────
Active tab: tint fill + 3px accent underline, per the spec's
"xala hûrgulî" note. Inactive tabs are plain text. */
#wiktku-start-form #wiktku-group-tabs,
#wiktku-start-form #wiktku-section-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
border-bottom: 1px solid var(--wikt-rule);
margin-bottom: 16px;
padding-bottom: 0;
}
/* Both bars are direct children of #wiktku-start-form, so a sticky child
stays stuck for as long as any part of the form is in view. Background
is opaque so content scrolling underneath doesn't show through.
Section-tabs pins below --wiktku-siteheader-h — an offset for Vector
2022's own sticky header, which otherwise sits on top of it at top:0
(Minerva has no such header, so mobile needs no offset); group-tabs
pins below that plus --wiktku-stabs-h (section-tabs' own height). Both
custom properties are set by syncStickyTabsOffset() in start.js and
fall back to 0 on skins without a Vector sticky header. */
#wiktku-start-form #wiktku-section-tabs {
position: sticky;
top: var(--wiktku-siteheader-h, 0px);
z-index: 20;
background: var(--wikt-card);
}
#wiktku-start-form #wiktku-group-tabs {
position: sticky;
top: calc(var(--wiktku-siteheader-h, 0px) + var(--wiktku-stabs-h, 0px));
z-index: 19;
background: var(--wikt-card);
}
#wiktku-start-form .wiktku-group-tab,
#wiktku-start-form .wiktku-section-tab {
font-size: 13.5px;
font-weight: 500;
color: var(--wikt-ink-3);
background: transparent;
border: 0;
border-bottom: 3px solid transparent;
border-radius: var(--wikt-r-pill) var(--wikt-r-pill) 0 0;
padding: 9px 14px 11px;
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-group-tab:hover,
#wiktku-start-form .wiktku-section-tab:hover {
background: var(--wikt-card-sunken);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-group-tab.wiktku-tab-active,
#wiktku-start-form .wiktku-section-tab.wiktku-tab-active {
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom-color: var(--wikt-accent);
}
/* ── Progressive-disclosure toggles ──────────────────────────────────
Shared by "Tercîhên pêşketî" (posparams) and "Malûmatên pêşketî"
(Mane): a rotating chevron plus a count of what's hidden. */
#wiktku-start-form .wiktku-adv-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin: 4px 0 12px;
padding-top: 16px;
border-top: 1px solid var(--wikt-rule-soft);
}
#wiktku-start-form .wiktku-adv-toggle {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 500;
color: var(--wikt-accent);
text-decoration: none;
cursor: pointer;
}
#wiktku-start-form .wiktku-adv-toggle:hover .wiktku-adv-toggle-text {
text-decoration: underline;
}
#wiktku-start-form .wiktku-adv-toggle-chevron {
width: 22px;
height: 22px;
flex: 0 0 22px;
display: grid;
place-items: center;
font-size: 11px;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
border-radius: 6px;
transform: rotate(-90deg);
transition: transform 0.15s ease;
}
/* Open state: dfn flips the class on the step, posparams on the link. */
#wiktku-start-form .wiktku-adv-toggle.wiktku-adv-open .wiktku-adv-toggle-chevron,
#wiktku-start-dfn:not(.wiktku-dfn-subs-hidden) .wiktku-adv-toggle-chevron {
transform: rotate(0deg);
}
#wiktku-start-form .wiktku-posparams-adv-panel {
border: 1px solid var(--wikt-rule-soft);
border-radius: 12px;
background: var(--wikt-card-sunken);
padding: 20px 18px;
margin-bottom: 4px;
}
/* Vertical rhythm between stacked field rows (e.g. "Tewandina lêkerê"),
wider than a row's own label-to-field gap so rows read as distinct
groups instead of bleeding together. */
#wiktku-start-form .wiktku-posparams-helpers {
display: flex;
flex-direction: column;
gap: 14px;
}
/* ── posparams grid ──────────────────────────────────────────────────
"Tor, ne rêz" — two columns, nothing narrower than 220px, so the six
gender/number fields stop being a six-across scanline. */
@media (min-width: 721px) {
#wiktku-start-form .wiktku-posparams-helpers,
#wiktku-start-form .wiktku-posparams-adv-panel {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 18px 22px;
align-items: start;
}
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-row,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-row {
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
gap: 7px;
}
/* The row's own gap already separates the label from its field — the
label's margin-bottom (set for the stacked mobile layout) would
otherwise double that spacing here. */
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-row label,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-row label {
margin-bottom: 0;
}
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-adv-toggle-row,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-adv-panel,
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-opt-panel {
grid-column: 1 / -1;
}
}
#wiktku-start-form .wiktku-posparams-opt-panel {
border-top: 1px solid var(--wikt-rule-soft);
padding-top: 14px;
margin-top: 4px;
}
#wiktku-start-form .wiktku-posparams-opt-row {
font-size: 13px;
color: var(--wikt-ink-3);
}
/* ── Sense blocks ────────────────────────────────────────────────────
Each sense is a sunken panel inside the Mane card, with its number as
an accent counter — so "how many meanings am I writing" is legible at
a glance. */
#wiktku-start-form .wiktku-dfn-subgroup {
margin: 14px 0 0;
padding-top: 14px;
border-top: 1px solid var(--wikt-rule-soft);
}
/* No divider above the first item — it would be a rule against the
sense's own text. The class is put on the first *shown* group by
updateFirstVisible() in start.js: :first-of-type would keep picking a
group that is still hidden behind the chip row, leaving the first
visible one with a stray rule above it. */
#wiktku-start-form .wiktku-dfn-subgroup-first {
margin-top: 8px;
padding-top: 0;
border-top: none;
}
#wiktku-start-form .wiktku-dfn-sublabel-term {
color: var(--wikt-ink);
}
/* Plain white field like any other, not a tinted fill — a tint would
read as "disabled" next to the white Mane box above it. */
#wiktku-start-form .wiktku-dfn-subfield,
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form .wiktku-word-input {
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
}
/* No "1." "2." markers on the sub-lists: numbered rows would read as a
form to fill top-down, when each row is really "another one if you
have one" — and the marker would land on its own line above the
block-level textarea, looking like a stray character. */
#wiktku-start-form .wiktku-dfn-sublist {
margin: 0;
padding-left: 0;
list-style: none;
}
#wiktku-start-form .wiktku-dfn-sublist > li {
margin-bottom: 6px;
}
/* ── Chips: tags, modifiers, template pills ──────────────────────────── */
#wiktku-start-form .wiktku-mod-pill,
#wiktku-start-form .wiktku-besendin-select + .wiktku-chip {
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
font-size: 13px;
padding: 6px 12px;
}
#wiktku-start-form .wiktku-mod-btn,
#wiktku-start-form .wiktku-mod-confirm,
#wiktku-start-form .wiktku-mod-cancel {
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-radius: 8px;
}
/* ── Chip + toggle fields ("1c" design) — Dever/Şîwe, Etîket, Kategorî:
a small "+ …" toggle on the sense, not an always-visible field (see
buildChipField in start.js, used by all three). Chips sit inline before
the toggle; clicking it opens a compact panel with optional quick-pick
suggestions plus free text below. Each field's own wrap class (e.g.
.wiktku-dfn-dever-wrap) only scopes chip reads/writes in start.js — all
the visual styling here is shared via .wiktku-dfn-chipfield-*. The three
toggles live together in .wiktku-dfn-chipfield-row; their panels render
separately in .wiktku-dfn-chipfield-panels below it, so opening one
field's panel never pushes the other toggles around. ── */
#wiktku-start-form .wiktku-dfn-chipfield-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 8px 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-panels {
display: flex;
flex-direction: column;
gap: 8px;
}
#wiktku-start-form .wiktku-dfn-chipfield-wrap {
margin: 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-chips {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
#wiktku-start-form .wiktku-dfn-chipfield-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
font-size: 12px;
padding: 3px 6px 3px 10px;
}
#wiktku-start-form .wiktku-dfn-chipfield-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-dfn-chipfield-remove:hover {
color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-dfn-chipfield-toggle {
font-size: 12px;
color: var(--wikt-ink-2);
background: var(--wikt-card);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
padding: 3px 10px;
cursor: pointer;
}
#wiktku-start-form .wiktku-dfn-chipfield-toggle:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* Maneid: one value, so a labelled box in the chip row rather than a picker panel. */
#wiktku-start-form .wiktku-dfn-maneid-label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--wikt-ink-2);
}
#wiktku-start-form input.wiktku-dfn-maneid-input {
width: 10em;
padding: 3px 8px;
font-size: 12px;
color: var(--wikt-ink);
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
}
#wiktku-start-form input.wiktku-dfn-maneid-input:focus {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
outline: 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-panel {
margin-top: 8px;
border: 1px solid var(--wikt-field-edge);
background: var(--wikt-card);
border-radius: var(--wikt-r-card, 6px);
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
/* max-width bounds .wiktku-dfn-chipfield-suggest so its pills (e.g.
Dever/Şîwe's 11 suggestions) wrap onto multiple lines instead of
stretching the panel into one long unbroken row. */
min-width: 260px;
max-width: 340px;
}
#wiktku-start-form .wiktku-dfn-chipfield-hint {
font-size: 13px;
color: var(--wikt-ink-2);
}
#wiktku-start-form .wiktku-dfn-chipfield-suggest {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
cursor: pointer;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
padding: 3px 8px 3px 10px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill-remove:hover {
color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-dfn-chipfield-pill:hover {
background: var(--wikt-accent-tint);
border-color: var(--wikt-accent-edge);
color: var(--wikt-accent-deep);
}
#wiktku-start-form .wiktku-dfn-chipfield-input-row {
display: flex;
gap: 6px;
}
#wiktku-start-form .wiktku-dfn-chipfield-input {
flex: 1;
/* Without this, flexbox gives a text input an automatic minimum size
based on its default content width, which is wider than this panel
ever gets on a phone — the row can't shrink to fit and "Qeyd bike"
gets pushed out past the card's edge instead of wrapping/shrinking. */
min-width: 0;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
font-size: 13px;
padding: 6px 8px;
background: var(--wikt-card);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-dfn-chipfield-add {
flex-shrink: 0;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
background: var(--wikt-card);
color: var(--wikt-ink-2);
font-family: var(--wikt-sans);
font-size: 13px;
padding: 6px 14px;
cursor: pointer;
}
#wiktku-start-form .wiktku-dfn-chipfield-add:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* ── Footer + buttons ────────────────────────────────────────────────
One primary filled pill; destructive actions are outline-only in the
danger colour so they stay quiet next to it. */
#wiktku-start-form #wiktku-start-footer {
background: var(--wikt-card-sunken);
border-top: 1px solid var(--wikt-rule);
border-radius: 0 0 var(--wikt-r-card) var(--wikt-r-card);
padding: 12px 16px;
gap: 8px;
}
/* OOUI ships each button widget with its own margin-right, which lands on top of the flex gap and
costs an extra 8px per button when the row is deciding whether to wrap. The gap spaces them. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement {
margin-right: 0;
}
/* --wikt-tap is the 44px touch minimum, which a mouse does not need; at 14px/22px the bar read as
five oversized slabs. Touch sizing is restored under the narrow/coarse rules below. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
border-radius: var(--wikt-r-pill) !important;
font-family: var(--wikt-sans);
font-size: 13px;
font-weight: 500;
padding: 7px 16px;
box-shadow: none !important;
min-height: 32px;
}
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
background: var(--wikt-accent) !important;
border-color: var(--wikt-accent) !important;
color: var(--wikt-on-accent) !important;
}
/* Destructive actions: outline, not fill. */
#wiktku-start-form #wiktku-start-footer .oo-ui-flaggedElement-destructive .oo-ui-buttonElement-button {
background: var(--wikt-card) !important;
border-color: var(--wikt-danger) !important;
color: var(--wikt-danger) !important;
}
#wiktku-start-form #wiktku-last-saved {
font-family: var(--wikt-mono);
font-size: 12px;
color: var(--wikt-muted);
}
/* ── Beşên din picker ────────────────────────────────────────────────
This dropdown is now the entry point to eighteen sections, so it gets
room and a label rather than sitting flush under the tab bar. */
#wiktku-start-form .wiktku-besendin-picker {
margin: 0 0 20px;
padding: 16px 18px;
background: var(--wikt-card);
border: 1px solid var(--wikt-card-edge);
border-radius: 12px;
}
#wiktku-start-form .wiktku-besendin-picker .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-besendin-picker input.wiktku-ac-input {
width: 100%;
max-width: 28em;
}
/* ── Notices ─────────────────────────────────────────────────────────── */
#wiktku-start-form .wiktku-addlang-notice {
background: var(--wikt-accent-tint);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-field);
color: var(--wikt-ink-2);
padding: 12px 16px;
margin-bottom: 16px;
}
#wiktku-start-form .wiktku-pron-preview {
background: var(--wikt-accent-tint);
border-color: var(--wikt-accent-edge);
border-radius: 12px;
}
#wiktku-start-form .wiktku-pron-preview-confirm,
#wiktku-start-form .wiktku-pron-preview-cancel {
border-radius: var(--wikt-r-pill);
padding: 8px 16px;
min-height: 36px;
}
/* Links carry the accent and a soft underline, per the spec's `a` rule. */
#wiktku-start-form a {
color: var(--wikt-accent);
text-decoration: none;
border-bottom: 1px solid var(--wikt-link-under);
}
#wiktku-start-form a:hover {
color: var(--wikt-accent-deep);
}
/* A frameless OOUI button ("Vala bike") is an <a> inside, but not a link. */
#wiktku-start-form .oo-ui-buttonElement-frameless > a.oo-ui-buttonElement-button {
border-bottom: 0;
}
#wiktku-start-form .wiktku-adv-toggle,
#wiktku-start-form .wiktku-group-tab,
#wiktku-start-form .wiktku-section-tab {
border-bottom-style: solid;
}
#wiktku-start-form .wiktku-adv-toggle {
border-bottom: 0;
}
/* ===================================================================
Dark mode
===================================================================
Every colour above is a token on #wiktku-start-form, so this is a
swap of that one block.
Only applies given an explicit dark signal (skin-theme-clientpref-night /
skin-nightmode, or skin-theme-clientpref-os plus prefers-color-scheme:
dark) — no bare prefers-color-scheme fallback, since legacy Vector sets
no theme class either and would otherwise go dark with nothing else on
the page to match.
Palette is warm, not neutral grey, to keep the cream design's character.
Accent lightens to #7ba7e8 since #2a5db0 on a dark card is ~2.4:1
contrast and reads as near-black.
=================================================================== */
/* A handful of rules still name literal light colours instead of tokens;
neutralise the ones that actually show through in night mode. */
html.skin-theme-clientpref-night #wiktku-start-form .wiktku-ac-results,
html.skin-nightmode #wiktku-start-form .wiktku-ac-results {
background: var(--wikt-card);
border-color: var(--wikt-rule);
color: var(--wikt-ink);
}
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os #wiktku-start-form .wiktku-ac-results {
background: var(--wikt-card);
border-color: var(--wikt-rule);
color: var(--wikt-ink);
}
}
/* ===================================================================
Autocomplete list: stop it escaping its card
===================================================================
A fixed width would run past the card's border on full-width pickers,
since the list is absolutely positioned at the wrapper's left edge.
width:100% ties it to the (shrink-to-fit) wrapper's input width instead;
min/max-width keep short pickers readable and stop viewport overflow. */
#wiktku-start-form ul.wiktku-ac-results {
width: 100%;
min-width: min(18em, 100%);
max-width: min(92vw, 34em);
box-sizing: border-box;
border-radius: var(--wikt-r-field);
box-shadow: none;
}
/* And the wrapper itself must not exceed the card it sits in. */
#wiktku-start-form .wiktku-native-select-wrap {
max-width: 100%;
}
#wiktku-start-form .wiktku-cavkani-wrap .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-cavkani-wrap input.wiktku-ac-input,
#wiktku-start-form .wiktku-pron-wrap .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-etym-wrap .wiktku-native-select-wrap {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* ===================================================================
Mobile footer
===================================================================
Below 720px the primary button keeps its full size and everything
else shrinks to a secondary scale, so the sticky footer doesn't
dominate a phone viewport.
=================================================================== */
@media (max-width: 720px) {
#wiktku-start-form {
padding: 12px;
font-size: 14px;
}
#wiktku-start-form .wiktku-start-step {
padding: 16px 14px 14px;
border-radius: 12px;
}
#wiktku-start-form .wiktku-start-step-name {
font-size: 20px !important;
}
#wiktku-start-form #wiktku-start-footer {
padding: 10px 10px;
gap: 6px;
row-gap: 8px;
}
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
font-size: 13px;
padding: 8px 14px;
min-height: 38px;
}
#wiktku-start-form .wiktku-prefs-btn {
width: 38px;
height: 38px;
background-size: 15px 15px;
}
/* Exactly two button rows, whatever the labels measure: send + editor, then the three small ones.
A flex container's ::after is itself a flex item, so this is the row break; `order` puts it
between the two groups. */
#wiktku-start-form #wiktku-start-footer::after {
content: "";
flex: 0 0 100%;
height: 0;
order: 2;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary {
order: 1;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-cache {
order: 3;
}
/* The gear rides with the primaries: those two leave room for it, the two cache labels do not.
Horizontal padding is trimmed just enough that all three still fit a 360px screen. */
#wiktku-start-form #wiktku-start-footer .wiktku-prefs-btn {
order: 1;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary .oo-ui-buttonElement-button {
padding-left: 12px;
padding-right: 12px;
}
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
padding-left: 14px;
padding-right: 14px;
}
}
/* Below the narrowest common phone the labels no longer fit two to a row at 13px, so the whole bar
steps down one size rather than spilling onto a third and fourth row. */
@media (max-width: 359px) {
#wiktku-start-form #wiktku-start-footer {
padding: 10px 8px;
column-gap: 4px;
}
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
font-size: 12px;
padding-left: 8px;
padding-right: 8px;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary .oo-ui-buttonElement-button,
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
padding-left: 8px;
padding-right: 8px;
}
/* The one you actually press stays thumb-sized. */
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
font-size: 14px;
padding: 10px 20px;
min-height: var(--wikt-tap);
}
/* Secondary actions share a row instead of each claiming a full one. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement {
flex: 0 1 auto;
}
#wiktku-start-form #wiktku-last-saved {
flex: 1 0 100%;
order: 10;
font-size: 11px;
white-space: normal;
}
#wiktku-start-form .wiktku-adv-toggle-row {
gap: 8px;
}
}
/* ===================================================================
IPA character palette (v3 spec)
===================================================================
Click-to-insert chart under Bilêvkirin, after fr.wiktionary's. Two
sub-tabs — a short Kurmancî set that opens by default, and the full
chart — because ~20 of the ~180 IPA symbols ever occur in Kurmancî
and showing all of them first is the same "wall of fields" problem
this redesign exists to fix.
=================================================================== */
/* Templates are not characters, so they sit apart from the chart —
pinned to the right of the sub-tab row. */
/* Full IPA chart: a dropdown picks one group (Bêdeng 1, Bêdeng 2, …)
instead of showing all ~180 symbols' rows at once. */
/* Ligatures like d͡ʒ and long vowels need a little more room than one
glyph; let those keys grow instead of clipping. */
/* A combining mark is shown on a dotted circle so it has something to
attach to; only the mark itself is inserted. */
@media (max-width: 720px) {
}
/* ── "+ Beş zêde bike" menu ──────────────────────────────────────────
Adding a part of speech and adding Bilêvkirin are different actions,
so the button opens a small menu instead of doing one of them. */
#wiktku-start-form .wiktku-addsection-menu {
position: absolute;
z-index: 60;
margin-top: 6px;
min-width: 15em;
background: var(--wikt-card);
border: 1px solid var(--wikt-rule);
border-radius: 10px;
padding: 4px;
display: flex;
flex-direction: column;
}
#wiktku-start-form .wiktku-addsection-item {
display: flex;
flex-direction: column;
gap: 1px;
padding: 8px 10px;
border-radius: 7px;
border-bottom: 0;
text-decoration: none;
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-addsection-item:hover {
background: var(--wikt-accent-tint);
}
#wiktku-start-form .wiktku-addsection-item-label {
font-size: 13.5px;
font-weight: 500;
}
#wiktku-start-form .wiktku-addsection-item-hint {
font-size: 11.5px;
color: var(--wikt-muted);
}
#wiktku-start-form #wiktku-section-tabs .oo-ui-buttonElement {
position: relative;
}
/* Bilêvkirin is a peer of the POS sections but not a part of speech —
a little extra space before it marks the boundary, no divider line. */
#wiktku-start-form .wiktku-section-tab-pron {
margin-left: 6px;
}
/* ===================================================================
Per-sense tabs
===================================================================
"Mane 1 / Mane 2 / + Mane zêde bike" switches which sense is shown;
only the active one is in the flow, so a multi-sense entry doesn't
turn into a long scroll. */
#wiktku-start-form .wiktku-dfn-sense-tabs {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
border-bottom: 1px solid var(--wikt-rule);
margin-bottom: 14px;
}
#wiktku-start-form .wiktku-dfn-sense-tab {
font-size: 13px;
font-weight: 500;
color: var(--wikt-ink-3);
background: transparent;
border: 0;
border-bottom: 3px solid transparent;
border-radius: var(--wikt-r-pill) var(--wikt-r-pill) 0 0;
padding: 7px 12px 9px;
cursor: pointer;
user-select: none;
transition: background 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-dfn-sense-tab:hover {
background: var(--wikt-card-sunken);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-dfn-sense-tab.wiktku-tab-active {
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom-color: var(--wikt-accent);
}
/* Delete button on the Mane / Beş tab bars — a real OOUI ButtonWidget
(icon: "trash", flags: ["destructive"], framed: false), so its glyph,
red destructive tint and hover/focus states all come from OOUI's own
WikimediaUI theme rather than being reimplemented here. This class
only places it: a little breathing room from the tab before it, and
vertically centered in the flex tab bar (OOUI's own icon-button box
is already close to the tabs' height, so no extra sizing is needed). */
#wiktku-start-form .wiktku-trash-btn {
margin-left: 4px;
}
#wiktku-start-form li.wiktku-dfn-sense {
border: 1px solid var(--wikt-card-edge);
border-radius: 12px;
background: var(--wikt-card);
margin-bottom: 10px;
padding: 14px;
list-style: none;
overflow: visible;
}
#wiktku-start-form li.wiktku-dfn-sense.wiktku-sense-collapsed {
display: none;
}
/* ===================================================================
Template rows in the pickers (v3 spec)
===================================================================
One row shape everywhere a template is chosen: the ⓘ explains terms
like "guharto/kevnbûyî/ku-Latn/şaşnivîs" without leaving the form,
and the 🔗 links out for when it doesn't.
=================================================================== */
#wiktku-start-form ul.wiktku-ac-results li.wiktku-ac-tplrow {
display: flex;
align-items: center;
gap: 8px;
min-height: 44px;
white-space: normal;
padding: 6px 10px;
}
#wiktku-start-form .wiktku-ac-tpltext {
flex: 1 1 auto;
min-width: 0;
font-family: var(--wikt-mono);
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Status, not category: green is the one to reach for, yellow means
"only if you specifically need it" (kevnbûyî, şaşnivîs, kêmpeyda). */
#wiktku-start-form .wiktku-tpl-dot {
flex: 0 0 8px;
width: 8px;
height: 8px;
border-radius: 999px;
}
#wiktku-start-form .wiktku-tpl-dot-ok { background: #3d8a55; }
#wiktku-start-form .wiktku-tpl-dot-special { background: #c9971f; }
#wiktku-start-form .wiktku-tpl-icons {
flex: 0 0 auto;
display: flex;
gap: 5px;
align-items: center;
}
#wiktku-start-form .wiktku-tpl-icon {
width: 26px;
height: 26px;
display: grid;
place-items: center;
font-size: 12px;
line-height: 1;
cursor: pointer;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom: 1px solid var(--wikt-accent-edge);
text-decoration: none;
padding: 0;
}
#wiktku-start-form .wiktku-tpl-icon-link { border-radius: 7px; }
#wiktku-start-form .wiktku-tpl-icon-info {
border-radius: 999px;
font-family: var(--wikt-serif);
font-style: italic;
font-weight: 600;
background: transparent;
border-color: var(--wikt-field-edge);
color: var(--wikt-muted);
}
#wiktku-start-form .wiktku-tpl-icon:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* ── The ⓘ panel ─────────────────────────────────────────────────────
Three parts, in the spec's order: what it does, its parameters, and a
way out to the template page. */
/* Floated over the page from the ⓘ that opened it, so no rule here hangs off the wizard's own id --
the panel is appended to <body> to stay clear of the form's scrolling and stacking. */
.wiktku-tpl-info {
position: fixed;
z-index: 1000;
max-height: 60vh;
overflow-y: auto;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
padding: 14px 16px;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
border-radius: 12px;
font-size: 13.5px;
line-height: 1.6;
color: var(--wikt-ink-2);
}
/* A tip above the dropdown it explains, pointing down at it -- the look of MediaWiki's own filter tips.
The skin's tokens give it the page's own light or dark colours. The arrow sits outside the box, so the
box no longer scrolls; a long parameter list scrolls on its own instead. */
.wiktku-tpl-info {
overflow: visible;
max-height: none;
background: var(--background-color-base, #fff);
color: var(--color-base, #202122);
border: 1px solid var(--border-color-base, #a2a9b1);
border-radius: 2px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
.wiktku-tpl-info::after {
content: "";
position: absolute;
left: var(--wiktku-arrow-x, 16px);
width: 12px;
height: 12px;
margin-left: -6px;
background: inherit;
border: inherit;
transform: rotate(45deg);
}
.wiktku-tpl-info-above::after {
bottom: -7px;
border-top: 0;
border-left: 0;
}
.wiktku-tpl-info-below::after {
top: -7px;
border-bottom: 0;
border-right: 0;
}
.wiktku-tpl-info-params ul {
max-height: 30vh;
overflow-y: auto;
}
.wiktku-tpl-info-name {
font-family: var(--wikt-mono);
font-size: 13px;
color: var(--wikt-accent-deep);
margin-bottom: 6px;
}
.wiktku-tpl-info-params {
margin-top: 8px;
}
.wiktku-tpl-info-params-title {
font-size: 12px;
color: var(--wikt-muted);
margin-bottom: 2px;
}
.wiktku-tpl-info-params ul {
margin: 0;
padding-left: 18px;
font-family: var(--wikt-mono);
font-size: 12.5px;
}
.wiktku-tpl-info-link {
display: inline-block;
margin-top: 10px;
font-size: 13px;
}
.wiktku-tpl-info-close {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 999px;
border: 0;
background: transparent;
color: var(--wikt-muted);
cursor: pointer;
font-size: 13px;
line-height: 1;
}
.wiktku-tpl-info-close:hover {
background: var(--wikt-card);
color: var(--wikt-ink);
}
/* ===================================================================
Footer actions
===================================================================
Everything sits on the bar: Bişîne, "Edîtora çavkaniyê veke", both
cache buttons, Tercîh and the saved-at note. The bar wraps rather
than hiding anything behind a menu.
=================================================================== */
#wiktku-start-form .wiktku-btn-danger .oo-ui-buttonElement-button,
#wiktku-start-form .wiktku-btn-danger.oo-ui-buttonElement .oo-ui-buttonElement-button {
color: var(--wikt-danger);
border-color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-cache-note {
flex: 1 1 100%;
order: 99;
font-size: 12px;
color: var(--wikt-muted);
}
#wiktku-start-form .wiktku-cache-note #wiktku-last-saved {
white-space: normal;
}
/* "+ Mane zêde bike" sits as the last item in .wiktku-dfn-sense-tabs. */
#wiktku-start-form .wiktku-add-sense {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 3px 0;
padding: 6px 14px;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
background: var(--wikt-card);
color: var(--wikt-ink-2);
font-family: var(--wikt-sans);
font-size: 13px;
cursor: pointer;
min-height: 34px;
}
#wiktku-start-form .wiktku-add-sense:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
#wiktku-start-form .wiktku-add-sense-plus {
color: var(--wikt-accent);
font-size: 15px;
line-height: 1;
}
/* "+ Beşeke din zêde bike" and "Bi otomatîk tijî bike" are OO.ui.ButtonWidgets
(see start.js), which otherwise render with OOUI's own framed chrome —
override the inner .oo-ui-buttonElement-button to match .wiktku-add-sense's
flat-pill shape above instead, same !important pattern already used for
#wiktku-start-footer's buttons. */
#wiktku-start-form .wiktku-add-sense-btn .oo-ui-buttonElement-button {
border: 1px solid var(--wikt-field-edge) !important;
border-radius: var(--wikt-r-pill) !important;
background: var(--wikt-card) !important;
color: var(--wikt-ink-2) !important;
font-family: var(--wikt-sans);
font-size: 13px;
font-weight: normal;
padding: 6px 14px;
min-height: 34px;
box-shadow: none !important;
}
#wiktku-start-form .wiktku-add-sense-btn .oo-ui-buttonElement-button:hover {
border-color: var(--wikt-accent) !important;
color: var(--wikt-accent) !important;
background: var(--wikt-accent-tint) !important;
}
/* ── Şablon picker, demoted to a link ────────────────────────────────
Picking a template replaces the definition rather than adding to it,
so it sits above the Mane box as a quiet alternative route, hidden
behind one line of text until it is actually wanted. */
#wiktku-start-form .wiktku-dfn-tpl-link {
display: inline-block;
margin-bottom: 8px;
font-size: 12.5px;
color: var(--wikt-muted);
border-bottom: 1px solid transparent;
}
#wiktku-start-form .wiktku-dfn-tpl-link:hover {
color: var(--wikt-accent);
border-bottom-color: var(--wikt-link-under);
}
#wiktku-start-form .wiktku-dfn-tpl-row {
margin-bottom: 10px;
padding-bottom: 12px;
border-bottom: 1px solid var(--wikt-rule-soft);
}
/* ── Template-row icons on touch screens ─────────────────────────────
26px is fine for a mouse and too small for a thumb, and these two sit
right next to each other at the end of a row you can also tap to
select. Give them real targets and some separation on small screens. */
@media (max-width: 720px), (pointer: coarse) {
#wiktku-start-form .wiktku-tpl-icon {
width: 34px;
height: 34px;
font-size: 14px;
}
#wiktku-start-form .wiktku-tpl-icons {
gap: 8px;
padding-left: 4px;
}
#wiktku-start-form ul.wiktku-ac-results li.wiktku-ac-tplrow {
min-height: 50px;
}
}
/* The rendered pronunciation preview is real parser output, so it needs
to sit as content rather than in the monospace wikitext treatment the
panel inherits. */
#wiktku-start-form .wiktku-pron-preview-body {
font-family: var(--wikt-sans);
font-size: 15px;
background: var(--wikt-card);
border: 1px solid var(--wikt-accent-edge);
border-radius: 8px;
padding: 10px 12px;
white-space: normal;
}
#wiktku-start-form .wiktku-pron-preview-body .IPA,
#wiktku-start-form .wiktku-pron-preview-body code,
#wiktku-start-form .wiktku-pron-preview-body tt {
font-family: var(--wikt-mono);
}
/* Parser output brings its own margins; strip the outer ones so the
preview box doesn't gain a blank first line. */
#wiktku-start-form .wiktku-pron-preview-body > *:first-child { margin-top: 0; }
#wiktku-start-form .wiktku-pron-preview-body > *:last-child { margin-bottom: 0; }
/* ===================================================================
Touch: the fields go back to thumb size
===================================================================
The base sizes above are tuned for a mouse. Anything coarse-pointered
or narrow gets the 44px minimum back, and >=16px text so iOS Safari
does not zoom the page when a field takes focus.
=================================================================== */
@media (max-width: 720px), (pointer: coarse) {
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form input.wiktku-ac-input,
#wiktku-start-form select {
min-height: var(--wikt-tap);
padding: 10px 14px;
font-size: 16px;
}
#wiktku-start-form textarea {
padding: 10px 14px;
font-size: 16px;
}
}
/* </nowiki> */
tk8sm3piv44nebzbxjhx91mkyg7m2dw
Bikarhêner:Wikihez/Gadget-test.js
2
1358395
6531660
6514261
2026-09-15T17:26:41Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none
6531660
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.start-wizard.js]]
*/
// Li ser [[vi:MediaWiki:Gadget-start.js]] hatiye refaktorkirin.
// implicit dependencies: StorageUtils (ObjectStorage), Gadget-citoid-helper.js (wikt.citoid)
/* global ObjectStorage, wikt */
$(function () {
if (window.location.href.includes("editintro=User:Wikihez/creation/intro")) {
return; // Do not run the script wextê WF:Bilez dişuxile
}
// The same gate DefSideBoxes, add-examples and this wizard's own launcher use, so a page one of them
// works on is never a page this one quietly refuses to open on.
var ns = mw.config.get("wgNamespaceNumber");
if (!require("ext.gadget.wikt.core").sections.isEntryPage()) {
return;
}
// Reconstructed entries (ns 108) are unattested proto-forms: there is no recorded usage to give an
// example of or quote, and no dialects to mark, so the sense block leaves out Nimûne, Jêgirtin and
// Dever/Şîwe there.
var isReconstruction = ns === 108;
var addLangMode = mw.util.getParamValue("wiktkuaddlang") === "1";
// ── Tercîh (user settings) panel stored via mw.storage (localStorage), globally
var WIKTKU_PREFS_KEY = "wiktku_start_prefs";
var WIKTKU_PREFS_DEFAULTS = {
// [] = show the full language list in the "ziman" step; otherwise
// only these codes are shown there (data-main.json codes, not names).
langCodes: [],
// true = never auto-open on a new/empty page — only via the
// "+Ziman/beşeke din" portlet link.
onlyOnClick: false,
// "auto" | "p-cactions" | "p-tb" | "p-views" — where the
// "+Ziman/beşeke din" portlet link is added.
portlet: "auto",
// true = suppress routine/informational mw.notify toasts (page
// loading, cache auto-restored, cache saved/cleared, "tê şandin…").
// Errors and direct results of an explicit action (like saving
// Tercîh itself) always show regardless of this setting.
quiet: false
};
// Defaults and storage live in wikt.core/prefs.js, because the launcher reads them on every page view.
var wiktkuPrefsApi = require("ext.gadget.wikt.core").prefs;
function wiktkuLoadPrefs() { return wiktkuPrefsApi.load(); }
function wiktkuSavePrefs(prefs) { return wiktkuPrefsApi.save(prefs); }
var wiktkuPrefs = wiktkuLoadPrefs();
// Routine/informational notices go through here so "quiet mode" can
// suppress them; errors and explicit-action confirmations (like the
// Tercîh save toast) call mw.notify directly instead, and always show.
function wiktkuNotify(message, options) {
if (wiktkuPrefs.quiet) return;
mw.notify(message, options);
}
// Clears every draft cache and resize height this script writes, for all pages. Tercîh itself is
// left alone: it is the setting being edited when this button is reached.
// The state it touches is assigned by build(), which has always run by the time this is callable.
function wiktkuClearAllScriptCache() {
// Cancel the pending autosave, or it would rewrite the cache from the still-filled form.
if (autosaveDebounceTimer) {
clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = null;
}
formDirty = false;
$("#wiktku-last-saved").text("");
// Keeps formCache's in-memory copy in step with the localStorage sweep below.
try { formCache.Set({}); } catch (e) { /* storage unavailable — the raw sweep below still runs */ }
if (!window.localStorage) return;
var toRemove = [];
try {
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (!key) continue;
// ObjectStorage wraps the draft key as "enwikt/ObjectStorage/<name>?v=<n>&Type=…", so it
// is matched as a substring; the mw.storage keys below are plain prefixes.
if (key.indexOf("wiktku_start_draft_") !== -1 || key.indexOf("wiktku_resize_height_") === 0 ||
key.indexOf(WIKTKU_SUGGESTIONS_KEY_PREFIX) === 0) {
toRemove.push(key);
}
}
} catch (e) { return; }
$.each(toRemove, function (_, key) {
try { localStorage.removeItem(key); } catch (e) { /* skip this key, keep clearing the rest */ }
});
}
// Lazily built the first time it's opened; reused after that (see
// wiktkuOpenPrefsDialog below, defined once we know OOUI is available).
var wiktkuPrefsWindowManager = null;
var wiktkuPrefsDialog = null;
function wiktkuOpenPrefsDialog() {
mw.loader.using(["oojs-ui-core", "oojs-ui-windows"], function () {
function WiktkuPrefsDialog(config) {
WiktkuPrefsDialog.super.call(this, config);
}
if (!wiktkuPrefsDialog) {
OO.inheritClass(WiktkuPrefsDialog, OO.ui.ProcessDialog);
WiktkuPrefsDialog.static.name = "wiktkuPrefsDialog";
WiktkuPrefsDialog.static.title = "Tercîhên min";
WiktkuPrefsDialog.static.actions = [
{ action: "save", label: "Qeyd bike", flags: ["primary", "progressive"] },
{ action: "cancel", label: wiktkuUtil.msg.cancel, flags: "safe" },
{ action: "clearcache", label: "Hemû cache paqij bike", flags: "destructive" }
];
WiktkuPrefsDialog.prototype.initialize = function () {
WiktkuPrefsDialog.super.prototype.initialize.call(this);
this.langCodesInput = new OO.ui.TextInputWidget({
placeholder: "ku, zza, ckb (vala bihêle ji bo hemû zimanan)"
});
this.onlyOnClickCheckbox = new OO.ui.CheckboxInputWidget({});
this.quietCheckbox = new OO.ui.CheckboxInputWidget({});
this.portletDropdown = new OO.ui.DropdownInputWidget({
options: [
{ data: "auto", label: "Otomatîk (li gorî rûyê wîkiyê)" },
{ data: "p-cactions", label: "p-cactions - Li qismê Amûran" },
{ data: "left-navigation", label: "left-navigation - Li qismê Gotar-Gotûbêj" },
{ data: "p-tb", label: "p-tb - Li qismê Giştî" },
{ data: "p-views", label: "p-views - Li qismê Bixwîne" }
]
});
// Two named groups rather than one "Eyar" heap: which languages the wizard offers, then
// how it behaves. The dialog title already says these are preferences.
var langSet = new OO.ui.FieldsetLayout({ label: "Ziman" });
langSet.addItems([
new OO.ui.FieldLayout(this.langCodesInput, {
label: "Kodên zimanan ên ku tê xuyakirin li qismê \"ziman\"",
align: "top",
help: "Kodan bi virgulê (,) ji hev veqetîne. Vala bihêle da ku hemû ziman werin nîşandan.",
helpInline: true
})
]);
var behaviourSet = new OO.ui.FieldsetLayout({ label: "Wextê xebitînê" });
behaviourSet.addItems([
new OO.ui.FieldLayout(this.onlyOnClickCheckbox, {
label: "Tenê veke wextê \"+Ziman/beşeke din\" were tikandin",
align: "inline",
help: "Eger were hilbijartin, ev amûr êdî otomatîk li ser rûpelên nû dest pê nake — divê meriv her carê pêl \"+Ziman/beşeke din\" bike ku bişixule.",
helpInline: true
}),
new OO.ui.FieldLayout(this.portletDropdown, {
label: "Cihê lînka \"+Ziman/beşeke din\"",
align: "top",
help: "\"Otomatîk\" li gorî rûyê wîkiyê hildibijêre.",
helpInline: true
}),
new OO.ui.FieldLayout(this.quietCheckbox, {
label: "Moda bêdeng (kêmtir agahdarî nîşan bide)",
align: "inline",
help: "Agahdariyên normal nîşan nede. Xeta her tim tên xuyakirin.",
helpInline: true
})
]);
this.$element.addClass("wiktku-prefs-dialog");
this.$body.addClass("wiktku-prefs-dialog-body")
.append(langSet.$element, behaviourSet.$element);
};
WiktkuPrefsDialog.prototype.getSetupProcess = function (data) {
return WiktkuPrefsDialog.super.prototype.getSetupProcess.call(this, data)
.next(function () {
this.langCodesInput.setValue(wiktkuPrefs.langCodes.join(", "));
this.onlyOnClickCheckbox.setSelected(!!wiktkuPrefs.onlyOnClick);
this.portletDropdown.setValue(wiktkuPrefs.portlet || "auto");
this.quietCheckbox.setSelected(!!wiktkuPrefs.quiet);
}, this);
};
WiktkuPrefsDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === "save") {
return new OO.ui.Process(function () {
var codes = dialog.langCodesInput.getValue()
.split(/[,\s]+/)
.map(function (c) { return c.trim().toLowerCase(); })
.filter(function (c) { return !!c; });
wiktkuPrefs = {
langCodes: codes,
onlyOnClick: dialog.onlyOnClickCheckbox.isSelected(),
portlet: dialog.portletDropdown.getValue() || "auto",
quiet: dialog.quietCheckbox.isSelected()
};
wiktkuSavePrefs(wiktkuPrefs);
mw.notify(
"Tercîh hatin qeydkirin. Rûpelê ji nû ve bar bike (F5).",
{ type: "success", tag: "wiktku-prefs-save" }
);
dialog.close({ action: "save" });
});
}
if (action === "cancel") {
return new OO.ui.Process(function () {
dialog.close({ action: "cancel" });
});
}
if (action === "clearcache") {
return new OO.ui.Process(function () {
if (!window.confirm(
"Tu dixwazî hemû cache-ya vê amûrê ji her rûpelê paqij bikî " +
"(danûstandinên qeydkirî yên hemû rûpelan, bilindahiyên qutiyan)? " +
"Tercîhên li jor nayên jêbirin. Ev tişt nayê vegerandin."
)) return;
wiktkuClearAllScriptCache();
mw.notify("Hemû cache hate paqijkirin.", { type: "success", tag: "wiktku-cache-clear-all" });
dialog.close({ action: "clearcache" });
});
}
return WiktkuPrefsDialog.super.prototype.getActionProcess.call(this, action);
};
wiktkuPrefsWindowManager = new OO.ui.WindowManager();
$(document.body).append(wiktkuPrefsWindowManager.$element);
wiktkuPrefsDialog = new WiktkuPrefsDialog();
wiktkuPrefsWindowManager.addWindows([wiktkuPrefsDialog]);
}
wiktkuPrefsWindowManager.openWindow(wiktkuPrefsDialog);
});
}
// The launcher gadget adds the portlet link and decides whether this page wants the wizard before
// loading it at all. Both are repeated here only when this file was loaded on its own.
var wiktkuLaunched = !!(window.wikt && window.wikt.start && window.wikt.start.launched);
if (!wiktkuLaunched && !document.getElementById("ca-wiktku-addlang")) {
wiktkuPrefsApi.addLauncherLink(wiktkuPrefs);
// If "onlyOnClick" is set, the wizard should ONLY ever open via the
// "+Ziman/beşeke din" link — never automatically
var wiktkuNewPageDetected = $(".mw-newarticletext, .mw-newarticletextanon").length > 0;
if (wiktkuPrefs.onlyOnClick ? !addLangMode : (!addLangMode && !wiktkuNewPageDetected)) {
return;
}
}
// ── Shared helpers ───────────────────────────────────────────────────
// Generic text/UI utilities live in their own package file now — see the
// header of util.js for why. Aliased to bare names here so the ~400 call
// sites below read exactly as they did when these were local functions.
var wiktkuEdit = require("ext.gadget.wikt.edit");
var wiktkuUtil = wiktkuEdit.util;
var wiktkuWikitext = wiktkuEdit.wt;
var wiktkuLangs = wiktkuEdit.langs;
// Citations (jêder-*, Kategorî:Çavkanî, Citoid) all go through one module -- see wikt.cite.js.
var wiktkuCite = require("ext.gadget.wikt.cite");
var wiktkuPosparams = wiktkuEdit.posparams;
// Assigned once data-main.json is in: re-merges the language choices, and is called a second time if the
// full language list has to be fetched. Declared here so the step renderer can reach it.
var wiktkuMergeLanguages = null;
var capitalize = wiktkuUtil.capitalize;
var decapitalize = wiktkuUtil.decapitalize;
var escapeRegExp = wiktkuUtil.escapeRegExp;
var stripWikilinks = wiktkuUtil.stripWikilinks;
var splitTercimeIzah = wiktkuUtil.splitTercimeIzah;
var insertAtCursor = wiktkuUtil.insertAtCursor;
var wrapNativeSelect = wiktkuUtil.wrapNativeSelect;
var initSelects = wiktkuUtil.initSelects;
var wiktkuInitManualResize = wiktkuUtil.initManualResize;
var langHeaderSource = wiktkuWikitext.langHeaderSource;
// Inserts a line-style template snippet ("* {{…}}") at the cursor,
// prefixing it with a newline unless it's going at the very start of the
// box or right after one already — otherwise picking a second template
// from the pron dropdown right after the first lands them both on the
// same line: "* {{ku-IPA}}* {{ku-kîte}}".
function insertLineSnippet(el, snippet) {
var before = el.value.slice(0, el.selectionStart);
if (before && !/\n$/.test(before)) snippet = "\n" + snippet;
insertAtCursor(el, snippet);
}
// One delete button per tab bar, acting on the tab in view. The trash glyph
// (oojs-ui.styles.icons-moderation) is fetched here rather than at startup: it is only needed once a
// second Mane or Beş exists. The button is built immediately and the icon appears when the CSS lands.
function makeTrashButton(title) {
mw.loader.using("oojs-ui.styles.icons-moderation");
return new OO.ui.ButtonWidget({
icon: "trash",
flags: ["destructive"],
framed: false,
invisibleLabel: true,
label: title,
title: title,
classes: ["wiktku-trash-btn"]
});
}
function findLangHeader(text, code) {
var m = new RegExp("\n?" + langHeaderSource(escapeRegExp(code))).exec(text);
return m ? { index: m.index, match: m[0] } : null;
}
// The category's members come from wikt.cite, which also lists each template's redirects so it can be
// found by any of its names. Entries are {label, target}: label is what was typed/shown, target the
// canonical template to transclude.
var catSrcTemplates = null;
function fetchCatSrcTemplates() {
return wiktkuCite.fetchCategoryTemplates().then(function (list) {
catSrcTemplates = list;
return list;
});
}
var pageName = mw.config.get("wgPageName").replaceAll(/_/g, " ");
if (ns) pageName = pageName.replace(/.*\//, "");
var userLang = mw.config.get("wgUserLanguage");
var formCache = new ObjectStorage("wiktku_start_draft_" + mw.config.get("wgPageName"), 1);
var langsByCode = {};
// Looks up the display label for a language code, falling back to the
// code itself if it isn't known yet (e.g. langsByCode not loaded).
function getLangLabel(code) {
var entry = langsByCode[code];
return entry ? entry.label : code;
}
// The mane box holds a Kurdish definition for a Kurdish entry, and a direct translation for every
// other language -- the {{îzah}} box under it carries the explanation.
function manePlaceholder(code) {
return code && code !== "ku" ? wiktkuUtil.msg.tercimePrompt : "Maneya vê kelîmeyê binivîse…";
}
// A Kurmancî example gets an explanation, any other a translation.
function nimuneTranslationPrompt(code) {
return code === "ku" ? "Îzaheke nimûneyê bi kurmancî…" : "Tercimeya nimûneyê bi kurmancî…";
}
// Populated from data-main.json at runtime.
var metadata = {};
var ZARAVA_LANG_CODES = ["ckb", "sdh", "lki", "hac", "zza"];
// Converts a {code: label} object (langChoices, posChoices)
// into the {code, label[, sortKey]} array the rest of the script uses.
// isLang: true adds a sortKey and shows "(code)" after the label —
// language choices only.
function choicesFromObject(obj, isLang) {
var result = [];
$.each(obj || {}, function (code, label) {
var choice = { code: code, label: label };
if (isLang) choice.sortKey = label;
result.push(choice);
});
return result;
}
function getZaravaKeys(choices) {
var byCode = {};
$.each(choices || [], function (_, c) { byCode[c.code] = c; });
var result = [];
$.each(ZARAVA_LANG_CODES, function (_, code) {
if (byCode[code]) result.push(byCode[code]);
});
return result;
}
function needsTransliteration(langCode) {
var meta = metadata[langCode];
if (!meta || !meta.sc) return false;
var scripts = Array.isArray(meta.sc) ? meta.sc : [meta.sc];
return scripts.length > 0 && scripts[0] !== "Latn";
}
// metadata[lang].sc is either a single script string or an array of
// several — only the latter means the language is actually written in
// more than one script.
function langHasMultipleScripts(langCode) {
var meta = metadata[langCode];
return !!(meta && Array.isArray(meta.sc) && meta.sc.length > 1);
}
// Kodên cinsiyetê yên di metadata.g de — ji data-main.json tê dagirtin
var GENDER_BASE_LABELS = {};
// Number codes (y, d, p, p+, tp, j, j+, j/nj, nj, nj+, hj!, hj?) from
// data-main.json's "numberLabels"
var NUMBER_LABELS = {};
var SENSE_GROUPS = [];
var WORDLIST_TEMPLATE = "kol3"; // replaced by data-main.json's first wordlistTemplates entry
// The pills the Dever/Şîwe picker opens with; everything else comes from Modul:labels by typing.
var DEVER_QUICK_PICKS = [];
var LEKTUR_CHOICES = [];
var LEKTUR_FORM_CHOICES = [];
// Distinguishes the Jimar dropdown (j= param) from the Cîns dropdown
// (c= param): a code that's a key in NUMBER_LABELS is a number code,
// everything else is a gender code. Compound codes like "m-p"/"n-p" are
// gender+plural pairs and correctly stay on the gender side.
function isNumberCode(code) {
return Object.prototype.hasOwnProperty.call(NUMBER_LABELS, code);
}
// Builds the Jimar dropdown's <option> text for one code: "j — jimarbar".
function numberOptionLabel(code) {
var def = NUMBER_LABELS[code];
if (!def) return code;
var text = def.display;
return text ? (code + " — " + text) : code;
}
// Sub-lists that only make sense for a plain (non-şablon) sense — hidden
// when a şablon (e.g. {{guharto|…}}) is chosen for that sense
function genderLabel(code) {
if (code === "p") return "Pirjimar";
var pluralMatch = code.match(/^(.+)-p$/);
if (pluralMatch && GENDER_BASE_LABELS[pluralMatch[1]]) {
return GENDER_BASE_LABELS[pluralMatch[1]] + " (pirjimar)";
}
return GENDER_BASE_LABELS[code] || code;
}
// ── Template explanations ────────────────────────────────────────────
// Every place a template can be picked shows the same row: a status
// dot, the name, a link to the template page, and an ⓘ. This is the ⓘ.
//
// Text comes from the template's own TemplateData where it has any, so
// the explanation stays right without anyone maintaining a second copy
// by hand — with the desc already in data-main.json as the fallback for
// templates that have none.
// {desc, params} for the ⓘ popup, from wikt.edit -- the fetch, the shaping and the cache are all
// there, shared with wikt.cite and DefSideBoxes, so a template is asked for once however many pickers
// show it. Null for a template with no TemplateData, and that answer is kept too, so clicking ⓘ on
// an undocumented template twice is one request.
var fetchTemplateData = wiktkuEdit.wt.templateDoc;
var prefetchTemplateData = wiktkuEdit.wt.templateDocBatch;
/** Rewrites each tagged <option> in `sel` as "name — description", using
* TemplateData. Only fills rows that have no description yet, so the
* hand-written ones in data-main.json (which carry usage examples the API
* does not) are left exactly as they are. */
function applyTplDescriptions(sel) {
if (!sel || !sel.length) return;
var changed = false;
sel.find("option[data-tpl]").each(function () {
var opt = $(this);
var name = opt.attr("data-tpl");
if (opt.text().indexOf(" — ") !== -1) return; // already described
var td = wiktkuEdit.wt.templateDocNow(name);
var desc = (td && td.desc) || localTplDesc(name);
if (!desc) return;
opt.text(name + " — " + wiktkuEdit.wt.shortDesc(desc));
changed = true;
});
// The picker caches option text for searching, so it has to re-read.
if (changed) sel.trigger("wiktku:refresh");
}
/** Description from data-main.json's "desc", for templates with no TemplateData on the wiki. Works for any
* pool: pronTpls, etymTpls, tplDefs, jederDefs. */
function localTplDesc(name) {
var pools = [steps.pron.pronTpls, steps.etym.etymTpls, steps.dfn.tplDefs, steps.dfn.jederDefs];
var found = "";
$.each(pools, function (_, pool) {
if (pool && pool[name] && pool[name].desc) { found = pool[name].desc; return false; }
});
return found;
}
// Under the ⓘ it was opened from, kept inside the viewport, and flipped above it when there is no
// room below. Narrow screens get the width they have rather than a fixed card off the edge.
//
// box is where that ⓘ was when it was clicked. The row it sits in is closed by then, so it can no
// longer be measured; the last known box is kept for the re-placements after the text arrives.
function positionTplInfo(panel, anchor, box) {
var el = $(anchor).get(0);
var rect = box || (el && el.getBoundingClientRect ? el.getBoundingClientRect() : null);
if (rect && !rect.width && !rect.height) {
rect = null;
}
if (rect) {
panel.data("wiktkuAnchorBox", rect);
} else {
rect = panel.data("wiktkuAnchorBox") || null;
}
var margin = 8;
var arrow = 8;
var width = Math.min(360, $(window).width() - margin * 2);
panel.css({ width: width + "px", top: "0px", left: "0px" });
var height = panel.outerHeight();
// Above the dropdown, pointing down at it, the way MediaWiki's own filter tips sit; below it only
// when there is no room above.
var above = !rect || rect.top - height - arrow >= margin;
var top = !rect ? margin : (above ? rect.top - height - arrow : rect.bottom + arrow);
var left = rect ? Math.min(Math.max(margin, rect.left), $(window).width() - width - margin) : margin;
// The arrow points at the start of the box it explains, kept clear of the tip's corners.
var arrowX = rect ? Math.min(Math.max(16, rect.left + 24 - left), width - 16) : 16;
panel.toggleClass("wiktku-tpl-info-above", above).toggleClass("wiktku-tpl-info-below", !above);
panel.css({ top: top + "px", left: left + "px" });
panel.get(0).style.setProperty("--wiktku-arrow-x", arrowX + "px");
}
/** Opens the explanation for `name` as a tip above the dropdown it was asked from. */
function showTemplateInfo(name, anchorWrap, anchorEl, anchorBox) {
$(".wiktku-tpl-info").remove();
var panel = $("<div class='wiktku-tpl-info'></div>");
panel.append($("<div class='wiktku-tpl-info-name'></div>").text("{{" + name + "}}"));
var body = $("<div class='wiktku-tpl-info-desc'></div>").text(wiktkuUtil.msg.loading);
panel.append(body);
var params = $("<div class='wiktku-tpl-info-params'></div>");
panel.append(params);
panel.append($("<a class='wiktku-tpl-info-link' target='_blank' rel='noopener'></a>")
.attr("href", wiktkuUtil.tplPageUrl(name))
.text(wiktkuUtil.msg.openTemplatePage + " →"));
function closePanel() {
panel.remove();
$(document).off(".wiktkuTplInfo");
$(window).off(".wiktkuTplInfo");
}
var close = $("<button type='button' class='wiktku-tpl-info-close' title='Bigire'>✕</button>");
close.on("click", closePanel);
panel.prepend(close);
$(document).on("keydown.wiktkuTplInfo", function (e) {
if (e.which === 27) {
closePanel();
}
});
$(document).on("mousedown.wiktkuTplInfo", function (e) {
if (!$(e.target).closest(".wiktku-tpl-info").length) {
closePanel();
}
});
// A panel that stayed put while the page moved under it would point at nothing.
$(window).on("resize.wiktkuTplInfo scroll.wiktkuTplInfo", function () {
positionTplInfo(panel, anchorWrap);
});
// Over the form, not inside it: as a block in the flow it pushed every field below it down the
// page for as long as it was open, and the picker it explains moved with them.
$(document.body).append(panel);
positionTplInfo(panel, anchorWrap);
fetchTemplateData(name).then(function (td) {
var desc = (td && td.desc) || localTplDesc(name);
body.text(desc || "Ji bo vê şablonê îzahek nehatiye nivîsandin.");
if (td && td.params && td.params.length) {
params.append($("<div class='wiktku-tpl-info-params-title'></div>").text("Parametre:"));
var listEl = $("<ul></ul>");
$.each(td.params, function (_, p) {
var line = p.key + (p.label ? " — " + p.label : "") + (p.required ? " (mecbûrî)" : "");
listEl.append($("<li></li>").text(line));
});
params.append(listEl);
}
// The panel is as tall as its text, which only arrives now.
positionTplInfo(panel, anchorWrap);
});
}
/** Marks a picker's <option>s as template rows so the searchable widget
* renders them with the dot / 🔗 / ⓘ affordances, and wires the ⓘ
* handler for that picker. `nameOf` maps an <option> to a template
* name — the pickers store different things in value= (a snippet for
* the insert dropdowns, the bare name for the sense selector). */
function markTemplateOptions(sel, nameOf) {
if (!sel || !sel.length) return;
var names = [];
sel.find("option").each(function () {
var opt = $(this);
var name = nameOf(opt);
if (!name) return;
opt.attr("data-tpl", name);
// Deprecated/typo templates are the "special cases only" ones.
opt.attr("data-tpl-status", /kevnbûyî|şaşnivîs|kêmpeyda/i.test(opt.text()) ? "yellow" : "green");
names.push(name);
});
sel.data("wiktkuTplInfo", showTemplateInfo);
// Fill in the descriptions. Matters most for the Mane picker, whose
// entries in data-main.json are names with no desc at all — "rastnivîs",
// "ku-Latn", "guharto" on their own. One request covers every
// template in the picker, cached for the per-row ⓘ as well.
prefetchTemplateData(names).always(function () {
applyTplDescriptions(sel);
});
}
/** Pulls the template name out of a snippet like "* {{ku-IPA}}" or
* "{{ji|SOURCELANG|CODE|…}}". */
function tplNameFromSnippet(snippet) {
var m = /\{\{\s*([^|}\n]+)/.exec(snippet || "");
return m ? $.trim(m[1]) : "";
}
// True if a sense has content beyond mane worth keeping (etîket, word
// lists, nimûne, jêder) — so it's still emitted with {{mane?}} if empty.
function senseHasExtraContent(s) {
if (s.maneid) return true;
if (s.etiket && $.grep(s.etiket, function (e) { return $.trim(e) !== ""; }).length) return true;
if (s.kategori && $.grep(s.kategori, function (k) { return $.trim(k) !== ""; }).length) return true;
var anyWords = false;
$.each(wordGroups(), function (_, g) {
if ((s[g.field] || []).length) anyWords = true;
});
if (anyWords || (s.dever && s.dever.length)) return true;
if (s.nimune && $.grep(s.nimune, function (n) {
return (typeof n === "object" ? n.text : n) !== "";
}).length > 0) return true;
if (senseHasJeder(s)) return true;
return false;
}
// A technically-"complete" entry (Ziman + Cureyê rêzimanî filled) with
// nothing in Mane, Bi zaravayên din or Werger is still meaningfully
// empty, so this is checked separately at submit-click time with its
// own message.
// Converts literal line breaks typed into a nimûne/jêgirtin box into
// wikitext <br> tags,
function nlToBr(text) {
return text ? wiktkuCite.nlToBr(text) : text;
}
// Line breaks in a field save as "<br>"; this marks them so that is visible while typing.
var highlightLinebreak = wiktkuUtil.makeLinebreakHighlighter({ prefix: "wiktku" });
// Each nimûne item can be { text, tr, t }: text, transliteration, translation.
function formatNimune(nimunes, lang) {
var hasNimune = nimunes && $.grep(nimunes, function (n) {
return (typeof n === "object" ? n.text : n) !== "";
}).length > 0;
if (!hasNimune) {
// A sense with no example is left as it is, with no "{{nimûne?}}" placeholder.
return "";
}
return $.map(nimunes, function (n) {
var text = typeof n === "object" ? n.text : n;
if (!text) return undefined;
// The builder DefSideBoxes and add-examples use: the translation in slot 3, rêz=1 for a short pair.
return "\n" + wiktkuCite.buildCitation({
mode: "plain",
marker: "#:",
language: lang,
title: pageName,
quote: text,
translation: typeof n === "object" ? n.t : "",
transcription: typeof n === "object" ? n.tr : ""
});
}).join("");
}
// Formats jêder (citation) entries into "#* {{jêder…|…}}" lines; skips
// entries with no quote text. Falls back to the generic {{jêder}}
// template when no specific template was chosen.
function formatJeder(jederList, lang) {
if (!jederList || !jederList.length) return "";
return $.map(jederList, function (jeder) {
if (!jeder || !jeder.quote) return undefined;
var mode = jeder.raw ? "raw"
: jeder.jederTpl ? "jeder"
: jeder.jederCatTpl ? "category"
: "plain";
return "\n" + wiktkuCite.buildCitation({
mode: mode,
marker: "#*",
language: lang,
template: jeder.jederTpl || jeder.jederCatTpl,
fields: jeder.jederTpl ? (jeder.jêderFields || {}) : (jeder.jederCatFields || {}),
langParamKey: jeder.jederCatLangParam,
quote: jeder.quote,
transcription: jeder.quoteTr,
translation: jeder.quoteT,
source: jeder.jederManual || (mode === "plain" && !jeder.url ? "çavkanî?" : ""),
sourceURL: jeder.url
});
}).join("");
}
// True if a sense has at least one jêder entry with quote text.
function senseHasJeder(sense) {
return !!(sense.jeder && $.grep(sense.jeder, function (j) {
return j && j.quote;
}).length > 0);
}
// Builds "{{tplName|lang|part1|part2|…}}" from a sense field (kategori,
// etiket), wrapped inline (" {{…}} ") or on its own line ("\n#: {{…}}").
function buildSenseTpl(sense, field, lang, tplName) {
if (!sense[field] || !sense[field].length) return "";
var parts = sense[field].map(function (p) { return $.trim(p); })
.filter(function (p) { return p !== ""; });
if (!parts.length) return "";
var call = "{{" + tplName + "|" + lang + "|" + parts.join("|") + "}}";
return call
}
// Dever/Şîwe values only ever get their own {{dever|…}} call for ku —
// no |lang| param, since the template is Kurdish-dialect-specific. For
// every other language there's no dever template at all; see
// buildEtiketTpl() below, which folds them into {{e|}} instead.
function buildDeverTpl(sense, lang) {
if (lang !== "ku" || !sense.dever || !sense.dever.length) return "";
var parts = sense.dever.map(function (p) { return $.trim(p); })
.filter(function (p) { return p !== ""; });
if (!parts.length) return "";
return "{{dever|" + parts.join("|") + "}}";
}
// Builds the {{e|lang|…}} etîket call. For non-ku senses, Dever/Şîwe
// values are appended after the regular etîket values instead of going
// into their own {{dever|}} call (see buildDeverTpl() above).
function buildEtiketTpl(sense, lang) {
var etiket = (sense.etiket || []).slice();
if (lang !== "ku" && sense.dever && sense.dever.length) {
etiket = etiket.concat(sense.dever);
}
return buildSenseTpl({ etiket: etiket }, "etiket", lang, "e");
}
/**
* Assembles the nimûne + jêder wikitext block for a sense. Shared by
* both the template-sense and normal-mane-sense branches below.
*/
function buildCite(sense, lang) {
return formatNimune(sense.nimune, lang) +
formatJeder(sense.jeder, lang);
}
// ── Inline modifier system ───────────────────────────────────────────────
// "{{maneid|lang|x}}", the anchor id a sense can be linked by. First of the leading calls, which is
// data-main.json's leadingTplOrder.
function buildManeidTpl(sense, lang) {
var id = $.trim(sense.maneid || "");
return id ? "{{maneid|" + lang + "|" + id + "}}" : "";
}
// Quick-pick suggestions for the Dever/Şîwe, Etîket and Kategorî chip pickers on each sense, taken
// from [[Modul:labels]] rather than kept by hand here. Free text is still always accepted; this only
// saves typing, and keeps the lists honest as the module changes.
//
// Fetched on the first time one of the three panels is opened, never on load: the wizard opens on
// pages that never touch these fields. Once it lands, the open panel re-renders itself.
var wiktkuLabelSets = null;
var wiktkuLabelsPending = null;
var wiktkuLabelListeners = [];
function wiktkuEnsureLabels() {
if (wiktkuLabelSets || wiktkuLabelsPending) return;
wiktkuLabelsPending = wiktkuEdit.labels.load().then(function (sets) {
wiktkuLabelSets = sets;
wiktkuLabelsPending = null;
$.each(wiktkuLabelListeners, function (_, fn) { fn(); });
}, function () {
wiktkuLabelsPending = null;
});
}
// Which list each picker shows. Dever/Şîwe is the regions of the language being edited; Etîket adds
// the qualifiers and subject labels on top; Kategorî is the subject labels alone.
function wiktkuLabelSuggestions(which) {
if (!wiktkuLabelSets) return [];
var lang = (getValues().lang || "ku");
if (which === "dever") return wiktkuEdit.labels.regionalFor(wiktkuLabelSets, lang);
if (which === "etiket") return wiktkuEdit.labels.etiketFor(wiktkuLabelSets, lang);
return wiktkuEdit.labels.kategoriNames(wiktkuLabelSets);
}
// Lower-cased alias -> canonical label, so typing a name the module lists as an alias still finds it.
// Built once per label load rather than per keystroke.
var wiktkuAliasCache = null;
function wiktkuLabelAliases() {
if (!wiktkuLabelSets) return {};
if (!wiktkuAliasCache) {
wiktkuAliasCache = wiktkuEdit.labels.aliasIndex(wiktkuLabelSets);
}
return wiktkuAliasCache;
}
// Appends a ⊕ button to a word input <li>. Clicking it shows a compact
// modifier picker; selecting one prompts for the value and appends
// <modifier:value> to the textarea's current content.
// Sub-groups (inside a sense/definition block) that get the same ⊕
// modifier button as makeWordListStep fields.
/**
* The word lists (Hevmane, Dijmane, Formên alternatîv, Termên koordîne, Jornav, Jêrnav), taken from
* data-main.json's senseGroups so the wizard, DefSideBoxes and the wikitext writer all name them once.
* That array's own order is the order the "#:" lines are written to the page in; "order" is the
* order they are shown in, which is not the same. cls is how each one's box is found in the DOM.
*/
function wordGroups() {
return SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return { cls: "wiktku-dfn-" + g.field, label: g.label, field: g.field, izah: g.hint || "" };
});
}
/** True for the six word lists, which get the ⊕-modifier row; Nimûne and Jêgirtin do not. */
function isWordGroupCls(cls) {
var found = false;
$.each(SENSE_GROUPS, function (_, g) {
if ("wiktku-dfn-" + g.field === cls) found = true;
});
return found;
}
/**
* Wraps a textarea in a <li> with a ⊕ modifier button and returns the <li>.
*
* `ta` stays hidden and holds the raw "word<key:value>…" text every reader and writer expects. What
* is typed into is a plain word box, with one pill per modifier above it; clicking a pill edits its
* value, ⊕ adds another.
*/
function makeModifierLi(ta) {
var narrow = (ta.attr("class") || "").split(/\s+/).indexOf("wiktku-start-narrow") !== -1;
// Not ta's own classes: code elsewhere counts elements by those, so a second matching textarea
// would skew the counts. wiktku-word-input carries the same look.
var wordInput = $("<textarea></textarea>")
.attr("rows", ta.attr("rows") || 1)
.addClass("wiktku-word-input" + (narrow ? " wiktku-start-narrow" : ""))
.attr("placeholder", ta.attr("placeholder") || "");
ta.hide();
// Placeholders on the (hidden) canonical field are set by shared code that has no reason to know
// wordInput exists -- mirror them instead of touching every call site.
if (window.MutationObserver) {
new MutationObserver(function () {
wordInput.attr("placeholder", ta.attr("placeholder") || "");
}).observe(ta[0], { attributes: true, attributeFilter: ["placeholder"] });
}
// ta stays the source of truth: the row writes back to it, and the caller's growth listener is
// bound to ta's "input" event.
var row = wiktkuUtil.makeModifierRow({
prefix: "wiktku-",
wordInput: wordInput,
modTitle: "Modifiyerê lê zêde bike",
removeTitle: wiktkuUtil.msg.removeWord,
removeModTitle: wiktkuUtil.msg.removeModifier,
onChange: function (raw) {
if (raw === null) return; // the row removed itself; ta goes with it
ta.val(raw);
ta.trigger("input");
},
// "bixe qutiyan" puts each comma-separated word in a row of its own. The list grows off the
// hidden field's own "input" event, so filling the trailing row is what makes the next one
// appear; the caret ends up in the empty row at the bottom. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way -- a
// comma left inside a parameter would not be.
onSplit: function (rest) {
// A tick later, not now: the row writes its own value to the hidden field after this
// call returns, and it is that field's "input" that grows the list -- so the empty row
// to put the next word in does not exist yet.
setTimeout(function () {
var $list = row.$li.closest("ol, ul");
rest.forEach(function (raw) {
if (!raw) return;
var setRaw = $list.children("li").last().data("setRawValue");
if (setRaw) setRaw(raw);
});
$list.children("li").last().find(".wiktku-word-input").focus();
}, 0);
}
});
row.setRaw(ta.val());
var li = row.$li;
// restoreSubList's "fill an existing row" path sets a raw "word<key:value>" value here.
li.data("setRawValue", function (raw) {
row.setRaw(raw);
ta.val(row.getRaw());
ta.trigger("input");
});
li.prepend(ta);
return li;
}
/**
* A "chip + toggle" field: values render as removable chips, and a "+ <label>" toggle opens a panel
* with quick-pick pills plus free text. panel is returned apart from wrap so callers can group all
* toggles in one row and all panels below, keeping toggles from shifting when one opens.
*
* Pills carry a × as well: hiding a built-in one, or typing a new value, is remembered per wrapCls
* in mw.storage.
*
* opts: { wrapCls, toggleLabel, hint, suggestions (array or function), placeholder, onOpen }
* Returns { wrap, panel, addChip, values, setOpenDefault, close }.
*/
var WIKTKU_SUGGESTIONS_KEY_PREFIX = "wiktku_suggestions_";
function loadSuggestionOverrides(wrapCls) {
if (!(window.mw && mw.storage)) return { removed: [], added: [] };
var raw;
try { raw = mw.storage.get(WIKTKU_SUGGESTIONS_KEY_PREFIX + wrapCls); } catch (e) { raw = null; }
var parsed = {};
if (raw) { try { parsed = JSON.parse(raw); } catch (e) { parsed = {}; } }
return { removed: parsed.removed || [], added: parsed.added || [] };
}
function saveSuggestionOverrides(wrapCls, overrides) {
if (!(window.mw && mw.storage)) return;
try { mw.storage.set(WIKTKU_SUGGESTIONS_KEY_PREFIX + wrapCls, JSON.stringify(overrides)); } catch (e) { /* storage unavailable — override just won't persist */ }
}
// One document listener for every sense's "Hevmane û Bêhtir" menu, not one per sense: a long entry can hold a
// dozen senses and each would otherwise bind its own.
var moreMenuCloserBound = false;
function bindMoreMenuCloser() {
if (moreMenuCloserBound) return;
moreMenuCloserBound = true;
$(document).on("click.wiktkuMore", function (e) {
if ($(e.target).closest(".wiktku-dfn-more-wrap").length) return;
$(".wiktku-dfn-more-menu").hide();
$(".wiktku-dfn-more-arrow").html("▸");
});
}
function buildChipField(opts) {
var wrap = $("<div></div>").addClass("wiktku-dfn-chipfield-wrap " + opts.wrapCls + "-wrap");
var chipsRow = $("<span class='wiktku-dfn-chipfield-chips'></span>");
var toggle = $("<button type='button' class='wiktku-dfn-chipfield-toggle'></button>").text(opts.toggleLabel);
var panel = $("<div class='wiktku-dfn-chipfield-panel'></div>").hide();
var suggestRow = $("<div class='wiktku-dfn-chipfield-suggest'></div>");
var input = $("<input type='text' class='wiktku-dfn-chipfield-input'>")
.attr("placeholder", opts.placeholder || ((opts.suggestions && opts.suggestions.length) ? "An ya nû binivîse…" : "Binivîse…"));
var addBtn = $("<button type='button' class='wiktku-dfn-chipfield-add'>Qeyd bike</button>");
// Once the person opens/closes the panel themselves, lang-driven
// syncing (see setOpenDefault) backs off rather than fighting them.
var userToggled = false;
// The pick list shown is opts.suggestions with the person's own
// edits layered on top — see loadSuggestionOverrides above.
var overrides = loadSuggestionOverrides(opts.wrapCls);
var removedSuggestions = overrides.removed;
var addedSuggestions = overrides.added;
function persistOverrides() {
saveSuggestionOverrides(opts.wrapCls, { removed: removedSuggestions, added: addedSuggestions });
}
// opts.suggestions is either a fixed array or a function returning one -- the label-driven
// pickers depend on the language being edited, and on data that arrives from Modul:labels after
// the field is built, so they cannot be a snapshot taken here.
function baseSuggestions() {
var list = typeof opts.suggestions === "function" ? opts.suggestions() : opts.suggestions;
return list || [];
}
function allSuggestions() {
return $.grep(baseSuggestions(), function (s) { return removedSuggestions.indexOf(s) === -1; })
.concat(addedSuggestions);
}
function learnSuggestion(val) {
if (!val || allSuggestions().indexOf(val) !== -1) return;
addedSuggestions.push(val);
persistOverrides();
}
function forgetSuggestion(name) {
var idx = addedSuggestions.indexOf(name);
if (idx !== -1) addedSuggestions.splice(idx, 1);
else if (removedSuggestions.indexOf(name) === -1) removedSuggestions.push(name);
persistOverrides();
renderSuggest();
// renderSuggest() just destroyed the pill's × button the click
// came from — which was focused — dropping focus outside the
// panel entirely. Left alone, that reads as "focus left the
// widget" to the focusout handler below and closes the whole
// panel. Move focus back inside before that check runs.
input.focus();
}
function values() {
var vals = [];
chipsRow.find(".wiktku-dfn-chipfield-chip").each(function () {
vals.push($(this).attr("data-value"));
});
return vals;
}
// Label-driven pickers are built before Modul:labels has been read, so re-render once it lands.
if (typeof opts.suggestions === "function") {
wiktkuLabelListeners.push(function () {
if (panel.is(":visible")) renderSuggest();
});
}
// At most this many pills at once. Modul:labels offers 87 regions, 362 etîket and 209 kategorî;
// drawing them all was a wall of pills nobody reads and the field it belonged to scrolled away.
var MAX_PILLS = 24;
// What shows with an empty box: the short hand-picked list if the caller gave one, otherwise the
// full set (capped) as before, for pickers whose lists are small enough to browse.
function restingSuggestions() {
var quick = typeof opts.quickPicks === "function" ? opts.quickPicks() : opts.quickPicks;
if (!quick || !quick.length) {
return allSuggestions();
}
// A value the person added themselves outranks the hand-picked list -- it is theirs.
return quick.concat($.grep(addedSuggestions, function (s) { return quick.indexOf(s) === -1; }));
}
// Typed text searches the WHOLE set, not just the pills on screen, and matches the module's own
// aliases too, so "Behdînanî" finds Badînî and "USA" finds DYA.
function matching(query) {
var q = $.trim(query).toLowerCase();
if (!q) {
return restingSuggestions();
}
var aliases = wiktkuLabelAliases();
var hits = $.grep(allSuggestions(), function (name) {
return name.toLowerCase().indexOf(q) !== -1;
});
$.each(aliases, function (alias, canonical) {
if (alias.indexOf(q) !== -1 && hits.indexOf(canonical) === -1 &&
allSuggestions().indexOf(canonical) !== -1) {
hits.push(canonical);
}
});
return hits;
}
function renderSuggest(query) {
var taken = values();
suggestRow.empty();
var shown = 0;
$.each(matching(query || ""), function (_, name) {
if (taken.indexOf(name) !== -1 || shown >= MAX_PILLS) return;
shown++;
var pill = $("<span role='button' class='wiktku-dfn-chipfield-pill'></span>").text(name);
pill.on("click", function () { addChip(name); input.val(""); renderSuggest(""); input.focus(); });
var pillRemove = $("<button type='button' class='wiktku-dfn-chipfield-pill-remove' title='Vê tewsiyeyê veşêre'>×</button>");
pillRemove.on("click", function (e) {
e.stopPropagation(); // don't also trigger the pill's own add-chip click
forgetSuggestion(name);
});
pill.append(pillRemove);
suggestRow.append(pill);
});
suggestRow.toggle(!!suggestRow.children().length);
}
function addChip(val) {
val = $.trim(val || "");
if (!val || values().indexOf(val) !== -1) return;
var chip = $("<span class='wiktku-dfn-chipfield-chip'></span>").attr("data-value", val).text(val);
var remove = $("<button type='button' class='wiktku-dfn-chipfield-remove' title='Jê bibe'>×</button>");
// Same focus-loss hazard as forgetSuggestion() below — refocus
// the input so removing a chip while the panel is open doesn't
// also close the panel.
remove.on("click", function () { chip.remove(); renderSuggest(); input.focus(); });
chip.append(remove);
chip.insertBefore(toggle); // chips in insertion order, toggle stays last
renderSuggest();
}
toggle.on("click", function () {
userToggled = true;
if (panel.is(":visible")) { panel.hide(); return; }
if (opts.onOpen) opts.onOpen();
renderSuggest();
panel.show();
input.focus();
});
// Anything typed and committed by hand joins the pick list too, not
// just the current sense's chips.
function commitInput() {
var val = $.trim(input.val() || "");
if (val) {
addChip(val);
learnSuggestion(val);
}
input.val("").focus();
}
addBtn.on("click", commitInput);
input.on("keydown", function (e) {
if (e.which === 13) { e.preventDefault(); commitInput(); }
});
// Typing searches the whole list, so a value that is not among the pills is still one keystroke
// from being found rather than something you have to know by heart and type out in full.
input.on("input", function () {
renderSuggest(input.val());
});
// Close the panel once focus leaves toggle+panel together — they're
// siblings in the DOM (not nested), so both must be checked.
function focusStillInside() {
return $.contains(wrap[0], document.activeElement) || $.contains(panel[0], document.activeElement);
}
wrap.add(panel).on("focusout", function () {
setTimeout(function () {
if (!focusStillInside()) panel.hide();
}, 150);
});
if (opts.hint) panel.append($("<div class='wiktku-dfn-chipfield-hint'></div>").text(opts.hint));
// Box first, pills under it. The other way round, every keystroke re-filtered the pills above the
// box and the box itself jumped up and down the page as the list grew and shrank.
panel.append(
$("<div class='wiktku-dfn-chipfield-input-row'></div>").append(input).append(addBtn)
).append(suggestRow);
chipsRow.append(toggle);
wrap.append(chipsRow);
return {
wrap: wrap,
panel: panel,
addChip: addChip,
values: values,
/** Sets the panel's default open/closed state — a no-op once
* the person has manually toggled it themselves this session. */
setOpenDefault: function (open) {
if (userToggled) return;
// Same as the toggle click handler: (re)build the suggestion
// pills whenever the panel opens, not just on a manual click —
// otherwise a panel opened by this default-state sync (e.g.
// Dever/Şîwe starting open for ku) shows an empty pill row
// until the person closes and reopens it themselves.
if (open) {
if (opts.onOpen) opts.onOpen();
renderSuggest();
}
panel.toggle(!!open);
},
/** Force-closes the panel as though the person had done it
* themselves, so a later setOpenDefault() doesn't reopen it out
* from under them — used by onOpen to keep only one of these
* fields' panels open at a time (see the call site). */
close: function () {
userToggled = true;
panel.hide();
},
/** Opens the panel from somewhere other than the toggle — the
* "Hevmane û Bêhtir" menu, which is where Etîket and Kategorî are asked
* for now. Counts as a manual toggle for the same reason close()
* does. */
open: function () {
userToggled = true;
if (opts.onOpen) opts.onOpen();
renderSuggest();
panel.show();
input.focus();
}
};
}
/**
* One page-level word list (Formên alternatîv, Hevmane, … Herwiha binêre): a "====" section holding
* the words for the whole entry rather than for one sense.
*
* Only the question is passed in. The heading and the step's own name come from data-sections.json's
* pageSubsections, filled in below once the data lands — the same table DefSideBoxes reads to find
* these sections, so the wizard cannot write a heading the panels then fail to recognise.
*/
function makeWordListStep(id, question_fmt) {
return {
id: id,
name: id,
question_fmt: question_fmt,
multiple: true,
narrow: true,
modifiers: true,
// Function instead of a plain string so it can name the current
// language — computed fresh at build time and re-applied by
// refresh() whenever the language changes.
placeholder: function () {
var lang = getValues().lang || "";
var langLabel = lang ? getLangLabel(lang) : "";
return "Kelîme" + (langLabel ? " bi " + langLabel : "") + "…";
},
visible: function (values) {
return values.lang && values.pos;
},
output: function (values) {
var words = $.grep(values[id] || [], function (w) { return !!w; });
if (!words.length) return "";
var header = steps[id].header || capitalize(steps[id].name);
var lang = values.lang;
var section = "\n==== " + header + " ====\n";
var params = "cure=" + header + "\n" +
$.map(words, function (w) {
return "|" + w;
}).join("\n");
return section +
"{{" + WORDLIST_TEMPLATE + "|" + lang + "|" + params + "\n}}\n";
} };
}
var steps = {
lang: {
id: "lang",
required: true,
name: "ziman",
question_fmt: '"' + pageName + '" bi çi zimanî ye?',
// Populated from data-main.json (langChoices).
choices: [],
output: function (values) {
return "== {{ziman|" + values.lang + "}} ==\n";
}
},
pron: {
id: "pron",
name: "bilêvkirin",
question_fmt: '"' + pageName + '" çawa tê telafûzkirin?',
// pronTpls populated from data-main.json
pronTpls: {},
visible: function (values) {
if (!values.lang) return false;
// addLangMode: skip pron only if this lang already has a section.
if (addLangMode) {
// Clearing the page makes existing content irrelevant.
var clearPageCheck = $("#wiktku-clear-page-check");
if (clearPageCheck.length && clearPageCheck.prop("checked")) {
return true;
}
var existing = $("#wpTextbox1").val() || "";
return !findLangHeader(existing, values.lang);
}
return true;
},
// Fills the insert-dropdown from pronTpls, filtered by language.
refreshTplSelect: function () {
var sel = $("#wiktku-start-pron .wiktku-tpl-insert-select");
if (!sel.length) return;
var lang = getValues().lang || "";
sel.find("option[value!='']").remove();
$.each(steps.pron.pronTpls, function (name, tpl) {
// Filter: if tpl.langs exists, only show if lang is in that list; otherwise show always
var allowed = !tpl.langs || tpl.langs.indexOf(lang) !== -1;
if (allowed) {
sel.append($("<option></option>")
.val(tpl.snippet)
.text(name + " — " + tpl.desc)
.attr("data-nolang", tpl.noLang ? "1" : ""));
}
});
markTemplateOptions(sel, function (opt) { return tplNameFromSnippet(opt.val()); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-pron-wrap").length) return null;
var wrap = $("<div class='wiktku-pron-wrap'></div>");
// Reassures the user this field is optional — the dropdown
// below writes the template for them, no syntax needed up front.
wrap.append(
$("<p class='wiktku-step-hint'></p>").text(
"Ev qism ne mecbûrî ye. Heke tu nizanî kîjan şablonê hilbijêrî, vala bihêle."
)
);
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-pron-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-pron-text")
.attr("rows", 4)
.attr("placeholder", "Nimûne: {{IPA|ku|/pɛɪ̯n/}} yan {{kîte|ku|he|ce|ki|rin}}");
// Template insert dropdown — filtered by language.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
var lang = getValues().lang || "";
$.each(steps.pron.pronTpls, function (name, tpl) {
// Filter: if tpl.langs exists, only show if lang is in that list; otherwise show always
var allowed = !tpl.langs || tpl.langs.indexOf(lang) !== -1;
if (allowed) {
sel.append($("<option></option>")
.val(tpl.snippet)
.text(name + " — " + tpl.desc)
.attr("data-nolang", tpl.noLang ? "1" : ""));
}
});
sel.on("change", function () {
var snippet = $(this).val();
if (!snippet) return;
var lang = getValues().lang || "";
// Check if the selected option has noLang flag
var selectedOption = sel.find("option:selected");
var hasNoLang = selectedOption.attr("data-nolang") === "1";
if (!hasNoLang) {
snippet = snippet.replace(/CODE/g, lang).replace(/SOURCELANG/g, "?");
insertLineSnippet(ta[0], snippet);
$(this).val("");
// Clear the picker's search box
sel.trigger("wiktku:refresh");
ta.focus();
return;
}
// The noLang templates (ku-IPA, ku-kîte, zza-IPA, zza-kîte)
// auto-generate content from a Lua module reading the page
// title, so show a live preview and require confirmation
// before inserting.
var tplLabel = selectedOption.text();
$(this).val("");
sel.trigger("wiktku:refresh");
steps.pron.showTplPreview(stepDiv, ta, snippet, tplLabel);
});
wrap.append(sel).append(ta);
stepDiv.append(wrap);
// The palette lives in wikt.edit, shared with DefSideBoxes' own Bilêvkirin box.
wrap.append(wiktkuUtil.buildIpaPalette({
insertText: function (text, back) {
insertAtCursor(ta[0], text);
if (back) {
var pos = ta[0].selectionStart - back;
ta[0].selectionStart = ta[0].selectionEnd = pos;
}
ta.focus();
}
}));
initSelects(wrap);
return ta;
},
// ── IPA character palette ji [[MediaWiki:Gadget-charinsert-core.js]]────────────
// Modelled on fr.wiktionary's: every character is a button that
// drops itself in at the cursor. Two sub-tabs, because the full
// IPA chart is ~180 symbols and roughly twenty of them ever
// appear in Kurmancî — "IPA (kurdî)" opens by default and the
// full chart is one click away for loanwords and other
// languages. Grouped with a label down the left so it reads as
// a chart rather than one long line of glyphs.
/** Inline preview + confirm panel for a "noLang" (auto-content)
* pronunciation template — fetches what the snippet expands to
* for this page and only inserts it if the user confirms. */
showTplPreview: function (stepDiv, ta, snippet, tplLabel) {
// Only one preview panel open at a time.
stepDiv.find(".wiktku-pron-preview").remove();
var panel = $("<div class='wiktku-pron-preview'></div>");
panel.append($("<div class='wiktku-pron-preview-label'></div>")
.text("Pêşdîtina " + tplLabel + ":"));
var body = $("<div class='wiktku-pron-preview-body'></div>").text(wiktkuUtil.msg.loading);
panel.append(body);
var actions = $("<div class='wiktku-pron-preview-actions'></div>");
var confirmBtn = $("<button type='button' class='wiktku-pron-preview-confirm'>✓ Tevlî bike</button>")
.prop("disabled", true);
var cancelBtn = $("<button type='button' class='wiktku-pron-preview-cancel'></button>")
.text("✕ " + wiktkuUtil.msg.cancel);
actions.append(confirmBtn).append(cancelBtn);
panel.append(actions);
ta.before(panel);
confirmBtn.on("click", function () {
insertLineSnippet(ta[0], snippet);
panel.remove();
ta.focus();
});
cancelBtn.on("click", function () {
panel.remove();
});
// action=parse, not expandtemplates: the preview needs rendered HTML, not expanded
// wikitext. The leading "* " is dropped so it renders as a line, not a bulleted list.
new mw.Api().post({
action: "parse",
title: pageName,
text: snippet.replace(/^\*\s*/, ""),
prop: "text",
wrapoutputclass: "",
disablelimitreport: 1,
disableeditsection: 1,
contentmodel: "wikitext",
formatversion: 2
}).done(function (data) {
var html = (data && data.parse && data.parse.text) || "";
if ($.trim($("<div>").html(html).text())) {
body.html(html);
// Category links render as nothing but still exist;
// and any real link inside a preview should not
// navigate away from a half-filled form.
body.find("a").attr("target", "_blank").attr("rel", "noopener");
} else {
body.text("(vala — ji bo vê rûpelê tiştek nehate çêkirin)");
}
confirmBtn.prop("disabled", false);
}).fail(function () {
body.text("Pêşdîtin nehate barkirin — tu dikarî hîn jî wê tevlî bikî.");
confirmBtn.prop("disabled", false);
});
},
output: function () {
var text = $.trim($("#wiktku-start-pron .wiktku-pron-text").val());
if (!text) return "";
var lines = text.split("\n");
var out = $.map(lines, function (line) {
line = $.trim(line);
if (!line) return undefined;
return line;
}).join("\n");
// "===" heading under Ziman and above the POS sections; the
// field is shared across sections, so it's emitted once by
// getWikitext() rather than per-POS-section (would otherwise
// print the pronunciation twice for a two-POS entry).
return "\n=== Bilêvkirin ===\n" + out + "\n";
} },
pos: {
id: "pos",
required: true,
name: "Cureyê rêzimanî",
question_fmt: 'Cureyê rêzimanî yê "' + pageName + '" çi ye?',
visible: function (values) {
return values.lang;
},
// Populated from posChoices (data-sections.json) and posExamples (data-main.json).
choices: [],
// Definition of whichever cureyê rêzimanî is currently chosen,
// shown as plain text right under the dropdown (see the
// .wiktku-start-pos-example build/refresh() above). Kept clear of
// the Mane textarea's placeholder, which answers "what to type
// here" rather than "what this grammatical category means".
posExamples: {},
example: function (values) {
var pos = values.pos || "";
return steps.pos.posExamples[pos] ||
"Cureyê rêzimanî yê vê kelîmeyê hilbijêre — navdêr, lêker, rengdêr hwd.";
},
output: function (values) {
var pos = values.pos || "";
var capitalizedPos = capitalize(pos);
// "form" opt-param (checked in posparams) marks this as a
// form-of/inflected entry — the section title becomes
// "Formeke <pos>ê" instead of the plain pos name. The
// template call itself still uses the unchanged pos name.
var isForm = $("#wiktku-start-posparams [data-pp-opt='form']").is(":checked");
var header = isForm
? "\n=== Formeke " + pos + "ê ===\n"
: "\n=== " + capitalizedPos + " ===\n";
// Read the live wikitext from the posparams textarea.
// Fall back to a plain {{pos|lang}} if the step hasn't been built yet.
var tplLine = $.trim($("#wiktku-start-posparams .wiktku-posparams-text").val());
if (!tplLine) {
tplLine = "{{" + pos + "|" + (values.lang || "") + "}}";
}
// {{ku-tewîn-lk}} line for xwerû Kurmancî verbs, parsed straight out of tplLine (not the posparams input fields, which a cache/section restore doesn't repopulate) once niha/borî/transitivity are all present.
var tewinLkLine = "";
if (pos === "lêker" && values.lang === "ku") {
var niha = wiktkuWikitext.callParamValue(tplLine, "niha");
var borî = wiktkuWikitext.callParamValue(tplLine, "borî");
var lekturCode = wiktkuWikitext.callParamValue(tplLine, "c");
var cParts = lekturCode ? /^(?:([xphb])-)?(n?gh)$/.exec(lekturCode) : null;
var lekturForm = cParts ? (cParts[1] || "") : "";
var lktransitivity = cParts ? cParts[2] : "";
var lkForm = lktransitivity === "gh" ? "gerguhêz" : (lktransitivity === "ngh" ? "negerguhêz" : "");
if (niha && borî && lekturForm === "x" && lkForm) {
tewinLkLine = "{{ku-tewîn-lk|" + pageName + "|form=" + lkForm + "|niha=" + niha + "|borî=" + borî + "}}\n";
}
}
// Image line (between the conjugation line, if any, and the template call)
var imageLine = steps.resim.output(values);
return header + tewinLkLine + imageLine + tplLine + "\n";
}
},
// cinsiyet (gender) — shown for navdêr/serenav only, with choices built
// from metadata[lang].g since not every language has grammatical gender.
cinsiyet: {
id: "cinsiyet",
name: "cinsiyet",
question_fmt: 'Cinsiyeta "' + pageName + '" çi ye?',
visible: function () { return false; }, // handled inside posparams
// Single source of truth for the selected gender code. The visible
// dropdown lives inside the posparams step (see steps.posparams),
// but it must always read/write THIS hidden field — not a field of
// its own — since getValuesForStep("cinsiyet") reads from here.
build: function (stepDiv) {
var field = stepDiv.find(".wiktku-start-step-field");
if (field.length) return field;
field = $("<input type='hidden'>")
.addClass("wiktku-start-step-field")
.attr("id", "wiktku-start-cinsiyet-field")
.val("");
stepDiv.append(field);
return field;
},
output: function () { return ""; } // consumed by posparams/pos.output
},
// jimar (grammatical number) — same hidden-field pattern as cinsiyet
// above, but writes into its own j= template parameter instead of
// sharing c=. Shown for navdêr/serenav only, with the same choices
// for every language — data-main.json's numberLabels (see syncHelpers).
jimar: {
id: "jimar",
name: "jimar",
question_fmt: 'Jimara "' + pageName + '" çi ye?',
visible: function () { return false; }, // handled inside posparams
build: function (stepDiv) {
var field = stepDiv.find(".wiktku-start-step-field");
if (field.length) return field;
field = $("<input type='hidden'>")
.addClass("wiktku-start-step-field")
.attr("id", "wiktku-start-jimar-field")
.val("");
stepDiv.append(field);
return field;
},
output: function () { return ""; } // consumed by posparams/pos.output
},
// Lêker transitivity — hidden step, handled inside posparams
lektur: {
id: "lektur",
name: "Cureyê lêkerê (fiîlê)",
question_fmt: "Cureyê lêkerê (fiîlê) çi ye?",
visible: function () { return false; },
output: function () { return ""; }
},
// tr — hidden step, handled inside posparams
tr: {
id: "tr",
name: "transliterasyon",
question_fmt: "Transliterasyona vê kelîmeyê",
multiple: false,
narrow: true,
visible: function () { return false; }
},
// The card itself lives in wikt.edit/posparams.js, shared with DefSideBoxes -- see there for the
// fields and the per-language gating. This is only the wizard's side: which language and part of
// speech it is for, and mirroring gender/number into the two hidden steps the wizard keeps.
posparams: {
id: "posparams",
// Fused onto the "pos" card in styles.css so the two read as one question. The "xisûsiyet"
// name is a no-CSS fallback: its h3 is hidden, and every field below is self-labelled.
name: "xisûsiyet",
question_fmt: "",
card: null,
visible: function (values) {
return !!(values.lang && values.pos);
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-posparams-step").length) return null;
var values = getValues();
steps.posparams.card = wiktkuPosparams.build({
lang: values.lang,
pos: values.pos,
onChange: refresh,
// steps.cinsiyet/steps.jimar own these hidden fields; the card reports rather than
// writes, so it needs no knowledge of them.
onSync: function (cinsiyet, jimar) {
$("#wiktku-start-cinsiyet-field").val(cinsiyet || "");
$("#wiktku-start-jimar-field").val(jimar || "");
}
});
stepDiv.append(steps.posparams.card.$element);
return null;
},
rebuildTa: function () {
if (steps.posparams.card) steps.posparams.card.rebuild();
},
/** Called from refresh() — re-gates every row for the current language/pos. */
syncHelpers: function () {
if (!steps.posparams.card) return;
var values = getValues();
steps.posparams.card.setContext(values.lang, values.pos);
},
output: function () { return ""; } // pos.output reads the textarea directly
},
dfn: {
id: "dfn",
name: "Mane",
question_fmt: function (values) {
var base = "Maneya kelîmeyê binivîse. Mane otomatîk tê lînkkirin!";
if (values.lang === "ku") {
return base + " Ji bo tercimeyan beşa 'Werger' bi kar bîne!";
}
return base;
},
multiple: true,
ordered: true,
rows: 3,
visible: function (values) {
return values.lang && values.pos;
},
// Builds one sense block (<li>): mane textarea, nimûne list, and
// sub-lists for alternatîv/hevmane/dijmane/etc.
buildSenseBlock: function () {
var block = $("<li class='wiktku-dfn-sense'></li>");
// — Mane textarea (declared first so the template handler can reference it) —
var dfnArea = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-mane-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", "wiktku-start-step-field wiktku-dfn-mane")
.attr("rows", 6)
.attr("placeholder", "Maneya vê kelîmeyê binivîse…");
// Nothing on keystrokes: only the Werger gloss list needs a live Mane edit, and rebuilding
// it re-reads every sense. Deferred to blur; the wikitext is read fresh on output anyway.
dfnArea.on("blur", function () { refresh(); });
// — Îzaha maneyî (optional second half of the mane) —
// A non-Kurdish entry's gloss is a translation followed by {{îzah|…}} explaining it. The
// mane box is always the mane box and the explanation opens under it when asked for, so
// the pair reads as one gloss rather than as a different kind of field. getSenses() joins
// the two back together, so every consumer downstream sees one gloss string.
var izahArea = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-izah-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", "wiktku-dfn-subfield wiktku-dfn-izah")
.attr("rows", 2)
.attr("placeholder", "Îzaha maneyî… Ji bo school-a îngilîzî li vir 'cihê tehsîlgirtinê' binivîse");
var izahWrap = $("<div class='wiktku-dfn-izah-wrap'></div>")
.append($("<span class='wiktku-dfn-sublabel'></span>")
.append($("<span class='wiktku-dfn-sublabel-term'></span>").text("Îzaha maneyî")).append(": "))
.append(izahArea)
.hide();
// Opening it from a restore, where the text decides rather than a click.
var izahOpen = false;
function setIzahOpen(open) {
izahOpen = !!open;
updateManeMode();
}
block.data("setIzahOpen", setIzahOpen);
izahArea.on("input", function () {
// dfnArea drives the live preview and the auto-linker; the îzah is part of the gloss,
// so a change to it has to reach them too.
dfnArea.trigger("input");
});
izahArea.on("blur", function () { refresh(); });
// The mane box gives way to the template title box when a template is chosen; the îzah
// goes with it, since a template gloss has no separate explanation.
function updateManeMode() {
var isTpl = !!tplSelect.val();
if (isTpl) {
dfnArea.hide();
izahWrap.hide();
return;
}
dfnArea.show();
dfnArea.attr("placeholder", manePlaceholder(getValues().lang || ""));
// {{îzah}} explains a translation, so it belongs to entries in another language; a
// Kurdish sense is its own definition and is not offered one. A box that already holds
// text stays on screen whatever the language -- getSenses() joins it into the gloss
// either way, and hiding it would make that happen unseen.
var lang = getValues().lang || "";
var offer = !!lang && lang !== "ku";
// A non-Kurdish gloss is a translation and its explanation, so the box opens with the
// mane box rather than waiting to be asked for.
var open = offer || izahOpen || $.trim(izahArea.val()) !== "";
izahWrap.toggle(open);
}
// Store on the block so refresh() can re-sync every block when
// lang changes, and restoreDfn() can re-sync after restoring.
block.data("updateManeMode", updateManeMode);
// — Template selector (optional — replaces mane when chosen) —
// Template definitions loaded from data-main.json.
var tplDefs = steps.dfn.tplDefs || {};
// Cache of all non-placeholder option elements, so updateTplOptions
// can detach/re-attach them without losing the full list.
var _allTplOptions = [];
var tplSelect = $(mw.html.element("select", { "class": "wiktku-dfn-tpl-select" }));
tplSelect.attr("data-placeholder", wiktkuUtil.msg.searchTemplate);
tplSelect.append($("<option value=''>— yan jî şablonek hilbijêre —</option>"));
$.each(tplDefs, function (name, tpl) {
// Shows "name — tpl.desc" when a tplDefs entry has a desc
// in data-main.json, same pattern as steps.pron's template picker.
var opt = $("<option></option>")
.val(name)
.text(tpl.desc ? name + " — " + tpl.desc : name)
.attr("data-langs", tpl.langs ? tpl.langs.join(",") : "")
.attr("data-nolang", tpl.noLang ? "1" : "");
tplSelect.append(opt);
_allTplOptions.push(opt[0]);
});
// Same status-dot / 🔗 / ⓘ row treatment as every other
// template picker. This one carries the bare name in value=,
// so the name mapper is the identity rather than a snippet
// parse; descriptions come from each template's TemplateData.
markTemplateOptions(tplSelect, function (opt) { return opt.val(); });
function updateTplOptions() {
var lang = getValues().lang || "";
var currentVal = tplSelect.val();
var currentStillValid = false;
// Detach all non-placeholder options, then re-attach only the visible
// ones — native <select> won't reliably hide options any other way.
tplSelect.find("option[value!='']").detach();
$.each(_allTplOptions, function (_, opt) {
var allowed = $(opt).attr("data-langs");
var visible = !allowed || allowed.split(",").indexOf(lang) !== -1;
if (visible) {
tplSelect.append(opt);
if ($(opt).val() === currentVal) currentStillValid = true;
}
});
// Options that were detached when the descriptions arrived
// missed that pass, so describe whatever is attached now.
// The cache is warm by this point, so this is synchronous.
applyTplDescriptions(tplSelect);
tplSelect.trigger("wiktku:refresh");
if (currentVal && !currentStillValid) {
tplSelect.val("");
tplTitleInput.hide().val("");
updateManeMode();
}
}
// Store updater on the element so refresh() can call it for all blocks.
tplSelect.data("updateOptions", updateTplOptions);
var tplTitleInput = $("<input type='text'>")
.addClass("wiktku-dfn-tpl-title wiktku-start-narrow")
.attr("placeholder", "Navê rûpela hedefê…")
.hide();
// Same reasoning as dfnArea above: refresh() (which rebuilds the
// Werger/Bi zaravayên din gloss list) is deferred to blur
// rather than run on every keystroke.
tplTitleInput.on("blur", function () { refresh(); });
// Şablon senses have no plain gloss, so the word-list groups (hevmane, dijmane, formên
// alternatîv…) make no sense for them — the class hides both those groups and their
// entries in the "Hevmane û Bêhtir" menu, so they cannot be asked for either. Etîket, Kategorî,
// Nimûne and Jêgirtin stay available.
function updateFieldVisibility() {
block.toggleClass("wiktku-sense-is-tpl", !!tplSelect.val());
}
// Store on the block so restoreDfn() can re-sync visibility
// after setting tplSelect's value.
block.data("updateFieldVisibility", updateFieldVisibility);
tplSelect.on("change", function () {
var chosen = $(this).val();
if (chosen) {
dfnArea.hide();
izahWrap.hide();
tplTitleInput.show().focus();
} else {
tplTitleInput.hide().val("");
updateManeMode();
}
updateFieldVisibility();
// Re-run refresh so the Werger step visibility updates.
refresh();
});
// The template picker is an alternative to writing a
// definition, so this quiet link sits above the Mane box
// rather than getting buried under Dever/Etîket/Kategorî.
// It only expands for the minority of senses that are a
// pointer at another word (guharto, şaşnivîs, kevnbûyî)
// rather than a meaning.
var tplRow = $("<div></div>").addClass("wiktku-dfn-tpl-row")
.append(tplSelect).append(" ").append(tplTitleInput)
.hide();
var tplLink = $("<a href='#' class='wiktku-dfn-tpl-link'></a>")
.text("Maneyê bi şablonan çêke (rastnivîs, şaşnivîs, awayekî din)");
tplLink.on("click", function (e) {
e.preventDefault();
tplRow.show();
$(this).hide();
tplRow.find("input.wiktku-ac-input").first().focus();
});
/** Opens the picker without a click — used on restore, when a
* sense already has a template and hiding it would make the
* chosen value invisible. */
function revealTplRow() { tplRow.show(); tplLink.hide(); }
block.data("revealTplRow", revealTplRow);
// Dever/Şîwe, Etîket and Kategorî as chip+toggle fields, so a sense that leaves them empty
// stays short. getOutput() folds Dever/Şîwe into {{dever|}} for ku, {{e|}} otherwise.
// Each closes the other two on open; onOpen runs from a click, so forward references hold.
var deverField = buildChipField({
wrapCls: "wiktku-dfn-dever",
toggleLabel: "+ Dever/Şîwe",
hint: "Bi vê maneyê li kû derê an di kîjan devokê de tê bikaranîn?",
// Kurmancî's pills are the handful in data-main.json; another language gets the regions
// Modul:labels tags for it. The rest are found by typing.
quickPicks: function () {
var lang = getValues().lang || "ku";
if (lang === "ku") return DEVER_QUICK_PICKS;
return wiktkuLabelSets ? wiktkuEdit.labels.taggedRegionsFor(wiktkuLabelSets, lang) : [];
},
suggestions: function () { return wiktkuLabelSuggestions("dever"); },
onOpen: function () { etiketField.close(); kategoriField.close(); wiktkuEnsureLabels(); }
});
block.data("addDeverChip", deverField.addChip);
var etiketField = buildChipField({
wrapCls: "wiktku-dfn-etiket",
toggleLabel: "+ Etîket",
hint: "xisûsen, Mêrdîn, anatomî, Badînî",
suggestions: function () { return wiktkuLabelSuggestions("etiket"); },
onOpen: function () { deverField.close(); kategoriField.close(); wiktkuEnsureLabels(); }
});
block.data("addEtiketChip", etiketField.addChip);
var kategoriField = buildChipField({
wrapCls: "wiktku-dfn-kategori",
toggleLabel: "+ Kategorî",
hint: "wekî etîket lê xuya nabe tenê kategorîze dike",
suggestions: function () { return wiktkuLabelSuggestions("kategori"); },
onOpen: function () { deverField.close(); etiketField.close(); wiktkuEnsureLabels(); }
});
block.data("addKategoriChip", kategoriField.addChip);
// Maneid -- the anchor another entry links a sense by, written "{{maneid|lang|x}}" at the
// head of the line. One value rather than a list, so it is a plain box and not a chip
// field; it still behaves like one to the "Hevmane û Bêhtir" menu through the small api below.
var maneidInput = $("<input type='text'>")
.addClass("wiktku-dfn-maneid-input wiktku-start-narrow")
.attr("placeholder", "id");
var maneidWrap = $("<div class='wiktku-dfn-chipfield-wrap wiktku-dfn-maneid-wrap'></div>")
.append($("<label class='wiktku-dfn-maneid-label'></label>")
.text("Maneid ")
.append(maneidInput))
.hide();
var maneidField = {
wrap: maneidWrap,
open: function () { maneidWrap.show(); maneidInput.focus(); },
values: function () {
var v = $.trim(maneidInput.val() || "");
return v ? [v] : [];
}
};
// Dever/Şîwe starts open for ku (where it's the common case)
// and closed otherwise; backs off once the person toggles it
// themselves (see buildChipField's setOpenDefault). Re-synced
// from refresh() whenever lang changes — see block.data below.
//
// Shown only for a language with regions: every Kurdish variety, and any other language
// Modul:labels tags one for (read once the labels arrive). One that already has a value stays.
function updateDeverOpenState() {
var lang = getValues().lang || "";
var hasRegions = !lang || wiktkuEdit.labels.isKurdish(lang) ||
(wiktkuLabelSets && wiktkuEdit.labels.regionalFor(wiktkuLabelSets, lang).length > 0);
if (!hasRegions && !wiktkuLabelSets) wiktkuEnsureLabels();
var show = hasRegions || deverField.values().length > 0;
deverField.wrap.toggle(show);
if (!show) deverField.panel.hide();
deverField.setOpenDefault(lang === "ku");
}
block.data("updateDeverOpenState", updateDeverOpenState);
wiktkuLabelListeners.push(updateDeverOpenState);
updateDeverOpenState();
// Toggles always sit together in one row; panels render in their
// own area below so opening one never displaces the others.
// The widget is still built on a reconstruction page so every reference to it stays valid;
// it just never reaches the DOM, so it shows nothing and contributes nothing to getOutput().
//
// The row is the same shape DefSideBoxes puts under a sense -- a few chips for the common
// things, everything else behind "Hevmane û Bêhtir" -- so the two gadgets are learned once. Etîket
// and Kategorî start in the menu and their toggle only joins the row once picked.
var chipFieldRow = $("<div class='wiktku-dfn-chipfield-row'></div>");
var chipFieldPanels = $("<div class='wiktku-dfn-chipfield-panels'></div>");
if (!isReconstruction) {
chipFieldRow.append(deverField.wrap);
chipFieldPanels.append(deverField.panel);
}
etiketField.wrap.hide();
kategoriField.wrap.hide();
chipFieldRow.append(etiketField.wrap).append(kategoriField.wrap).append(maneidWrap);
chipFieldPanels.append(etiketField.panel).append(kategoriField.panel);
block.append(tplLink);
block.append(tplRow);
block.append(dfnArea);
block.append(izahWrap);
block.append(chipFieldRow);
block.append(chipFieldPanels);
// Apply lang filter on initial build.
updateTplOptions();
updateManeMode();
// Sub-field groups. Every one of them is built hidden and asked for by name -- from a chip
// in the row above (Nimûne, Jêgirtin) or from the "Hevmane û Bêhtir" menu -- so a sense that needs
// none of them is four boxes tall instead of ten. Jêgirtin is built after the loop as a
// compound widget and inserted after Nimûne, so the visible order matches this list.
var allGroups = [
{ cls: "wiktku-dfn-nimune", label: "Nimûne", rows: 2, narrow: false, attested: true }
].concat($.map(wordGroups(), function (g) {
return { cls: g.cls, label: g.label, rows: 1, narrow: true, izah: g.izah };
}));
if (isReconstruction) {
allGroups = $.grep(allGroups, function (g) { return !g.attested; });
}
$.each(allGroups, function (_, g) {
// wiktku-dfn-wordgroup marks the six word lists, which a şablon sense hides -- both the
// group and its entry in the menu (see the .wiktku-sense-is-tpl rules).
var groupDiv = $("<div></div>").addClass("wiktku-dfn-subgroup wiktku-dfn-subgroup-hidden " + g.cls + "-group");
if (g.cls !== "wiktku-dfn-nimune") groupDiv.addClass("wiktku-dfn-wordgroup");
// Declared once here rather than with `var` in each branch
// below — both branches assign the same pair, just with a
// different lbl and list, so one declaration avoids the
// (harmless but lint-flagged) re-declaration.
var lbl, list;
// The language the example is to be written in, named in the box itself. Read fresh
// rather than fixed at build time: the entry's language is often chosen later.
function nimunePlaceholder() {
var code = getValues().lang || "";
return wiktkuUtil.examplePrompt(code && getLangLabel(code), false);
}
function buildNimuneLi() {
var lang = getValues().lang || "";
var li = $("<li></li>").addClass("wiktku-dfn-nimune-li");
var mainTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-text-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", g.rows)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-text")
.attr("placeholder", nimunePlaceholder());
mainTa.on("input", function () {
highlightLinebreak(this);
var val = $.trim($(this).val());
var isLast = li.is(list.children("li:last-child"));
if (val && isLast) {
list.append(buildNimuneLi());
}
});
li.append(mainTa);
var subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
if (needsTransliteration(lang)) {
subWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-tr")
.attr("placeholder", "Transliterasyona bi alfabeya latînî")
);
}
if (lang) {
subWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-t")
.attr("placeholder", nimuneTranslationPrompt(lang))
);
}
if (subWrap.children().length) li.append(subWrap);
return li;
}
// The placeholder names the current language, but that's
// often not chosen yet when this block is first built —
// so keep a function around that refresh() (and onSubInput,
// for newly-grown fields) can call again once lang is set
// or changes.
function subPlaceholder() {
if (g.noLangSuffix) return g.label + "…";
var lang = getValues().lang || "";
var langLabel = lang ? getLangLabel(lang) : "";
return g.label + (langLabel ? " bi " + langLabel : "") + "…";
}
function updateSubPlaceholders() {
list.find("> li > textarea.wiktku-dfn-subfield").attr("placeholder", subPlaceholder());
}
if (g.cls === "wiktku-dfn-nimune") {
// Nimûne — compound widget with tr/t sub-fields.
lbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text(g.label))
.append(": ");
list = $("<ol></ol>").addClass("wiktku-dfn-sublist " + g.cls + "-list");
groupDiv.data("buildNimuneLi", buildNimuneLi);
// Refreshed with the word groups' own boxes when the entry's language changes.
list.data("updatePlaceholders", function () {
list.find("> li > textarea.wiktku-dfn-nimune-text")
.attr("placeholder", nimunePlaceholder());
});
list.append(buildNimuneLi());
groupDiv.append(lbl).append(list);
block.append(groupDiv);
} else {
// Regular always-visible group — label + growing textarea list.
lbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text(g.label))
.append(": ")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-hint").text(g.izah));
var useModifiers = isWordGroupCls(g.cls);
list = $("<ol></ol>").addClass("wiktku-dfn-sublist " + g.cls + "-list");
list.data("updatePlaceholders", updateSubPlaceholders);
var firstInput = $("<textarea></textarea>")
.attr("name", g.cls + "-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", g.rows)
.attr("class", "wiktku-dfn-subfield" + (g.narrow ? " wiktku-start-narrow" : ""))
.attr("placeholder", subPlaceholder());
firstInput.on("input", function () {
steps.dfn.onSubInput(list, this, g.rows, g.narrow, useModifiers);
});
list.append(useModifiers ? makeModifierLi(firstInput) : $("<li></li>").append(firstInput));
groupDiv.append(lbl).append(list);
block.append(groupDiv);
}
});
// — Jêder group — growing list, same structure as Nimûne. —
if (!isReconstruction) (function () {
var jederGroupDiv = $("<div></div>").addClass("wiktku-dfn-subgroup wiktku-dfn-subgroup-hidden wiktku-dfn-jeder-group");
var jederLbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text("Jêgirtin"))
.append(": ");
var jederList = $("<ol></ol>").addClass("wiktku-dfn-sublist wiktku-dfn-jeder-list");
// Refreshed with every other sub-box when the entry's language changes.
jederList.data("updatePlaceholders", function () {
jederList.find("> li > textarea.wiktku-dfn-nimune-quote")
.attr("placeholder", jederPlaceholder());
});
// Same shape as the nimûne box: the language belongs in the prompt, and is read fresh
// because the entry's language is often chosen after the block was built.
function jederPlaceholder() {
var code = getValues().lang || "";
return wiktkuUtil.examplePrompt(code && getLangLabel(code), true);
}
function buildJederLi() {
var lang = getValues().lang || "";
var li = $("<li></li>").addClass("wiktku-dfn-jeder-li");
// Quote text textarea
var quoteTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote")
.attr("placeholder", jederPlaceholder());
quoteTa.on("input", function () {
highlightLinebreak(this);
var val = $.trim($(this).val());
var isLast = li.is(jederList.children("li:last-child"));
if (val && isLast) {
jederList.append(buildJederLi());
}
});
li.append(quoteTa);
// tr/t sub-fields for the quote — shown as needed for the language.
var quoteSubWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
if (needsTransliteration(lang)) {
quoteSubWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-tr")
.attr("placeholder", "Transliterasyona jêgirtinê…")
);
}
if (lang && lang !== "ku") {
quoteSubWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-t")
.attr("placeholder", "Tercimeya jêgirtinê bi kurmancî")
);
}
if (quoteSubWrap.children().length) li.append(quoteSubWrap);
// The source, in the add-examples form's order and wording: the template, then the link
// with its Citoid button, then the template's own fields.
var jederDefs = steps.dfn.jederDefs || {};
var srcSel = $("<select></select>").addClass("wiktku-dfn-quote-src-sel wiktku-plain-select");
// "" is auto-detect: a Citoid fetch picks the template.
srcSel.append($("<option value=''></option>").text(wiktkuUtil.msg.citoidAuto));
srcSel.append($("<option></option>").val("__manual__").text(wiktkuUtil.msg.sourceByHand));
$.each(jederDefs, function (tplName, def) {
srcSel.append($("<option></option>").val(tplName).text(def.label));
});
srcSel.append($("<option></option>").val("__category__").text(wiktkuUtil.msg.sourceFromCategory));
// Paste a citation built on another page into the quote box and have it saved exactly
// as typed -- the quickest way to reuse one jêgirtin across many entries.
srcSel.append($("<option></option>").val("__raw__").text(wiktkuUtil.msg.sourceRaw));
var srcSelWrap = $("<div></div>").addClass("wiktku-dfn-quote-field")
.append($("<label></label>").text("Şablona çavkaniyê"), srcSel);
// Saved as url= in every mode but Kategorî and raw, which have no box for it.
var urlLbl = $("<label></label>");
var urlInput = $("<input type='text'>").addClass("wiktku-dfn-quote-url");
var citoidBtn = new OO.ui.ButtonWidget({
label: wiktkuUtil.msg.citoidFill,
flags: [ "progressive" ],
classes: [ "wiktku-dfn-quote-citoid-btn" ]
});
var citoidStatus = $("<span></span>").addClass("wiktku-dfn-quote-citoid-status");
var urlHint = $("<p></p>").addClass("wiktku-step-hint wiktku-dfn-quote-url-hint");
var citoidWrap = $("<div></div>").addClass("wiktku-dfn-quote-field wiktku-dfn-quote-citoid")
.append(urlLbl, urlInput, citoidBtn.$element, citoidStatus, urlHint);
// The add-examples form's labels: plain "attach a link" wording for a hand-written
// source, Citoid wording otherwise.
function setUrlWording(chosen) {
if (chosen === "__manual__") {
urlLbl.text("Adresa webê ya nimûneyê");
urlInput.attr("placeholder", "URLyeke wekî https://…");
urlHint.text("Tenê binivîse eger lînk jixwe di referansa çavkaniyê de tinebe.");
} else {
urlLbl.text("Lînk, DOI an ISBNya çavkaniyê");
urlInput.attr("placeholder", "URLyek wekî https://… an jî DOI/ISBN");
urlHint.text(chosen
? "Ev dibe lînka çavkaniyê (url=). Ji bo şablonên jêder-*, dikare bi Citoid an bi destan jî bên dagirtin."
: "Li qutiyê URL, DOI an ISBN binivîse û pê lê bike butona '" + wiktkuUtil.msg.citoidFill + "' da ku otomatîk çavkanî were dagirtin.");
}
}
setUrlWording("");
// Fills only empty boxes, so a repeat fetch never overwrites what was typed by hand.
function fillEmptyFields(fields) {
$.each(fields, function (key, val) {
var input = srcFields.find("[data-field-key='" + key + "']");
if (input.length && !$.trim(input.val())) {
input.val(val);
}
});
}
// Prefills Dem from the link's own "t=" once jêder-vîdeo is active.
function autofillVideoTimestamp() {
var citoid = window.wikt && window.wikt.citoid;
var dem = srcFields.find("[data-field-key='dem']");
if (srcSel.val() !== "jêder-vîdeo" || !dem.length || $.trim(dem.val()) ||
!(citoid && citoid.extractVideoTimestamp)) {
return;
}
var t = citoid.extractVideoTimestamp($.trim(urlInput.val()));
if (t) dem.val(t);
}
function runCitoidFetch() {
var query = $.trim(urlInput.val());
if (!query) {
mw.notify(wiktkuUtil.msg.citoidNeedsUrl);
return;
}
citoidBtn.setDisabled(true);
citoidStatus.text("Tê barkirin…");
// Through wikt.cite rather than straight to the citoid gadget: the wrapper adds what
// Citoid returns but citoid-helper's mapping drops -- the access date, the ISSN and
// the DOI.
wiktkuCite.fetchFromCitoid(query).then(function (result) {
// Citoid picks the template only while none is chosen; a hand-picked one is kept.
var current = srcSel.val();
if (!current && jederDefs[result.jederKey]) {
srcSel.val(result.jederKey).trigger("change");
current = result.jederKey;
}
if (jederDefs[current]) {
// Citoid shaped its fields for the type it detected; re-derived for another.
fillEmptyFields(current === result.jederKey ? result.fields
: wiktkuCite.citoidFieldsFor(current, result.raw, query));
autofillVideoTimestamp();
} else {
// No template for this kind of source: a hand-written source line from what
// Citoid returned, only while that box is still empty.
if (current !== "__manual__") {
srcSel.val("__manual__").trigger("change");
}
var manualField = srcFields.find(".wiktku-dfn-quote-src-manual");
if (!$.trim(manualField.val())) {
var parts = [];
$.each(["nivîskar", "sernav", "websîte", "kovar", "weşanxane", "sal"], function (_, k) {
if (result.fields[k]) parts.push(result.fields[k]);
});
manualField.val(parts.join(", "));
}
}
citoidStatus.text(wiktkuUtil.msg.citoidFilled);
}, function (errorMsg) {
// Citoid cannot read some sites (x.com): Websîte with a guessed name instead.
var citoid = window.wikt && window.wikt.citoid;
var site = citoid && citoid.websiteNameForUrl && citoid.websiteNameForUrl(query);
if (site && jederDefs["jêder-websîte"]) {
if (srcSel.val() !== "jêder-websîte") {
srcSel.val("jêder-websîte").trigger("change");
}
fillEmptyFields({ "websîte": site });
citoidStatus.text("Citoid nikare vê malperê bixwîne; şablona Websîte hate hilbijartin, ji kerema xwe mayî bi destan tijî bike.");
return;
}
citoidStatus.text(typeof errorMsg === "string" ? errorMsg : "Nehat dîtin. Kontrol bike ka URL/DOI/ISBN rast e.");
}).always(function () {
citoidBtn.setDisabled(false);
});
}
citoidBtn.on("click", runCitoidFetch);
urlInput.on("input", autofillVideoTimestamp);
urlInput.on("keydown", function (e) {
if (e.which === 13) { // Enter
e.preventDefault();
runCitoidFetch();
}
});
// Dynamic source fields container.
var srcFields = $("<div></div>").addClass("wiktku-dfn-quote-src-fields");
// "Malûmatên çavkaniyê" over a jêder-* template's boxes, with the button that empties
// them and the link.
var clearBtn = new OO.ui.ButtonWidget({
label: "Vala bike",
framed: false,
title: "Lînkê û qadên jêder-* yên vê beşê vala bike",
classes: [ "wiktku-dfn-quote-src-clear" ]
});
clearBtn.on("click", function () {
urlInput.val("");
citoidStatus.text("");
srcFields.find(".wiktku-dfn-quote-src-input").val("");
});
var srcHead = $("<div></div>").addClass("wiktku-dfn-quote-src-head")
.append($("<span></span>").addClass("wiktku-dfn-quote-src-head-label").text("Malûmatên çavkaniyê"),
clearBtn.$element)
.hide();
// One labelled box for a citation parameter. Wide/narrow/medium layout: sernav-type
// fields get a full-width multiline box, short ones like sal/rûpel/isbn stay narrow.
// Sizes come from wikt.cite, so a Kategorî:Çavkanî template that happens to use the
// same parameter names as jêder-* is laid out the same way rather than generically.
function buildSrcFieldRow(field) {
var size = wiktkuCite.fieldSize(field.key);
var width = field.width || size.width || "medium";
var multiline = field.multiline !== undefined ? field.multiline : size.multiline;
var row = $("<div></div>")
.addClass("wiktku-dfn-quote-src-row wiktku-dfn-quote-src-row--" + width);
var fieldLbl = $("<label></label>").text(field.label + ": ");
var inp = $(multiline ? "<textarea></textarea>" : "<input type='text'>")
.addClass("wiktku-dfn-quote-src-input")
.attr("data-field-key", field.key)
.attr("placeholder", field.label + "…");
if (multiline) {
inp.attr("rows", 2).addClass("wiktku-dfn-quote-src-input--title");
}
// sal and tarîx say the same thing twice and Modul:quote refuses both; filling
// one clears the other rather than letting the citation break on save.
var partner = wiktkuCite.exclusivePartner(field.key);
if (partner) {
inp.on("input", function () {
if (!$.trim($(this).val())) return;
$(this).closest(".wiktku-dfn-quote-src-fields")
.find(".wiktku-dfn-quote-src-input[data-field-key='" + partner + "']")
.val("");
});
}
return row.append(fieldLbl).append(inp);
}
srcSel.on("change", function () {
srcFields.empty();
var chosen = $(this).val();
// As in the add-examples form: no link for Kategorî or raw, no Citoid for a
// hand-written source, and the fields' header only over a jêder-* template.
citoidWrap.toggle(chosen !== "__category__" && chosen !== "__raw__");
citoidBtn.toggle(chosen !== "__manual__");
citoidStatus.text("");
setUrlWording(chosen);
srcHead.toggle(!!jederDefs[chosen]);
if (!chosen) {
return;
}
if (chosen === "__raw__") {
// Nothing to fill in: the quote box above already holds the finished
// template, and it is written out exactly as typed. Only the entry name
// inside its jêgirtin= is emboldened (see wikt.cite's boldQuoteParam).
srcFields.append(
$("<p></p>")
.addClass("wiktku-step-hint wiktku-dfn-quote-raw-hint")
.text("Şablona temam li qutiya jêgirtinê ya jorê binivîse — wek xwe tê qeydkirin.")
);
return;
}
if (chosen === "__manual__") {
// Freeform source, written by the user — becomes the 2nd
// positional param of {{jêder|lang|…}} instead of çavkanî?.
srcFields.append(
$("<textarea></textarea>")
.attr("rows", 4)
.addClass("wiktku-dfn-subfield wiktku-dfn-quote-src-manual")
.attr("placeholder", "Çavkaniyê bi destê xwe binivîse…")
);
return;
}
if (chosen === "__category__") {
// Same [[Kategorî:Çavkanî]] search widget DefSideBoxes' own Çavkanî subsection box
// uses -- picking a result switches the row into the hidden manual mode with
// {{Şablon}} pre-filled. The picked şablon becomes the citation template itself:
// its own TemplateData parameters are drawn as fields below the picker, in the
// same markup the jêder-* branch uses, so readJederList() reads both with one loop.
var catPicker = wiktkuCite.buildCategoryTemplateSearch({
placeholder: wiktkuUtil.msg.searchTemplate,
describe: false,
onPick: function (entry) {
li.data("catTemplate", entry.target);
li.data("catLangParam", null);
catPicker.$status.text("Qutiyên şablonê tên barkirin…");
srcFields.find(".wiktku-dfn-quote-src-row").remove();
wiktkuCite.fieldSpecs(entry.target).then(function (spec) {
catPicker.$status.text("");
li.data("catLangParam", spec.langParamKey || null);
$.each(spec.fields, function (_, field) {
srcFields.append(buildSrcFieldRow(field));
});
}, function () {
catPicker.$status.text("Qutiyên şablonê nehatin anîn.");
});
}
});
srcFields.append(catPicker.$element);
return;
}
var def = jederDefs[chosen];
if (!def) return;
$.each(def.fields, function (_, field) {
srcFields.append(buildSrcFieldRow(field));
});
autofillVideoTimestamp();
});
// Exposed so draft restore can rebuild a saved category template's fields.
// A whole citation line pasted into the quote box ("#* {{jêder|…}}") means it was
// copied off another entry; switch to raw mode rather than wrapping it in a second
// {{nimûne}}. Only while no source type has been chosen by hand.
li.find(".wiktku-dfn-nimune-quote").on("input", function () {
if (srcSel.val()) return;
if (wiktkuCite.looksLikeCitationLine($(this).val())) {
srcSel.val("__raw__").trigger("change");
}
});
li.data("buildSrcFieldRow", buildSrcFieldRow);
li.append(srcSelWrap, citoidWrap, srcHead, srcFields);
return li;
}
jederGroupDiv.data("buildJederLi", buildJederLi);
jederList.append(buildJederLi());
jederGroupDiv.append(jederLbl).append(jederList);
// Straight after Nimûne rather than last: Jêgirtin is the
// same kind of thing (an attested use of the word) and the
// two belong together — see the allGroups comment above.
var nimuneGroup = block.find(".wiktku-dfn-nimune-group");
if (nimuneGroup.length) jederGroupDiv.insertAfter(nimuneGroup);
else block.append(jederGroupDiv);
}());
// Apply the initial hide/show state for the sub-groups (matches
// tplSelect's starting value, which is normally empty).
updateFieldVisibility();
// ── Which sub-field goes where: chips in the row above for the two everyday ones, the rest
// behind "Hevmane û Bêhtir". Same arrangement as DefSideBoxes' own sense menu, deliberately -- a
// person who has used one gadget should not have to learn the other.
(function () {
var PICK_DEFS = [
{ cls: "wiktku-dfn-nimune", label: "Nimûne", chip: true, attested: true },
{ cls: "wiktku-dfn-jeder", label: "Jêgirtin", chip: true, attested: true }
].concat($.map(wordGroups(), function (g) {
return { cls: g.cls, label: g.label, word: true };
}));
// Etîket and Kategorî are chip fields, not sub-groups, so they are revealed by showing
// their toggle rather than a whole box.
var CHIP_FIELD_DEFS = [
{ cls: "wiktku-dfn-etiket", label: "Etîket", field: etiketField },
{ cls: "wiktku-dfn-kategori", label: "Kategorî", field: kategoriField },
{ cls: "wiktku-dfn-maneid", label: "Maneid", field: maneidField }
];
function groupHasContent(groupDiv) {
return groupDiv.find("textarea, input").filter(function () {
return $.trim($(this).val() || "") !== "";
}).length > 0;
}
// Shows a group and takes its chip / menu entry out of the picker -- both carry the same
// data-pick, so one selector covers whichever of the two this group had.
// Which group carries no divider above it: the first one actually on screen, not the
// first in the DOM -- see the .wiktku-dfn-subgroup-first rule.
function updateFirstVisible() {
var $all = block.find(".wiktku-dfn-subgroup").removeClass("wiktku-dfn-subgroup-first");
$all.not(".wiktku-dfn-subgroup-hidden").first().addClass("wiktku-dfn-subgroup-first");
}
function revealGroup(cls, focus) {
var groupDiv = block.find("." + cls + "-group");
if (!groupDiv.length) return;
groupDiv.removeClass("wiktku-dfn-subgroup-hidden");
block.find("[data-pick='" + cls + "']").hide();
updateFirstVisible();
if (focus) groupDiv.find("textarea, input").first().focus();
}
block.data("revealSenseGroup", revealGroup);
// Hides it again and gives the picker its entry back. Anything typed is thrown away, so a
// group holding something asks first.
function hideGroup(cls) {
var groupDiv = block.find("." + cls + "-group");
if (!groupDiv.length) return;
if (groupHasContent(groupDiv) &&
!window.confirm("Tiştên di vê qutiyê de winda dibin. Dewam bike?")) {
return;
}
groupDiv.children("ol").children("li").not(":first").remove();
groupDiv.find("textarea, input").val("");
// The jêgirtin row's source <select> owns the fields under it; putting it back to ""
// is what clears them.
groupDiv.find("select.wiktku-dfn-quote-src-sel").val("").trigger("change");
groupDiv.addClass("wiktku-dfn-subgroup-hidden");
block.find("[data-pick='" + cls + "']").show();
updateFirstVisible();
}
// Drafts, restoreDfn() and the ⊕-grown lists all put content in without going through
// revealGroup(), so anything that ended up filled is shown on the next refresh pass.
function syncPicks() {
$.each(PICK_DEFS, function (_, def) {
var groupDiv = block.find("." + def.cls + "-group");
if (groupDiv.length && groupDiv.hasClass("wiktku-dfn-subgroup-hidden") &&
groupHasContent(groupDiv)) {
revealGroup(def.cls, false);
}
});
$.each(CHIP_FIELD_DEFS, function (_, def) {
if (def.field.values().length) {
def.field.wrap.show();
block.find("[data-pick='" + def.cls + "']").hide();
}
});
}
block.data("syncSensePicks", syncPicks);
// Every group gets an × in its label, so a box opened by mistake can go away again --
// the counterpart of DefSideBoxes' own box-remove link.
$.each(PICK_DEFS, function (_, def) {
var groupDiv = block.find("." + def.cls + "-group");
if (!groupDiv.length) return;
var $x = $("<button type='button' class='wiktku-dfn-subgroup-remove'>×</button>")
.attr("title", def.label + " veşêre");
$x.on("click", function (e) {
e.preventDefault();
hideGroup(def.cls);
});
groupDiv.children(".wiktku-dfn-sublabel").first().append($x);
});
// — the chips, beside Dever/Şîwe —
$.each(PICK_DEFS, function (_, def) {
if (!def.chip || (isReconstruction && def.attested)) return;
var $chip = $("<button type='button' class='wiktku-dfn-chipfield-toggle'></button>")
.attr("data-pick", def.cls)
.text("+ " + def.label);
$chip.on("click", function (e) {
e.preventDefault();
revealGroup(def.cls, true);
});
chipFieldRow.append($chip);
});
// — "Hevmane û Bêhtir" —
var moreArrow = $("<span class='wiktku-dfn-more-arrow'>▸</span>");
var moreToggle = $("<button type='button' class='wiktku-dfn-chipfield-toggle wiktku-dfn-more-toggle'></button>")
.append(document.createTextNode(wiktkuUtil.msg.moreMenu + " ")).append(moreArrow);
var moreMenu = $("<div class='wiktku-dfn-more-menu'></div>").hide();
function closeMenu() {
moreMenu.hide();
moreArrow.html("▸");
}
moreToggle.on("click", function (e) {
e.preventDefault();
$(".wiktku-dfn-more-menu").not(moreMenu).hide();
moreMenu.toggle();
moreArrow.html(moreMenu.is(":visible") ? "▾" : "▸");
});
function menuEntry(label, cls, extraCls) {
var $link = $("<a href='#' class='wiktku-dfn-more-item'></a>").text(label);
if (cls) $link.attr("data-pick", cls);
if (extraCls) $link.addClass(extraCls);
moreMenu.append($link);
return $link;
}
// Group 1: the two label fields. Their toggle only joins the chip row once asked for.
$.each(CHIP_FIELD_DEFS, function (_, def) {
var $link = menuEntry("+ " + def.label, def.cls);
$link.on("click", function (e) {
e.preventDefault();
closeMenu();
def.field.wrap.show();
def.field.open();
$link.hide();
});
});
// Group 2: the word lists. wiktku-dfn-pick-word so a şablon sense hides these entries
// along with the groups themselves.
var firstWord = true;
$.each(PICK_DEFS, function (_, def) {
if (!def.word) return;
var $link = menuEntry("+ " + def.label, def.cls,
"wiktku-dfn-pick-word" + (firstWord ? " wiktku-dfn-more-group-start" : ""));
firstWord = false;
$link.on("click", function (e) {
e.preventDefault();
closeMenu();
revealGroup(def.cls, true);
});
});
// Group 3: no "Maneê rake" counterpart -- the tab row already carries one bin for the
// open sense, and a second delete in here would be a second way to do the same thing.
var $newSense = menuEntry(wiktkuUtil.msg.addAnotherSense, null, "wiktku-dfn-more-group-start");
$newSense.on("click", function (e) {
e.preventDefault();
closeMenu();
steps.dfn.addSense($("#wiktku-start-dfn"), block);
});
chipFieldRow.append($("<span class='wiktku-dfn-more-wrap'></span>").append(moreToggle, moreMenu));
bindMoreMenuCloser();
syncPicks();
updateFirstVisible();
}());
return block;
},
/** Shows only `block` among the sense list; hides every other one. */
setOpenSense: function (stepDiv, block) {
stepDiv.find("li.wiktku-dfn-sense").each(function () {
$(this).toggleClass("wiktku-sense-collapsed", !$(this).is(block));
});
steps.dfn.renderSenseTabs(stepDiv);
},
/** Rebuilds the "Mane 1 / Mane 2 / [trash] / + Maneyeke din" tab row from the current sense list. */
renderSenseTabs: function (stepDiv) {
var tabsDiv = stepDiv.find(".wiktku-dfn-sense-tabs");
if (!tabsDiv.length) return;
tabsDiv.empty();
var blocks = stepDiv.find("li.wiktku-dfn-sense");
var openLi = null;
blocks.each(function (i) {
var li = $(this);
var isOpen = !li.hasClass("wiktku-sense-collapsed");
if (isOpen) openLi = li;
var tab = $("<span class='wiktku-dfn-sense-tab'></span>")
.text("Mane " + (i + 1))
.toggleClass("wiktku-tab-active", isOpen);
tab.on("click", function () { steps.dfn.setOpenSense(stepDiv, li); });
tabsDiv.append(tab);
});
// One delete button for the whole bar — not one per tab — so
// there's only ever a single, deliberate hit-target for a
// destructive action, positioned away from the tabs people
// click to switch senses. Always keep at least one Mane.
if (blocks.length > 1 && openLi) {
var openIdx = blocks.index(openLi);
var trash = makeTrashButton("Vê maneyê jê bibe (Mane " + (openIdx + 1) + ")");
trash.on("click", function () {
if (!window.confirm("Tu dixwazî Mane " + (openIdx + 1) + " jê bibî? Ev tişt nayê vegerandin.")) return;
steps.dfn.removeSense(stepDiv, openLi);
});
tabsDiv.append(trash.$element);
}
var addBtn = $("<button type='button' class='wiktku-add-sense'></button>")
.append($("<span class='wiktku-add-sense-plus'>+</span>"))
.append(document.createTextNode(wiktkuUtil.msg.anotherSense));
addBtn.on("click", function (e) {
e.preventDefault();
steps.dfn.addSense(stepDiv);
});
tabsDiv.append(addBtn);
},
/** Adds a sense, opens it, and hides the others. With afterBlock, the new one lands directly
* under that sense rather than at the end — "Maneyeke din zêde bike" in a sense's own
* "Hevmane û Bêhtir" menu is asking for one next to *this* meaning. */
addSense: function (stepDiv, afterBlock) {
var list = stepDiv.find("ol.wiktku-dfn-list");
var newBlock = steps.dfn.buildSenseBlock(stepDiv);
if (afterBlock && afterBlock.length) newBlock.insertAfter(afterBlock);
else list.append(newBlock);
initSelects(newBlock);
steps.dfn.setOpenSense(stepDiv, newBlock);
newBlock.find("textarea.wiktku-dfn-mane").first().focus();
refresh();
return newBlock;
},
/** Removes one sense block. Assumes the caller already confirmed
* and already checked more than one sense remains. If the
* removed sense was the open one, opens whichever sense ends up
* taking its place (or the last one, if it was the last). */
removeSense: function (stepDiv, li) {
var list = stepDiv.find("ol.wiktku-dfn-list");
var blocks = list.find("li.wiktku-dfn-sense");
var removedIdx = blocks.index(li);
var wasOpen = !li.hasClass("wiktku-sense-collapsed");
li.remove();
if (wasOpen) {
var remaining = list.find("li.wiktku-dfn-sense");
var next = remaining.eq(removedIdx);
if (!next.length) next = remaining.last();
if (next.length) steps.dfn.setOpenSense(stepDiv, next);
}
steps.dfn.renderSenseTabs(stepDiv);
refresh();
},
/** Grows a sub-list (nimûne/formên alternatîv/hevmane/dijmane/termên koordîne) on input. */
onSubInput: function (list, input, rows, narrow, modifiers) {
var val = $.trim($(input).val());
var item = $(input).parent();
var isLast = item.is(list.children("li:last-child"));
if (val && isLast) {
var next = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-sub-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", rows)
.attr("class", "wiktku-dfn-subfield" + (narrow ? " wiktku-start-narrow" : ""));
next.on("input", function () {
steps.dfn.onSubInput(list, this, rows, narrow, modifiers);
});
list.append(modifiers ? makeModifierLi(next) : $("<li></li>").append(next));
// New field starts with no placeholder — give it (and any
// others in the list) the current one.
var updatePlaceholders = list.data("updatePlaceholders");
if (typeof updatePlaceholders === "function") updatePlaceholders();
}
},
// Populated from data-main.json (tplDefs, jederDefs).
tplDefs: {},
jederDefs: {},
build: function (stepDiv) {
if (stepDiv.find("ol.wiktku-dfn-list").length) return null;
var tabsDiv = $("<div class='wiktku-dfn-sense-tabs'></div>");
stepDiv.append(tabsDiv);
var list = $("<ol class='wiktku-dfn-list'></ol>");
list.append(steps.dfn.buildSenseBlock(stepDiv));
stepDiv.append(list);
steps.dfn.renderSenseTabs(stepDiv);
return null;
},
/** Reads all sense blocks from the DOM and returns an array of sense objects. */
getSenses: function (stepDiv) {
var senses = [];
stepDiv.find("li.wiktku-dfn-sense").each(function () {
var block = $(this);
var tplSel = block.find("select.wiktku-dfn-tpl-select");
var tpl = tplSel.val() || "";
// noLang: template already encodes the language — don't add |lang| param.
var tplNoLang = tpl
? tplSel.find("option[value='" + tpl + "']").attr("data-nolang") === "1"
: false;
var tplTitle = $.trim(block.find("input.wiktku-dfn-tpl-title").val());
// The mane box and the îzah box are one gloss on the page: "tercime {{îzah|…}}". Joined
// here so everything downstream (getOutput, the linkify pass, the cache) keeps seeing a
// single string, exactly as it did when the pair was two boxes.
var mane = $.trim(block.find("textarea.wiktku-dfn-mane").val());
var izah = $.trim(block.find("textarea.wiktku-dfn-izah").val());
if (izah) {
mane = $.trim(mane + " {{îzah|" + izah + "}}");
}
function readList(cls) {
var vals = [];
// Matches only the canonical field, not the visible word-input mirror makeModifierLi() adds alongside it.
block.find("." + cls + "-list textarea.wiktku-dfn-subfield").each(function () {
var v = $.trim($(this).val());
if (v) vals.push(v);
});
return vals;
}
// Nimûne items are compound: { text, tr?, t? }
function readNimuneList() {
var items = [];
block.find(".wiktku-dfn-nimune-list li.wiktku-dfn-nimune-li").each(function () {
var text = $.trim($(this).find(".wiktku-dfn-nimune-text").val());
if (!text) return;
var obj = { text: text };
var tr = $.trim($(this).find(".wiktku-dfn-nimune-tr").val());
var t = $.trim($(this).find(".wiktku-dfn-nimune-t").val());
if (tr) obj.tr = tr;
if (t) obj.t = t;
items.push(obj);
});
return items;
}
// Jêder is a growing list of citations, one per li — mirrors readNimuneList().
function readJederList() {
var items = [];
block.find(".wiktku-dfn-jeder-list li.wiktku-dfn-jeder-li").each(function () {
var li = $(this);
var quote = $.trim(li.find(".wiktku-dfn-nimune-quote").val());
if (!quote) return;
var obj = { quote: quote };
var qtr = $.trim(li.find(".wiktku-dfn-nimune-quote-tr").val());
var qt = $.trim(li.find(".wiktku-dfn-nimune-quote-t").val());
if (qtr) obj.quoteTr = qtr;
if (qt) obj.quoteT = qt;
var tplName = li.find(".wiktku-dfn-quote-src-sel").val();
var url = tplName === "__raw__" || tplName === "__category__" ? ""
: $.trim(li.find(".wiktku-dfn-quote-url").val());
if (url) obj.url = url;
if (tplName === "__manual__") {
var manual = $.trim(li.find(".wiktku-dfn-quote-src-manual").val());
if (manual) obj.jederManual = manual;
} else if (tplName === "__raw__") {
// The quote box already holds a finished citation template; it is written
// out untouched, so there is nothing else on this row to read.
obj.raw = true;
} else if (tplName === "__category__") {
// A picked Kategorî:Çavkanî template IS the citation template, with its own
// TemplateData parameters -- rendered into the same markup the jêder-* branch
// below uses, so one loop reads both.
var catName = li.data("catTemplate");
if (catName) {
obj.jederCatTpl = catName;
var catFields = {};
li.find(".wiktku-dfn-quote-src-input").each(function () {
var ck = $(this).attr("data-field-key");
var cv = $.trim($(this).val());
if (ck && cv) catFields[ck] = cv;
});
// A restore in progress has no boxes yet -- its TemplateData is still on
// the way -- so the values it is restoring stand in until they exist.
// See restoreDfn, which parks them here and clears them once they land.
if (!Object.keys(catFields).length) {
catFields = li.data("catFields") || {};
}
obj.jederCatFields = catFields;
var catLang = li.data("catLangParam");
if (catLang) obj.jederCatLangParam = catLang;
}
} else if (tplName) {
obj.jederTpl = tplName;
var fields = {};
li.find(".wiktku-dfn-quote-src-input").each(function () {
var k = $(this).attr("data-field-key");
var v = $.trim($(this).val());
if (k && v) fields[k] = v;
});
obj.jêderFields = wiktkuCite.withLinkAsUrl(tplName, fields, url);
}
items.push(obj);
});
return items;
}
// Dever/Şîwe, Etîket and Kategorî are chip fields (buildChipField),
// not textareas, so readList() doesn't reach them — each chip's
// value lives in data-value, scoped by the field's wrap class.
function readChipList(wrapCls) {
var vals = [];
block.find("." + wrapCls + "-wrap .wiktku-dfn-chipfield-chip").each(function () {
var v = $.trim($(this).attr("data-value") || "");
if (v) vals.push(v);
});
return vals;
}
var sense = {
tpl: tpl,
tplNoLang: tplNoLang,
tplTitle: tplTitle,
mane: mane,
maneid: $.trim(block.find("input.wiktku-dfn-maneid-input").val() || ""),
etiket: readChipList("wiktku-dfn-etiket"),
kategori: readChipList("wiktku-dfn-kategori"),
nimune: readNimuneList(),
jeder: readJederList(),
dever: readChipList("wiktku-dfn-dever")
};
// alternativ, hevmane, dijmane, koordine, jornav, jernav — one property each, named by
// senseGroups' "field" so getOutput() and restoreDfn() find them under the same names.
$.each(wordGroups(), function (_, g) {
sense[g.field] = readList(g.cls);
});
senses.push(sense);
});
return senses;
},
getOutput: function (values, callback) {
var stepDiv = $("#wiktku-start-dfn");
var senses = steps.dfn.getSenses(stepDiv);
var lang = values.lang;
// A sense counts as "filled" if it has a mane, a chosen template + title,
// or any other content (etîket/hevmane/nimûne/jêder…) worth keeping.
var hasMane = senses.some(function (s) {
return s.mane !== "" || (s.tpl && s.tplTitle) || senseHasExtraContent(s);
});
var defLines = []; // one entry per kept sense (goes to linkify)
var citeLines = []; // parallel — nimûne+jêder for that same sense
var izahLines = []; // parallel — raw îzah gloss for that same sense
// True only once some sense's *actual* dfn text (post tercime/îzah
// split) is non-empty.
var hasLinkableMane = false;
$.each(senses, function (_, sense) {
// Declared once here rather than with `var` in each branch
// below — the template-sense branch returns early, so only
// one of the two assignments ever actually runs, but both
// are lexically in this same function scope.
var idx, cite;
// — Template sense (e.g. {{guharto|ku|somepage}}) —
if (sense.tpl) {
if (!sense.tplTitle) return; // skip empty template senses
var tplManeidTpl = buildManeidTpl(sense, lang);
var tplKatTpl = buildSenseTpl(sense, "kategori", lang, "K");
var tplDeverTpl = buildDeverTpl(sense, lang);
var tplLabelTpl = buildEtiketTpl(sense, lang);
var tplCall = sense.tplNoLang
? "{{" + sense.tpl + "|" + sense.tplTitle + "}}"
: "{{" + sense.tpl + "|" + lang + "|" + sense.tplTitle + "}}";
idx = defLines.length;
cite = buildCite(sense, lang);
defLines.push("# " + (tplManeidTpl ? tplManeidTpl + " " : "") + (tplKatTpl ? tplKatTpl + " " : "") + (tplDeverTpl ? tplDeverTpl + " " : "") +
(tplLabelTpl ? tplLabelTpl + " " : "") + tplCall +
(cite ? "{{wiktku-cite-" + idx + "}}" : ""));
citeLines.push(cite);
izahLines.push(""); // template senses have no îzah
return;
}
// — Normal mane sense —
var dfn = sense.mane;
if (!dfn && !senseHasExtraContent(sense)) return; // truly empty
// Pull the îzah gloss (if any) out of the combined tercime+îzah
// string — it's spliced back in separately below, after its
// own linkify pass (see izahLines / the second linkifyText call).
var izahRaw = null;
var tercimeIzah = splitTercimeIzah(dfn);
if (tercimeIzah) {
dfn = tercimeIzah.tercime;
izahRaw = stripWikilinks(tercimeIzah.izah);
}
if (dfn) {
dfn = stripWikilinks(dfn);
// Sentence-case + trailing period are for ku definitions,
// which are written as full glosses.
if (lang === "ku") {
if (dfn[0] !== dfn[0].toUpperCase()) dfn = capitalize(dfn);
if (!dfn.match(/[.…?!)\]]$/)) dfn += ".";
}
// Real prose — this is what makes the sense worth running
// through the main linkify pass.
hasLinkableMane = true;
} else if (izahRaw === null) {
// No mane at all — this is just the "{{mane?|lang}}"
// placeholder, a bare template call with no plain words
// in it. Do NOT count it as linkable: sending nothing but
// bracketed templates through the main linkify pass hangs
// the linkify gadget (see the hasLinkableMane/hasIzah
// check below).
//
// Every language, not just ku: a sense kept for its jêgirtin or word lists
// alone still needs the marker, or the line goes out as a bare "# ".
dfn = "{{mane?|" + lang + "}}";
}
var maneidTpl = buildManeidTpl(sense, lang);
var katTpl = buildSenseTpl(sense, "kategori", lang, "K");
var deverTpl = buildDeverTpl(sense, lang);
var labelTpl = buildEtiketTpl(sense, lang);
var out = "# " + (maneidTpl ? maneidTpl + " " : "") + (katTpl ? katTpl + " " : "") + (deverTpl ? deverTpl + " " : "") + (labelTpl ? labelTpl + " " : "") + dfn;
idx = defLines.length;
var hasThisIzah = izahRaw !== null && $.trim(izahRaw) !== "";
// Bare {{...}} token — like {{wiktku-cite-N}}, this survives
// linkify's first pass untouched (it treats template calls as
// opaque), then gets swapped for {{îzah|…}} once the actual
// gloss text has been linkified in its own separate pass.
if (hasThisIzah) {
out += (dfn ? " " : "") + "{{wiktku-izah-" + idx + "}}";
}
function wordLine(tpl, words) {
if (!words.length) return "";
return "\n#: {{" + tpl + "|" + lang + "|" + words.join("|") + "}}";
}
// Template name and sense property both come from senseGroups, so this and
// DefSideBoxes write the same "#:" lines.
$.each(SENSE_GROUPS, function (_, g) {
out += wordLine(g.key, sense[g.field] || []);
});
cite = buildCite(sense, lang);
defLines.push(out + (cite ? "{{wiktku-cite-" + idx + "}}" : ""));
citeLines.push(cite);
izahLines.push(hasThisIzah ? izahRaw : "");
});
// Splice citeLines back into a linkified string, replacing each
// {{wiktku-cite-N}} token with its stored nimûne/jêder block.
function restoreCites(t) {
return t.replace(/\{\{wiktku-cite-(\d+)\}\}/g, function (_, n) {
return citeLines[parseInt(n, 10)] || "";
});
}
// Runs text through the linkify gadget if it's loaded, otherwise
// just passes it through unchanged. Shared by both linkify passes
// below (main mane text, and — separately — îzah glosses).
var linkifyText = wiktkuUtil.linkifyText;
var hasIzah = izahLines.some(function (t) { return t && $.trim(t) !== ""; });
var text = defLines.join("\n") + "\n";
if (!hasMane) {
// Nothing written yet: the sense is still marked as needing one, but no example
// placeholder goes with it.
text = "# {{mane?|" + lang + "}}\n";
callback(text);
return;
}
// Runs the îzah-only linkify pass against `base` (either the raw
// text, when there's no linkable mane to run the main pass on
// first, or the already-linkified main text). Shared by both
// branches below so the token-splice logic only lives once.
function runIzahPass(base) {
var izahJoined = $.map(izahLines, function (raw, idx) {
if (!raw) return undefined;
return "{{wiktku-izah-open-" + idx + "}}" + raw + "{{wiktku-izah-close-" + idx + "}}";
}).join("\n");
linkifyText(izahJoined, function (izahLinked) {
var izahByIdx = {};
var re = /\{\{wiktku-izah-open-(\d+)\}\}([\s\S]*?)\{\{wiktku-izah-close-\1\}\}/g;
var m;
while ((m = re.exec(izahLinked)) !== null) {
izahByIdx[parseInt(m[1], 10)] = m[2];
}
var withIzah = base.replace(/\{\{wiktku-izah-(\d+)\}\}/g, function (_, n) {
var linked = izahByIdx[parseInt(n, 10)];
return linked !== undefined ? "{{îzah|" + linked + "}}" : "";
});
callback(restoreCites(withIzah));
});
}
if (!hasLinkableMane) {
if (!hasIzah) {
// All filled senses are template senses (or otherwise have
// no plain text at all) — nothing here needs linkify.
callback(restoreCites(text));
return;
}
// There's no plain mane text anywhere (e.g. tercime left blank
// with only îzah filled in), so every defLine is just a bare
// template call with nothing to word-link. Sending a line
// like "# {{wiktku-izah-0}}" through the main linkify pass
// gives it no plain words to work with, makes the linkify
// gadget hang — so skip that pass entirely and run only
// the îzah pass, splicing straight into the unlinkified text.
var loadIzahOnly = function () { runIzahPass(text); };
mw.loader.using("ext.gadget.linkify", loadIzahOnly, loadIzahOnly);
return;
}
var didTryToLoad = function () {
// First pass: the main definition text (mane), same as before. On failure the text
// comes back unchanged, so the save carries on with what the user wrote -- only the
// automatic linking is lost, and saying so is better than losing it silently.
linkifyText(text, function (mainLinked, error) {
if (error) {
mw.notify(error, { type: "warn" });
}
if (!hasIzah) {
callback(restoreCites(mainLinked));
return;
}
// Second pass: îzah glosses, linkified via their own
// separate call. Each gloss is wrapped in its own
// {{wiktku-izah-open/close-N}} pair; like the existing
// {{wiktku-cite-N}} tokens, these sit inside double
// braces so linkify leaves the tokens themselves alone —
// only the raw gloss text between each pair gets linked.
runIzahPass(mainLinked);
});
};
mw.loader.using("ext.gadget.linkify", didTryToLoad, didTryToLoad);
}
},
tewandin: {
id: "tewandin",
name: "tewandin",
question_fmt: 'Tewandina "' + pageName + '" binivîse',
visible: function (values) {
return values.lang && values.pos;
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-tewandin-wrap").length) return null;
var wrap = $("<div class='wiktku-tewandin-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-tewandin-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-tewandin-text")
.attr("rows", 4);
wrap.append(ta);
stepDiv.append(wrap);
function autoPopulate() {
if (!ta.length) return;
var vals = getValues();
var c = vals.cinsiyet || "";
var p = vals.pos || "";
if (!pageName.includes(" ") && c && GENDER_BASE_LABELS[c] &&
(p === "navdêr" || p === "serenav") && vals.lang === "ku" &&
c !== "mndudil") {
var label = (c === "mnmane") ? "mê û nêr" : GENDER_BASE_LABELS[c];
var out = "{{ku-tewîn-nav|" + label;
if (p === "serenav") out += "|y";
out += "}}";
if (!$.trim(ta.val())) ta.val(out);
}
}
// The example placeholder only fits Kurmancî nouns — showing it
// for a verb or another language would suggest the wrong template.
function updatePlaceholder() {
if (!ta.length) return;
var vals = getValues();
ta.attr("placeholder", (vals.lang === "ku" && vals.pos === "navdêr") ? "{{ku-tewîn-nav|nêr}}" : "");
}
setTimeout(function () {
autoPopulate();
updatePlaceholder();
$("#wiktku-start-form").on("change.tewandin",
"#wiktku-start-lang-field, #wiktku-start-cinsiyet-field, #wiktku-start-pos-field",
function () { autoPopulate(); updatePlaceholder(); });
}, 0);
return ta;
},
output: function () {
var manualText = $.trim($("#wiktku-start-tewandin .wiktku-tewandin-text").val());
if (manualText) {
return "\n==== Tewandin ====\n" + manualText + "\n";
}
return "";
}
},
// resim — one or more images
resim: {
id: "resim",
name: "resim",
question_fmt: "Resmekê lê zêde bike",
visible: function (values) { return !!(values.lang && values.pos); },
// The rows are wikt.edit's, shared with DefSideBoxes, which offers the same three boxes.
field: null,
build: function (stepDiv) {
if (stepDiv.find(".wiktku-resim-wrap").length) return null;
steps.resim.field = wiktkuUtil.buildImageListField({
prefix: "wiktku-",
rowClass: "wiktku-posparams-row",
inputClass: "wiktku-start-step-field wiktku-start-narrow",
pageName: pageName
});
stepDiv.append($("<div class='wiktku-resim-wrap'></div>").append(steps.resim.field.$element));
return null;
},
/** {file, caption, alt} per filled row, with the caption wrapped for a non-Kurdish entry. */
getImages: function (values) {
var lang = (values && values.lang) || getValues().lang || "";
if (!steps.resim.field) return [];
return $.map(steps.resim.field.values(), function (image) {
return {
file: image.file,
caption: wiktkuWikitext.captionForLang(image.caption, lang),
alt: image.alt,
size: image.size,
extra: image.extra
};
});
},
output: function (values) {
var images = steps.resim.getImages(values);
if (!images.length) return "";
return $.grep($.map(images, wiktkuWikitext.buildImageLine), Boolean).join("\n") + "\n";
}
},
// h-bnr — {{h-bnr|Page1|Page2|…}} prepended before the lang section (shared, not per-section)
bnrh: {
id: "bnrh",
name: "herwiha binêre (h-bnr)",
question_fmt: "Şablona h-bnr li qismê herî jor a rûpelê",
//visible: function (values) { return !!values.lang; },
multiple: true,
narrow: true,
placeholder: "Ji bo 'test' bnr 'Test', 'teşt'",
output: function (values) {
var call = wiktkuWikitext.buildHbnrCall(values.bnrh);
return call ? call + "\n" : "";
}
},
bikaranin: {
id: "bikaranin",
name: "Notên bikaranînê",
question_fmt: "Notên bikaranînê yên li ser vê kelîmeyê",
visible: function (values) {
return values.lang && values.pos;
},
multiple: false,
narrow: false,
output: function (values) {
var text = $.trim(values.bikaranin || "");
if (!text) return "";
return "\n==== Notên bikaranînê ====\n" + text + "\n";
},
},
xwendin: {
id: "xwendin",
name: "Bêhtir bixwîne",
question_fmt: "Lînkên derveyî eleqedarî vê madeyê",
visible: function (values) {
return values.lang && values.pos;
},
multiple: false,
narrow: false,
placeholder: function (values) {
if (!values || !values.lang) {
return `* {{wpedî}} ji bo ku meqaleya Wîkîpediyayê lînk bidî`;
}
const ziman = values.lang === "ku" ? "" : `|ziman=${values.lang}`;
return `* {{wpedî${ziman}}} ji bo ku meqaleya Wîkîpediyayê lînk bidî`;
},
output: function (values) {
var text = $.trim(values.xwendin || "");
if (!text) return "";
return "\n==== Bêhtir bixwîne ====\n" + text + "\n";
},
},
alfabeyendin: {
id: "alfabeyendin",
name: "Bi alfabeyên din",
question_fmt: function (values) {
var base = "Nivîsîna vê kelîmeyê bi alfabeyên din"
var langLabel = values.lang ? getLangLabel(values.lang) : "";
if (langLabel) {
return base + " ên " + langLabel;
}
return base;
},
// Latin-script Kurdish itself, and languages with only one script
// on record, don't need this — but any language metadata.json
// lists more than one script for (data-main.json's "sc") clearly does.
visible: function (values) {
if (!(values.lang && values.pos)) return false;
var lang = values.lang;
return lang === "ku" || ZARAVA_LANG_CODES.indexOf(lang) !== -1 || langHasMultipleScripts(lang);
},
multiple: false,
narrow: false,
placeholder: "Ji bo kurmancî: * {{ku-ar|şiklê alfaba erebî}}",
output: function (values) {
var text = $.trim(values.alfabeyendin || "");
if (!text) return "";
return "\n==== Bi alfabeyên din ====\n" + text + "\n";
},
},
formalt: makeWordListStep("formalt", "Formên alternatîv ên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
hevmane: makeWordListStep("hevmane", "Hevmaneyên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
dijmane: makeWordListStep("dijmane", "Dijmaneyên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
jornav: makeWordListStep("jornav", "Jornavên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
jernav: makeWordListStep("jernav", "Jêrnavên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
je: makeWordListStep("je", "Kelîmeyên jê çêbûyî binivîse"),
tekildar: makeWordListStep("tekildar", "Kelîmeyên ji aliyê etîmolojiyê ve têkildarî vê kelîmeyê binivîse"),
binereherwiha: makeWordListStep("binereherwiha", "Kelîmeyên din ên eleqedar binivîse"),
etym: {
id: "etym",
name: "etîmolojî",
question_fmt: "Etîmolojiya kelîmeyê binivîse",
// etymTpls populated from data-main.json
etymTpls: {},
visible: function (values) {
return values.lang && values.pos;
},
// Fills the insert-dropdown from the current etymTpls object.
refreshTplSelect: function () {
var sel = $("#wiktku-start-etym .wiktku-tpl-insert-select");
if (!sel.length) return;
sel.find("option[value!='']").remove();
$.each(steps.etym.etymTpls, function (name, tpl) {
sel.append($("<option></option>").val(tpl.snippet).text(name + " — " + tpl.desc));
});
markTemplateOptions(sel, function (opt) { return tplNameFromSnippet(opt.val()); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-etym-wrap").length) return null;
var wrap = $("<div class='wiktku-etym-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-etym-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-etym-text")
.attr("rows", 4)
.attr("placeholder", "Ji {{ji|en|ku|apple}}. yan rasterast binivîse…");
// Template insert dropdown — options also refreshed after data-main.json loads.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
$.each(steps.etym.etymTpls, function (name, tpl) {
sel.append($("<option></option>").val(tpl.snippet).text(name + " — " + tpl.desc));
});
sel.on("change", function () {
var snippet = $(this).val();
if (!snippet) return;
var lang = getValues().lang || "";
snippet = snippet.replace(/CODE/g, lang).replace(/SOURCELANG/g, "?");
$(this).val("");
sel.trigger("wiktku:refresh");
// Same param dialog DefSideBoxes' own Etîmolojî editor opens on a fresh insert: filled
// from the template's own TemplateData, with CODE/SOURCELANG already substituted above
// as the fields' starting values, so the user only has to replace the "?" placeholders
// rather than hunt for them in raw wikitext afterward.
wiktkuCite.openTemplateDialog(snippet).then(function (edited) {
if (!edited) return; // cancelled, or "Jê bibe" on a call that was never inserted
insertAtCursor(ta[0], edited);
ta.focus();
});
});
wrap.append(sel);
wrap.append(ta);
stepDiv.append(wrap);
initSelects(wrap);
return ta;
},
output: function (values) {
var text = $.trim($("#wiktku-start-etym .wiktku-etym-text").val());
if (text) {
return "\n==== Etîmolojî ====\n" + text + "\n";
}
// {{ji?}} (a redirect to {{bêetîmolojî}}) is only ever added automatically to Kurdish
// entries.
if (values.lang !== "ku") return "";
// Multi-word page names skip the {{ji?|lang}} placeholder entirely -- "source unknown?" doesn't apply to a phrase.
if (pageName.includes(" ")) return "";
// No etymology entered — emit {{ji?|lang}} as a placeholder.
// Skip it only when every filled sense is a şablon sense
// a completely empty entry with no şablon chosen still gets the placeholder
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasFilledTplSense = dfnSenses.some(function (s) {
return !!s.tpl && !!s.tplTitle;
});
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
if (hasFilledTplSense && !hasRegularMane) return "";
return "\n==== Etîmolojî ====\n{{ji?|" + values.lang + "}}\n";
} },
dunde: {
id: "dunde",
name: "dûnde",
question_fmt: "Eger ev kelîme ji vî zimanî ketibe zimanekî din, vî zimanî û kelîmeya wî binivîse",
visible: function (values) {
return !!values.pos;
},
disabledKeys: function (values) {
return {"navz": true, [values.lang]: true}
},
// Language dropdown + growing word-input list; a new row appends
// when the dropdown gets a value. Mirrors steps.trans, but flat.
buildRow: function (stepDiv) {
var row = $("<li class='wiktku-dunde-row wiktku-lang-row'></li>");
// Language selector — the searchable picker is applied after insertion into DOM.
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zimanekî bigere");
populateMenus(langSel, steps.dunde.keys || [], steps.dunde.disabledKeys);
// When a language is chosen, ensure a next empty row exists.
langSel.on("change", function () {
steps.dunde.onLangChange(stepDiv, row);
});
// Word list for this language.
var wordList = $("<ul class='wiktku-dunde-words wiktku-lang-row-words'></ul>");
var firstWord = steps.dunde.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
// The searchable picker is attached by initSelects() once the row
// is in the DOM (see onLangChange/build below).
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-dunde-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Kelîme…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-dunde-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.dunde.makeWordInput();
list.append($("<li></li>").append(next));
}
});
return input;
},
onLangChange: function (stepDiv, row) {
var list = stepDiv.find("ul.wiktku-dunde-list");
var isLast = row.is(list.children("li.wiktku-dunde-row:last-child"));
if (isLast) {
var newRow = steps.dunde.buildRow(stepDiv);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
// Keep disabled keys in sync across all lang selectors.
steps.dunde.syncDisabledKeys(stepDiv);
},
/** Disables already-selected languages in all other dropdowns. */
syncDisabledKeys: function (stepDiv) {
var taken = $.extend({}, steps.dunde.disabledKeys);
stepDiv.find("li.wiktku-dunde-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
stepDiv.find("li.wiktku-dunde-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
build: function (stepDiv) {
if (stepDiv.find("ul.wiktku-dunde-list").length) return null;
var list = $("<ul class='wiktku-dunde-list wiktku-lang-row-list'></ul>");
list.append(steps.dunde.buildRow(stepDiv));
stepDiv.append(list);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return null;
},
/** Reads all [langcode, words[]] rows from the DOM. */
getRows: function (stepDiv) {
var result = [];
stepDiv.find("li.wiktku-dunde-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-dunde-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
output: function () {
var stepDiv = $("#wiktku-start-dunde");
var rows = steps.dunde.getRows(stepDiv);
if (!rows.length) return "";
var lines = $.grep($.map(rows, function (row) {
return wiktkuWikitext.buildDundeLine(row[0], row[1]);
}), Boolean);
return "\n==== Dûnde ====\n" + lines.join("\n") + "\n";
}
},
zarava: {
id: "zarava",
name: "bi zaravayên din",
question_fmt: "'"+ pageName + "' di zaravayên din de çawa tê gotin?",
visible: function (values) {
return steps.trans.visible(values);
},
disabledKeys: {},
buildRow: function (group) {
var row = $("<li class='wiktku-zarava-row wiktku-lang-row'></li>");
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zaravayekî bigere");
populateMenus(langSel, steps.zarava.keys || [], steps.zarava.disabledKeys);
langSel.on("change", function () {
steps.zarava.onLangChange(group, row);
refresh();
});
var wordList = $("<ul class='wiktku-zarava-words wiktku-lang-row-words'></ul>");
var firstWord = steps.zarava.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-zarava-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Tercime…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-zarava-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.zarava.makeWordInput();
list.append($("<li></li>").append(next));
}
});
// Blur, not input -- refresh() re-syncs the whole form, same
// reasoning as the Mane textareas' blur-only refresh below.
input.on("blur", function () { refresh(); });
return input;
},
onLangChange: function (group, row) {
var list = group.find("ul.wiktku-zarava-list");
var isLast = row.is(list.children("li.wiktku-zarava-row:last-child"));
if (isLast) {
var newRow = steps.zarava.buildRow(group);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
steps.zarava.syncDisabledKeys(group);
},
/** Disables already-selected dialects in all other dropdowns within one group. */
syncDisabledKeys: function (group) {
var taken = $.extend({}, steps.zarava.disabledKeys);
group.find("li.wiktku-zarava-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
group.find("li.wiktku-zarava-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
/** Builds (or fetches) the .wiktku-zarava-group for one gloss. */
buildGroup: function (stepDiv, gi) {
var group = $("<div class='wiktku-zarava-group'></div>").attr("data-gloss-pos", gi);
var list = $("<ul class='wiktku-zarava-list wiktku-lang-row-list'></ul>");
list.append(steps.zarava.buildRow(group));
group.append(list);
stepDiv.append(group);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return group;
},
/** Syncs one .wiktku-zarava-group per current gloss (same logic as steps.trans.build). */
build: function (stepDiv) {
var glosses = steps.trans.glossList();
if (!glosses.length) {
stepDiv.find(".wiktku-zarava-group").remove();
return null;
}
var existingGroups = stepDiv.find(".wiktku-zarava-group");
if (existingGroups.length > glosses.length) {
existingGroups.slice(glosses.length).remove();
}
$.each(glosses, function (gi, gloss) {
var group = stepDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
if (!group.length) group = steps.zarava.buildGroup(stepDiv, gi);
var label = group.find(".wiktku-zarava-group-label");
if (glosses.length > 1) {
if (!label.length) {
label = $("<div class='wiktku-zarava-group-label'></div>");
group.prepend(label);
}
label.text(gloss.index + " " + gloss.text);
} else if (label.length) {
label.remove();
}
});
return null;
},
/** Reads the dialect rows for a single gloss group. */
getGroupTranslations: function (group) {
var result = [];
group.find("li.wiktku-zarava-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-zarava-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
/** Reads all rows from the DOM, grouped by gloss position. */
getTranslations: function (stepDiv) {
var result = [];
stepDiv.find(".wiktku-zarava-group").each(function () {
result.push(steps.zarava.getGroupTranslations($(this)));
});
return result;
},
output: function (values) {
if (values.lang !== "ku") return "";
var stepDiv = $("#wiktku-start-zarava");
var glosses = steps.trans.glossList();
if (!glosses.length) return "";
// A completely empty entry still gets a placeholder section
// to fill in later; an individual placeholder gloss is only
// skipped once some other sense already has real content.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
var blocks = $.map(glosses, function (gloss, gi) {
var group = stepDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
var rows = group.length ? steps.zarava.getGroupTranslations(group) : [];
if (!gloss.text && !rows.length && hasRegularMane) return undefined;
if (rows.length) {
rows.sort(function (a, b) {
var aLang = langsByCode[a[0]] || {};
var bLang = langsByCode[b[0]] || {};
return (aLang.sortKey || "").localeCompare(bLang.sortKey || "", "ku");
});
}
var wordLines = $.map(rows, function (row) {
var lang = row[0];
var words = row[1];
var wikiWords = words.map(function (w) {
return "{{W|" + lang + "|" + w + "}}";
}).join(", ");
return "* {{Z|" + lang + "}}: " + wikiWords;
}).join("\n");
var block = "{{werger-ser|" + (gloss.text || "?") + "|" + gloss.index + "}}";
if (wordLines) block += "\n" + wordLines;
block += "\n{{werger-bin}}";
return block;
});
if (!blocks.length) return "";
return "\n==== Bi zaravayên din ====\n" + blocks.join("\n") + "\n";
}
},
trans: {
id: "trans",
name: "werger",
question_fmt: "Tercimeyên vê maneyê bi zimanên din binivîse",
visible: function (values) {
if (!(values.lang === "ku" && values.pos)) return false;
// Hide only when every sense is a filled template sense (no plain
// gloss to attach translations to). A mix, or nothing filled in
// yet, still shows Werger.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRealSense = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
var hasFilledTplSense = dfnSenses.some(function (s) {
return s.tpl && s.tplTitle;
});
return hasRealSense || !hasFilledTplSense;
},
disabledKeys: {
"navz": true,
"ku": true,
"ckb": true,
"sdh": true,
"lki": true,
"hac": true,
"zza": true },
// Language dropdown + growing word-input list, scoped to one gloss's
// group (each gloss has its own group and row list).
buildRow: function (group) {
var row = $("<li class='wiktku-trans-row wiktku-lang-row'></li>");
// Language selector — the searchable picker is applied after insertion into DOM.
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zimanekî bigere");
populateMenus(langSel, steps.trans.keys || [], steps.trans.disabledKeys);
// When a language is chosen, ensure a next empty row exists.
langSel.on("change", function () {
steps.trans.onLangChange(group, row);
refresh();
});
// Word list for this language.
var wordList = $("<ul class='wiktku-trans-words wiktku-lang-row-words'></ul>");
var firstWord = steps.trans.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
// The searchable picker is attached by initSelects() once the row
// is in the DOM (see onLangChange/buildGroup below).
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-trans-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Tercime…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-trans-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.trans.makeWordInput();
list.append($("<li></li>").append(next));
}
});
// Blur, not input -- refresh() re-syncs the whole form, same
// reasoning as the Mane textareas' blur-only refresh below.
input.on("blur", function () { refresh(); });
return input;
},
onLangChange: function (group, row) {
var list = group.find("ul.wiktku-trans-list");
var isLast = row.is(list.children("li.wiktku-trans-row:last-child"));
if (isLast) {
var newRow = steps.trans.buildRow(group);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
// Keep disabled keys in sync across this gloss's lang selectors
// only — the same language can be used for different senses.
steps.trans.syncDisabledKeys(group);
},
/** Disables already-selected languages in all other dropdowns within one group. */
syncDisabledKeys: function (group) {
var taken = $.extend({}, steps.trans.disabledKeys);
group.find("li.wiktku-trans-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
group.find("li.wiktku-trans-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
// Builds (or fetches) the .wiktku-trans-group for one gloss.
buildGroup: function (stepDiv, gi) {
var group = $("<div class='wiktku-trans-group'></div>").attr("data-gloss-pos", gi);
var list = $("<ul class='wiktku-trans-list wiktku-lang-row-list'></ul>");
list.append(steps.trans.buildRow(group));
group.append(list);
stepDiv.append(group);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return group;
},
// Syncs one .wiktku-trans-group per current gloss, keyed by
// data-gloss-pos so groups stay matched across rebuilds.
build: function (stepDiv) {
var glosses = steps.trans.glossList();
if (!glosses.length) {
stepDiv.find(".wiktku-trans-group").remove();
return null;
}
// Drop groups for glosses that no longer exist (always from the
// tail — groups are only ever appended in gloss order below).
var existingGroups = stepDiv.find(".wiktku-trans-group");
if (existingGroups.length > glosses.length) {
existingGroups.slice(glosses.length).remove();
}
$.each(glosses, function (gi, gloss) {
var group = stepDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
if (!group.length) group = steps.trans.buildGroup(stepDiv, gi);
// Only label groups when there's more than one gloss to
// distinguish between — a single gloss needs no caption.
var label = group.find(".wiktku-trans-group-label");
if (glosses.length > 1) {
if (!label.length) {
label = $("<div class='wiktku-trans-group-label'></div>");
group.prepend(label);
}
label.text(gloss.index + " " + gloss.text);
} else if (label.length) {
label.remove();
}
});
return null;
},
/** Reads the translation rows for a single gloss group. */
getGroupTranslations: function (group) {
var result = [];
group.find("li.wiktku-trans-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-trans-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
// Reads all translation rows, grouped by gloss position.
getTranslations: function (stepDiv) {
var result = [];
stepDiv.find(".wiktku-trans-group").each(function () {
result.push(steps.trans.getGroupTranslations($(this)));
});
return result;
},
// Builds the numbered "{ index, gloss }" list, one per dfn sense
// with a real mane (skips template senses). index is 1-based and
// matches the sense's position in the "#" definition list.
// When nothing's filled in, returns one placeholder entry so
// Werger still has a group to attach to; when every sense is a
// filled template sense, stays empty (matches trans.visible()).
glossList: function () {
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var glosses = [];
var hasFilledTplSense = false;
$.each(dfnSenses, function (i, s) {
if (s.tpl) {
if (s.tplTitle) hasFilledTplSense = true;
return; // template sense — no gloss to caption with
}
var dfn = s.mane;
if (!dfn) {
// No mane, but other fields are filled — still gets a
// gloss group since dfn.getOutput() emits {{mane?}} for it.
if (senseHasExtraContent(s)) glosses.push({ index: i + 1, text: "" });
return;
}
if (dfn[0] !== dfn[0].toLowerCase()) dfn = decapitalize(dfn);
dfn = stripWikilinks(dfn).replace(/[.…?!)\]،؟]+$/, "");
glosses.push({ index: i + 1, text: dfn });
});
if (!glosses.length && !hasFilledTplSense) {
glosses.push({ index: 1, text: "" });
}
return glosses;
},
output: function (values) {
if (values.lang !== "ku") return "";
var stepDiv = $("#wiktku-start-trans");
var glosses = steps.trans.glossList();
// Every sense is a template sense — there's no real gloss
// anywhere and no group in the DOM either (glossList() only
// returns empty in that case), so skip the section entirely.
if (!glosses.length) return "";
// A completely empty entry (nothing filled in dfn at all, no
// şablon chosen either) still gets a placeholder Werger section
// to fill in later. Only
// skip an individual placeholder gloss below when some other
// sense actually has real content, making that particular
// placeholder redundant.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
// Each gloss now has its own group of translation rows —
// attach every gloss's own words under its own werger-ser/-bin
// pair instead of dumping everything under the first one.
var blocks = $.map(glosses, function (gloss, gi) {
var group = stepDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
var rows = group.length ? steps.trans.getGroupTranslations(group) : [];
// Placeholder gloss (no real mane entered yet) with nothing
// translated into it. If some other sense already has real
// content, this placeholder is redundant — skip it. If the
// whole entry is empty, keep it so Werger still gets a
// section to fill in.
if (!gloss.text && !rows.length && hasRegularMane) return undefined;
if (rows.length) {
// Sort by Kurdish sort key.
rows.sort(function (a, b) {
var aLang = langsByCode[a[0]] || {};
var bLang = langsByCode[b[0]] || {};
return (aLang.sortKey || "").localeCompare(bLang.sortKey || "", "ku");
});
}
var wordLines = $.map(rows, function (row) {
var lang = row[0];
var words = row[1];
var wikiWords = words.map(function (w) {
return "{{W|" + lang + "|" + w + "}}";
}).join(", ");
return "* {{Z|" + lang + "}}: " + wikiWords;
}).join("\n");
var block = "{{werger-ser|" + (gloss.text || "?") + "|" + gloss.index + "}}";
if (wordLines) block += "\n" + wordLines;
block += "\n{{werger-bin}}";
return block;
});
if (!blocks.length) return "";
return "\n==== Werger ====\n" + blocks.join("\n") + "\n";
}
},
cavkani: {
id: "cavkani",
name: "çavkanî",
question_fmt: "Çavkaniyên din (ne jêgirtin) lê zêde bike",
visible: function (values) {
return !!values.pos;
},
// Fills the insert-dropdown from the members of [[Kategorî:Çavkanî]].
refreshTplSelect: function () {
var sel = $("#wiktku-start-cavkani .wiktku-tpl-insert-select");
if (!sel.length) return;
sel.find("option[value!='']").remove();
$.each(catSrcTemplates || [], function (_, entry) {
sel.append($("<option></option>").val(entry.target).text(entry.label));
});
markTemplateOptions(sel, function (opt) { return opt.val(); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-cavkani-wrap").length) return null;
var wrap = $("<div class='wiktku-cavkani-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-cavkani-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-cavkani-text")
.attr("rows", 4)
.attr("placeholder", wiktkuUtil.msg.pickOrTypeTemplate);
// Template insert dropdown, populated from [[Kategorî:Çavkanî]] —
// options refreshed once the category members are fetched.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
var status = $("<span class='wiktku-cavkani-status'></span>");
// Same param dialog DefSideBoxes' own Çavkanî picker opens: the chosen template's
// TemplateData becomes a form, so "rûpel=", "sernav=" and the rest are filled in before
// anything lands in the box, instead of a bare {{Ç:…}} to go back and complete by hand.
sel.on("change", function () {
var name = $(this).val();
$(this).val("");
sel.trigger("wiktku:refresh");
if (!name) return;
wiktkuCite.openTemplateDialog("{{" + name + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or removed a call that was never inserted
}
insertAtCursor(ta[0], "* " + edited + "\n");
ta.trigger("focus");
});
});
wrap.append(sel);
wrap.append(status).append(ta);
stepDiv.append(wrap);
// build() runs again for every new beş (see the "+ Beşeke din"
// handler), after the initial page-wide pass — so this select needs
// its own init or it stays a plain, unsearchable native <select>.
initSelects(wrap);
if (catSrcTemplates) {
steps.cavkani.refreshTplSelect();
} else {
status.text(wiktkuUtil.msg.templatesLoading);
fetchCatSrcTemplates().then(function () {
steps.cavkani.refreshTplSelect();
status.text("");
}, function () {
status.text(wiktkuUtil.msg.templatesFailed);
});
}
return ta;
},
output: function () {
var text = $.trim($("#wiktku-start-cavkani .wiktku-cavkani-text").val());
if (!text) return "";
return "\n==== Çavkanî ====\n" + text + "\n";
}
},
};
steps.trans.keys = steps.lang.choices;
steps.dunde.keys = steps.lang.choices;
steps.zarava.keys = getZaravaKeys(steps.lang.choices);
// This array only controls DOM build order — nothing else keys off it
// (see the comment above stepIdToGroup). Mane tab order: Mane (senses,
// with malûmatên pêşketî nested inside each, see steps.dfn.build) →
// Wêne → Bi zaravayên din → Werger, so dfn/resim/zarava/trans sit
// adjacent to each other in that order.
var ordered_steps = [
steps.lang, steps.pos, steps.posparams,
steps.cinsiyet, steps.jimar, steps.lektur, steps.tr,
// Reading order: the senses, then what belongs to the entry as a whole. Etîmolojî follows
// "Bi alfabeyên din" and keeps a permanent slot; dunde and the three "eleqedar" lists are
// reached through the Beşên din picker like every other optional section.
steps.dfn, steps.tewandin, steps.alfabeyendin, steps.etym, steps.zarava, steps.trans,
steps.dunde, steps.je, steps.tekildar, steps.binereherwiha,
// Bilêvkirin isn't part of any STEP_GROUPS tab, but still needs a
// slot here because this array is what builds the DOM.
steps.pron,
// Beşên din fields — behind the picker.
steps.resim,
steps.bnrh, steps.bikaranin, steps.formalt, steps.hevmane, steps.dijmane, steps.jornav, steps.jernav,
steps.cavkani, steps.xwendin];
// ── Sections ─────────────────────────────────────────────────────────────
// One group, no tab bar. What is always on screen: the senses (whose own blocks carry Nimûne,
// Jêgirtin, Hevmane and Dijmane — see steps.dfn.allGroups), Tewandin, alfabeyendin, zarava, trans
// and Etîmolojî. Everything else is added from the "Beşên din" picker, which sits at the top of the
// form; a newly-picked section is inserted directly under it (see renderBesenDinPicker()), so the
// most recently added one is always the one nearest the picker.
//
// Tewandin keeps its own free-text {{ku-tewîn-nav}} field and does NOT absorb the Cins/zayend form
// parameters (m=/n=/p=), which stay on the headword template.
// Behind the "Beşên din" picker: shown only after an explicit open, the way DefSideBoxes' own
// "+ Beşek lê zêde bike" works -- nothing is on screen until it is asked for.
var BESENDIN_STEP_IDS = ["resim", "formalt", "hevmane", "dijmane", "jornav", "jernav", "bikaranin",
"cavkani", "xwendin", "bnrh", "dunde", "je", "tekildar", "binereherwiha"];
// One group, so no tab bar: the senses, the fields that go with them, and Etîmolojî, which is common
// enough to keep a permanent slot. Everything else is reached through the picker above.
var STEP_GROUPS = [
{ id: "mane", label: "Mane", steps: ["dfn", "tewandin", "alfabeyendin", "zarava", "trans", "etym"] }
];
// Step id -> index into STEP_GROUPS; undefined for the always-visible ungrouped steps.
var stepIdToGroup = {};
$.each(STEP_GROUPS, function (gi, g) {
$.each(g.steps, function (_, sid) { stepIdToGroup[sid] = gi; });
});
// Step ids that belong to each section (not shared across sections).
var PER_SECTION_STEP_IDS = [
"pos", "posparams", "cinsiyet", "jimar", "lektur", "tr",
"resim", "dfn", "tewandin", "formalt", "bikaranin", "alfabeyendin", "hevmane", "dijmane", "jornav", "jernav", "je", "tekildar", "etym", "dunde", "binereherwiha", "zarava", "trans", "cavkani", "xwendin"
];
// ── Multi-section state ──────────────────────────────────────────────────
// Each entry is a plain snapshot: { pos, cinsiyet, lektur, tr, dfn (senses[]),
// je, tekildar, binereherwiha, trans (rows[]) }.
var sections = [{}]; // section 0 starts empty (DOM is the truth)
var activeSection = 0;
// ── Bilêvkirin as a standalone section ───────────────────────────────
// Pronunciation belongs to the word, not to one part of speech, so it
// sits beside Navdêr/Lêker in the tab bar rather than inside any of them.
// Modelled as a flag rather than an entry in `sections` since
// activeSection is used as an array index in many places — viewing
// Bilêvkirin leaves activeSection pointing at the POS section you were on.
var viewingPron = false; // is it the tab currently on screen
// Which STEP_GROUPS tab is currently shown. Lives out here (like
// activeSection above) so refresh() — which is not nested inside build()
// — can read it and so renderGroupTabs()/updateGroupTabIndicators() below
// can be called from refresh() too. Both operate on the tab bar via its
// #wiktku-group-tabs id rather than a closured element reference, for the
// same reason.
var activeGroup = 0;
// Unsaved-changes / autosave state. Lives out here (same reason as
// activeGroup above) so destroy() — which runs on final submit and on
// switching to the source editor, and is not nested inside build() —
// can clear the timers and the beforeunload warning. build() assigns
// into these; it doesn't re-declare them.
var formDirty = false;
var autosaveDebounceTimer = null;
var autosaveInterval = null;
/** Re-renders the step-group tab bar to reflect the current activeGroup. */
// Scrolls the given tab bar to the top of the viewport, below the sticky offset, so a switched-to tab
// does not open off-screen. Deferred one frame: switching can shorten the page, and the browser may
// clamp the scroll position before the measurement runs.
function scrollTabBarIntoView(barId) {
window.requestAnimationFrame(function () {
var bar = document.getElementById(barId);
if (!bar) return;
var form = document.getElementById("wiktku-start-form");
var stabsH = form ? (parseFloat(form.style.getPropertyValue("--wiktku-stabs-h")) || 0) : 0;
var siteHeaderH = form ? (parseFloat(form.style.getPropertyValue("--wiktku-siteheader-h")) || 0) : 0;
var top = bar.getBoundingClientRect().top + window.pageYOffset - stabsH - siteHeaderH;
$("html, body").animate({ scrollTop: Math.max(0, top) }, 200);
});
}
function renderGroupTabs() {
var groupTabsDiv = $("#wiktku-group-tabs");
if (!groupTabsDiv.length) return;
// One group is nothing to switch between. Done here rather than only where the bar is built,
// because the form is rebuilt on language/part-of-speech changes and a bar left over from an
// earlier build would otherwise sit there until refresh() next got round to hiding it.
if (STEP_GROUPS.length < 2) {
groupTabsDiv.remove();
return;
}
groupTabsDiv.empty();
$.each(STEP_GROUPS, function (i, g) {
var tab = $("<span></span>")
.addClass("wiktku-group-tab")
.attr("data-group-idx", i)
.text(g.label)
.toggleClass("wiktku-tab-active", i === activeGroup);
(function (idx) {
tab.on("click", function () {
if (idx === activeGroup) return;
activeGroup = idx;
renderGroupTabs();
renderBesenDinPicker();
refresh();
scrollTabBarIntoView("wiktku-group-tabs");
});
}(i));
groupTabsDiv.append(tab);
});
}
/**
* Toggles a "needs attention" dot on any group tab that still has a
* required, unfilled field hidden inside it — so switching tabs doesn't
* leave the person wondering why "Bişîne" stays disabled.
*/
function updateGroupTabIndicators(values) {
var tabs = $("#wiktku-group-tabs .wiktku-group-tab");
if (!tabs.length) return;
$.each(STEP_GROUPS, function (i, g) {
var missing = false;
$.each(g.steps, function (_, sid) {
var step = steps[sid];
if (!step || !step.required) return;
var val = values[sid];
if (val === "" || (step.multiple && $.isArray(val) && val.join("") === "")) {
missing = true;
}
});
tabs.eq(i).toggleClass("wiktku-tab-missing", missing && i !== activeGroup);
});
}
// "Beşên din" reveals one field at a time via a picker rather than
// stacking them all. Per-section state, snapshotted in
// saveCurrentSection() and restored in restoreSection() below, and
// auto-seeded from any field that already has content (so a returning
// draft doesn't hide something already filled in).
var openedOtherSections = {};
var BESENDIN_SET = {};
$.each(BESENDIN_STEP_IDS, function (_, sid) { BESENDIN_SET[sid] = true; });
// The picker sits at the top of the form, above the senses, and is on screen whenever the form is.
/** Re-renders the "Beşên din" picker to list only the not-yet-opened sections. */
function renderBesenDinPicker() {
var wrap = $("#wiktku-besendin-picker");
if (!wrap.length) return;
wrap.empty();
var pickerValues = getValues();
// Every section it offers belongs to a part-of-speech section, so there is nothing to add until
// one is chosen. Checked here rather than only in refresh(): this function is also called on form
// build and on section changes, and each of those would otherwise show the picker on its own.
if (!pickerValues.pos) {
wrap.hide();
return;
}
var unopened = $.grep(BESENDIN_STEP_IDS, function (sid) {
if (openedOtherSections[sid]) return false;
// Don't offer a section that would open onto nothing: several of
// these are gated (Bi zaravayên din / Werger need lang=ku, most
// need a pos). refresh() would hide it again straight away, so
// picking it would read as a broken dropdown.
var step = steps[sid];
if (step && typeof step.visible === "function") {
try {
if (!step.visible(pickerValues)) return false;
} catch (e) { /* a step whose visible() needs DOM we lack yet — offer it */ }
}
return true;
});
if (!unopened.length) {
wrap.hide(); // everything's open — nothing left to pick
return;
}
// Same voice and styling as a step's own question line, not a section heading -- this picker sits
// between cards rather than being one.
wrap.append($("<label class='wiktku-start-step-question'></label>").text("Binebeşek lê zêde bike"));
var sel = $("<select class='wiktku-besendin-select'></select>");
sel.attr("data-placeholder", "— binebeşên din hilbijêre —");
sel.append($("<option value=''>— binebeşên din hilbijêre —</option>"));
$.each(unopened, function (_, sid) {
var step = steps[sid];
if (!step) return;
sel.append($("<option></option>").val(sid).text(capitalize(step.name)));
});
sel.on("change", function () {
var sid = $(this).val();
if (!sid) return;
openedOtherSections[sid] = true;
// Newly opened section moves right under the picker, ahead of
// whatever's already open, instead of wherever it happened to
// sit in the fixed step order.
$("#wiktku-start-" + sid).insertAfter(wrap);
renderBesenDinPicker();
refresh();
});
wrap.append(sel);
initSelects(wrap);
wrap.show();
}
/** Marks any BESENDIN_STEP_IDS field that already has real content as
* "opened", so restoring a draft doesn't bury filled-in sections behind
* the picker. Reuses each step's own output() as the emptiness check —
* it already knows what "empty" means per field, and it reads the DOM
* the restore just populated. Call this AFTER the DOM has the restored
* values in it. */
function seedOpenedFromContent() {
var values = getValues();
$.each(BESENDIN_STEP_IDS, function (_, sid) {
if (openedOtherSections[sid]) return;
var step = steps[sid];
if (!step || typeof step.output !== "function") return;
try {
if (step.output(values)) openedOtherSections[sid] = true;
} catch (e) { /* a step whose output() needs more state than we have yet — skip it */ }
});
}
function snapshotDfn() {
return steps.dfn.getSenses($("#wiktku-start-dfn"));
}
// Rebuilds the dfn DOM from a previously snapshotted senses array.
function restoreDfn(senses) {
var stepDiv = $("#wiktku-start-dfn");
// Tear down the existing tab bar + list so build() recreates them cleanly.
stepDiv.find(".wiktku-dfn-sense-tabs").remove();
stepDiv.find("ol.wiktku-dfn-list").remove();
steps.dfn.build(stepDiv);
var list = stepDiv.find("ol.wiktku-dfn-list");
$.each(senses, function (i, sense) {
// Ensure enough blocks exist (build() seeds one).
var blocks = list.find("li.wiktku-dfn-sense");
var block = blocks.eq(i);
if (!block.length) {
var nb = steps.dfn.buildSenseBlock(stepDiv);
list.append(nb);
block = list.find("li.wiktku-dfn-sense").last();
}
// Template selector.
block.find("select.wiktku-dfn-tpl-select").val(sense.tpl || "");
if (sense.tpl) {
block.find("textarea.wiktku-dfn-mane").hide();
block.find("input.wiktku-dfn-tpl-title").val(sense.tplTitle || "").show();
// The picker is collapsed behind a link by default; a restored
// sense that already uses a template has to show it, or the
// chosen template is invisible and looks lost.
var reveal = block.data("revealTplRow");
if (typeof reveal === "function") reveal();
} else {
block.find("textarea.wiktku-dfn-mane").val(sense.mane || "").show();
block.find("input.wiktku-dfn-tpl-title").hide().val("");
}
// dfnArea is the only thing actually saved, so split it back into
// tercime/îzah for editing (mirrors syncNonKuToMane's join format).
var maneParts = splitTercimeIzah(sense.mane);
block.find("textarea.wiktku-dfn-mane").val(maneParts ? $.trim(maneParts.tercime) : (sense.mane || ""));
block.find(".wiktku-dfn-izah").val(maneParts ? $.trim(maneParts.izah) : "");
var setIzahOpen = block.data("setIzahOpen");
if (setIzahOpen) setIzahOpen(!!(maneParts && $.trim(maneParts.izah)));
// Re-sync the formên alternatîv/hevmane/… hide state now that
// tplSelect's value was set (its own "change"
// handler doesn't fire from .val(), so this doesn't happen otherwise).
var updateVis = block.data("updateFieldVisibility");
if (updateVis) updateVis();
// Same for the mane-box mode (plain vs tercime+îzah), since this
// also depends on tplSelect's (and lang's) value.
var updateMane = block.data("updateManeMode");
if (updateMane) updateMane();
// Restore a sub-list (etiket, formên alternatîv, hevmane …) from a values array.
function restoreSubList(cls, values) {
if (!values || !values.length) return;
var groupDiv = block.find("." + cls + "-group");
var subList = groupDiv.find("ol, ul").first();
if (!subList.length) return; // group not found
var useModifiers = isWordGroupCls(cls);
// Fill existing items, append new ones as needed.
$.each(values, function (j, val) {
var items = subList.find("li");
if (j < items.length) {
// useModifiers rows keep the word and its modifier tags in
// separate elements (see makeModifierLi) — setRawValue()
// re-splits val() the same way parsing it fresh would,
// instead of dumping the raw "word<key:value>" string into
// the visible word box.
var setRaw = items.eq(j).data("setRawValue");
if (useModifiers && typeof setRaw === "function") {
setRaw(val);
} else {
items.eq(j).find("textarea").val(val);
}
} else {
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-restore-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.val(val);
ta.on("input", function () {
steps.dfn.onSubInput(subList, this, 1, true, useModifiers);
});
subList.append(useModifiers ? makeModifierLi(ta) : $("<li></li>").append(ta));
}
});
if (!useModifiers) return;
// A fresh empty row goes on the end so there's always somewhere
// to add the next word without first having to click into a
// filled one.
var filled = subList.find("li.wiktku-word-li");
if (filled.last().is(":last-child")) {
var nextTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-restore-next-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow");
nextTa.on("input", function () {
steps.dfn.onSubInput(subList, this, 1, true, useModifiers);
});
subList.append(makeModifierLi(nextTa));
}
}
// Populate the word lists that the draft has something for. senseGroups' "field" is both the
// property getSenses() read them into and the tail of the box's own class.
$.each(wordGroups(), function (_, g) {
var vals = sense[g.field];
if (vals && vals.length) restoreSubList(g.cls, vals);
});
// Dever/Şîwe, Etîket, Kategorî — chip fields, not textarea
// sub-lists, restored via the adders buildSenseBlock stashed on
// the block.
block.find("input.wiktku-dfn-maneid-input").val(sense.maneid || "");
var chipRestoreMap = {
addDeverChip: sense.dever,
addEtiketChip: sense.etiket,
addKategoriChip: sense.kategori
};
$.each(chipRestoreMap, function (dataKey, vals) {
var addChip = block.data(dataKey);
if (addChip && vals && vals.length) {
$.each(vals, function (_, v) { addChip(v); });
}
});
// Nimûne items are objects { text, tr?, t? } — restore using the builder.
if (sense.nimune && sense.nimune.length) {
var nimuneGroupDiv = block.find(".wiktku-dfn-nimune-group");
var nimuneList = nimuneGroupDiv.find("ol.wiktku-dfn-nimune-list");
var buildFn = nimuneGroupDiv.data("buildNimuneLi");
nimuneList.empty();
$.each(sense.nimune, function (_, n) {
var text = typeof n === "object" ? (n.text || "") : n;
if (!text) return;
var li = buildFn ? buildFn() : $("<li class='wiktku-dfn-nimune-li'></li>");
li.find(".wiktku-dfn-nimune-text").val(text).each(function () { highlightLinebreak(this); });
if (n.tr) li.find(".wiktku-dfn-nimune-tr").val(n.tr);
if (n.t) li.find(".wiktku-dfn-nimune-t").val(n.t);
nimuneList.append(li);
});
// Always leave a blank trailing item.
if (buildFn) nimuneList.append(buildFn());
}
// Restore jêder entries — accepts current array shape or a legacy
// single object (older saved sections) for backwards compat.
var jederArr = $.isArray(sense.jeder) ? sense.jeder : (sense.jeder ? [sense.jeder] : []);
if (jederArr.length) {
var jederGroupDiv = block.find(".wiktku-dfn-jeder-group");
var jederList = jederGroupDiv.find("ol.wiktku-dfn-jeder-list");
var buildJederFn = jederGroupDiv.data("buildJederLi");
jederList.empty();
$.each(jederArr, function (_, j) {
if (!j || !j.quote) return;
var li = buildJederFn ? buildJederFn() : $("<li class='wiktku-dfn-jeder-li'></li>");
li.find(".wiktku-dfn-nimune-quote").val(j.quote).each(function () { highlightLinebreak(this); });
if (j.quoteTr) li.find(".wiktku-dfn-nimune-quote-tr").val(j.quoteTr);
if (j.quoteT) li.find(".wiktku-dfn-nimune-quote-t").val(j.quoteT);
if (j.url) li.find(".wiktku-dfn-quote-url").val(j.url);
if (j.jederManual) {
var manualSel = li.find(".wiktku-dfn-quote-src-sel");
manualSel.val("__manual__").trigger("change");
li.find(".wiktku-dfn-quote-src-manual").val(j.jederManual);
} else if (j.raw) {
li.find(".wiktku-dfn-quote-src-sel").val("__raw__").trigger("change");
} else if (j.jederCatTpl) {
// Reopen the category picker on the saved template and put its fields back. The
// fields arrive asynchronously (TemplateData), so the saved values are applied
// once they exist rather than straight away.
var catSelEl = li.find(".wiktku-dfn-quote-src-sel");
catSelEl.val("__category__").trigger("change");
li.data("catTemplate", j.jederCatTpl);
if (j.jederCatLangParam) li.data("catLangParam", j.jederCatLangParam);
li.find(".wiktku-dfn-quote-src-cat-input").val(j.jederCatTpl);
// Kept on the row from the moment the restore starts. Submitting walks every section
// -- restore, then read -- and the read is synchronous while the boxes below only
// exist once TemplateData comes back, so without this the values are read from boxes
// that are not there yet and every parameter but the quote is lost. readJederList()
// falls back to these until the boxes catch up.
li.data("catFields", j.jederCatFields || {});
wiktkuCite.fieldSpecs(j.jederCatTpl).then(function (spec) {
var srcFields = li.find(".wiktku-dfn-quote-src-fields");
li.data("catLangParam", j.jederCatLangParam || spec.langParamKey || null);
$.each(spec.fields, function (_, field) {
srcFields.append(li.data("buildSrcFieldRow")(field));
});
$.each(j.jederCatFields || {}, function (key, val) {
li.find(".wiktku-dfn-quote-src-input[data-field-key='" + key + "']").val(val);
});
// The boxes hold them now, so they are the truth again -- clearing this keeps a
// field the user empties from being refilled from the stale copy.
li.removeData("catFields");
});
} else if (j.jederTpl) {
var srcSel = li.find(".wiktku-dfn-quote-src-sel");
srcSel.val(j.jederTpl).trigger("change");
if (j.jêderFields) {
$.each(j.jêderFields, function (key, val) {
li.find(".wiktku-dfn-quote-src-input[data-field-key='" + key + "']").val(val);
});
}
}
jederList.append(li);
});
// Always leave a blank trailing item, same as Nimûne.
if (buildJederFn) jederList.append(buildJederFn());
}
});
// Unlike addSense(), build()/buildSenseBlock() above don't wrap their own <select>s -- do it once for the whole restored list (idempotent, so already-wrapped selects are skipped).
initSelects(list);
// Everything above filled boxes that are still behind the chip row, so each block is asked to
// show whichever of its sub-fields the draft actually had something in.
list.find("li.wiktku-dfn-sense").each(function () {
var syncPicks = $(this).data("syncSensePicks");
if (syncPicks) syncPicks();
});
// Restoring a draft opens the first sense's tab by default.
steps.dfn.setOpenSense(stepDiv, list.find("li.wiktku-dfn-sense").first());
}
// Snapshots simple (non-dfn) per-section steps from the DOM into obj.
function saveCurrentSection() {
var snap = {};
$.each(PER_SECTION_STEP_IDS, function (_, id) {
if (id === "dfn") {
snap.dfn = snapshotDfn();
return;
}
if (id === "etym") {
snap.etym = $.trim($("#wiktku-start-etym .wiktku-etym-text").val());
return;
}
if (id === "cavkani") {
snap.cavkani = $.trim($("#wiktku-start-cavkani .wiktku-cavkani-text").val());
return;
}
if (id === "posparams") {
snap.posparams = $.trim($("#wiktku-start-posparams .wiktku-posparams-text").val());
return;
}
if (id === "resim") {
// One entry per row, including blank ones, so a mid-edit draft
// (e.g. a filename typed but no caption yet) survives a cache
// restore instead of being silently dropped.
snap.resim = $("#wiktku-start-resim li.wiktku-resim-item").map(function () {
return {
file: $.trim($(this).find(".wiktku-resim-file").val()),
caption: $.trim($(this).find(".wiktku-resim-caption").val()),
alt: $.trim($(this).find(".wiktku-resim-alt").val())
};
}).get();
return;
}
var step = steps[id];
if (!step) return;
var stepDiv = $("#wiktku-start-" + id);
if (id === "trans") {
snap.trans = steps.trans.getTranslations(stepDiv);
return;
}
if (id === "zarava") {
snap.zarava = steps.zarava.getTranslations(stepDiv);
return;
}
if (id === "dunde") {
snap.dunde = steps.dunde.getRows(stepDiv);
return;
}
// Generic: read all step fields.
var fields = stepDiv.find(".wiktku-start-step-field");
if (step.multiple) {
snap[id] = fields.map(function () { return $(this).val(); }).get();
} else {
snap[id] = fields.val();
}
});
// Which "Beşên din" sub-sections this section has already opened
// via the picker — per-section, like everything else here.
snap.openedOtherSections = $.extend({}, openedOtherSections);
sections[activeSection] = snap;
}
// Restores a previously saved snapshot into the form DOM.
function restoreSection(snap) {
$.each(PER_SECTION_STEP_IDS, function (_, id) {
if (id === "dfn") {
restoreDfn(snap.dfn || []);
return;
}
if (id === "etym") {
$("#wiktku-start-etym .wiktku-etym-wrap").remove();
steps.etym.build($("#wiktku-start-etym"));
$("#wiktku-start-etym .wiktku-etym-text").val(snap.etym || "");
return;
}
if (id === "cavkani") {
$("#wiktku-start-cavkani .wiktku-cavkani-wrap").remove();
steps.cavkani.build($("#wiktku-start-cavkani"));
$("#wiktku-start-cavkani .wiktku-cavkani-text").val(snap.cavkani || "");
return;
}
if (id === "posparams") {
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-step").remove();
steps.posparams.build(ppDiv);
var savedTa = snap.posparams || "";
ppDiv.find(".wiktku-posparams-text").val(savedTa);
// Re-parse saved textarea to restore helper widget states.
if (savedTa) {
// Read with the shared parser, so a nested "c=" is never taken for the call's own, and a
// saved "z=" restores the gender the same as "c=".
var savedC = wiktkuWikitext.callParamValue(savedTa, "c");
var savedJ = wiktkuWikitext.callParamValue(savedTa, "j");
var savedTr = wiktkuWikitext.callParamValue(savedTa, "tr");
// Write into the shared hidden field (not the visible
// select, which has no <option>s yet) — syncHelpers()
// below reads it back to restore the dropdown once the
// gender/number options for this language are populated.
if (savedC) $("#wiktku-start-cinsiyet-field").val(savedC);
if (savedJ) $("#wiktku-start-jimar-field").val(savedJ);
if (savedTr) ppDiv.find(".wiktku-posparams-field[data-pp-key='tr']").val(savedTr);
// For lêker, c= holds the verb-type code instead of a
// gender — split it back across the two dropdowns it is
// now assembled from ("x-gh" → form "x" + base "gh").
if (savedC && /^lêker/.test(snap.pos || "")) {
var lekturParts = /^(?:([xphb])-)?(n?gh)$/.exec(savedC);
if (lekturParts) {
ppDiv.find(".wiktku-posparams-lektur-sel").val(lekturParts[2]);
ppDiv.find(".wiktku-posparams-lekturform-sel").val(lekturParts[1] || "");
}
}
// Restore checked optional params via the same param/value overrides rebuildTa uses.
ppDiv.find(".wiktku-posparams-opt-panel input[type='checkbox']").each(function () {
var key = $(this).val();
var override = wiktkuPosparams.OPT_PARAM_OVERRIDES[key];
var needle = override ? "|" + override.param + "=" + override.value : "|" + key + "=1";
$(this).prop("checked", savedTa.indexOf(needle) !== -1);
});
}
steps.posparams.syncHelpers(ppDiv);
return;
}
if (id === "resim") {
var resimDiv = $("#wiktku-start-resim");
resimDiv.find(".wiktku-resim-wrap").remove();
steps.resim.build(resimDiv);
// Accept both the new array-of-rows format and the older
// single {file, caption} object saved before multi-image
// support existed.
var resimSnap = snap.resim;
var rows = $.isArray(resimSnap) ? resimSnap : (resimSnap ? [resimSnap] : []);
// setValues() keeps a blank trailing row of its own, same as Nimûne/Dûnde.
if (rows.length && steps.resim.field) steps.resim.field.setValues(rows);
return;
}
if (id === "trans") {
// Rebuild trans groups (dfn must already be restored by this
// point in the $.each loop, so glossList() reflects it).
var transDiv = $("#wiktku-start-trans");
transDiv.find(".wiktku-trans-group").remove();
steps.trans.build(transDiv);
var groupsSnap = snap.trans || [];
$.each(groupsSnap, function (gi, rows) {
var group = transDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
if (!group.length) return; // gloss no longer exists — drop its rows
var list = group.find("ul.wiktku-trans-list");
$.each(rows, function (j, row) {
var lang = row[0], words = row[1];
var lastRow = list.find("li.wiktku-trans-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-trans-words");
$.each(words, function (k, word) {
var wordItems = wordList.find("li");
if (k < wordItems.length) {
wordItems.eq(k).find("textarea").val(word);
} else {
// .val() below doesn't fire "input", so the normal
// auto-grow never runs — append a matching item
// ourselves instead of silently dropping the word.
var extraInput = steps.trans.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
// Leave one trailing empty field so the row can still grow
// when the user types, matching a freshly-built row.
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.trans.makeWordInput()));
}
});
});
return;
}
if (id === "zarava") {
// Rebuild zarava groups (dfn must already be restored by this
// point in the $.each loop, so glossList() reflects it).
var zaravaDiv = $("#wiktku-start-zarava");
zaravaDiv.find(".wiktku-zarava-group").remove();
steps.zarava.build(zaravaDiv);
var zaravaGroupsSnap = snap.zarava || [];
$.each(zaravaGroupsSnap, function (gi, rows) {
var group = zaravaDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
if (!group.length) return; // gloss no longer exists — drop its rows
var list = group.find("ul.wiktku-zarava-list");
$.each(rows, function (j, row) {
var lang = row[0], words = row[1];
var lastRow = list.find("li.wiktku-zarava-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-zarava-words");
$.each(words, function (k, word) {
var wordItems = wordList.find("li");
if (k < wordItems.length) {
wordItems.eq(k).find("textarea").val(word);
} else {
var extraInput = steps.zarava.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.zarava.makeWordInput()));
}
});
});
return;
}
if (id === "dunde") {
// Rebuild dunde rows.
var dundeDiv = $("#wiktku-start-dunde");
dundeDiv.find("ul.wiktku-dunde-list").remove();
steps.dunde.build(dundeDiv);
var dundeRows = snap.dunde || [];
$.each(dundeRows, function (i, row) {
var lang = row[0], words = row[1];
var list = dundeDiv.find("ul.wiktku-dunde-list");
var lastRow = list.find("li.wiktku-dunde-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-dunde-words");
$.each(words, function (j, word) {
var wordItems = wordList.find("li");
if (j < wordItems.length) {
wordItems.eq(j).find("textarea").val(word);
} else {
// .val() below doesn't fire "input", so the normal
// auto-grow never runs — append a matching item
// ourselves instead of silently dropping the word.
var extraInput = steps.dunde.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
// Leave one trailing empty field so the row can still grow
// when the user types, matching a freshly-built row.
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.dunde.makeWordInput()));
}
});
return;
}
var step = steps[id];
if (!step) return;
var stepDiv = $("#wiktku-start-" + id);
var fields = stepDiv.find(".wiktku-start-step-field");
var val = snap[id];
if (val === undefined || val === null) return;
if (step.multiple) {
var arr = val;
fields.each(function (i) {
if (i < arr.length) $(this).val(arr[i]);
});
} else {
fields.val(val);
}
// Tell the pickers about the programmatic value change.
fields.filter("select").trigger("wiktku:refresh");
});
// Restore which "Beşên din" sub-sections were already opened for
// this section, then seed in anything the DOM restore above just
// filled in but that wasn't marked opened yet.
openedOtherSections = $.extend({}, snap.openedOtherSections || {});
seedOpenedFromContent();
renderBesenDinPicker();
}
function getWikitextForCurrentSection(callback) {
var values = getValues();
var entry = steps.pos.output(values);
// Bilêvkirin is NOT emitted here — it is one shared field for the
// whole word, so emitting it per section duplicated it. getWikitext()
// prints it once, above the POS sections.
steps.dfn.getOutput(values, function (output) {
entry += output;
entry += steps.tewandin.output(values);
entry += steps.bikaranin.output(values);
entry += steps.alfabeyendin.output(values);
entry += steps.formalt.output(values);
entry += steps.hevmane.output(values);
entry += steps.dijmane.output(values);
entry += steps.jornav.output(values);
entry += steps.jernav.output(values);
entry += steps.je.output(values);
entry += steps.tekildar.output(values);
entry += steps.etym.output(values);
entry += steps.dunde.output(values);
entry += steps.binereherwiha.output(values);
entry += steps.zarava.output(values);
entry += steps.trans.output(values);
entry += steps.cavkani.output(values);
entry += steps.xwendin.output(values);
callback(entry, values);
});
}
// Two sections of the same part of speech would otherwise both be "=== Rengdêr ===", which makes them
// indistinguishable on the page, in the table of contents and in every link that targets one. Run over
// the finished sections rather than guessed from the pos alone, so "Rengdêr" and "Formeke rengdêrê"
// -- different headings from the same pos -- are correctly left alone.
function numberRepeatedHeadings(results) {
var headingRE = /^(===\s*)([^=\n]+?)(\s*===)$/m;
var counts = {};
$.each(results, function (_, text) {
var m = headingRE.exec(text || "");
if (m) counts[m[2]] = (counts[m[2]] || 0) + 1;
});
var seen = {};
$.each(results, function (i, text) {
var m = headingRE.exec(text || "");
if (!m || counts[m[2]] < 2) {
return;
}
seen[m[2]] = (seen[m[2]] || 0) + 1;
results[i] = text.replace(headingRE, m[1] + m[2] + " " + seen[m[2]] + m[3]);
});
}
function getWikitextAllSections(callback) {
saveCurrentSection();
var results = new Array(sections.length);
var sharedValues = getValues(); // lang/pron are shared
function processSection(i) {
// Each hop through here can be reached asynchronously: the dfn
// step waits on mw.loader.using("ext.gadget.linkify") before
// calling back, so from that point on the submit handler's own
// try/catch is no longer on the stack. Guard every hop instead,
// or a throw after linkify loads vanishes into an unhandled
// rejection and the form just sits there looking busy.
try {
if (i >= sections.length) {
// All done — restore original active section.
restoreSection(sections[activeSection]);
refresh();
numberRepeatedHeadings(results);
callback(results, sharedValues);
return;
}
restoreSection(sections[i]);
refresh();
getWikitextForCurrentSection(function (sectionText) {
results[i] = sectionText;
processSection(i + 1);
});
} catch (e) {
if (typeof onSubmitError === "function") onSubmitError(e);
else throw e;
}
}
processSection(0);
}
// Assembles wikitext for all sections plus the shared lang header.
function getWikitext(callback) {
getWikitextAllSections(function (sectionTexts, sharedValues) {
var bnrh = steps.bnrh.output(sharedValues);
var entry = steps.lang.output(sharedValues);
// One pronunciation per word, printed once between the language
// header and the POS sections — see steps.pron.output.
entry += steps.pron.output(sharedValues);
entry += sectionTexts.join("\n");
// Reconstruction-namespace pages (ns 108) mark the language
// section with {{jinûvesazkirî|langcode}} directly above it —
// only prepended when generating fresh wikitext, same as h-bnr
// below (addLangMode handles an existing page separately).
if (!addLangMode && ns === 108) entry = "{{jinûvesazkirî|" + sharedValues.lang + "}}\n" + entry;
// h-bnr goes before the language section — only prepended when
// generating fresh wikitext (not addLangMode, which handles it separately).
if (!addLangMode && bnrh) entry = bnrh + entry;
// Edit summary uses section 0's values. Read from the saved snapshot
// (not the live DOM) so it doesn't mix section 0's pos with whichever
// section happens to be active/restored at this point.
var dfnSenses = (sections[0] && sections[0].dfn) || [];
var dfnSummary = dfnSenses.map(function (s) {
if (s.mane) return s.mane;
// Template sense (e.g. {{guharto|ku|somepage}}) — same
// construction as the tplCall built in getOutput, so the
// summary shows what actually gets written to the entry
// instead of going blank.
if (s.tpl && s.tplTitle) {
return s.tplNoLang
? "{{" + s.tpl + "|" + s.tplTitle + "}}"
: "{{" + s.tpl + "|" + sharedValues.lang + "|" + s.tplTitle + "}}";
}
return "";
}).filter(Boolean).join("; ");
var summary = "([[Mediawiki:Gadget-start.js|WF:NÛ]] v.2.0) +" +
sharedValues.lang + ":" + sections[0].pos + ":" + dfnSummary;
var summaryField = document.getElementById("wpSummary");
if (addLangMode && summaryField.value) {
summary = summaryField.value + " " + summary;
}
summaryField.value = summary;
callback(entry);
});
}
var continueBtn;
// Set by build() to its submitFailed(). Lives out here because the
// wikitext-assembly functions below are outside build()'s closure but
// still need a way to report a failure to the person rather than dying
// silently in the console.
var onSubmitError = null;
// Whether Mane/Bi zaravayên din/Werger has anything in it — kept
// outside refresh() so the click handler can read the latest value.
function getValuesForStep(stepId) {
var fields = $("#wiktku-start-" + stepId + " .wiktku-start-step-field");
var step = steps[stepId];
if (step.keys) {
var values = {};
fields.each(function (i, field) {
var key = $(field).prevAll(".wiktku-start-step-keyfield").val();
if (key) values[key] = $(field).val();
});
return values;
}
else if (step.multiple) {
return fields.map(function () {
return $(this).val();
}).get();
}
return fields.val();
}
function getValues() {
var values = {};
$.each(steps, function (stepId) {
values[stepId] = getValuesForStep(stepId);
});
return values;
}
function populateMenus(inputs, choices, disabledValues, placeholderText) {
if (!disabledValues) disabledValues = {};
var vals = inputs.map(function (i, input) {
return $(input).val();
});
inputs.empty();
inputs.append($("<option></option>").text(placeholderText || ""));
$.each(choices, function (j, choice) {
// Append the language code in parentheses so that typing a code
// (e.g. "ku") in the search box still finds the language name.
var displayText = capitalize(choice.label);
if (choice.sortKey !== undefined) {
// This is a language choice {code, label, sortKey} — show code.
displayText = capitalize(choice.label) + " (" + choice.code + ")";
}
var option = $(mw.html.element("option", {
value: choice.code }, displayText));
if (disabledValues[choice.code]) option.prop("disabled", true);
inputs.append(option);
});
inputs.val(function (i) {
return vals[i];
});
}
// Keys to disable for a key menu: already-taken or explicitly disabled.
function getDisabledKeys(step) {
var occupiedKeys = {};
$("#wiktku-start-" + step.id + " .wiktku-start-step-keyfield").each(function () {
occupiedKeys[$(this).val()] = true;
});
if (step.disabledKeys) $.extend(occupiedKeys, step.disabledKeys);
return occupiedKeys;
}
function buildKeyInput(step) {
if (!step.keys) return undefined;
var input = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
input.attr("data-placeholder", "Hilbijêre " + step.keyName);
populateMenus(input, step.keys, getDisabledKeys(step));
// Keep the key menus mutually exclusive: whatever one row selects is
// disabled in every other row. Recomputed from the DOM on each change.
input.on("change", function () {
var all = $("#wiktku-start-" + step.id + " .wiktku-start-step-keyfield");
var taken = {};
all.each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
all.not(this).each(function () {
var self = $(this), own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (code) $(this).prop("disabled", !!taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
});
// Return the wrapper; the select lives inside it and still fires change.
return wrapNativeSelect(input);
}
// Adds another text field to the list if the last one now has text.
function refreshForTextField(currentInput, step) {
currentInput = $(currentInput);
var value = currentInput.val();
var item = currentInput.parent();
if (value && !item.next("li").length) {
var nextInput = $("<textarea></textarea>")
.attr("name", "wiktku-field-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", currentInput.attr("class"))
.attr("rows", currentInput.attr("rows") || 1);
if (step.placeholder) {
nextInput.attr("placeholder", typeof step.placeholder === "function" ? step.placeholder() : step.placeholder);
}
nextInput.on("input", function (evt) {
refreshForTextField(evt.target, step);
});
var nextKeyInput = buildKeyInput(step);
var nextItem = step.modifiers
? makeModifierLi(nextInput).prepend(nextKeyInput || "")
: $("<li></li>").append(nextKeyInput).append(nextInput);
item.parent().append(nextItem);
if (nextKeyInput) initSelects(nextItem);
}
var canContinue = !continueBtn.isDisabled();
if ((value && !canContinue) || (!value && canContinue)) {
refresh(step);
}
}
function prepareInput(input, step) {
if (!input) return;
if (step.required) input.prop('required', true);
input.change(step, refresh);
}
// Shows/hides steps based on whether their prerequisites are met, and
// updates placeholders where applicable.
function refresh(evt) {
var values = getValues();
var changedStep = evt && evt.data;
var canContinue = true;
// Re-filter template options in every sense block whenever lang changes.
$("#wiktku-start-dfn .wiktku-dfn-tpl-select").each(function () {
var fn = $(this).data("updateOptions");
if (typeof fn === "function") fn();
});
// Re-filter the pronunciation template dropdown when language changes -- etym/cavkani's lists don't depend on lang, so they're only built once at data load instead of on every refresh().
steps.pron.refreshTplSelect();
// Re-render any step's question label that depends on current values
// (e.g. dfn's Werger-section reminder, which only applies to ku).
$.each(steps, function (stepId, step) {
if (typeof step.question_fmt === "function") {
$("#wiktku-start-" + stepId + " .wiktku-start-step-question")
.text(step.question_fmt(values));
}
});
// Pos's definition/example text, right under its dropdown — see
// steps.pos.example and the .wiktku-start-pos-example build above.
$("#wiktku-start-pos .wiktku-start-pos-example").text(steps.pos.example(values));
// Sync every sense block's mane-box mode (plain mane vs tercime+îzah)
// to the current language, in case lang changes after blocks exist.
// Same pass also re-syncs Dever/Şîwe's default open/closed state
// (open for ku, closed otherwise — see updateDeverOpenState) and
// reveals any sub-field group that has ended up with content in it
// while still behind the chip row (a draft, a restore).
$("#wiktku-start-dfn li.wiktku-dfn-sense").each(function () {
var updateMane = $(this).data("updateManeMode");
if (updateMane) updateMane();
var updateDeverOpen = $(this).data("updateDeverOpenState");
if (updateDeverOpen) updateDeverOpen();
var syncPicks = $(this).data("syncSensePicks");
if (syncPicks) syncPicks();
});
// Sync tr/t sub-fields in every nimûne item to the current language.
// tr shown when language needs transliteration; t once a language is chosen.
(function () {
var lang = values.lang || "";
var showTr = needsTransliteration(lang);
var showT = !!lang;
$("#wiktku-start-dfn .wiktku-dfn-nimune-list li.wiktku-dfn-nimune-li").each(function () {
var li = $(this);
// Ensure the sub-wrapper exists when we need it.
var subWrap = li.find(".wiktku-dfn-nimune-sub");
// tr field
var trTa = li.find(".wiktku-dfn-nimune-tr");
if (showTr && !trTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
li.append(subWrap);
}
trTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-tr")
.attr("placeholder", "Transliterasyon…");
subWrap.prepend(trTa);
} else if (!showTr && trTa.length) {
trTa.remove();
}
// t field
var tTa = li.find(".wiktku-dfn-nimune-t");
if (showT && !tTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
li.append(subWrap);
}
tTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-t");
subWrap.append(tTa);
} else if (!showT && tTa.length) {
tTa.remove();
}
tTa.attr("placeholder", nimuneTranslationPrompt(lang));
// Remove the wrapper if it ended up empty.
subWrap = li.find(".wiktku-dfn-nimune-sub");
if (subWrap.length && !subWrap.children().length) subWrap.remove();
});
}());
// Re-sync the "<label> bi <ziman>…" placeholder on every always-visible
// sub-field list (etîket, kategorî, hevmane, …), since the language may
// not have been chosen yet when these lists were first built, or may
// have changed since.
$("#wiktku-start-dfn .wiktku-dfn-sublist").each(function () {
var upd = $(this).data("updatePlaceholders");
if (typeof upd === "function") upd();
});
// Same idea for word-list steps (jê, têkildar, binêre herwiha, …)
// whose placeholder is a function of the current language.
$.each(steps, function (stepId, step) {
if (typeof step.placeholder === "function") {
$("#wiktku-start-" + stepId + " textarea.wiktku-start-step-field")
.attr("placeholder", step.placeholder());
}
});
// Sync tr/t sub-fields in every jêder citation to the current language.
// tr shown when language needs transliteration; t shown when lang !== "ku".
(function () {
var lang = values.lang || "";
var showTr = needsTransliteration(lang);
var showT = lang && lang !== "ku";
$("#wiktku-start-dfn .wiktku-dfn-jeder-list li.wiktku-dfn-jeder-li").each(function () {
var li = $(this);
// Ensure the sub-wrapper exists when we need it. It must sit right
// after the quote textarea and before the source selector.
var subWrap = li.find(".wiktku-dfn-quote-sub");
// tr field
var trTa = li.find(".wiktku-dfn-nimune-quote-tr");
if (showTr && !trTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
// Insert after the quote textarea's resize-wrap (if the manual
// resize handle has already been attached to it), not after the
// bare textarea -- otherwise this lands *inside* that wrap,
// between the textarea and its own grip, breaking jêgirtin's
// handle and nesting the tr/t boxes inside it.
var quoteTa = li.find(".wiktku-dfn-nimune-quote");
var quoteResizeWrap = quoteTa.closest(".wiktku-resize-wrap");
(quoteResizeWrap.length ? quoteResizeWrap : quoteTa).after(subWrap);
}
trTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-tr")
.attr("placeholder", "Transliterasyona jêgirtinê…");
subWrap.prepend(trTa);
} else if (!showTr && trTa.length) {
trTa.remove();
}
// t field
var tTa = li.find(".wiktku-dfn-nimune-quote-t");
if (showT && !tTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
// See the tr-field branch above for why this targets the
// resize-wrap instead of the bare textarea.
var quoteTa2 = li.find(".wiktku-dfn-nimune-quote");
var quoteResizeWrap2 = quoteTa2.closest(".wiktku-resize-wrap");
(quoteResizeWrap2.length ? quoteResizeWrap2 : quoteTa2).after(subWrap);
}
tTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-t")
.attr("placeholder", "Tercimeya jêgirtinê bi kurmancî…");
subWrap.append(tTa);
} else if (!showT && tTa.length) {
tTa.remove();
}
// Remove the wrapper if it ended up empty.
subWrap = li.find(".wiktku-dfn-quote-sub");
if (subWrap.length && !subWrap.children().length) subWrap.remove();
});
}());
$.each(steps, function (stepId, step) {
var groupIdx = stepIdToGroup[stepId];
var hiddenByTab = (groupIdx !== undefined && groupIdx !== activeGroup) ||
// Beşên din picker: stays hidden even while its tab is
// active until explicitly opened from the dropdown.
(BESENDIN_SET[stepId] && !openedOtherSections[stepId]);
// Bilêvkirin is its own section now, so the two views are
// mutually exclusive: on it, only pron shows (plus Ziman, which
// is shared and stays above every section); off it, pron is
// hidden no matter which tab is active. pron is deliberately
// ungrouped — stepIdToGroup has no entry for it — so without
// this branch it would show on all four tabs at once.
if (stepId === "pron") {
hiddenByTab = hiddenByTab || !viewingPron;
} else if (viewingPron && stepId !== "lang") {
hiddenByTab = true;
}
if (hiddenByTab || (typeof(step.visible) === "function" && !step.visible(values))) {
$("#wiktku-start-" + stepId).hide();
}
else {
var stepDiv = $("#wiktku-start-" + stepId);
if (changedStep !== step && typeof(step.build) === "function") {
prepareInput(step.build(stepDiv, values), step);
}
// After build, sync the posparams helper widgets whenever pos or lang changes.
if (step.id === "posparams" && typeof step.syncHelpers === "function") {
step.syncHelpers(stepDiv);
}
var input = stepDiv.find(".wiktku-start-step-field");
if (typeof(step.example) === "function") {
// .find() above already reaches nested fields (e.g. the dfn
// step's mane textareas inside each sense block), so one
// assignment covers every step; dfn keeps its own fallback.
var exampleText = step.example(values) || (step.id === "dfn" ? "Mane…" : "");
input.attr("placeholder", exampleText);
}
stepDiv.slideDown();
}
if (step.required &&
(values[stepId] === "" ||
(step.multiple && values[stepId].join("") === ""))) {
canContinue = false;
}
});
// One group means no bar to show; it only appears again if STEP_GROUPS ever grows back.
$("#wiktku-group-tabs").toggle(STEP_GROUPS.length > 1 && !!values.pos && !viewingPron);
// Bilêvkirin hides the picker outright; otherwise renderBesenDinPicker() decides, showing itself
// only once a part of speech is chosen and there are sections left to offer. The decision lives
// there rather than here because that function has other callers, which would each need the same
// test.
var besenDinActive = !viewingPron;
if (!besenDinActive) {
$("#wiktku-besendin-picker").hide();
}
// The option list is filtered by each step's visible(), which depends
// on lang/pos — so it has to be rebuilt when those change, not just
// when a section is opened. Safe to call from here: rendering the
// picker doesn't itself call refresh(), only picking an option does.
if (besenDinActive) renderBesenDinPicker();
updateGroupTabIndicators(values);
// Ziman and Cureyê rêzimanî are all it takes: an entry with a heading and a template but no
// definition yet is a skeleton somebody means to come back to, not a mistake to be blocked.
continueBtn.setDisabled(!canContinue);
}
function destroy() {
if (autosaveDebounceTimer) clearTimeout(autosaveDebounceTimer);
if (autosaveInterval) clearInterval(autosaveInterval);
formDirty = false;
$(window).off("beforeunload.wiktkuUnsaved");
$("#wiktku-start-form, .noafterwizard").slideUp(function () {
$(this).remove();
});
$(".noarticletext .nowizard").slideDown();
// #editform was kept laid-out but off-screen (see build()) instead of
// display:none, precisely so the editor never had to size itself
// against a zero-width/height box. Just bring it back on-screen —
// no slideDown needed since it was never actually collapsed.
$("#editform").removeClass("wiktku-offscreen");
// Belt-and-braces: if WikiEditor/CodeMirror/mobile's auto-grow still
// cached a stale size from some earlier point, nudge them to
// recalculate now that the box is genuinely visible.
$(window).trigger("resize");
if (window.CodeMirror) {
var cm = $("#wpTextbox1").data("CodeMirror") || $("#wpTextbox1").data("cmInstance");
if (cm && typeof cm.refresh === "function") cm.refresh();
}
$("#ca-edit").removeClass("selected");
$("#ca-wiktku-nowizard").addClass("selected");
}
// Priority order for Kurdish dialects and navz.
// Codes listed here come before all other languages; unlisted codes
// are sorted alphabetically by their display name via localeCompare.
var LANG_PRIORITY = ["navz", "ku", "hac", "sdh", "lki", "ckb", "zza"];
// Inserts a new "== {{ziman|code}} ==" section at the correct sorted
// position: navz, then ku/hac/sdh/lki/ckb/zza, then alphabetical by name.
function insertNewLangSection(existing, wikitext) {
var newCodeMatch = wikitext.match(new RegExp("^\n?" + langHeaderSource("[^}]+")));
var newCode = newCodeMatch ? newCodeMatch[1] : "";
var sections = [];
var preamble = "";
var anyHeaderSrc = "\n?" + langHeaderSource("[^}]+");
var firstMatch = new RegExp("^" + anyHeaderSrc, "m").exec(existing);
if (!firstMatch) {
// No existing lang sections — just write the new section.
// Add \n\n only when there is real non-whitespace content above.
var trimmed = existing.replace(/\s+$/, "");
return trimmed ? trimmed + "\n\n" + wikitext : wikitext;
}
// Everything before the first == {{ziman|...}} == is the preamble.
var firstSectionStart = firstMatch.index;
// Walk back to the start of that line.
var lineStart = existing.lastIndexOf("\n", firstSectionStart - 1);
var preambleEnd = lineStart !== -1 ? lineStart + 1 : 0;
preamble = existing.slice(0, preambleEnd);
// Parse each language section: header line + everything until the next header.
var body = existing.slice(preambleEnd);
// (?![\s\S]) matches only true end-of-string, since a bare "$" with
// /m would stop at the section's first line break instead.
var langSectionRe = new RegExp(
"^(" + anyHeaderSrc + "\\n?)([\\s\\S]*?)(?=\\n" + anyHeaderSrc + "|(?![\\s\\S]))",
"gm"
);
var m;
while ((m = langSectionRe.exec(body)) !== null) {
sections.push({
code: m[2],
content: m[0] // full text of this section including header
});
}
// Determine sort rank: priority list first (by index), then alphabetical.
function getLangName(code) {
var entry = langsByCode[code];
return entry ? entry.label : code;
}
function sortRank(code) {
var idx = LANG_PRIORITY.indexOf(code);
return idx !== -1 ? idx : LANG_PRIORITY.length;
}
function compareLangs(a, b) {
var ra = sortRank(a), rb = sortRank(b);
if (ra !== rb) return ra - rb;
// Both are outside priority list — sort by display name.
return getLangName(a).localeCompare(getLangName(b), userLang);
}
// Insert the new section into the sorted list.
var newSection = { code: newCode, content: wikitext };
var inserted = false;
for (var i = 0; i < sections.length; i++) {
if (compareLangs(newCode, sections[i].code) < 0) {
sections.splice(i, 0, newSection);
inserted = true;
break;
}
}
if (!inserted) sections.push(newSection);
// Re-join: preamble + sections, each separated by exactly \n\n.
// Trim trailing whitespace off each section's content first so we
// control the spacing precisely.
var joined = sections.map(function (s) {
return s.content.replace(/\s+$/, "");
}).join("\n\n");
// Only add a separator after the preamble when it has real content.
var preambleTrimmed = preamble.replace(/\s+$/, "");
var prefix = preambleTrimmed ? preambleTrimmed + "\n\n" : "";
return prefix + joined + "\n";
}
function build() {
$(".nowizard, .nowizard .noafterwizard").hide();
var callToAction = $("#wiktku-calltoaction");
var origCallToActionText = callToAction.text();
$(mw.util.addPortletLink("p-views", "#", "Edîtora çavkaniyê veke",
"ca-wiktku-nowizard", "Vê rûpelê bi edîtora çavkaniyê çêke",
undefined, "#ca-watch")).click(function (evt) {
evt.preventDefault();
callToAction.text(origCallToActionText);
destroy();
});
if (callToAction.length) {
callToAction.text(origCallToActionText +
" Forma li jêr ji bo çêkirina madeyeke nû bi kar bîne.");
}
var startForm = $("<form id='wiktku-start-form'></form>");
var clearPageCheck = null;
if (addLangMode) {
startForm.append($("<p class='wiktku-addlang-notice'></p>").html(
"Bi vê formê tu yê zimanekî nû li rûpelê îlawe bikî. Eger ev ziman jixwe hebe, beşa nû li dawiya vî zimanî dê were îlawekirin. " +
"Ji kerema xwe netîceya dawî kontrol bike, eger problemek derkeve, li <a href='" +
mw.util.getUrl('Mediawiki talk:Gadget-start.js') +
"'>nîqaşa vê amûrê</a> problemê ragihîne."
));
clearPageCheck = $("<input type='checkbox' id='wiktku-clear-page-check'>");
var clearPageLabel = $("<label for='wiktku-clear-page-check'></label>")
.text(" Muhtewaya rûpelê paqij bike (her tiştî jê bibe û rûpelê ji nû ve binivîse)");
startForm.append(
$("<p class='wiktku-addlang-notice'></p>")
.append(clearPageCheck)
.append(clearPageLabel)
);
// Re-evaluate step visibility (e.g. the pron step) whenever this
// is toggled, since it changes what counts as "existing content".
clearPageCheck.on("change", function () {
refresh();
});
}
var spinner;
// Tab bar for switching between POS sections.
var sectionTabsDiv = $("<div id='wiktku-section-tabs'></div>");
// Tab bar for switching between step-groups (topics) within a section.
// renderGroupTabs()/updateGroupTabIndicators() (which populate and
// maintain it) live outside build() — see near "var activeSection".
var groupTabsDiv = $("<div id='wiktku-group-tabs'></div>");
// Both tab bars are sticky. group-tabs sits below section-tabs' rendered height, which varies
// when the bar wraps, so it is measured and published as a CSS custom property.
// Vector 2022's own sticky header would cover section-tabs, so its occupied height goes into
// --wiktku-siteheader-h. It slides with scroll, hence the re-measure on scroll as well as resize.
function siteHeaderOffset() {
var el = document.querySelector(".vector-sticky-header");
if (!el) return 0;
var rect = el.getBoundingClientRect();
// Present in the DOM at all times on Vector 2022, but only
// actually occupying screen space once scrolled into place —
// before that it sits translated above the viewport (negative/
// zero rect), which must not add a phantom gap under our bars.
if (rect.bottom <= 0 || rect.top > 4) return 0;
return Math.max(0, rect.bottom);
}
var stickyOffsetQueued = false;
function syncStickyTabsOffset() {
var h = sectionTabsDiv.is(":visible") ? (sectionTabsDiv.outerHeight() || 0) : 0;
startForm.css("--wiktku-stabs-h", h + "px");
startForm.css("--wiktku-siteheader-h", siteHeaderOffset() + "px");
}
function queueStickyTabsOffsetSync() {
if (stickyOffsetQueued) return;
stickyOffsetQueued = true;
window.requestAnimationFrame(function () {
stickyOffsetQueued = false;
syncStickyTabsOffset();
});
}
$(window).on("resize.wiktkuStickyTabs scroll.wiktkuStickyTabs", queueStickyTabsOffsetSync);
// "Beşên din" picker — the search box for adding a section, at the top of the form;
// renderBesenDinPicker() fills it.
var besenDinPickerDiv = $("<div id='wiktku-besendin-picker' class='wiktku-besendin-picker'></div>");
/** Puts the form back in a usable state after a failed submit and
* says what happened. Without this the button stays disabled and
* the spinner spins forever on any exception — the person is left
* staring at a form that looks busy but is dead, with the reason
* visible only in the browser console. */
function submitFailed(err) {
spinner.hide();
continueBtn.setDisabled(false);
continueBtn.$element.removeClass("wiktku-btn-sending");
var detail = (err && (err.message || err.toString())) || "sedem nediyar";
mw.notify(
"Şandin bi ser neket: " + detail +
" — tiştê ku te nivîsî winda nebûye. Heke ev dubare bibe, " +
"bi 'Edîtora çavkaniyê veke' berdewam bike.",
{ type: "error", tag: "wiktku-submit", autoHide: false }
);
// Deliberately console.error as well: the notification is for the
// person, this is for whoever they report the bug to.
if (window.console && console.error) console.error("[wiktku] submit failed", err);
}
onSubmitError = submitFailed;
startForm.submit(function (evt) {
evt.preventDefault();
continueBtn.setDisabled(true);
continueBtn.$element.addClass("wiktku-btn-sending");
spinner.show();
try {
getWikitext(function (wikitext) {
try {
if (addLangMode && clearPageCheck && clearPageCheck.prop("checked")) {
// User asked to wipe the page — write fresh content.
$("#wpTextbox1").val(wikitext);
} else if (addLangMode) {
var textbox = $("#wpTextbox1");
var existing = textbox.val() || "";
// ── h-bnr merge ──────────────────────────────────────────
// If the user filled bnr-h inputs, either merge into the
// existing {{h-bnr|…}} call or prepend a new one.
var bnrhOut = steps.bnrh.output(getValues());
if (bnrhOut) {
var newPages = bnrhOut.replace(/^\{\{h-bnr\|/, "").replace(/\}\}\n?$/, "").split("|");
var bnrhRe = /\{\{h-bnr\|([^}]*)\}\}/i;
if (bnrhRe.test(existing)) {
existing = existing.replace(bnrhRe, function (_, inner) {
var existingPages = inner.split("|").map(function (p) { return p.trim(); });
$.each(newPages, function (_, p) {
p = p.trim();
if (p && existingPages.indexOf(p) === -1) existingPages.push(p);
});
return "{{h-bnr|" + existingPages.join("|") + "}}";
});
} else {
existing = bnrhOut + existing;
}
}
// If this language's section already exists (whitespace
// differences aside), merge into it; otherwise insert new.
var langHeaderMatch = wikitext.match(new RegExp("^(\n?" + langHeaderSource("[^}]+") + "\\n)"));
if (langHeaderMatch) {
var langHeader = langHeaderMatch[1]; // e.g. "== {{ziman|ku}} ==\n"
var newCode = langHeaderMatch[2];
var existingHeader = findLangHeader(existing, newCode);
if (existingHeader) {
// Strip the header from the new output.
var newContent = wikitext.slice(langHeader.length);
// Find the insertion point: end of the existing lang section.
// A new L2 heading marks the start of the next language block.
var searchFrom = existingHeader.index + existingHeader.match.length;
// Find the next == … == heading after this section's start.
var nextL2 = existing.indexOf("\n== ", searchFrom);
var insertAt = nextL2 !== -1 ? nextL2 : existing.length;
// Ensure there's a blank line before the inserted block.
var before = existing.slice(0, insertAt);
var after = existing.slice(insertAt);
var sep = /\n\s*$/.test(before) ? "" : "\n";
var merged = before + sep + newContent + after;
// The section just added may repeat a part of speech the page already had. Both
// are renumbered across the whole language section, so a lone "Rengdêr" that was
// there before becomes "Rengdêr 1" and the new one "Rengdêr 2" -- leaving the old
// one unnumbered would make the pair unreadable on the page.
var secStart = existingHeader.index;
var nextLang = merged.indexOf("\n== ", secStart + existingHeader.match.length);
var secEnd = nextLang === -1 ? merged.length : nextLang;
textbox.val(
merged.slice(0, secStart) +
wiktkuEdit.wt.numberRepeatedPosHeadings(merged.slice(secStart, secEnd)) +
merged.slice(secEnd)
);
} else {
// New language — insert at the correct sorted position.
textbox.val(insertNewLangSection(existing, wikitext));
}
} else {
// Unexpected format — insert at sorted position anyway.
textbox.val(insertNewLangSection(existing, wikitext));
}
} else {
$("#wpTextbox1").val(wikitext);
}
$("html, body").animate({scrollTop: 0});
callToAction.text("Hindik ma!");
// The draft is now live in the editor, submitted or not — keeping
// it cached would just offer to restore already-used content
// the next time the wizard opens on this page.
try { formCache.Set({}); } catch (e) { /* storage unavailable — harmless, the content is already in the editor */ }
destroy();
} catch (e) {
// Thrown while writing the result into #wpTextbox1.
submitFailed(e);
}
});
} catch (e) {
// Catches anything thrown synchronously inside getWikitext.
submitFailed(e);
}
});
// The step the "Beşên din" picker is inserted directly above.
var PICKER_BEFORE_STEP = "tewandin";
// Tracks whether groupTabsDiv has been inserted yet.
var groupTabsInserted = false;
$.each(ordered_steps, function (i, step) {
var stepDiv = $(mw.html.element("div", {
"class": "wiktku-start-step",
id: "wiktku-start-" + step.id }));
var name = capitalize(step.name);
var nameH3 = $("<h3></h3>").addClass("wiktku-start-step-name");
var nameLabel = $("<label></label>")
.attr("for", "wiktku-start-" + step.id + "-field")
.text(name);
nameH3.append(nameLabel);
stepDiv.append(nameH3);
if (step.question_fmt) {
// question_fmt can be a plain string, or a function of the
// current values (used by steps whose question text depends
// on e.g. the chosen language) — refresh() keeps it in sync
// afterwards via the .wiktku-start-step-question class.
var questionText = typeof step.question_fmt === "function"
? step.question_fmt(getValues())
: step.question_fmt;
var questionLabel = $(mw.html.element("label", {
"class": "wiktku-start-step-question",
"for": "wiktku-start-" + step.id + "-field" }, questionText));
stepDiv.append(questionLabel);
}
var input;
if (step.choices) {
input = $(mw.html.element("select", {
"class": "wiktku-start-step-field",
id: "wiktku-start-" + step.id + "-field" }));
input.attr("data-placeholder",
capitalize(step.name) + " hilbijêre ");
populateMenus(input, step.choices);
// The language step ships ~300 languages; if a search comes up empty, fetch the other ~3400
// and refill this select before telling the user there is no such language.
if (step.id === "lang") {
input.data("wiktkuOnNoMatch", function (query, refresh) {
if (!wiktkuMergeLanguages || wiktkuLangs.isFull()) {
return;
}
wiktkuLangs.ensureFull().then(function (all) {
populateMenus(input, wiktkuMergeLanguages(all));
refresh();
});
});
}
if (step.multiple) input.attr("multiple", true);
// initSelects() runs over the whole form once it is in the DOM.
stepDiv.append(input);
// Pos-specific: the definition/example for whichever cureyê
// rêzimanî is currently chosen, as plain text right under the
// dropdown — kept in sync by refresh() via .wiktku-start-pos-example.
if (step.id === "pos") {
stepDiv.append(
$("<p class='wiktku-step-hint wiktku-start-pos-example'></p>").text(step.example(getValues()))
);
}
} else if (typeof(step.build) === "function") {
input = step.build(stepDiv);
} else {
input = $("<textarea></textarea>")
.attr("class", "wiktku-start-step-field")
.attr("id", "wiktku-start-" + step.id + "-field")
.attr("rows", step.rows || 2);
if (step.narrow) input.addClass("wiktku-start-narrow");
if (step.placeholder) {
input.attr("placeholder", typeof step.placeholder === "function" ? step.placeholder() : step.placeholder);
}
if (step.multiple) {
input.on("input", function (evt) {
refreshForTextField(evt.target, step);
});
var keyInput = buildKeyInput(step);
// Same one-box-per-word layout as the per-sense ⊕-modifier
// groups (see makeModifierLi) when this step has modifiers.
var list = $(step.ordered ? "<ol></ol>" : "<ul></ul>")
.append(step.modifiers
? makeModifierLi(input)
: $("<li></li>").append(keyInput).append(input));
stepDiv.append(list);
}
else stepDiv.append(input);
}
prepareInput(input, step);
stepDiv.hide();
// Section tabs (switch between POS sections) sit above the
// per-section fields, starting at "pos".
if (step.id === "pos") {
startForm.append(sectionTabsDiv);
}
// Step-group tabs sit just before the first grouped step in
// ordered_steps. Derived from stepIdToGroup so this keeps
// working no matter how STEP_GROUPS/ordered_steps get reordered.
if (!groupTabsInserted && stepIdToGroup[step.id] !== undefined) {
// With a single group there is nothing to switch between, so the bar is never built --
// rather than built and then hidden, which left it on screen whenever the form was
// rebuilt before refresh() next ran.
if (STEP_GROUPS.length > 1) {
startForm.append(groupTabsDiv);
}
groupTabsInserted = true;
}
// The picker sits between the senses and Tewandin: sections are added underneath it, so a
// newly picked one lands right here and pushes the rest of the form down.
if (step.id === PICKER_BEFORE_STEP) {
startForm.append(besenDinPickerDiv);
}
startForm.append(stepDiv);
});
// ── Section tab helpers ──────────────────────────────────────────────
// ── Add-section button — lives in the tab bar, disabled until pos chosen ──
var addSectionBtn = new OO.ui.ButtonWidget({
label: "+ Beşeke din",
disabled: true,
classes: [ "wiktku-add-sense-btn" ]
});
/** Adds a new, empty POS section and switches to it. */
function addPosSection() {
saveCurrentSection();
viewingPron = false;
sections.push({});
activeSection = sections.length - 1;
$.each(PER_SECTION_STEP_IDS, function (_, id) {
// Declared once here rather than with `var` in the "dfn"
// branch and again in the fallback branch below — each
// branch returns before the other runs, but both are
// lexically in this same function scope.
var stepDiv;
if (id === "dfn") {
stepDiv = $("#wiktku-start-dfn");
stepDiv.find(".wiktku-dfn-sense-tabs").remove();
stepDiv.find("ol.wiktku-dfn-list").remove();
steps.dfn.build(stepDiv);
return;
}
if (id === "etym") {
$("#wiktku-start-etym .wiktku-etym-wrap").remove();
steps.etym.build($("#wiktku-start-etym"));
return;
}
if (id === "cavkani") {
$("#wiktku-start-cavkani .wiktku-cavkani-wrap").remove();
steps.cavkani.build($("#wiktku-start-cavkani"));
return;
}
if (id === "posparams") {
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-step").remove();
return;
}
if (id === "resim") {
$("#wiktku-start-resim .wiktku-resim-wrap").remove();
return;
}
if (id === "trans") {
var transDiv = $("#wiktku-start-trans");
transDiv.find(".wiktku-trans-group").remove();
return;
}
if (id === "zarava") {
var zaravaDiv = $("#wiktku-start-zarava");
zaravaDiv.find(".wiktku-zarava-group").remove();
return;
}
if (id === "dunde") {
var dundeDiv = $("#wiktku-start-dunde");
dundeDiv.find("ul.wiktku-dunde-list").remove();
return;
}
stepDiv = $("#wiktku-start-" + id);
stepDiv.find(".wiktku-start-step-field").val("");
stepDiv.find("select.wiktku-start-step-field").trigger("wiktku:refresh");
});
// Fresh section — no "Beşên din" sub-section has been opened yet.
openedOtherSections = {};
renderBesenDinPicker();
renderSectionTabs();
refresh();
}
addSectionBtn.on("click", addPosSection);
/** Removes one POS section. Assumes the caller already confirmed and
* already checked more than one section remains. If the removed
* section was the one on screen, its in-progress DOM edits are
* discarded along with it (never saved back into sections[]) and a
* neighboring section is restored in its place. */
function removePosSection(idx) {
sections.splice(idx, 1);
if (idx === activeSection) {
viewingPron = false;
activeSection = Math.min(idx, sections.length - 1);
restoreSection(sections[activeSection]);
refresh();
} else if (idx < activeSection) {
activeSection -= 1;
}
renderSectionTabs();
}
/** Re-render the tab bar to reflect current sections[] and activeSection. */
function renderSectionTabs() {
// Detach (not remove) the button first: .empty() strips bound
// event data from any children left inside it, which would
// silently kill addSectionBtn's click handler on every re-render.
addSectionBtn.$element.detach();
sectionTabsDiv.empty();
var lang = getValues().lang || "";
if (!lang) {
sectionTabsDiv.hide();
syncStickyTabsOffset();
return;
}
sectionTabsDiv.show();
// Bilêvkirin leads the bar, ahead of the POS sections.
var pronTab = $("<span></span>")
.addClass("wiktku-section-tab wiktku-section-tab-pron")
.text("Bilêvkirin")
.toggleClass("wiktku-tab-active", viewingPron);
pronTab.on("click", function () {
if (viewingPron) return;
saveCurrentSection();
viewingPron = true;
refresh();
renderSectionTabs();
scrollTabBarIntoView("wiktku-section-tabs");
});
sectionTabsDiv.append(pronTab);
$.each(sections, function (i) {
var posVal = sections[i].pos || "";
// Numbered while there is more than one section of this part of speech, so two tabs are
// never both "Rengdêr" -- the same numbering the headings get on save.
var samePos = $.grep(sections, function (s) { return (s.pos || "") === posVal; });
var nth = 0;
$.each(sections, function (j, s) {
if ((s.pos || "") === posVal && j <= i) nth++;
});
var tabLabel = posVal
? capitalize(posVal) + (samePos.length > 1 ? " " + nth : "")
: ("Beş " + (i + 1));
var tab = $("<span></span>")
.addClass("wiktku-section-tab")
.text(tabLabel)
.toggleClass("wiktku-tab-active", i === activeSection && !viewingPron);
(function (idx) {
tab.on("click", function () {
if (idx === activeSection && !viewingPron) return;
// Leaving Bilêvkirin doesn't need a save — its field
// is shared and never part of a section snapshot.
if (!viewingPron) saveCurrentSection();
viewingPron = false;
activeSection = idx;
restoreSection(sections[activeSection]);
refresh();
renderSectionTabs();
scrollTabBarIntoView("wiktku-section-tabs");
});
}(i));
sectionTabsDiv.append(tab);
});
// One delete button for the whole bar — not one per tab — same
// reasoning as the Mane tabs' trash button: deletes whichever
// Beş is currently open, so there's only one thing a click on it
// can possibly remove. Not shown on Bilêvkirin (nothing to
// delete there) or when it's the only Beş left.
if (sections.length > 1 && !viewingPron) {
var activeTabLabel = sections[activeSection] && sections[activeSection].pos
? capitalize(sections[activeSection].pos)
: ("Beş " + (activeSection + 1));
var sectionTrash = makeTrashButton("Vê beşê jê bibe (" + activeTabLabel + ")");
sectionTrash.on("click", function () {
if (!window.confirm("Tu dixwazî \"" + activeTabLabel + "\" jê bibî? Ev tişt nayê vegerandin.")) return;
removePosSection(activeSection);
});
sectionTabsDiv.append(sectionTrash.$element);
}
// Enable "+ Beşeke din" only when the active section has a pos chosen.
var activePos = sections[activeSection] && sections[activeSection].pos
? sections[activeSection].pos
: getValues().pos;
addSectionBtn.setDisabled(!activePos);
sectionTabsDiv.append(addSectionBtn.$element);
syncStickyTabsOffset();
}
renderSectionTabs();
/**
* Re-populates a growing "multiple" field list (bnrh, je, tekildar, …) from a saved array.
* Values are set with .val() plus an "input" event so refreshForTextField() grows the rows,
* exactly as typing into the last row does.
*/
function restoreMultiField(id, values) {
var container = $("#wiktku-start-" + id);
$.each(values || [], function (i, v) {
var field = container.find(".wiktku-start-step-field").eq(i);
if (!field.length || !v) return;
field.val(v).trigger("input");
});
}
/**
* Restores a full cached draft: shared lang/pron/bnrh fields, then
* every section (via the same restoreSection()/refresh()/
* renderSectionTabs() sequence already used when switching tabs).
* @param cached {{lang: string, pron: string, bnrh: Array, sections: Array, activeSection: number}}
*/
function restoreFromCache(cached) {
if (cached.lang) {
$("#wiktku-start-lang-field").val(cached.lang);
$("#wiktku-start-lang-field").trigger("wiktku:refresh");
}
// Rebuild dependent steps (pos, pron wrap, etc.) now that lang is set.
refresh();
if (cached.pron !== undefined) {
$("#wiktku-start-pron .wiktku-pron-text").val(cached.pron);
}
if (cached.bnrh) {
restoreMultiField("bnrh", cached.bnrh);
}
sections = (cached.sections && cached.sections.length) ? cached.sections : [{}];
activeSection = (typeof cached.activeSection === "number" &&
cached.activeSection >= 0 && cached.activeSection < sections.length)
? cached.activeSection : 0;
restoreSection(sections[activeSection]);
refresh();
renderSectionTabs();
updateLastSavedIndicator(cached.savedAt || null);
formDirty = false;
wiktkuNotify("Danûstandina berê ya qeydkirî ji cache hate barkirin.",
{ type: "info", tag: "wiktku-cache-restore" });
}
// Update the active tab's label whenever pos changes.
startForm.on("change", "#wiktku-start-pos-field", function () {
// renderSectionTabs() labels each tab from sections[i].pos, which
// is only synced from the DOM by saveCurrentSection() (on tab
// switch/section add). Without this, the active tab's own label
// stays on the previous pos (or "Beş N") until the user switches
// away and back. Update it directly so the rename is immediate.
if (sections[activeSection]) sections[activeSection].pos = $(this).val();
renderSectionTabs();
// A new cureyê rêzimanî means any previously chosen cinsiyet/
// cureyê lêkerê no longer applies — clear both selections (and
// the shared hidden cinsiyet field, since rebuildTa()/syncHelpers()
// fall back to it) before wiping the textarea, so nothing stale
// gets carried over into the rebuilt template.
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-cinsiyet-sel").val("");
ppDiv.find(".wiktku-posparams-jimar-sel").val("");
ppDiv.find(".wiktku-posparams-lektur-sel").prop("selectedIndex", 0);
ppDiv.find(".wiktku-posparams-lekturform-sel").prop("selectedIndex", 0);
$("#wiktku-start-cinsiyet-field").val("");
$("#wiktku-start-jimar-field").val("");
ppDiv.find(".wiktku-posparams-text").val("");
// refresh() runs steps.posparams.syncHelpers(), which toggles the
// param rows for the new pos and — since the textarea is now
// empty — calls rebuildTa() to repopulate it.
refresh();
});
// Show the tab bar as soon as a language is selected.
startForm.on("change", "#wiktku-start-lang-field", function () {
renderSectionTabs();
});
var footer = $("<div id='wiktku-start-footer'></div>");
spinner = $(mw.html.element("img", {
id: "wiktku-start-spinner",
src: "//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif",
alt: "Tê barkirin…",
title: "Tê barkirin…" }));
footer.append(spinner.hide());
continueBtn = new OO.ui.ButtonWidget({
id: "wiktku-unhide-editform",
label: "Bişîne",
flags: [ "primary", "progressive" ]
});
continueBtn.on("click", function () {
// Immediate feedback that the click registered
continueBtn.$element.addClass("wiktku-btn-flash");
setTimeout(function () {
continueBtn.$element.removeClass("wiktku-btn-flash");
}, 500);
wiktkuNotify("Tê şandin…", { type: "info", tag: "wiktku-submit" });
startForm.trigger("submit");
});
continueBtn.$element.addClass("wiktku-footer-primary");
footer.append(continueBtn.$element);
// "Edîtora çavkaniyê veke" button — inline fallback for the portlet link
var sourceEditorBtn = new OO.ui.ButtonWidget({
label: "Edîtora çavkaniyê veke",
flags: [ "destructive" ],
});
sourceEditorBtn.on("click", function () {
destroy();
});
sourceEditorBtn.$element.addClass("wiktku-footer-primary");
footer.append(sourceEditorBtn.$element);
// ── Cache save/clear buttons ─────────────────────────────────────────
// Manual, page-scoped save of the entire wizard (all sections, not
// just the active one) via [[MediaWiki:Gadget-StorageUtils.js]]'s
// ObjectStorage. Restoring happens silently on load at the end of
// build(), once the full language list is available.
//
// Saving also happens automatically (debounced while typing, plus a
// periodic fallback) — see "Autosave" below — since relying on the
// person to remember to click "Li cache qeyd bike" before an
// accidental tab close/reload defeats the point of having a cache
// at all.
function buildCacheSnapshot() {
return {
lang: getValues().lang || "",
pron: $("#wiktku-start-pron .wiktku-pron-text").val() || "",
// bnrh is shared (page-level), same as lang/pron above — not
// per-section, so it doesn't live in sections[].
bnrh: getValuesForStep("bnrh"),
sections: sections,
activeSection: activeSection,
savedAt: Date.now()
};
}
// ── Last-saved indicator ─────────────────────────────────────────────
var lastSavedEl = $("<span id='wiktku-last-saved'></span>");
function formatSavedTime(ts) {
try {
return new Date(ts).toLocaleTimeString(mw.config.get("wgUserLanguage"),
{ hour: "2-digit", minute: "2-digit" });
} catch (e) {
return new Date(ts).toLocaleTimeString();
}
}
function updateLastSavedIndicator(ts) {
lastSavedEl.text(ts ? ("Cache: qeydkirî di saet " + formatSavedTime(ts) + " de") : "");
}
// ── Dirty-state tracking + autosave ──────────────────────────────────
// formDirty mirrors "does the live form differ from what's in cache".
// Anything the person types/selects sets it; a successful save (auto
// or manual) clears it; destroy() (final submit, or switching to the
// source editor) also clears it since the wizard's job is over at
// that point either way. formDirty/autosaveDebounceTimer/autosaveInterval
// are declared in the outer scope (near activeGroup) — assigned here,
// not re-declared — so destroy() can reach them too.
var AUTOSAVE_DEBOUNCE_MS = 8000; // save shortly after typing pauses
var AUTOSAVE_INTERVAL_MS = 60000; // …and at least this often regardless
function performAutosave() {
if (!formDirty) return;
saveCurrentSection();
try {
formCache.Set(buildCacheSnapshot());
formDirty = false;
updateLastSavedIndicator(Date.now());
} catch (e) {
// Silent on autosave failure (storage blocked/full, etc.) — the
// manual button still surfaces a proper error if the person
// tries it directly, no need to interrupt typing with a toast.
}
}
function scheduleAutosave() {
formDirty = true;
if (autosaveDebounceTimer) clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = setTimeout(performAutosave, AUTOSAVE_DEBOUNCE_MS);
}
startForm.on("input change", "input, textarea, select", scheduleAutosave);
autosaveInterval = setInterval(performAutosave, AUTOSAVE_INTERVAL_MS);
// Warn on accidental tab close/reload while there's anything unsaved.
$(window).on("beforeunload.wiktkuUnsaved", function (e) {
if (!formDirty) return undefined;
e.preventDefault();
e.returnValue = "";
return "";
});
var saveCacheBtn = new OO.ui.ButtonWidget({
label: "Li cache qeyd bike",
title: "Halê vê formê yê niha (ziman, bilêvkirin û hemû beş) li cache (localStorage) qeyd bike, da ku eger rûpel bêyî xwestin were girtin an ji nû ve were barkirin, tu karibî berdewam bikî."
});
saveCacheBtn.on("click", function () {
saveCurrentSection();
// ObjectStorage.Set() doesn't return a success/failure value, so
// the only way to detect a real failure (storage blocked, quota
// exceeded, JSON.stringify choking on something unexpected) is to
// catch it here rather than inspect a (nonexistent) return value.
try {
var snapshot = buildCacheSnapshot();
formCache.Set(snapshot);
formDirty = false;
updateLastSavedIndicator(snapshot.savedAt);
wiktkuNotify("Halê formê li cache hate qeydkirin.", { type: "success", tag: "wiktku-cache-save" });
} catch (e) {
mw.notify("Qeydkirina li cache bi ser neket.", { type: "error", tag: "wiktku-cache-save" });
}
});
var clearCacheBtn = new OO.ui.ButtonWidget({
label: "Cache paqij bike",
flags: [ "destructive" ],
title: "Datayên qeydkirî yên vê rûpelê ji cache jê bibe (forma ku niha xuya dike naguherîne)."
});
clearCacheBtn.on("click", function () {
formCache.Set({});
// Cancel the scheduled autosave, or it would rewrite the cache from the still-filled form.
// The form is left clean, so autosave resumes only once something is edited again.
if (autosaveDebounceTimer) {
clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = null;
}
formDirty = false;
updateLastSavedIndicator(null);
wiktkuNotify("Cache hate paqijkirin.", { type: "success", tag: "wiktku-cache-clear" });
});
// Everything sits on the bar: no overflow menu, nothing a click away.
// Settings button — opens the Tercîh dialog. Plain OOUI icon names depend on which icon-pack
// module happens to be loaded, so this uses the actual Codex "settings" (color-subtle) SVG from
// Commons directly, the same way the spinner above hotlinks its gif.
var prefsBtn = $("<button></button>", {
type: "button",
"class": "wiktku-prefs-btn",
title: "Tercîh — sazkariyên vê amûrê biguherîne",
"aria-label": "Tercîh"
});
prefsBtn.on("click", function (e) {
e.preventDefault();
wiktkuOpenPrefsDialog();
});
clearCacheBtn.$element.addClass("wiktku-btn-danger");
saveCacheBtn.$element.addClass("wiktku-footer-cache");
clearCacheBtn.$element.addClass("wiktku-footer-cache");
footer.append(saveCacheBtn.$element)
.append(clearCacheBtn.$element)
.append(prefsBtn)
.append($("<span class='wiktku-cache-note'></span>").append(lastSavedEl));
startForm.append(footer);
// Moved off-screen rather than display:none. WikiEditor and the mobile editor measure
// #wpTextbox1 while the wizard is up, and a hidden ancestor reports zero, which they then keep.
// Off-screen keeps it laid out and measurable.
$("#editform").addClass("wiktku-offscreen").before(startForm);
// renderGroupTabs() looks up #wiktku-group-tabs via a live DOM
// selector, so it must run after startForm is actually in the document.
renderGroupTabs();
renderBesenDinPicker();
// One pass over the whole form: every select that hasn't already been
// initialised, and isn't opted out with .wiktku-plain-select, gets
// the searchable picker.
initSelects(startForm);
refresh();
// Give every draggable textarea a custom resize grip (works
// consistently on Safari/iOS, unlike the native corner grip — see
// the "Manual drag-to-resize handle" comment in styles.css), and
// keep applying it to textareas added later on (new sense blocks,
// quote-source fields, nimûne rows, …).
wiktkuInitManualResize(startForm);
new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
$(mutation.addedNodes).each(function () {
if (this.nodeType !== 1) return;
wiktkuInitManualResize(this);
});
});
}).observe(startForm[0], { childList: true, subtree: true });
// Now that the full language list is loaded, silently restore any
// draft the editor previously saved for this page.
var cachedDraft = formCache.Get();
if (cachedDraft && cachedDraft.sections && cachedDraft.sections.length) {
restoreFromCache(cachedDraft);
}
}
// Only oojs-ui-core blocks the initial render now — icons-moderation
// (the trash glyph) is fetched lazily by makeTrashButton() instead, so
// slow connections aren't held up loading it before anyone needs it.
mw.loader.using(["oojs-ui-core"], function () {
function wiktkuProceedWithData(wiktkuData) {
var d = wiktkuData || {};
// Populate metadata.
if (d.metadata) $.extend(metadata, d.metadata);
// Populate gender labels.
if (d.genderLabels) $.extend(GENDER_BASE_LABELS, d.genderLabels);
// Jimar (number) code definitions
if (d.numberLabels) $.extend(NUMBER_LABELS, d.numberLabels);
if (d.senseGroups) SENSE_GROUPS = d.senseGroups;
// Headings for the page-level word lists, from the same table DefSideBoxes reads.
$.each(d.pageSubsections || [], function (_, sub) {
if (sub.kind !== "wordlist" || !steps[sub.id]) return;
steps[sub.id].header = sub.header;
steps[sub.id].name = decapitalize(sub.header);
});
if (d.wordlistTemplates && d.wordlistTemplates.length) WORDLIST_TEMPLATE = d.wordlistTemplates[0];
if (d.deverQuickPicks) DEVER_QUICK_PICKS = d.deverQuickPicks;
if (d.lekturChoices) LEKTUR_CHOICES = d.lekturChoices;
if (d.lekturFormChoices) LEKTUR_FORM_CHOICES = d.lekturFormChoices;
// Populate lang choices (pinned list).
if (d.langChoices) steps.lang.choices = choicesFromObject(d.langChoices, true);
// Populate langsByCode and merge full language list from d.ziman.
// d.ziman is a {code: name} object with all known languages.
$.each(steps.lang.choices, function (i, choice) {
if (choice.sortKey === undefined) choice.sortKey = choice.label;
langsByCode[choice.code] = choice;
});
// Only the ~300 languages of ziman-core.json are merged in at load; the other ~3400 arrive
// through wiktkuMergeLanguages() the first time somebody searches for one of them. The pinned
// choices are kept aside so a second merge rebuilds the list instead of appending to it.
var wiktkuPinnedChoices = steps.lang.choices;
wiktkuMergeLanguages = function (zimanNames) {
var pinnedCodes = {};
$.each(wiktkuPinnedChoices, function (i, c) { pinnedCodes[c.code] = true; });
var extraChoices = [];
$.each(zimanNames || {}, function (code, name) {
// A proto-language is never the language of an ordinary entry, but it is the whole point
// of a reconstruction page, so ns 108 keeps them in the list.
if (!isReconstruction && /-pro$/i.test(code)) return;
if (/ziman[eê]\s+destan/i.test(name)) return;
if (pinnedCodes[code]) return;
var choice = { code: code, label: name, sortKey: name };
extraChoices.push(choice);
langsByCode[code] = choice;
});
// One Intl.Collator built once and reused for the whole sort, instead of localeCompare() rebuilding a collator on every pair -- same order, far faster over ~3700 entries.
var langCollator = new Intl.Collator(userLang);
extraChoices.sort(function (a, b) {
return langCollator.compare(a.sortKey, b.sortKey);
});
steps.lang.choices = wiktkuPinnedChoices.concat(extraChoices);
// The translation/dialect steps reference any language, so they follow the same list.
steps.trans.keys = steps.lang.choices;
steps.dunde.keys = steps.lang.choices;
steps.zarava.keys = getZaravaKeys(steps.lang.choices);
return steps.lang.choices;
};
wiktkuMergeLanguages(wiktkuLangs.names());
// Keep the full merged list around for trans/dunde/zarava — those
// should still be able to reference any language even when the
// Tercîh panel restricts the main "ziman" step below — then, if
// the user set a restriction there, narrow steps.lang.choices
// itself to just those codes.
var wiktkuFullLangChoices = steps.lang.choices;
if (wiktkuPrefs.langCodes && wiktkuPrefs.langCodes.length) {
var wiktkuAllowedLangSet = {};
$.each(wiktkuPrefs.langCodes, function (i, code) { wiktkuAllowedLangSet[code] = true; });
var wiktkuRestrictedChoices = $.grep(wiktkuFullLangChoices, function (c) {
return !!wiktkuAllowedLangSet[c.code];
});
// Fall back to the full list instead of showing an empty
// dropdown if none of the saved codes matched anything
// (typo'd code, data-main.json changed since, …).
steps.lang.choices = wiktkuRestrictedChoices.length ? wiktkuRestrictedChoices : wiktkuFullLangChoices;
}
// Populate POS choices — also a plain {code: label} object, from data-sections.json.
if (d.posChoices) steps.pos.choices = choicesFromObject(d.posChoices, false);
// Populate template definitions for the dfn sense-block builder.
if (d.tplDefs) steps.dfn.tplDefs = d.tplDefs;
// Populate pos placeholder examples.
if (d.posExamples) steps.pos.posExamples = d.posExamples;
// Populate citation template definitions for the quote widget.
if (d.jederDefs) steps.dfn.jederDefs = d.jederDefs;
// Populate pronunciation and etymology template insert lists.
if (d.pronTpls) {
steps.pron.pronTpls = d.pronTpls;
steps.pron.refreshTplSelect();
}
if (d.etymTpls) {
steps.etym.etymTpls = d.etymTpls;
steps.etym.refreshTplSelect();
}
// Sync trans/dunde/zarava keys with the full language list
// (not the possibly Tercîh-restricted steps.lang.choices).
steps.trans.keys = wiktkuFullLangChoices;
steps.dunde.keys = wiktkuFullLangChoices;
steps.zarava.keys = getZaravaKeys(wiktkuFullLangChoices);
build();
}
// The data ships inside wikt.edit, so it is already parsed and in memory.
wiktkuProceedWithData(wiktkuEdit.data);
});
});
//</nowiki>
jhferbd3cpfd2bubn5kvhuqbgrx0mqm
6531669
6531660
2026-09-15T17:37:25Z
Wikihez
7344
Citoid fill button in the wizard's pill style; no link underline on frameless buttons
6531669
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.start-wizard.js]]
*/
// Li ser [[vi:MediaWiki:Gadget-start.js]] hatiye refaktorkirin.
// implicit dependencies: StorageUtils (ObjectStorage), Gadget-citoid-helper.js (wikt.citoid)
/* global ObjectStorage, wikt */
$(function () {
if (window.location.href.includes("editintro=User:Wikihez/creation/intro")) {
return; // Do not run the script wextê WF:Bilez dişuxile
}
// The same gate DefSideBoxes, add-examples and this wizard's own launcher use, so a page one of them
// works on is never a page this one quietly refuses to open on.
var ns = mw.config.get("wgNamespaceNumber");
if (!require("ext.gadget.wikt.core").sections.isEntryPage()) {
return;
}
// Reconstructed entries (ns 108) are unattested proto-forms: there is no recorded usage to give an
// example of or quote, and no dialects to mark, so the sense block leaves out Nimûne, Jêgirtin and
// Dever/Şîwe there.
var isReconstruction = ns === 108;
var addLangMode = mw.util.getParamValue("wiktkuaddlang") === "1";
// ── Tercîh (user settings) panel stored via mw.storage (localStorage), globally
var WIKTKU_PREFS_KEY = "wiktku_start_prefs";
var WIKTKU_PREFS_DEFAULTS = {
// [] = show the full language list in the "ziman" step; otherwise
// only these codes are shown there (data-main.json codes, not names).
langCodes: [],
// true = never auto-open on a new/empty page — only via the
// "+Ziman/beşeke din" portlet link.
onlyOnClick: false,
// "auto" | "p-cactions" | "p-tb" | "p-views" — where the
// "+Ziman/beşeke din" portlet link is added.
portlet: "auto",
// true = suppress routine/informational mw.notify toasts (page
// loading, cache auto-restored, cache saved/cleared, "tê şandin…").
// Errors and direct results of an explicit action (like saving
// Tercîh itself) always show regardless of this setting.
quiet: false
};
// Defaults and storage live in wikt.core/prefs.js, because the launcher reads them on every page view.
var wiktkuPrefsApi = require("ext.gadget.wikt.core").prefs;
function wiktkuLoadPrefs() { return wiktkuPrefsApi.load(); }
function wiktkuSavePrefs(prefs) { return wiktkuPrefsApi.save(prefs); }
var wiktkuPrefs = wiktkuLoadPrefs();
// Routine/informational notices go through here so "quiet mode" can
// suppress them; errors and explicit-action confirmations (like the
// Tercîh save toast) call mw.notify directly instead, and always show.
function wiktkuNotify(message, options) {
if (wiktkuPrefs.quiet) return;
mw.notify(message, options);
}
// Clears every draft cache and resize height this script writes, for all pages. Tercîh itself is
// left alone: it is the setting being edited when this button is reached.
// The state it touches is assigned by build(), which has always run by the time this is callable.
function wiktkuClearAllScriptCache() {
// Cancel the pending autosave, or it would rewrite the cache from the still-filled form.
if (autosaveDebounceTimer) {
clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = null;
}
formDirty = false;
$("#wiktku-last-saved").text("");
// Keeps formCache's in-memory copy in step with the localStorage sweep below.
try { formCache.Set({}); } catch (e) { /* storage unavailable — the raw sweep below still runs */ }
if (!window.localStorage) return;
var toRemove = [];
try {
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (!key) continue;
// ObjectStorage wraps the draft key as "enwikt/ObjectStorage/<name>?v=<n>&Type=…", so it
// is matched as a substring; the mw.storage keys below are plain prefixes.
if (key.indexOf("wiktku_start_draft_") !== -1 || key.indexOf("wiktku_resize_height_") === 0 ||
key.indexOf(WIKTKU_SUGGESTIONS_KEY_PREFIX) === 0) {
toRemove.push(key);
}
}
} catch (e) { return; }
$.each(toRemove, function (_, key) {
try { localStorage.removeItem(key); } catch (e) { /* skip this key, keep clearing the rest */ }
});
}
// Lazily built the first time it's opened; reused after that (see
// wiktkuOpenPrefsDialog below, defined once we know OOUI is available).
var wiktkuPrefsWindowManager = null;
var wiktkuPrefsDialog = null;
function wiktkuOpenPrefsDialog() {
mw.loader.using(["oojs-ui-core", "oojs-ui-windows"], function () {
function WiktkuPrefsDialog(config) {
WiktkuPrefsDialog.super.call(this, config);
}
if (!wiktkuPrefsDialog) {
OO.inheritClass(WiktkuPrefsDialog, OO.ui.ProcessDialog);
WiktkuPrefsDialog.static.name = "wiktkuPrefsDialog";
WiktkuPrefsDialog.static.title = "Tercîhên min";
WiktkuPrefsDialog.static.actions = [
{ action: "save", label: "Qeyd bike", flags: ["primary", "progressive"] },
{ action: "cancel", label: wiktkuUtil.msg.cancel, flags: "safe" },
{ action: "clearcache", label: "Hemû cache paqij bike", flags: "destructive" }
];
WiktkuPrefsDialog.prototype.initialize = function () {
WiktkuPrefsDialog.super.prototype.initialize.call(this);
this.langCodesInput = new OO.ui.TextInputWidget({
placeholder: "ku, zza, ckb (vala bihêle ji bo hemû zimanan)"
});
this.onlyOnClickCheckbox = new OO.ui.CheckboxInputWidget({});
this.quietCheckbox = new OO.ui.CheckboxInputWidget({});
this.portletDropdown = new OO.ui.DropdownInputWidget({
options: [
{ data: "auto", label: "Otomatîk (li gorî rûyê wîkiyê)" },
{ data: "p-cactions", label: "p-cactions - Li qismê Amûran" },
{ data: "left-navigation", label: "left-navigation - Li qismê Gotar-Gotûbêj" },
{ data: "p-tb", label: "p-tb - Li qismê Giştî" },
{ data: "p-views", label: "p-views - Li qismê Bixwîne" }
]
});
// Two named groups rather than one "Eyar" heap: which languages the wizard offers, then
// how it behaves. The dialog title already says these are preferences.
var langSet = new OO.ui.FieldsetLayout({ label: "Ziman" });
langSet.addItems([
new OO.ui.FieldLayout(this.langCodesInput, {
label: "Kodên zimanan ên ku tê xuyakirin li qismê \"ziman\"",
align: "top",
help: "Kodan bi virgulê (,) ji hev veqetîne. Vala bihêle da ku hemû ziman werin nîşandan.",
helpInline: true
})
]);
var behaviourSet = new OO.ui.FieldsetLayout({ label: "Wextê xebitînê" });
behaviourSet.addItems([
new OO.ui.FieldLayout(this.onlyOnClickCheckbox, {
label: "Tenê veke wextê \"+Ziman/beşeke din\" were tikandin",
align: "inline",
help: "Eger were hilbijartin, ev amûr êdî otomatîk li ser rûpelên nû dest pê nake — divê meriv her carê pêl \"+Ziman/beşeke din\" bike ku bişixule.",
helpInline: true
}),
new OO.ui.FieldLayout(this.portletDropdown, {
label: "Cihê lînka \"+Ziman/beşeke din\"",
align: "top",
help: "\"Otomatîk\" li gorî rûyê wîkiyê hildibijêre.",
helpInline: true
}),
new OO.ui.FieldLayout(this.quietCheckbox, {
label: "Moda bêdeng (kêmtir agahdarî nîşan bide)",
align: "inline",
help: "Agahdariyên normal nîşan nede. Xeta her tim tên xuyakirin.",
helpInline: true
})
]);
this.$element.addClass("wiktku-prefs-dialog");
this.$body.addClass("wiktku-prefs-dialog-body")
.append(langSet.$element, behaviourSet.$element);
};
WiktkuPrefsDialog.prototype.getSetupProcess = function (data) {
return WiktkuPrefsDialog.super.prototype.getSetupProcess.call(this, data)
.next(function () {
this.langCodesInput.setValue(wiktkuPrefs.langCodes.join(", "));
this.onlyOnClickCheckbox.setSelected(!!wiktkuPrefs.onlyOnClick);
this.portletDropdown.setValue(wiktkuPrefs.portlet || "auto");
this.quietCheckbox.setSelected(!!wiktkuPrefs.quiet);
}, this);
};
WiktkuPrefsDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === "save") {
return new OO.ui.Process(function () {
var codes = dialog.langCodesInput.getValue()
.split(/[,\s]+/)
.map(function (c) { return c.trim().toLowerCase(); })
.filter(function (c) { return !!c; });
wiktkuPrefs = {
langCodes: codes,
onlyOnClick: dialog.onlyOnClickCheckbox.isSelected(),
portlet: dialog.portletDropdown.getValue() || "auto",
quiet: dialog.quietCheckbox.isSelected()
};
wiktkuSavePrefs(wiktkuPrefs);
mw.notify(
"Tercîh hatin qeydkirin. Rûpelê ji nû ve bar bike (F5).",
{ type: "success", tag: "wiktku-prefs-save" }
);
dialog.close({ action: "save" });
});
}
if (action === "cancel") {
return new OO.ui.Process(function () {
dialog.close({ action: "cancel" });
});
}
if (action === "clearcache") {
return new OO.ui.Process(function () {
if (!window.confirm(
"Tu dixwazî hemû cache-ya vê amûrê ji her rûpelê paqij bikî " +
"(danûstandinên qeydkirî yên hemû rûpelan, bilindahiyên qutiyan)? " +
"Tercîhên li jor nayên jêbirin. Ev tişt nayê vegerandin."
)) return;
wiktkuClearAllScriptCache();
mw.notify("Hemû cache hate paqijkirin.", { type: "success", tag: "wiktku-cache-clear-all" });
dialog.close({ action: "clearcache" });
});
}
return WiktkuPrefsDialog.super.prototype.getActionProcess.call(this, action);
};
wiktkuPrefsWindowManager = new OO.ui.WindowManager();
$(document.body).append(wiktkuPrefsWindowManager.$element);
wiktkuPrefsDialog = new WiktkuPrefsDialog();
wiktkuPrefsWindowManager.addWindows([wiktkuPrefsDialog]);
}
wiktkuPrefsWindowManager.openWindow(wiktkuPrefsDialog);
});
}
// The launcher gadget adds the portlet link and decides whether this page wants the wizard before
// loading it at all. Both are repeated here only when this file was loaded on its own.
var wiktkuLaunched = !!(window.wikt && window.wikt.start && window.wikt.start.launched);
if (!wiktkuLaunched && !document.getElementById("ca-wiktku-addlang")) {
wiktkuPrefsApi.addLauncherLink(wiktkuPrefs);
// If "onlyOnClick" is set, the wizard should ONLY ever open via the
// "+Ziman/beşeke din" link — never automatically
var wiktkuNewPageDetected = $(".mw-newarticletext, .mw-newarticletextanon").length > 0;
if (wiktkuPrefs.onlyOnClick ? !addLangMode : (!addLangMode && !wiktkuNewPageDetected)) {
return;
}
}
// ── Shared helpers ───────────────────────────────────────────────────
// Generic text/UI utilities live in their own package file now — see the
// header of util.js for why. Aliased to bare names here so the ~400 call
// sites below read exactly as they did when these were local functions.
var wiktkuEdit = require("ext.gadget.wikt.edit");
var wiktkuUtil = wiktkuEdit.util;
var wiktkuWikitext = wiktkuEdit.wt;
var wiktkuLangs = wiktkuEdit.langs;
// Citations (jêder-*, Kategorî:Çavkanî, Citoid) all go through one module -- see wikt.cite.js.
var wiktkuCite = require("ext.gadget.wikt.cite");
var wiktkuPosparams = wiktkuEdit.posparams;
// Assigned once data-main.json is in: re-merges the language choices, and is called a second time if the
// full language list has to be fetched. Declared here so the step renderer can reach it.
var wiktkuMergeLanguages = null;
var capitalize = wiktkuUtil.capitalize;
var decapitalize = wiktkuUtil.decapitalize;
var escapeRegExp = wiktkuUtil.escapeRegExp;
var stripWikilinks = wiktkuUtil.stripWikilinks;
var splitTercimeIzah = wiktkuUtil.splitTercimeIzah;
var insertAtCursor = wiktkuUtil.insertAtCursor;
var wrapNativeSelect = wiktkuUtil.wrapNativeSelect;
var initSelects = wiktkuUtil.initSelects;
var wiktkuInitManualResize = wiktkuUtil.initManualResize;
var langHeaderSource = wiktkuWikitext.langHeaderSource;
// Inserts a line-style template snippet ("* {{…}}") at the cursor,
// prefixing it with a newline unless it's going at the very start of the
// box or right after one already — otherwise picking a second template
// from the pron dropdown right after the first lands them both on the
// same line: "* {{ku-IPA}}* {{ku-kîte}}".
function insertLineSnippet(el, snippet) {
var before = el.value.slice(0, el.selectionStart);
if (before && !/\n$/.test(before)) snippet = "\n" + snippet;
insertAtCursor(el, snippet);
}
// One delete button per tab bar, acting on the tab in view. The trash glyph
// (oojs-ui.styles.icons-moderation) is fetched here rather than at startup: it is only needed once a
// second Mane or Beş exists. The button is built immediately and the icon appears when the CSS lands.
function makeTrashButton(title) {
mw.loader.using("oojs-ui.styles.icons-moderation");
return new OO.ui.ButtonWidget({
icon: "trash",
flags: ["destructive"],
framed: false,
invisibleLabel: true,
label: title,
title: title,
classes: ["wiktku-trash-btn"]
});
}
function findLangHeader(text, code) {
var m = new RegExp("\n?" + langHeaderSource(escapeRegExp(code))).exec(text);
return m ? { index: m.index, match: m[0] } : null;
}
// The category's members come from wikt.cite, which also lists each template's redirects so it can be
// found by any of its names. Entries are {label, target}: label is what was typed/shown, target the
// canonical template to transclude.
var catSrcTemplates = null;
function fetchCatSrcTemplates() {
return wiktkuCite.fetchCategoryTemplates().then(function (list) {
catSrcTemplates = list;
return list;
});
}
var pageName = mw.config.get("wgPageName").replaceAll(/_/g, " ");
if (ns) pageName = pageName.replace(/.*\//, "");
var userLang = mw.config.get("wgUserLanguage");
var formCache = new ObjectStorage("wiktku_start_draft_" + mw.config.get("wgPageName"), 1);
var langsByCode = {};
// Looks up the display label for a language code, falling back to the
// code itself if it isn't known yet (e.g. langsByCode not loaded).
function getLangLabel(code) {
var entry = langsByCode[code];
return entry ? entry.label : code;
}
// The mane box holds a Kurdish definition for a Kurdish entry, and a direct translation for every
// other language -- the {{îzah}} box under it carries the explanation.
function manePlaceholder(code) {
return code && code !== "ku" ? wiktkuUtil.msg.tercimePrompt : "Maneya vê kelîmeyê binivîse…";
}
// A Kurmancî example gets an explanation, any other a translation.
function nimuneTranslationPrompt(code) {
return code === "ku" ? "Îzaheke nimûneyê bi kurmancî…" : "Tercimeya nimûneyê bi kurmancî…";
}
// Populated from data-main.json at runtime.
var metadata = {};
var ZARAVA_LANG_CODES = ["ckb", "sdh", "lki", "hac", "zza"];
// Converts a {code: label} object (langChoices, posChoices)
// into the {code, label[, sortKey]} array the rest of the script uses.
// isLang: true adds a sortKey and shows "(code)" after the label —
// language choices only.
function choicesFromObject(obj, isLang) {
var result = [];
$.each(obj || {}, function (code, label) {
var choice = { code: code, label: label };
if (isLang) choice.sortKey = label;
result.push(choice);
});
return result;
}
function getZaravaKeys(choices) {
var byCode = {};
$.each(choices || [], function (_, c) { byCode[c.code] = c; });
var result = [];
$.each(ZARAVA_LANG_CODES, function (_, code) {
if (byCode[code]) result.push(byCode[code]);
});
return result;
}
function needsTransliteration(langCode) {
var meta = metadata[langCode];
if (!meta || !meta.sc) return false;
var scripts = Array.isArray(meta.sc) ? meta.sc : [meta.sc];
return scripts.length > 0 && scripts[0] !== "Latn";
}
// metadata[lang].sc is either a single script string or an array of
// several — only the latter means the language is actually written in
// more than one script.
function langHasMultipleScripts(langCode) {
var meta = metadata[langCode];
return !!(meta && Array.isArray(meta.sc) && meta.sc.length > 1);
}
// Kodên cinsiyetê yên di metadata.g de — ji data-main.json tê dagirtin
var GENDER_BASE_LABELS = {};
// Number codes (y, d, p, p+, tp, j, j+, j/nj, nj, nj+, hj!, hj?) from
// data-main.json's "numberLabels"
var NUMBER_LABELS = {};
var SENSE_GROUPS = [];
var WORDLIST_TEMPLATE = "kol3"; // replaced by data-main.json's first wordlistTemplates entry
// The pills the Dever/Şîwe picker opens with; everything else comes from Modul:labels by typing.
var DEVER_QUICK_PICKS = [];
var LEKTUR_CHOICES = [];
var LEKTUR_FORM_CHOICES = [];
// Distinguishes the Jimar dropdown (j= param) from the Cîns dropdown
// (c= param): a code that's a key in NUMBER_LABELS is a number code,
// everything else is a gender code. Compound codes like "m-p"/"n-p" are
// gender+plural pairs and correctly stay on the gender side.
function isNumberCode(code) {
return Object.prototype.hasOwnProperty.call(NUMBER_LABELS, code);
}
// Builds the Jimar dropdown's <option> text for one code: "j — jimarbar".
function numberOptionLabel(code) {
var def = NUMBER_LABELS[code];
if (!def) return code;
var text = def.display;
return text ? (code + " — " + text) : code;
}
// Sub-lists that only make sense for a plain (non-şablon) sense — hidden
// when a şablon (e.g. {{guharto|…}}) is chosen for that sense
function genderLabel(code) {
if (code === "p") return "Pirjimar";
var pluralMatch = code.match(/^(.+)-p$/);
if (pluralMatch && GENDER_BASE_LABELS[pluralMatch[1]]) {
return GENDER_BASE_LABELS[pluralMatch[1]] + " (pirjimar)";
}
return GENDER_BASE_LABELS[code] || code;
}
// ── Template explanations ────────────────────────────────────────────
// Every place a template can be picked shows the same row: a status
// dot, the name, a link to the template page, and an ⓘ. This is the ⓘ.
//
// Text comes from the template's own TemplateData where it has any, so
// the explanation stays right without anyone maintaining a second copy
// by hand — with the desc already in data-main.json as the fallback for
// templates that have none.
// {desc, params} for the ⓘ popup, from wikt.edit -- the fetch, the shaping and the cache are all
// there, shared with wikt.cite and DefSideBoxes, so a template is asked for once however many pickers
// show it. Null for a template with no TemplateData, and that answer is kept too, so clicking ⓘ on
// an undocumented template twice is one request.
var fetchTemplateData = wiktkuEdit.wt.templateDoc;
var prefetchTemplateData = wiktkuEdit.wt.templateDocBatch;
/** Rewrites each tagged <option> in `sel` as "name — description", using
* TemplateData. Only fills rows that have no description yet, so the
* hand-written ones in data-main.json (which carry usage examples the API
* does not) are left exactly as they are. */
function applyTplDescriptions(sel) {
if (!sel || !sel.length) return;
var changed = false;
sel.find("option[data-tpl]").each(function () {
var opt = $(this);
var name = opt.attr("data-tpl");
if (opt.text().indexOf(" — ") !== -1) return; // already described
var td = wiktkuEdit.wt.templateDocNow(name);
var desc = (td && td.desc) || localTplDesc(name);
if (!desc) return;
opt.text(name + " — " + wiktkuEdit.wt.shortDesc(desc));
changed = true;
});
// The picker caches option text for searching, so it has to re-read.
if (changed) sel.trigger("wiktku:refresh");
}
/** Description from data-main.json's "desc", for templates with no TemplateData on the wiki. Works for any
* pool: pronTpls, etymTpls, tplDefs, jederDefs. */
function localTplDesc(name) {
var pools = [steps.pron.pronTpls, steps.etym.etymTpls, steps.dfn.tplDefs, steps.dfn.jederDefs];
var found = "";
$.each(pools, function (_, pool) {
if (pool && pool[name] && pool[name].desc) { found = pool[name].desc; return false; }
});
return found;
}
// Under the ⓘ it was opened from, kept inside the viewport, and flipped above it when there is no
// room below. Narrow screens get the width they have rather than a fixed card off the edge.
//
// box is where that ⓘ was when it was clicked. The row it sits in is closed by then, so it can no
// longer be measured; the last known box is kept for the re-placements after the text arrives.
function positionTplInfo(panel, anchor, box) {
var el = $(anchor).get(0);
var rect = box || (el && el.getBoundingClientRect ? el.getBoundingClientRect() : null);
if (rect && !rect.width && !rect.height) {
rect = null;
}
if (rect) {
panel.data("wiktkuAnchorBox", rect);
} else {
rect = panel.data("wiktkuAnchorBox") || null;
}
var margin = 8;
var arrow = 8;
var width = Math.min(360, $(window).width() - margin * 2);
panel.css({ width: width + "px", top: "0px", left: "0px" });
var height = panel.outerHeight();
// Above the dropdown, pointing down at it, the way MediaWiki's own filter tips sit; below it only
// when there is no room above.
var above = !rect || rect.top - height - arrow >= margin;
var top = !rect ? margin : (above ? rect.top - height - arrow : rect.bottom + arrow);
var left = rect ? Math.min(Math.max(margin, rect.left), $(window).width() - width - margin) : margin;
// The arrow points at the start of the box it explains, kept clear of the tip's corners.
var arrowX = rect ? Math.min(Math.max(16, rect.left + 24 - left), width - 16) : 16;
panel.toggleClass("wiktku-tpl-info-above", above).toggleClass("wiktku-tpl-info-below", !above);
panel.css({ top: top + "px", left: left + "px" });
panel.get(0).style.setProperty("--wiktku-arrow-x", arrowX + "px");
}
/** Opens the explanation for `name` as a tip above the dropdown it was asked from. */
function showTemplateInfo(name, anchorWrap, anchorEl, anchorBox) {
$(".wiktku-tpl-info").remove();
var panel = $("<div class='wiktku-tpl-info'></div>");
panel.append($("<div class='wiktku-tpl-info-name'></div>").text("{{" + name + "}}"));
var body = $("<div class='wiktku-tpl-info-desc'></div>").text(wiktkuUtil.msg.loading);
panel.append(body);
var params = $("<div class='wiktku-tpl-info-params'></div>");
panel.append(params);
panel.append($("<a class='wiktku-tpl-info-link' target='_blank' rel='noopener'></a>")
.attr("href", wiktkuUtil.tplPageUrl(name))
.text(wiktkuUtil.msg.openTemplatePage + " →"));
function closePanel() {
panel.remove();
$(document).off(".wiktkuTplInfo");
$(window).off(".wiktkuTplInfo");
}
var close = $("<button type='button' class='wiktku-tpl-info-close' title='Bigire'>✕</button>");
close.on("click", closePanel);
panel.prepend(close);
$(document).on("keydown.wiktkuTplInfo", function (e) {
if (e.which === 27) {
closePanel();
}
});
$(document).on("mousedown.wiktkuTplInfo", function (e) {
if (!$(e.target).closest(".wiktku-tpl-info").length) {
closePanel();
}
});
// A panel that stayed put while the page moved under it would point at nothing.
$(window).on("resize.wiktkuTplInfo scroll.wiktkuTplInfo", function () {
positionTplInfo(panel, anchorWrap);
});
// Over the form, not inside it: as a block in the flow it pushed every field below it down the
// page for as long as it was open, and the picker it explains moved with them.
$(document.body).append(panel);
positionTplInfo(panel, anchorWrap);
fetchTemplateData(name).then(function (td) {
var desc = (td && td.desc) || localTplDesc(name);
body.text(desc || "Ji bo vê şablonê îzahek nehatiye nivîsandin.");
if (td && td.params && td.params.length) {
params.append($("<div class='wiktku-tpl-info-params-title'></div>").text("Parametre:"));
var listEl = $("<ul></ul>");
$.each(td.params, function (_, p) {
var line = p.key + (p.label ? " — " + p.label : "") + (p.required ? " (mecbûrî)" : "");
listEl.append($("<li></li>").text(line));
});
params.append(listEl);
}
// The panel is as tall as its text, which only arrives now.
positionTplInfo(panel, anchorWrap);
});
}
/** Marks a picker's <option>s as template rows so the searchable widget
* renders them with the dot / 🔗 / ⓘ affordances, and wires the ⓘ
* handler for that picker. `nameOf` maps an <option> to a template
* name — the pickers store different things in value= (a snippet for
* the insert dropdowns, the bare name for the sense selector). */
function markTemplateOptions(sel, nameOf) {
if (!sel || !sel.length) return;
var names = [];
sel.find("option").each(function () {
var opt = $(this);
var name = nameOf(opt);
if (!name) return;
opt.attr("data-tpl", name);
// Deprecated/typo templates are the "special cases only" ones.
opt.attr("data-tpl-status", /kevnbûyî|şaşnivîs|kêmpeyda/i.test(opt.text()) ? "yellow" : "green");
names.push(name);
});
sel.data("wiktkuTplInfo", showTemplateInfo);
// Fill in the descriptions. Matters most for the Mane picker, whose
// entries in data-main.json are names with no desc at all — "rastnivîs",
// "ku-Latn", "guharto" on their own. One request covers every
// template in the picker, cached for the per-row ⓘ as well.
prefetchTemplateData(names).always(function () {
applyTplDescriptions(sel);
});
}
/** Pulls the template name out of a snippet like "* {{ku-IPA}}" or
* "{{ji|SOURCELANG|CODE|…}}". */
function tplNameFromSnippet(snippet) {
var m = /\{\{\s*([^|}\n]+)/.exec(snippet || "");
return m ? $.trim(m[1]) : "";
}
// True if a sense has content beyond mane worth keeping (etîket, word
// lists, nimûne, jêder) — so it's still emitted with {{mane?}} if empty.
function senseHasExtraContent(s) {
if (s.maneid) return true;
if (s.etiket && $.grep(s.etiket, function (e) { return $.trim(e) !== ""; }).length) return true;
if (s.kategori && $.grep(s.kategori, function (k) { return $.trim(k) !== ""; }).length) return true;
var anyWords = false;
$.each(wordGroups(), function (_, g) {
if ((s[g.field] || []).length) anyWords = true;
});
if (anyWords || (s.dever && s.dever.length)) return true;
if (s.nimune && $.grep(s.nimune, function (n) {
return (typeof n === "object" ? n.text : n) !== "";
}).length > 0) return true;
if (senseHasJeder(s)) return true;
return false;
}
// A technically-"complete" entry (Ziman + Cureyê rêzimanî filled) with
// nothing in Mane, Bi zaravayên din or Werger is still meaningfully
// empty, so this is checked separately at submit-click time with its
// own message.
// Converts literal line breaks typed into a nimûne/jêgirtin box into
// wikitext <br> tags,
function nlToBr(text) {
return text ? wiktkuCite.nlToBr(text) : text;
}
// Line breaks in a field save as "<br>"; this marks them so that is visible while typing.
var highlightLinebreak = wiktkuUtil.makeLinebreakHighlighter({ prefix: "wiktku" });
// Each nimûne item can be { text, tr, t }: text, transliteration, translation.
function formatNimune(nimunes, lang) {
var hasNimune = nimunes && $.grep(nimunes, function (n) {
return (typeof n === "object" ? n.text : n) !== "";
}).length > 0;
if (!hasNimune) {
// A sense with no example is left as it is, with no "{{nimûne?}}" placeholder.
return "";
}
return $.map(nimunes, function (n) {
var text = typeof n === "object" ? n.text : n;
if (!text) return undefined;
// The builder DefSideBoxes and add-examples use: the translation in slot 3, rêz=1 for a short pair.
return "\n" + wiktkuCite.buildCitation({
mode: "plain",
marker: "#:",
language: lang,
title: pageName,
quote: text,
translation: typeof n === "object" ? n.t : "",
transcription: typeof n === "object" ? n.tr : ""
});
}).join("");
}
// Formats jêder (citation) entries into "#* {{jêder…|…}}" lines; skips
// entries with no quote text. Falls back to the generic {{jêder}}
// template when no specific template was chosen.
function formatJeder(jederList, lang) {
if (!jederList || !jederList.length) return "";
return $.map(jederList, function (jeder) {
if (!jeder || !jeder.quote) return undefined;
var mode = jeder.raw ? "raw"
: jeder.jederTpl ? "jeder"
: jeder.jederCatTpl ? "category"
: "plain";
return "\n" + wiktkuCite.buildCitation({
mode: mode,
marker: "#*",
language: lang,
template: jeder.jederTpl || jeder.jederCatTpl,
fields: jeder.jederTpl ? (jeder.jêderFields || {}) : (jeder.jederCatFields || {}),
langParamKey: jeder.jederCatLangParam,
quote: jeder.quote,
transcription: jeder.quoteTr,
translation: jeder.quoteT,
source: jeder.jederManual || (mode === "plain" && !jeder.url ? "çavkanî?" : ""),
sourceURL: jeder.url
});
}).join("");
}
// True if a sense has at least one jêder entry with quote text.
function senseHasJeder(sense) {
return !!(sense.jeder && $.grep(sense.jeder, function (j) {
return j && j.quote;
}).length > 0);
}
// Builds "{{tplName|lang|part1|part2|…}}" from a sense field (kategori,
// etiket), wrapped inline (" {{…}} ") or on its own line ("\n#: {{…}}").
function buildSenseTpl(sense, field, lang, tplName) {
if (!sense[field] || !sense[field].length) return "";
var parts = sense[field].map(function (p) { return $.trim(p); })
.filter(function (p) { return p !== ""; });
if (!parts.length) return "";
var call = "{{" + tplName + "|" + lang + "|" + parts.join("|") + "}}";
return call
}
// Dever/Şîwe values only ever get their own {{dever|…}} call for ku —
// no |lang| param, since the template is Kurdish-dialect-specific. For
// every other language there's no dever template at all; see
// buildEtiketTpl() below, which folds them into {{e|}} instead.
function buildDeverTpl(sense, lang) {
if (lang !== "ku" || !sense.dever || !sense.dever.length) return "";
var parts = sense.dever.map(function (p) { return $.trim(p); })
.filter(function (p) { return p !== ""; });
if (!parts.length) return "";
return "{{dever|" + parts.join("|") + "}}";
}
// Builds the {{e|lang|…}} etîket call. For non-ku senses, Dever/Şîwe
// values are appended after the regular etîket values instead of going
// into their own {{dever|}} call (see buildDeverTpl() above).
function buildEtiketTpl(sense, lang) {
var etiket = (sense.etiket || []).slice();
if (lang !== "ku" && sense.dever && sense.dever.length) {
etiket = etiket.concat(sense.dever);
}
return buildSenseTpl({ etiket: etiket }, "etiket", lang, "e");
}
/**
* Assembles the nimûne + jêder wikitext block for a sense. Shared by
* both the template-sense and normal-mane-sense branches below.
*/
function buildCite(sense, lang) {
return formatNimune(sense.nimune, lang) +
formatJeder(sense.jeder, lang);
}
// ── Inline modifier system ───────────────────────────────────────────────
// "{{maneid|lang|x}}", the anchor id a sense can be linked by. First of the leading calls, which is
// data-main.json's leadingTplOrder.
function buildManeidTpl(sense, lang) {
var id = $.trim(sense.maneid || "");
return id ? "{{maneid|" + lang + "|" + id + "}}" : "";
}
// Quick-pick suggestions for the Dever/Şîwe, Etîket and Kategorî chip pickers on each sense, taken
// from [[Modul:labels]] rather than kept by hand here. Free text is still always accepted; this only
// saves typing, and keeps the lists honest as the module changes.
//
// Fetched on the first time one of the three panels is opened, never on load: the wizard opens on
// pages that never touch these fields. Once it lands, the open panel re-renders itself.
var wiktkuLabelSets = null;
var wiktkuLabelsPending = null;
var wiktkuLabelListeners = [];
function wiktkuEnsureLabels() {
if (wiktkuLabelSets || wiktkuLabelsPending) return;
wiktkuLabelsPending = wiktkuEdit.labels.load().then(function (sets) {
wiktkuLabelSets = sets;
wiktkuLabelsPending = null;
$.each(wiktkuLabelListeners, function (_, fn) { fn(); });
}, function () {
wiktkuLabelsPending = null;
});
}
// Which list each picker shows. Dever/Şîwe is the regions of the language being edited; Etîket adds
// the qualifiers and subject labels on top; Kategorî is the subject labels alone.
function wiktkuLabelSuggestions(which) {
if (!wiktkuLabelSets) return [];
var lang = (getValues().lang || "ku");
if (which === "dever") return wiktkuEdit.labels.regionalFor(wiktkuLabelSets, lang);
if (which === "etiket") return wiktkuEdit.labels.etiketFor(wiktkuLabelSets, lang);
return wiktkuEdit.labels.kategoriNames(wiktkuLabelSets);
}
// Lower-cased alias -> canonical label, so typing a name the module lists as an alias still finds it.
// Built once per label load rather than per keystroke.
var wiktkuAliasCache = null;
function wiktkuLabelAliases() {
if (!wiktkuLabelSets) return {};
if (!wiktkuAliasCache) {
wiktkuAliasCache = wiktkuEdit.labels.aliasIndex(wiktkuLabelSets);
}
return wiktkuAliasCache;
}
// Appends a ⊕ button to a word input <li>. Clicking it shows a compact
// modifier picker; selecting one prompts for the value and appends
// <modifier:value> to the textarea's current content.
// Sub-groups (inside a sense/definition block) that get the same ⊕
// modifier button as makeWordListStep fields.
/**
* The word lists (Hevmane, Dijmane, Formên alternatîv, Termên koordîne, Jornav, Jêrnav), taken from
* data-main.json's senseGroups so the wizard, DefSideBoxes and the wikitext writer all name them once.
* That array's own order is the order the "#:" lines are written to the page in; "order" is the
* order they are shown in, which is not the same. cls is how each one's box is found in the DOM.
*/
function wordGroups() {
return SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return { cls: "wiktku-dfn-" + g.field, label: g.label, field: g.field, izah: g.hint || "" };
});
}
/** True for the six word lists, which get the ⊕-modifier row; Nimûne and Jêgirtin do not. */
function isWordGroupCls(cls) {
var found = false;
$.each(SENSE_GROUPS, function (_, g) {
if ("wiktku-dfn-" + g.field === cls) found = true;
});
return found;
}
/**
* Wraps a textarea in a <li> with a ⊕ modifier button and returns the <li>.
*
* `ta` stays hidden and holds the raw "word<key:value>…" text every reader and writer expects. What
* is typed into is a plain word box, with one pill per modifier above it; clicking a pill edits its
* value, ⊕ adds another.
*/
function makeModifierLi(ta) {
var narrow = (ta.attr("class") || "").split(/\s+/).indexOf("wiktku-start-narrow") !== -1;
// Not ta's own classes: code elsewhere counts elements by those, so a second matching textarea
// would skew the counts. wiktku-word-input carries the same look.
var wordInput = $("<textarea></textarea>")
.attr("rows", ta.attr("rows") || 1)
.addClass("wiktku-word-input" + (narrow ? " wiktku-start-narrow" : ""))
.attr("placeholder", ta.attr("placeholder") || "");
ta.hide();
// Placeholders on the (hidden) canonical field are set by shared code that has no reason to know
// wordInput exists -- mirror them instead of touching every call site.
if (window.MutationObserver) {
new MutationObserver(function () {
wordInput.attr("placeholder", ta.attr("placeholder") || "");
}).observe(ta[0], { attributes: true, attributeFilter: ["placeholder"] });
}
// ta stays the source of truth: the row writes back to it, and the caller's growth listener is
// bound to ta's "input" event.
var row = wiktkuUtil.makeModifierRow({
prefix: "wiktku-",
wordInput: wordInput,
modTitle: "Modifiyerê lê zêde bike",
removeTitle: wiktkuUtil.msg.removeWord,
removeModTitle: wiktkuUtil.msg.removeModifier,
onChange: function (raw) {
if (raw === null) return; // the row removed itself; ta goes with it
ta.val(raw);
ta.trigger("input");
},
// "bixe qutiyan" puts each comma-separated word in a row of its own. The list grows off the
// hidden field's own "input" event, so filling the trailing row is what makes the next one
// appear; the caret ends up in the empty row at the bottom. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way -- a
// comma left inside a parameter would not be.
onSplit: function (rest) {
// A tick later, not now: the row writes its own value to the hidden field after this
// call returns, and it is that field's "input" that grows the list -- so the empty row
// to put the next word in does not exist yet.
setTimeout(function () {
var $list = row.$li.closest("ol, ul");
rest.forEach(function (raw) {
if (!raw) return;
var setRaw = $list.children("li").last().data("setRawValue");
if (setRaw) setRaw(raw);
});
$list.children("li").last().find(".wiktku-word-input").focus();
}, 0);
}
});
row.setRaw(ta.val());
var li = row.$li;
// restoreSubList's "fill an existing row" path sets a raw "word<key:value>" value here.
li.data("setRawValue", function (raw) {
row.setRaw(raw);
ta.val(row.getRaw());
ta.trigger("input");
});
li.prepend(ta);
return li;
}
/**
* A "chip + toggle" field: values render as removable chips, and a "+ <label>" toggle opens a panel
* with quick-pick pills plus free text. panel is returned apart from wrap so callers can group all
* toggles in one row and all panels below, keeping toggles from shifting when one opens.
*
* Pills carry a × as well: hiding a built-in one, or typing a new value, is remembered per wrapCls
* in mw.storage.
*
* opts: { wrapCls, toggleLabel, hint, suggestions (array or function), placeholder, onOpen }
* Returns { wrap, panel, addChip, values, setOpenDefault, close }.
*/
var WIKTKU_SUGGESTIONS_KEY_PREFIX = "wiktku_suggestions_";
function loadSuggestionOverrides(wrapCls) {
if (!(window.mw && mw.storage)) return { removed: [], added: [] };
var raw;
try { raw = mw.storage.get(WIKTKU_SUGGESTIONS_KEY_PREFIX + wrapCls); } catch (e) { raw = null; }
var parsed = {};
if (raw) { try { parsed = JSON.parse(raw); } catch (e) { parsed = {}; } }
return { removed: parsed.removed || [], added: parsed.added || [] };
}
function saveSuggestionOverrides(wrapCls, overrides) {
if (!(window.mw && mw.storage)) return;
try { mw.storage.set(WIKTKU_SUGGESTIONS_KEY_PREFIX + wrapCls, JSON.stringify(overrides)); } catch (e) { /* storage unavailable — override just won't persist */ }
}
// One document listener for every sense's "Hevmane û Bêhtir" menu, not one per sense: a long entry can hold a
// dozen senses and each would otherwise bind its own.
var moreMenuCloserBound = false;
function bindMoreMenuCloser() {
if (moreMenuCloserBound) return;
moreMenuCloserBound = true;
$(document).on("click.wiktkuMore", function (e) {
if ($(e.target).closest(".wiktku-dfn-more-wrap").length) return;
$(".wiktku-dfn-more-menu").hide();
$(".wiktku-dfn-more-arrow").html("▸");
});
}
function buildChipField(opts) {
var wrap = $("<div></div>").addClass("wiktku-dfn-chipfield-wrap " + opts.wrapCls + "-wrap");
var chipsRow = $("<span class='wiktku-dfn-chipfield-chips'></span>");
var toggle = $("<button type='button' class='wiktku-dfn-chipfield-toggle'></button>").text(opts.toggleLabel);
var panel = $("<div class='wiktku-dfn-chipfield-panel'></div>").hide();
var suggestRow = $("<div class='wiktku-dfn-chipfield-suggest'></div>");
var input = $("<input type='text' class='wiktku-dfn-chipfield-input'>")
.attr("placeholder", opts.placeholder || ((opts.suggestions && opts.suggestions.length) ? "An ya nû binivîse…" : "Binivîse…"));
var addBtn = $("<button type='button' class='wiktku-dfn-chipfield-add'>Qeyd bike</button>");
// Once the person opens/closes the panel themselves, lang-driven
// syncing (see setOpenDefault) backs off rather than fighting them.
var userToggled = false;
// The pick list shown is opts.suggestions with the person's own
// edits layered on top — see loadSuggestionOverrides above.
var overrides = loadSuggestionOverrides(opts.wrapCls);
var removedSuggestions = overrides.removed;
var addedSuggestions = overrides.added;
function persistOverrides() {
saveSuggestionOverrides(opts.wrapCls, { removed: removedSuggestions, added: addedSuggestions });
}
// opts.suggestions is either a fixed array or a function returning one -- the label-driven
// pickers depend on the language being edited, and on data that arrives from Modul:labels after
// the field is built, so they cannot be a snapshot taken here.
function baseSuggestions() {
var list = typeof opts.suggestions === "function" ? opts.suggestions() : opts.suggestions;
return list || [];
}
function allSuggestions() {
return $.grep(baseSuggestions(), function (s) { return removedSuggestions.indexOf(s) === -1; })
.concat(addedSuggestions);
}
function learnSuggestion(val) {
if (!val || allSuggestions().indexOf(val) !== -1) return;
addedSuggestions.push(val);
persistOverrides();
}
function forgetSuggestion(name) {
var idx = addedSuggestions.indexOf(name);
if (idx !== -1) addedSuggestions.splice(idx, 1);
else if (removedSuggestions.indexOf(name) === -1) removedSuggestions.push(name);
persistOverrides();
renderSuggest();
// renderSuggest() just destroyed the pill's × button the click
// came from — which was focused — dropping focus outside the
// panel entirely. Left alone, that reads as "focus left the
// widget" to the focusout handler below and closes the whole
// panel. Move focus back inside before that check runs.
input.focus();
}
function values() {
var vals = [];
chipsRow.find(".wiktku-dfn-chipfield-chip").each(function () {
vals.push($(this).attr("data-value"));
});
return vals;
}
// Label-driven pickers are built before Modul:labels has been read, so re-render once it lands.
if (typeof opts.suggestions === "function") {
wiktkuLabelListeners.push(function () {
if (panel.is(":visible")) renderSuggest();
});
}
// At most this many pills at once. Modul:labels offers 87 regions, 362 etîket and 209 kategorî;
// drawing them all was a wall of pills nobody reads and the field it belonged to scrolled away.
var MAX_PILLS = 24;
// What shows with an empty box: the short hand-picked list if the caller gave one, otherwise the
// full set (capped) as before, for pickers whose lists are small enough to browse.
function restingSuggestions() {
var quick = typeof opts.quickPicks === "function" ? opts.quickPicks() : opts.quickPicks;
if (!quick || !quick.length) {
return allSuggestions();
}
// A value the person added themselves outranks the hand-picked list -- it is theirs.
return quick.concat($.grep(addedSuggestions, function (s) { return quick.indexOf(s) === -1; }));
}
// Typed text searches the WHOLE set, not just the pills on screen, and matches the module's own
// aliases too, so "Behdînanî" finds Badînî and "USA" finds DYA.
function matching(query) {
var q = $.trim(query).toLowerCase();
if (!q) {
return restingSuggestions();
}
var aliases = wiktkuLabelAliases();
var hits = $.grep(allSuggestions(), function (name) {
return name.toLowerCase().indexOf(q) !== -1;
});
$.each(aliases, function (alias, canonical) {
if (alias.indexOf(q) !== -1 && hits.indexOf(canonical) === -1 &&
allSuggestions().indexOf(canonical) !== -1) {
hits.push(canonical);
}
});
return hits;
}
function renderSuggest(query) {
var taken = values();
suggestRow.empty();
var shown = 0;
$.each(matching(query || ""), function (_, name) {
if (taken.indexOf(name) !== -1 || shown >= MAX_PILLS) return;
shown++;
var pill = $("<span role='button' class='wiktku-dfn-chipfield-pill'></span>").text(name);
pill.on("click", function () { addChip(name); input.val(""); renderSuggest(""); input.focus(); });
var pillRemove = $("<button type='button' class='wiktku-dfn-chipfield-pill-remove' title='Vê tewsiyeyê veşêre'>×</button>");
pillRemove.on("click", function (e) {
e.stopPropagation(); // don't also trigger the pill's own add-chip click
forgetSuggestion(name);
});
pill.append(pillRemove);
suggestRow.append(pill);
});
suggestRow.toggle(!!suggestRow.children().length);
}
function addChip(val) {
val = $.trim(val || "");
if (!val || values().indexOf(val) !== -1) return;
var chip = $("<span class='wiktku-dfn-chipfield-chip'></span>").attr("data-value", val).text(val);
var remove = $("<button type='button' class='wiktku-dfn-chipfield-remove' title='Jê bibe'>×</button>");
// Same focus-loss hazard as forgetSuggestion() below — refocus
// the input so removing a chip while the panel is open doesn't
// also close the panel.
remove.on("click", function () { chip.remove(); renderSuggest(); input.focus(); });
chip.append(remove);
chip.insertBefore(toggle); // chips in insertion order, toggle stays last
renderSuggest();
}
toggle.on("click", function () {
userToggled = true;
if (panel.is(":visible")) { panel.hide(); return; }
if (opts.onOpen) opts.onOpen();
renderSuggest();
panel.show();
input.focus();
});
// Anything typed and committed by hand joins the pick list too, not
// just the current sense's chips.
function commitInput() {
var val = $.trim(input.val() || "");
if (val) {
addChip(val);
learnSuggestion(val);
}
input.val("").focus();
}
addBtn.on("click", commitInput);
input.on("keydown", function (e) {
if (e.which === 13) { e.preventDefault(); commitInput(); }
});
// Typing searches the whole list, so a value that is not among the pills is still one keystroke
// from being found rather than something you have to know by heart and type out in full.
input.on("input", function () {
renderSuggest(input.val());
});
// Close the panel once focus leaves toggle+panel together — they're
// siblings in the DOM (not nested), so both must be checked.
function focusStillInside() {
return $.contains(wrap[0], document.activeElement) || $.contains(panel[0], document.activeElement);
}
wrap.add(panel).on("focusout", function () {
setTimeout(function () {
if (!focusStillInside()) panel.hide();
}, 150);
});
if (opts.hint) panel.append($("<div class='wiktku-dfn-chipfield-hint'></div>").text(opts.hint));
// Box first, pills under it. The other way round, every keystroke re-filtered the pills above the
// box and the box itself jumped up and down the page as the list grew and shrank.
panel.append(
$("<div class='wiktku-dfn-chipfield-input-row'></div>").append(input).append(addBtn)
).append(suggestRow);
chipsRow.append(toggle);
wrap.append(chipsRow);
return {
wrap: wrap,
panel: panel,
addChip: addChip,
values: values,
/** Sets the panel's default open/closed state — a no-op once
* the person has manually toggled it themselves this session. */
setOpenDefault: function (open) {
if (userToggled) return;
// Same as the toggle click handler: (re)build the suggestion
// pills whenever the panel opens, not just on a manual click —
// otherwise a panel opened by this default-state sync (e.g.
// Dever/Şîwe starting open for ku) shows an empty pill row
// until the person closes and reopens it themselves.
if (open) {
if (opts.onOpen) opts.onOpen();
renderSuggest();
}
panel.toggle(!!open);
},
/** Force-closes the panel as though the person had done it
* themselves, so a later setOpenDefault() doesn't reopen it out
* from under them — used by onOpen to keep only one of these
* fields' panels open at a time (see the call site). */
close: function () {
userToggled = true;
panel.hide();
},
/** Opens the panel from somewhere other than the toggle — the
* "Hevmane û Bêhtir" menu, which is where Etîket and Kategorî are asked
* for now. Counts as a manual toggle for the same reason close()
* does. */
open: function () {
userToggled = true;
if (opts.onOpen) opts.onOpen();
renderSuggest();
panel.show();
input.focus();
}
};
}
/**
* One page-level word list (Formên alternatîv, Hevmane, … Herwiha binêre): a "====" section holding
* the words for the whole entry rather than for one sense.
*
* Only the question is passed in. The heading and the step's own name come from data-sections.json's
* pageSubsections, filled in below once the data lands — the same table DefSideBoxes reads to find
* these sections, so the wizard cannot write a heading the panels then fail to recognise.
*/
function makeWordListStep(id, question_fmt) {
return {
id: id,
name: id,
question_fmt: question_fmt,
multiple: true,
narrow: true,
modifiers: true,
// Function instead of a plain string so it can name the current
// language — computed fresh at build time and re-applied by
// refresh() whenever the language changes.
placeholder: function () {
var lang = getValues().lang || "";
var langLabel = lang ? getLangLabel(lang) : "";
return "Kelîme" + (langLabel ? " bi " + langLabel : "") + "…";
},
visible: function (values) {
return values.lang && values.pos;
},
output: function (values) {
var words = $.grep(values[id] || [], function (w) { return !!w; });
if (!words.length) return "";
var header = steps[id].header || capitalize(steps[id].name);
var lang = values.lang;
var section = "\n==== " + header + " ====\n";
var params = "cure=" + header + "\n" +
$.map(words, function (w) {
return "|" + w;
}).join("\n");
return section +
"{{" + WORDLIST_TEMPLATE + "|" + lang + "|" + params + "\n}}\n";
} };
}
var steps = {
lang: {
id: "lang",
required: true,
name: "ziman",
question_fmt: '"' + pageName + '" bi çi zimanî ye?',
// Populated from data-main.json (langChoices).
choices: [],
output: function (values) {
return "== {{ziman|" + values.lang + "}} ==\n";
}
},
pron: {
id: "pron",
name: "bilêvkirin",
question_fmt: '"' + pageName + '" çawa tê telafûzkirin?',
// pronTpls populated from data-main.json
pronTpls: {},
visible: function (values) {
if (!values.lang) return false;
// addLangMode: skip pron only if this lang already has a section.
if (addLangMode) {
// Clearing the page makes existing content irrelevant.
var clearPageCheck = $("#wiktku-clear-page-check");
if (clearPageCheck.length && clearPageCheck.prop("checked")) {
return true;
}
var existing = $("#wpTextbox1").val() || "";
return !findLangHeader(existing, values.lang);
}
return true;
},
// Fills the insert-dropdown from pronTpls, filtered by language.
refreshTplSelect: function () {
var sel = $("#wiktku-start-pron .wiktku-tpl-insert-select");
if (!sel.length) return;
var lang = getValues().lang || "";
sel.find("option[value!='']").remove();
$.each(steps.pron.pronTpls, function (name, tpl) {
// Filter: if tpl.langs exists, only show if lang is in that list; otherwise show always
var allowed = !tpl.langs || tpl.langs.indexOf(lang) !== -1;
if (allowed) {
sel.append($("<option></option>")
.val(tpl.snippet)
.text(name + " — " + tpl.desc)
.attr("data-nolang", tpl.noLang ? "1" : ""));
}
});
markTemplateOptions(sel, function (opt) { return tplNameFromSnippet(opt.val()); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-pron-wrap").length) return null;
var wrap = $("<div class='wiktku-pron-wrap'></div>");
// Reassures the user this field is optional — the dropdown
// below writes the template for them, no syntax needed up front.
wrap.append(
$("<p class='wiktku-step-hint'></p>").text(
"Ev qism ne mecbûrî ye. Heke tu nizanî kîjan şablonê hilbijêrî, vala bihêle."
)
);
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-pron-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-pron-text")
.attr("rows", 4)
.attr("placeholder", "Nimûne: {{IPA|ku|/pɛɪ̯n/}} yan {{kîte|ku|he|ce|ki|rin}}");
// Template insert dropdown — filtered by language.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
var lang = getValues().lang || "";
$.each(steps.pron.pronTpls, function (name, tpl) {
// Filter: if tpl.langs exists, only show if lang is in that list; otherwise show always
var allowed = !tpl.langs || tpl.langs.indexOf(lang) !== -1;
if (allowed) {
sel.append($("<option></option>")
.val(tpl.snippet)
.text(name + " — " + tpl.desc)
.attr("data-nolang", tpl.noLang ? "1" : ""));
}
});
sel.on("change", function () {
var snippet = $(this).val();
if (!snippet) return;
var lang = getValues().lang || "";
// Check if the selected option has noLang flag
var selectedOption = sel.find("option:selected");
var hasNoLang = selectedOption.attr("data-nolang") === "1";
if (!hasNoLang) {
snippet = snippet.replace(/CODE/g, lang).replace(/SOURCELANG/g, "?");
insertLineSnippet(ta[0], snippet);
$(this).val("");
// Clear the picker's search box
sel.trigger("wiktku:refresh");
ta.focus();
return;
}
// The noLang templates (ku-IPA, ku-kîte, zza-IPA, zza-kîte)
// auto-generate content from a Lua module reading the page
// title, so show a live preview and require confirmation
// before inserting.
var tplLabel = selectedOption.text();
$(this).val("");
sel.trigger("wiktku:refresh");
steps.pron.showTplPreview(stepDiv, ta, snippet, tplLabel);
});
wrap.append(sel).append(ta);
stepDiv.append(wrap);
// The palette lives in wikt.edit, shared with DefSideBoxes' own Bilêvkirin box.
wrap.append(wiktkuUtil.buildIpaPalette({
insertText: function (text, back) {
insertAtCursor(ta[0], text);
if (back) {
var pos = ta[0].selectionStart - back;
ta[0].selectionStart = ta[0].selectionEnd = pos;
}
ta.focus();
}
}));
initSelects(wrap);
return ta;
},
// ── IPA character palette ji [[MediaWiki:Gadget-charinsert-core.js]]────────────
// Modelled on fr.wiktionary's: every character is a button that
// drops itself in at the cursor. Two sub-tabs, because the full
// IPA chart is ~180 symbols and roughly twenty of them ever
// appear in Kurmancî — "IPA (kurdî)" opens by default and the
// full chart is one click away for loanwords and other
// languages. Grouped with a label down the left so it reads as
// a chart rather than one long line of glyphs.
/** Inline preview + confirm panel for a "noLang" (auto-content)
* pronunciation template — fetches what the snippet expands to
* for this page and only inserts it if the user confirms. */
showTplPreview: function (stepDiv, ta, snippet, tplLabel) {
// Only one preview panel open at a time.
stepDiv.find(".wiktku-pron-preview").remove();
var panel = $("<div class='wiktku-pron-preview'></div>");
panel.append($("<div class='wiktku-pron-preview-label'></div>")
.text("Pêşdîtina " + tplLabel + ":"));
var body = $("<div class='wiktku-pron-preview-body'></div>").text(wiktkuUtil.msg.loading);
panel.append(body);
var actions = $("<div class='wiktku-pron-preview-actions'></div>");
var confirmBtn = $("<button type='button' class='wiktku-pron-preview-confirm'>✓ Tevlî bike</button>")
.prop("disabled", true);
var cancelBtn = $("<button type='button' class='wiktku-pron-preview-cancel'></button>")
.text("✕ " + wiktkuUtil.msg.cancel);
actions.append(confirmBtn).append(cancelBtn);
panel.append(actions);
ta.before(panel);
confirmBtn.on("click", function () {
insertLineSnippet(ta[0], snippet);
panel.remove();
ta.focus();
});
cancelBtn.on("click", function () {
panel.remove();
});
// action=parse, not expandtemplates: the preview needs rendered HTML, not expanded
// wikitext. The leading "* " is dropped so it renders as a line, not a bulleted list.
new mw.Api().post({
action: "parse",
title: pageName,
text: snippet.replace(/^\*\s*/, ""),
prop: "text",
wrapoutputclass: "",
disablelimitreport: 1,
disableeditsection: 1,
contentmodel: "wikitext",
formatversion: 2
}).done(function (data) {
var html = (data && data.parse && data.parse.text) || "";
if ($.trim($("<div>").html(html).text())) {
body.html(html);
// Category links render as nothing but still exist;
// and any real link inside a preview should not
// navigate away from a half-filled form.
body.find("a").attr("target", "_blank").attr("rel", "noopener");
} else {
body.text("(vala — ji bo vê rûpelê tiştek nehate çêkirin)");
}
confirmBtn.prop("disabled", false);
}).fail(function () {
body.text("Pêşdîtin nehate barkirin — tu dikarî hîn jî wê tevlî bikî.");
confirmBtn.prop("disabled", false);
});
},
output: function () {
var text = $.trim($("#wiktku-start-pron .wiktku-pron-text").val());
if (!text) return "";
var lines = text.split("\n");
var out = $.map(lines, function (line) {
line = $.trim(line);
if (!line) return undefined;
return line;
}).join("\n");
// "===" heading under Ziman and above the POS sections; the
// field is shared across sections, so it's emitted once by
// getWikitext() rather than per-POS-section (would otherwise
// print the pronunciation twice for a two-POS entry).
return "\n=== Bilêvkirin ===\n" + out + "\n";
} },
pos: {
id: "pos",
required: true,
name: "Cureyê rêzimanî",
question_fmt: 'Cureyê rêzimanî yê "' + pageName + '" çi ye?',
visible: function (values) {
return values.lang;
},
// Populated from posChoices (data-sections.json) and posExamples (data-main.json).
choices: [],
// Definition of whichever cureyê rêzimanî is currently chosen,
// shown as plain text right under the dropdown (see the
// .wiktku-start-pos-example build/refresh() above). Kept clear of
// the Mane textarea's placeholder, which answers "what to type
// here" rather than "what this grammatical category means".
posExamples: {},
example: function (values) {
var pos = values.pos || "";
return steps.pos.posExamples[pos] ||
"Cureyê rêzimanî yê vê kelîmeyê hilbijêre — navdêr, lêker, rengdêr hwd.";
},
output: function (values) {
var pos = values.pos || "";
var capitalizedPos = capitalize(pos);
// "form" opt-param (checked in posparams) marks this as a
// form-of/inflected entry — the section title becomes
// "Formeke <pos>ê" instead of the plain pos name. The
// template call itself still uses the unchanged pos name.
var isForm = $("#wiktku-start-posparams [data-pp-opt='form']").is(":checked");
var header = isForm
? "\n=== Formeke " + pos + "ê ===\n"
: "\n=== " + capitalizedPos + " ===\n";
// Read the live wikitext from the posparams textarea.
// Fall back to a plain {{pos|lang}} if the step hasn't been built yet.
var tplLine = $.trim($("#wiktku-start-posparams .wiktku-posparams-text").val());
if (!tplLine) {
tplLine = "{{" + pos + "|" + (values.lang || "") + "}}";
}
// {{ku-tewîn-lk}} line for xwerû Kurmancî verbs, parsed straight out of tplLine (not the posparams input fields, which a cache/section restore doesn't repopulate) once niha/borî/transitivity are all present.
var tewinLkLine = "";
if (pos === "lêker" && values.lang === "ku") {
var niha = wiktkuWikitext.callParamValue(tplLine, "niha");
var borî = wiktkuWikitext.callParamValue(tplLine, "borî");
var lekturCode = wiktkuWikitext.callParamValue(tplLine, "c");
var cParts = lekturCode ? /^(?:([xphb])-)?(n?gh)$/.exec(lekturCode) : null;
var lekturForm = cParts ? (cParts[1] || "") : "";
var lktransitivity = cParts ? cParts[2] : "";
var lkForm = lktransitivity === "gh" ? "gerguhêz" : (lktransitivity === "ngh" ? "negerguhêz" : "");
if (niha && borî && lekturForm === "x" && lkForm) {
tewinLkLine = "{{ku-tewîn-lk|" + pageName + "|form=" + lkForm + "|niha=" + niha + "|borî=" + borî + "}}\n";
}
}
// Image line (between the conjugation line, if any, and the template call)
var imageLine = steps.resim.output(values);
return header + tewinLkLine + imageLine + tplLine + "\n";
}
},
// cinsiyet (gender) — shown for navdêr/serenav only, with choices built
// from metadata[lang].g since not every language has grammatical gender.
cinsiyet: {
id: "cinsiyet",
name: "cinsiyet",
question_fmt: 'Cinsiyeta "' + pageName + '" çi ye?',
visible: function () { return false; }, // handled inside posparams
// Single source of truth for the selected gender code. The visible
// dropdown lives inside the posparams step (see steps.posparams),
// but it must always read/write THIS hidden field — not a field of
// its own — since getValuesForStep("cinsiyet") reads from here.
build: function (stepDiv) {
var field = stepDiv.find(".wiktku-start-step-field");
if (field.length) return field;
field = $("<input type='hidden'>")
.addClass("wiktku-start-step-field")
.attr("id", "wiktku-start-cinsiyet-field")
.val("");
stepDiv.append(field);
return field;
},
output: function () { return ""; } // consumed by posparams/pos.output
},
// jimar (grammatical number) — same hidden-field pattern as cinsiyet
// above, but writes into its own j= template parameter instead of
// sharing c=. Shown for navdêr/serenav only, with the same choices
// for every language — data-main.json's numberLabels (see syncHelpers).
jimar: {
id: "jimar",
name: "jimar",
question_fmt: 'Jimara "' + pageName + '" çi ye?',
visible: function () { return false; }, // handled inside posparams
build: function (stepDiv) {
var field = stepDiv.find(".wiktku-start-step-field");
if (field.length) return field;
field = $("<input type='hidden'>")
.addClass("wiktku-start-step-field")
.attr("id", "wiktku-start-jimar-field")
.val("");
stepDiv.append(field);
return field;
},
output: function () { return ""; } // consumed by posparams/pos.output
},
// Lêker transitivity — hidden step, handled inside posparams
lektur: {
id: "lektur",
name: "Cureyê lêkerê (fiîlê)",
question_fmt: "Cureyê lêkerê (fiîlê) çi ye?",
visible: function () { return false; },
output: function () { return ""; }
},
// tr — hidden step, handled inside posparams
tr: {
id: "tr",
name: "transliterasyon",
question_fmt: "Transliterasyona vê kelîmeyê",
multiple: false,
narrow: true,
visible: function () { return false; }
},
// The card itself lives in wikt.edit/posparams.js, shared with DefSideBoxes -- see there for the
// fields and the per-language gating. This is only the wizard's side: which language and part of
// speech it is for, and mirroring gender/number into the two hidden steps the wizard keeps.
posparams: {
id: "posparams",
// Fused onto the "pos" card in styles.css so the two read as one question. The "xisûsiyet"
// name is a no-CSS fallback: its h3 is hidden, and every field below is self-labelled.
name: "xisûsiyet",
question_fmt: "",
card: null,
visible: function (values) {
return !!(values.lang && values.pos);
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-posparams-step").length) return null;
var values = getValues();
steps.posparams.card = wiktkuPosparams.build({
lang: values.lang,
pos: values.pos,
onChange: refresh,
// steps.cinsiyet/steps.jimar own these hidden fields; the card reports rather than
// writes, so it needs no knowledge of them.
onSync: function (cinsiyet, jimar) {
$("#wiktku-start-cinsiyet-field").val(cinsiyet || "");
$("#wiktku-start-jimar-field").val(jimar || "");
}
});
stepDiv.append(steps.posparams.card.$element);
return null;
},
rebuildTa: function () {
if (steps.posparams.card) steps.posparams.card.rebuild();
},
/** Called from refresh() — re-gates every row for the current language/pos. */
syncHelpers: function () {
if (!steps.posparams.card) return;
var values = getValues();
steps.posparams.card.setContext(values.lang, values.pos);
},
output: function () { return ""; } // pos.output reads the textarea directly
},
dfn: {
id: "dfn",
name: "Mane",
question_fmt: function (values) {
var base = "Maneya kelîmeyê binivîse. Mane otomatîk tê lînkkirin!";
if (values.lang === "ku") {
return base + " Ji bo tercimeyan beşa 'Werger' bi kar bîne!";
}
return base;
},
multiple: true,
ordered: true,
rows: 3,
visible: function (values) {
return values.lang && values.pos;
},
// Builds one sense block (<li>): mane textarea, nimûne list, and
// sub-lists for alternatîv/hevmane/dijmane/etc.
buildSenseBlock: function () {
var block = $("<li class='wiktku-dfn-sense'></li>");
// — Mane textarea (declared first so the template handler can reference it) —
var dfnArea = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-mane-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", "wiktku-start-step-field wiktku-dfn-mane")
.attr("rows", 6)
.attr("placeholder", "Maneya vê kelîmeyê binivîse…");
// Nothing on keystrokes: only the Werger gloss list needs a live Mane edit, and rebuilding
// it re-reads every sense. Deferred to blur; the wikitext is read fresh on output anyway.
dfnArea.on("blur", function () { refresh(); });
// — Îzaha maneyî (optional second half of the mane) —
// A non-Kurdish entry's gloss is a translation followed by {{îzah|…}} explaining it. The
// mane box is always the mane box and the explanation opens under it when asked for, so
// the pair reads as one gloss rather than as a different kind of field. getSenses() joins
// the two back together, so every consumer downstream sees one gloss string.
var izahArea = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-izah-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", "wiktku-dfn-subfield wiktku-dfn-izah")
.attr("rows", 2)
.attr("placeholder", "Îzaha maneyî… Ji bo school-a îngilîzî li vir 'cihê tehsîlgirtinê' binivîse");
var izahWrap = $("<div class='wiktku-dfn-izah-wrap'></div>")
.append($("<span class='wiktku-dfn-sublabel'></span>")
.append($("<span class='wiktku-dfn-sublabel-term'></span>").text("Îzaha maneyî")).append(": "))
.append(izahArea)
.hide();
// Opening it from a restore, where the text decides rather than a click.
var izahOpen = false;
function setIzahOpen(open) {
izahOpen = !!open;
updateManeMode();
}
block.data("setIzahOpen", setIzahOpen);
izahArea.on("input", function () {
// dfnArea drives the live preview and the auto-linker; the îzah is part of the gloss,
// so a change to it has to reach them too.
dfnArea.trigger("input");
});
izahArea.on("blur", function () { refresh(); });
// The mane box gives way to the template title box when a template is chosen; the îzah
// goes with it, since a template gloss has no separate explanation.
function updateManeMode() {
var isTpl = !!tplSelect.val();
if (isTpl) {
dfnArea.hide();
izahWrap.hide();
return;
}
dfnArea.show();
dfnArea.attr("placeholder", manePlaceholder(getValues().lang || ""));
// {{îzah}} explains a translation, so it belongs to entries in another language; a
// Kurdish sense is its own definition and is not offered one. A box that already holds
// text stays on screen whatever the language -- getSenses() joins it into the gloss
// either way, and hiding it would make that happen unseen.
var lang = getValues().lang || "";
var offer = !!lang && lang !== "ku";
// A non-Kurdish gloss is a translation and its explanation, so the box opens with the
// mane box rather than waiting to be asked for.
var open = offer || izahOpen || $.trim(izahArea.val()) !== "";
izahWrap.toggle(open);
}
// Store on the block so refresh() can re-sync every block when
// lang changes, and restoreDfn() can re-sync after restoring.
block.data("updateManeMode", updateManeMode);
// — Template selector (optional — replaces mane when chosen) —
// Template definitions loaded from data-main.json.
var tplDefs = steps.dfn.tplDefs || {};
// Cache of all non-placeholder option elements, so updateTplOptions
// can detach/re-attach them without losing the full list.
var _allTplOptions = [];
var tplSelect = $(mw.html.element("select", { "class": "wiktku-dfn-tpl-select" }));
tplSelect.attr("data-placeholder", wiktkuUtil.msg.searchTemplate);
tplSelect.append($("<option value=''>— yan jî şablonek hilbijêre —</option>"));
$.each(tplDefs, function (name, tpl) {
// Shows "name — tpl.desc" when a tplDefs entry has a desc
// in data-main.json, same pattern as steps.pron's template picker.
var opt = $("<option></option>")
.val(name)
.text(tpl.desc ? name + " — " + tpl.desc : name)
.attr("data-langs", tpl.langs ? tpl.langs.join(",") : "")
.attr("data-nolang", tpl.noLang ? "1" : "");
tplSelect.append(opt);
_allTplOptions.push(opt[0]);
});
// Same status-dot / 🔗 / ⓘ row treatment as every other
// template picker. This one carries the bare name in value=,
// so the name mapper is the identity rather than a snippet
// parse; descriptions come from each template's TemplateData.
markTemplateOptions(tplSelect, function (opt) { return opt.val(); });
function updateTplOptions() {
var lang = getValues().lang || "";
var currentVal = tplSelect.val();
var currentStillValid = false;
// Detach all non-placeholder options, then re-attach only the visible
// ones — native <select> won't reliably hide options any other way.
tplSelect.find("option[value!='']").detach();
$.each(_allTplOptions, function (_, opt) {
var allowed = $(opt).attr("data-langs");
var visible = !allowed || allowed.split(",").indexOf(lang) !== -1;
if (visible) {
tplSelect.append(opt);
if ($(opt).val() === currentVal) currentStillValid = true;
}
});
// Options that were detached when the descriptions arrived
// missed that pass, so describe whatever is attached now.
// The cache is warm by this point, so this is synchronous.
applyTplDescriptions(tplSelect);
tplSelect.trigger("wiktku:refresh");
if (currentVal && !currentStillValid) {
tplSelect.val("");
tplTitleInput.hide().val("");
updateManeMode();
}
}
// Store updater on the element so refresh() can call it for all blocks.
tplSelect.data("updateOptions", updateTplOptions);
var tplTitleInput = $("<input type='text'>")
.addClass("wiktku-dfn-tpl-title wiktku-start-narrow")
.attr("placeholder", "Navê rûpela hedefê…")
.hide();
// Same reasoning as dfnArea above: refresh() (which rebuilds the
// Werger/Bi zaravayên din gloss list) is deferred to blur
// rather than run on every keystroke.
tplTitleInput.on("blur", function () { refresh(); });
// Şablon senses have no plain gloss, so the word-list groups (hevmane, dijmane, formên
// alternatîv…) make no sense for them — the class hides both those groups and their
// entries in the "Hevmane û Bêhtir" menu, so they cannot be asked for either. Etîket, Kategorî,
// Nimûne and Jêgirtin stay available.
function updateFieldVisibility() {
block.toggleClass("wiktku-sense-is-tpl", !!tplSelect.val());
}
// Store on the block so restoreDfn() can re-sync visibility
// after setting tplSelect's value.
block.data("updateFieldVisibility", updateFieldVisibility);
tplSelect.on("change", function () {
var chosen = $(this).val();
if (chosen) {
dfnArea.hide();
izahWrap.hide();
tplTitleInput.show().focus();
} else {
tplTitleInput.hide().val("");
updateManeMode();
}
updateFieldVisibility();
// Re-run refresh so the Werger step visibility updates.
refresh();
});
// The template picker is an alternative to writing a
// definition, so this quiet link sits above the Mane box
// rather than getting buried under Dever/Etîket/Kategorî.
// It only expands for the minority of senses that are a
// pointer at another word (guharto, şaşnivîs, kevnbûyî)
// rather than a meaning.
var tplRow = $("<div></div>").addClass("wiktku-dfn-tpl-row")
.append(tplSelect).append(" ").append(tplTitleInput)
.hide();
var tplLink = $("<a href='#' class='wiktku-dfn-tpl-link'></a>")
.text("Maneyê bi şablonan çêke (rastnivîs, şaşnivîs, awayekî din)");
tplLink.on("click", function (e) {
e.preventDefault();
tplRow.show();
$(this).hide();
tplRow.find("input.wiktku-ac-input").first().focus();
});
/** Opens the picker without a click — used on restore, when a
* sense already has a template and hiding it would make the
* chosen value invisible. */
function revealTplRow() { tplRow.show(); tplLink.hide(); }
block.data("revealTplRow", revealTplRow);
// Dever/Şîwe, Etîket and Kategorî as chip+toggle fields, so a sense that leaves them empty
// stays short. getOutput() folds Dever/Şîwe into {{dever|}} for ku, {{e|}} otherwise.
// Each closes the other two on open; onOpen runs from a click, so forward references hold.
var deverField = buildChipField({
wrapCls: "wiktku-dfn-dever",
toggleLabel: "+ Dever/Şîwe",
hint: "Bi vê maneyê li kû derê an di kîjan devokê de tê bikaranîn?",
// Kurmancî's pills are the handful in data-main.json; another language gets the regions
// Modul:labels tags for it. The rest are found by typing.
quickPicks: function () {
var lang = getValues().lang || "ku";
if (lang === "ku") return DEVER_QUICK_PICKS;
return wiktkuLabelSets ? wiktkuEdit.labels.taggedRegionsFor(wiktkuLabelSets, lang) : [];
},
suggestions: function () { return wiktkuLabelSuggestions("dever"); },
onOpen: function () { etiketField.close(); kategoriField.close(); wiktkuEnsureLabels(); }
});
block.data("addDeverChip", deverField.addChip);
var etiketField = buildChipField({
wrapCls: "wiktku-dfn-etiket",
toggleLabel: "+ Etîket",
hint: "xisûsen, Mêrdîn, anatomî, Badînî",
suggestions: function () { return wiktkuLabelSuggestions("etiket"); },
onOpen: function () { deverField.close(); kategoriField.close(); wiktkuEnsureLabels(); }
});
block.data("addEtiketChip", etiketField.addChip);
var kategoriField = buildChipField({
wrapCls: "wiktku-dfn-kategori",
toggleLabel: "+ Kategorî",
hint: "wekî etîket lê xuya nabe tenê kategorîze dike",
suggestions: function () { return wiktkuLabelSuggestions("kategori"); },
onOpen: function () { deverField.close(); etiketField.close(); wiktkuEnsureLabels(); }
});
block.data("addKategoriChip", kategoriField.addChip);
// Maneid -- the anchor another entry links a sense by, written "{{maneid|lang|x}}" at the
// head of the line. One value rather than a list, so it is a plain box and not a chip
// field; it still behaves like one to the "Hevmane û Bêhtir" menu through the small api below.
var maneidInput = $("<input type='text'>")
.addClass("wiktku-dfn-maneid-input wiktku-start-narrow")
.attr("placeholder", "id");
var maneidWrap = $("<div class='wiktku-dfn-chipfield-wrap wiktku-dfn-maneid-wrap'></div>")
.append($("<label class='wiktku-dfn-maneid-label'></label>")
.text("Maneid ")
.append(maneidInput))
.hide();
var maneidField = {
wrap: maneidWrap,
open: function () { maneidWrap.show(); maneidInput.focus(); },
values: function () {
var v = $.trim(maneidInput.val() || "");
return v ? [v] : [];
}
};
// Dever/Şîwe starts open for ku (where it's the common case)
// and closed otherwise; backs off once the person toggles it
// themselves (see buildChipField's setOpenDefault). Re-synced
// from refresh() whenever lang changes — see block.data below.
//
// Shown only for a language with regions: every Kurdish variety, and any other language
// Modul:labels tags one for (read once the labels arrive). One that already has a value stays.
function updateDeverOpenState() {
var lang = getValues().lang || "";
var hasRegions = !lang || wiktkuEdit.labels.isKurdish(lang) ||
(wiktkuLabelSets && wiktkuEdit.labels.regionalFor(wiktkuLabelSets, lang).length > 0);
if (!hasRegions && !wiktkuLabelSets) wiktkuEnsureLabels();
var show = hasRegions || deverField.values().length > 0;
deverField.wrap.toggle(show);
if (!show) deverField.panel.hide();
deverField.setOpenDefault(lang === "ku");
}
block.data("updateDeverOpenState", updateDeverOpenState);
wiktkuLabelListeners.push(updateDeverOpenState);
updateDeverOpenState();
// Toggles always sit together in one row; panels render in their
// own area below so opening one never displaces the others.
// The widget is still built on a reconstruction page so every reference to it stays valid;
// it just never reaches the DOM, so it shows nothing and contributes nothing to getOutput().
//
// The row is the same shape DefSideBoxes puts under a sense -- a few chips for the common
// things, everything else behind "Hevmane û Bêhtir" -- so the two gadgets are learned once. Etîket
// and Kategorî start in the menu and their toggle only joins the row once picked.
var chipFieldRow = $("<div class='wiktku-dfn-chipfield-row'></div>");
var chipFieldPanels = $("<div class='wiktku-dfn-chipfield-panels'></div>");
if (!isReconstruction) {
chipFieldRow.append(deverField.wrap);
chipFieldPanels.append(deverField.panel);
}
etiketField.wrap.hide();
kategoriField.wrap.hide();
chipFieldRow.append(etiketField.wrap).append(kategoriField.wrap).append(maneidWrap);
chipFieldPanels.append(etiketField.panel).append(kategoriField.panel);
block.append(tplLink);
block.append(tplRow);
block.append(dfnArea);
block.append(izahWrap);
block.append(chipFieldRow);
block.append(chipFieldPanels);
// Apply lang filter on initial build.
updateTplOptions();
updateManeMode();
// Sub-field groups. Every one of them is built hidden and asked for by name -- from a chip
// in the row above (Nimûne, Jêgirtin) or from the "Hevmane û Bêhtir" menu -- so a sense that needs
// none of them is four boxes tall instead of ten. Jêgirtin is built after the loop as a
// compound widget and inserted after Nimûne, so the visible order matches this list.
var allGroups = [
{ cls: "wiktku-dfn-nimune", label: "Nimûne", rows: 2, narrow: false, attested: true }
].concat($.map(wordGroups(), function (g) {
return { cls: g.cls, label: g.label, rows: 1, narrow: true, izah: g.izah };
}));
if (isReconstruction) {
allGroups = $.grep(allGroups, function (g) { return !g.attested; });
}
$.each(allGroups, function (_, g) {
// wiktku-dfn-wordgroup marks the six word lists, which a şablon sense hides -- both the
// group and its entry in the menu (see the .wiktku-sense-is-tpl rules).
var groupDiv = $("<div></div>").addClass("wiktku-dfn-subgroup wiktku-dfn-subgroup-hidden " + g.cls + "-group");
if (g.cls !== "wiktku-dfn-nimune") groupDiv.addClass("wiktku-dfn-wordgroup");
// Declared once here rather than with `var` in each branch
// below — both branches assign the same pair, just with a
// different lbl and list, so one declaration avoids the
// (harmless but lint-flagged) re-declaration.
var lbl, list;
// The language the example is to be written in, named in the box itself. Read fresh
// rather than fixed at build time: the entry's language is often chosen later.
function nimunePlaceholder() {
var code = getValues().lang || "";
return wiktkuUtil.examplePrompt(code && getLangLabel(code), false);
}
function buildNimuneLi() {
var lang = getValues().lang || "";
var li = $("<li></li>").addClass("wiktku-dfn-nimune-li");
var mainTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-text-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", g.rows)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-text")
.attr("placeholder", nimunePlaceholder());
mainTa.on("input", function () {
highlightLinebreak(this);
var val = $.trim($(this).val());
var isLast = li.is(list.children("li:last-child"));
if (val && isLast) {
list.append(buildNimuneLi());
}
});
li.append(mainTa);
var subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
if (needsTransliteration(lang)) {
subWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-tr")
.attr("placeholder", "Transliterasyona bi alfabeya latînî")
);
}
if (lang) {
subWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-t")
.attr("placeholder", nimuneTranslationPrompt(lang))
);
}
if (subWrap.children().length) li.append(subWrap);
return li;
}
// The placeholder names the current language, but that's
// often not chosen yet when this block is first built —
// so keep a function around that refresh() (and onSubInput,
// for newly-grown fields) can call again once lang is set
// or changes.
function subPlaceholder() {
if (g.noLangSuffix) return g.label + "…";
var lang = getValues().lang || "";
var langLabel = lang ? getLangLabel(lang) : "";
return g.label + (langLabel ? " bi " + langLabel : "") + "…";
}
function updateSubPlaceholders() {
list.find("> li > textarea.wiktku-dfn-subfield").attr("placeholder", subPlaceholder());
}
if (g.cls === "wiktku-dfn-nimune") {
// Nimûne — compound widget with tr/t sub-fields.
lbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text(g.label))
.append(": ");
list = $("<ol></ol>").addClass("wiktku-dfn-sublist " + g.cls + "-list");
groupDiv.data("buildNimuneLi", buildNimuneLi);
// Refreshed with the word groups' own boxes when the entry's language changes.
list.data("updatePlaceholders", function () {
list.find("> li > textarea.wiktku-dfn-nimune-text")
.attr("placeholder", nimunePlaceholder());
});
list.append(buildNimuneLi());
groupDiv.append(lbl).append(list);
block.append(groupDiv);
} else {
// Regular always-visible group — label + growing textarea list.
lbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text(g.label))
.append(": ")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-hint").text(g.izah));
var useModifiers = isWordGroupCls(g.cls);
list = $("<ol></ol>").addClass("wiktku-dfn-sublist " + g.cls + "-list");
list.data("updatePlaceholders", updateSubPlaceholders);
var firstInput = $("<textarea></textarea>")
.attr("name", g.cls + "-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", g.rows)
.attr("class", "wiktku-dfn-subfield" + (g.narrow ? " wiktku-start-narrow" : ""))
.attr("placeholder", subPlaceholder());
firstInput.on("input", function () {
steps.dfn.onSubInput(list, this, g.rows, g.narrow, useModifiers);
});
list.append(useModifiers ? makeModifierLi(firstInput) : $("<li></li>").append(firstInput));
groupDiv.append(lbl).append(list);
block.append(groupDiv);
}
});
// — Jêder group — growing list, same structure as Nimûne. —
if (!isReconstruction) (function () {
var jederGroupDiv = $("<div></div>").addClass("wiktku-dfn-subgroup wiktku-dfn-subgroup-hidden wiktku-dfn-jeder-group");
var jederLbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text("Jêgirtin"))
.append(": ");
var jederList = $("<ol></ol>").addClass("wiktku-dfn-sublist wiktku-dfn-jeder-list");
// Refreshed with every other sub-box when the entry's language changes.
jederList.data("updatePlaceholders", function () {
jederList.find("> li > textarea.wiktku-dfn-nimune-quote")
.attr("placeholder", jederPlaceholder());
});
// Same shape as the nimûne box: the language belongs in the prompt, and is read fresh
// because the entry's language is often chosen after the block was built.
function jederPlaceholder() {
var code = getValues().lang || "";
return wiktkuUtil.examplePrompt(code && getLangLabel(code), true);
}
function buildJederLi() {
var lang = getValues().lang || "";
var li = $("<li></li>").addClass("wiktku-dfn-jeder-li");
// Quote text textarea
var quoteTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote")
.attr("placeholder", jederPlaceholder());
quoteTa.on("input", function () {
highlightLinebreak(this);
var val = $.trim($(this).val());
var isLast = li.is(jederList.children("li:last-child"));
if (val && isLast) {
jederList.append(buildJederLi());
}
});
li.append(quoteTa);
// tr/t sub-fields for the quote — shown as needed for the language.
var quoteSubWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
if (needsTransliteration(lang)) {
quoteSubWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-tr")
.attr("placeholder", "Transliterasyona jêgirtinê…")
);
}
if (lang && lang !== "ku") {
quoteSubWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-t")
.attr("placeholder", "Tercimeya jêgirtinê bi kurmancî")
);
}
if (quoteSubWrap.children().length) li.append(quoteSubWrap);
// The source, in the add-examples form's order and wording: the template, then the link
// with its Citoid button, then the template's own fields.
var jederDefs = steps.dfn.jederDefs || {};
var srcSel = $("<select></select>").addClass("wiktku-dfn-quote-src-sel wiktku-plain-select");
// "" is auto-detect: a Citoid fetch picks the template.
srcSel.append($("<option value=''></option>").text(wiktkuUtil.msg.citoidAuto));
srcSel.append($("<option></option>").val("__manual__").text(wiktkuUtil.msg.sourceByHand));
$.each(jederDefs, function (tplName, def) {
srcSel.append($("<option></option>").val(tplName).text(def.label));
});
srcSel.append($("<option></option>").val("__category__").text(wiktkuUtil.msg.sourceFromCategory));
// Paste a citation built on another page into the quote box and have it saved exactly
// as typed -- the quickest way to reuse one jêgirtin across many entries.
srcSel.append($("<option></option>").val("__raw__").text(wiktkuUtil.msg.sourceRaw));
var srcSelWrap = $("<div></div>").addClass("wiktku-dfn-quote-field")
.append($("<label></label>").text("Şablona çavkaniyê"), srcSel);
// Saved as url= in every mode but Kategorî and raw, which have no box for it.
var urlLbl = $("<label></label>");
var urlInput = $("<input type='text'>").addClass("wiktku-dfn-quote-url");
var citoidBtn = new OO.ui.ButtonWidget({
label: wiktkuUtil.msg.citoidFill,
classes: [ "wiktku-dfn-quote-citoid-btn", "wiktku-add-sense-btn" ]
});
var citoidStatus = $("<span></span>").addClass("wiktku-dfn-quote-citoid-status");
var urlHint = $("<p></p>").addClass("wiktku-step-hint wiktku-dfn-quote-url-hint");
var citoidWrap = $("<div></div>").addClass("wiktku-dfn-quote-field wiktku-dfn-quote-citoid")
.append(urlLbl, urlInput, citoidBtn.$element, citoidStatus, urlHint);
// The add-examples form's labels: plain "attach a link" wording for a hand-written
// source, Citoid wording otherwise.
function setUrlWording(chosen) {
if (chosen === "__manual__") {
urlLbl.text("Adresa webê ya nimûneyê");
urlInput.attr("placeholder", "URLyeke wekî https://…");
urlHint.text("Tenê binivîse eger lînk jixwe di referansa çavkaniyê de tinebe.");
} else {
urlLbl.text("Lînk, DOI an ISBNya çavkaniyê");
urlInput.attr("placeholder", "URLyek wekî https://… an jî DOI/ISBN");
urlHint.text(chosen
? "Ev dibe lînka çavkaniyê (url=). Ji bo şablonên jêder-*, dikare bi Citoid an bi destan jî bên dagirtin."
: "Li qutiyê URL, DOI an ISBN binivîse û pê lê bike butona '" + wiktkuUtil.msg.citoidFill + "' da ku otomatîk çavkanî were dagirtin.");
}
}
setUrlWording("");
// Fills only empty boxes, so a repeat fetch never overwrites what was typed by hand.
function fillEmptyFields(fields) {
$.each(fields, function (key, val) {
var input = srcFields.find("[data-field-key='" + key + "']");
if (input.length && !$.trim(input.val())) {
input.val(val);
}
});
}
// Prefills Dem from the link's own "t=" once jêder-vîdeo is active.
function autofillVideoTimestamp() {
var citoid = window.wikt && window.wikt.citoid;
var dem = srcFields.find("[data-field-key='dem']");
if (srcSel.val() !== "jêder-vîdeo" || !dem.length || $.trim(dem.val()) ||
!(citoid && citoid.extractVideoTimestamp)) {
return;
}
var t = citoid.extractVideoTimestamp($.trim(urlInput.val()));
if (t) dem.val(t);
}
function runCitoidFetch() {
var query = $.trim(urlInput.val());
if (!query) {
mw.notify(wiktkuUtil.msg.citoidNeedsUrl);
return;
}
citoidBtn.setDisabled(true);
citoidStatus.text("Tê barkirin…");
// Through wikt.cite rather than straight to the citoid gadget: the wrapper adds what
// Citoid returns but citoid-helper's mapping drops -- the access date, the ISSN and
// the DOI.
wiktkuCite.fetchFromCitoid(query).then(function (result) {
// Citoid picks the template only while none is chosen; a hand-picked one is kept.
var current = srcSel.val();
if (!current && jederDefs[result.jederKey]) {
srcSel.val(result.jederKey).trigger("change");
current = result.jederKey;
}
if (jederDefs[current]) {
// Citoid shaped its fields for the type it detected; re-derived for another.
fillEmptyFields(current === result.jederKey ? result.fields
: wiktkuCite.citoidFieldsFor(current, result.raw, query));
autofillVideoTimestamp();
} else {
// No template for this kind of source: a hand-written source line from what
// Citoid returned, only while that box is still empty.
if (current !== "__manual__") {
srcSel.val("__manual__").trigger("change");
}
var manualField = srcFields.find(".wiktku-dfn-quote-src-manual");
if (!$.trim(manualField.val())) {
var parts = [];
$.each(["nivîskar", "sernav", "websîte", "kovar", "weşanxane", "sal"], function (_, k) {
if (result.fields[k]) parts.push(result.fields[k]);
});
manualField.val(parts.join(", "));
}
}
citoidStatus.text(wiktkuUtil.msg.citoidFilled);
}, function (errorMsg) {
// Citoid cannot read some sites (x.com): Websîte with a guessed name instead.
var citoid = window.wikt && window.wikt.citoid;
var site = citoid && citoid.websiteNameForUrl && citoid.websiteNameForUrl(query);
if (site && jederDefs["jêder-websîte"]) {
if (srcSel.val() !== "jêder-websîte") {
srcSel.val("jêder-websîte").trigger("change");
}
fillEmptyFields({ "websîte": site });
citoidStatus.text("Citoid nikare vê malperê bixwîne; şablona Websîte hate hilbijartin, ji kerema xwe mayî bi destan tijî bike.");
return;
}
citoidStatus.text(typeof errorMsg === "string" ? errorMsg : "Nehat dîtin. Kontrol bike ka URL/DOI/ISBN rast e.");
}).always(function () {
citoidBtn.setDisabled(false);
});
}
citoidBtn.on("click", runCitoidFetch);
urlInput.on("input", autofillVideoTimestamp);
urlInput.on("keydown", function (e) {
if (e.which === 13) { // Enter
e.preventDefault();
runCitoidFetch();
}
});
// Dynamic source fields container.
var srcFields = $("<div></div>").addClass("wiktku-dfn-quote-src-fields");
// "Malûmatên çavkaniyê" over a jêder-* template's boxes, with the button that empties
// them and the link.
var clearBtn = new OO.ui.ButtonWidget({
label: "Vala bike",
framed: false,
title: "Lînkê û qadên jêder-* yên vê beşê vala bike",
classes: [ "wiktku-dfn-quote-src-clear" ]
});
clearBtn.on("click", function () {
urlInput.val("");
citoidStatus.text("");
srcFields.find(".wiktku-dfn-quote-src-input").val("");
});
var srcHead = $("<div></div>").addClass("wiktku-dfn-quote-src-head")
.append($("<span></span>").addClass("wiktku-dfn-quote-src-head-label").text("Malûmatên çavkaniyê"),
clearBtn.$element)
.hide();
// One labelled box for a citation parameter. Wide/narrow/medium layout: sernav-type
// fields get a full-width multiline box, short ones like sal/rûpel/isbn stay narrow.
// Sizes come from wikt.cite, so a Kategorî:Çavkanî template that happens to use the
// same parameter names as jêder-* is laid out the same way rather than generically.
function buildSrcFieldRow(field) {
var size = wiktkuCite.fieldSize(field.key);
var width = field.width || size.width || "medium";
var multiline = field.multiline !== undefined ? field.multiline : size.multiline;
var row = $("<div></div>")
.addClass("wiktku-dfn-quote-src-row wiktku-dfn-quote-src-row--" + width);
var fieldLbl = $("<label></label>").text(field.label + ": ");
var inp = $(multiline ? "<textarea></textarea>" : "<input type='text'>")
.addClass("wiktku-dfn-quote-src-input")
.attr("data-field-key", field.key)
.attr("placeholder", field.label + "…");
if (multiline) {
inp.attr("rows", 2).addClass("wiktku-dfn-quote-src-input--title");
}
// sal and tarîx say the same thing twice and Modul:quote refuses both; filling
// one clears the other rather than letting the citation break on save.
var partner = wiktkuCite.exclusivePartner(field.key);
if (partner) {
inp.on("input", function () {
if (!$.trim($(this).val())) return;
$(this).closest(".wiktku-dfn-quote-src-fields")
.find(".wiktku-dfn-quote-src-input[data-field-key='" + partner + "']")
.val("");
});
}
return row.append(fieldLbl).append(inp);
}
srcSel.on("change", function () {
srcFields.empty();
var chosen = $(this).val();
// As in the add-examples form: no link for Kategorî or raw, no Citoid for a
// hand-written source, and the fields' header only over a jêder-* template.
citoidWrap.toggle(chosen !== "__category__" && chosen !== "__raw__");
citoidBtn.toggle(chosen !== "__manual__");
citoidStatus.text("");
setUrlWording(chosen);
srcHead.toggle(!!jederDefs[chosen]);
if (!chosen) {
return;
}
if (chosen === "__raw__") {
// Nothing to fill in: the quote box above already holds the finished
// template, and it is written out exactly as typed. Only the entry name
// inside its jêgirtin= is emboldened (see wikt.cite's boldQuoteParam).
srcFields.append(
$("<p></p>")
.addClass("wiktku-step-hint wiktku-dfn-quote-raw-hint")
.text("Şablona temam li qutiya jêgirtinê ya jorê binivîse — wek xwe tê qeydkirin.")
);
return;
}
if (chosen === "__manual__") {
// Freeform source, written by the user — becomes the 2nd
// positional param of {{jêder|lang|…}} instead of çavkanî?.
srcFields.append(
$("<textarea></textarea>")
.attr("rows", 4)
.addClass("wiktku-dfn-subfield wiktku-dfn-quote-src-manual")
.attr("placeholder", "Çavkaniyê bi destê xwe binivîse…")
);
return;
}
if (chosen === "__category__") {
// Same [[Kategorî:Çavkanî]] search widget DefSideBoxes' own Çavkanî subsection box
// uses -- picking a result switches the row into the hidden manual mode with
// {{Şablon}} pre-filled. The picked şablon becomes the citation template itself:
// its own TemplateData parameters are drawn as fields below the picker, in the
// same markup the jêder-* branch uses, so readJederList() reads both with one loop.
var catPicker = wiktkuCite.buildCategoryTemplateSearch({
placeholder: wiktkuUtil.msg.searchTemplate,
describe: false,
onPick: function (entry) {
li.data("catTemplate", entry.target);
li.data("catLangParam", null);
catPicker.$status.text("Qutiyên şablonê tên barkirin…");
srcFields.find(".wiktku-dfn-quote-src-row").remove();
wiktkuCite.fieldSpecs(entry.target).then(function (spec) {
catPicker.$status.text("");
li.data("catLangParam", spec.langParamKey || null);
$.each(spec.fields, function (_, field) {
srcFields.append(buildSrcFieldRow(field));
});
}, function () {
catPicker.$status.text("Qutiyên şablonê nehatin anîn.");
});
}
});
srcFields.append(catPicker.$element);
return;
}
var def = jederDefs[chosen];
if (!def) return;
$.each(def.fields, function (_, field) {
srcFields.append(buildSrcFieldRow(field));
});
autofillVideoTimestamp();
});
// Exposed so draft restore can rebuild a saved category template's fields.
// A whole citation line pasted into the quote box ("#* {{jêder|…}}") means it was
// copied off another entry; switch to raw mode rather than wrapping it in a second
// {{nimûne}}. Only while no source type has been chosen by hand.
li.find(".wiktku-dfn-nimune-quote").on("input", function () {
if (srcSel.val()) return;
if (wiktkuCite.looksLikeCitationLine($(this).val())) {
srcSel.val("__raw__").trigger("change");
}
});
li.data("buildSrcFieldRow", buildSrcFieldRow);
li.append(srcSelWrap, citoidWrap, srcHead, srcFields);
return li;
}
jederGroupDiv.data("buildJederLi", buildJederLi);
jederList.append(buildJederLi());
jederGroupDiv.append(jederLbl).append(jederList);
// Straight after Nimûne rather than last: Jêgirtin is the
// same kind of thing (an attested use of the word) and the
// two belong together — see the allGroups comment above.
var nimuneGroup = block.find(".wiktku-dfn-nimune-group");
if (nimuneGroup.length) jederGroupDiv.insertAfter(nimuneGroup);
else block.append(jederGroupDiv);
}());
// Apply the initial hide/show state for the sub-groups (matches
// tplSelect's starting value, which is normally empty).
updateFieldVisibility();
// ── Which sub-field goes where: chips in the row above for the two everyday ones, the rest
// behind "Hevmane û Bêhtir". Same arrangement as DefSideBoxes' own sense menu, deliberately -- a
// person who has used one gadget should not have to learn the other.
(function () {
var PICK_DEFS = [
{ cls: "wiktku-dfn-nimune", label: "Nimûne", chip: true, attested: true },
{ cls: "wiktku-dfn-jeder", label: "Jêgirtin", chip: true, attested: true }
].concat($.map(wordGroups(), function (g) {
return { cls: g.cls, label: g.label, word: true };
}));
// Etîket and Kategorî are chip fields, not sub-groups, so they are revealed by showing
// their toggle rather than a whole box.
var CHIP_FIELD_DEFS = [
{ cls: "wiktku-dfn-etiket", label: "Etîket", field: etiketField },
{ cls: "wiktku-dfn-kategori", label: "Kategorî", field: kategoriField },
{ cls: "wiktku-dfn-maneid", label: "Maneid", field: maneidField }
];
function groupHasContent(groupDiv) {
return groupDiv.find("textarea, input").filter(function () {
return $.trim($(this).val() || "") !== "";
}).length > 0;
}
// Shows a group and takes its chip / menu entry out of the picker -- both carry the same
// data-pick, so one selector covers whichever of the two this group had.
// Which group carries no divider above it: the first one actually on screen, not the
// first in the DOM -- see the .wiktku-dfn-subgroup-first rule.
function updateFirstVisible() {
var $all = block.find(".wiktku-dfn-subgroup").removeClass("wiktku-dfn-subgroup-first");
$all.not(".wiktku-dfn-subgroup-hidden").first().addClass("wiktku-dfn-subgroup-first");
}
function revealGroup(cls, focus) {
var groupDiv = block.find("." + cls + "-group");
if (!groupDiv.length) return;
groupDiv.removeClass("wiktku-dfn-subgroup-hidden");
block.find("[data-pick='" + cls + "']").hide();
updateFirstVisible();
if (focus) groupDiv.find("textarea, input").first().focus();
}
block.data("revealSenseGroup", revealGroup);
// Hides it again and gives the picker its entry back. Anything typed is thrown away, so a
// group holding something asks first.
function hideGroup(cls) {
var groupDiv = block.find("." + cls + "-group");
if (!groupDiv.length) return;
if (groupHasContent(groupDiv) &&
!window.confirm("Tiştên di vê qutiyê de winda dibin. Dewam bike?")) {
return;
}
groupDiv.children("ol").children("li").not(":first").remove();
groupDiv.find("textarea, input").val("");
// The jêgirtin row's source <select> owns the fields under it; putting it back to ""
// is what clears them.
groupDiv.find("select.wiktku-dfn-quote-src-sel").val("").trigger("change");
groupDiv.addClass("wiktku-dfn-subgroup-hidden");
block.find("[data-pick='" + cls + "']").show();
updateFirstVisible();
}
// Drafts, restoreDfn() and the ⊕-grown lists all put content in without going through
// revealGroup(), so anything that ended up filled is shown on the next refresh pass.
function syncPicks() {
$.each(PICK_DEFS, function (_, def) {
var groupDiv = block.find("." + def.cls + "-group");
if (groupDiv.length && groupDiv.hasClass("wiktku-dfn-subgroup-hidden") &&
groupHasContent(groupDiv)) {
revealGroup(def.cls, false);
}
});
$.each(CHIP_FIELD_DEFS, function (_, def) {
if (def.field.values().length) {
def.field.wrap.show();
block.find("[data-pick='" + def.cls + "']").hide();
}
});
}
block.data("syncSensePicks", syncPicks);
// Every group gets an × in its label, so a box opened by mistake can go away again --
// the counterpart of DefSideBoxes' own box-remove link.
$.each(PICK_DEFS, function (_, def) {
var groupDiv = block.find("." + def.cls + "-group");
if (!groupDiv.length) return;
var $x = $("<button type='button' class='wiktku-dfn-subgroup-remove'>×</button>")
.attr("title", def.label + " veşêre");
$x.on("click", function (e) {
e.preventDefault();
hideGroup(def.cls);
});
groupDiv.children(".wiktku-dfn-sublabel").first().append($x);
});
// — the chips, beside Dever/Şîwe —
$.each(PICK_DEFS, function (_, def) {
if (!def.chip || (isReconstruction && def.attested)) return;
var $chip = $("<button type='button' class='wiktku-dfn-chipfield-toggle'></button>")
.attr("data-pick", def.cls)
.text("+ " + def.label);
$chip.on("click", function (e) {
e.preventDefault();
revealGroup(def.cls, true);
});
chipFieldRow.append($chip);
});
// — "Hevmane û Bêhtir" —
var moreArrow = $("<span class='wiktku-dfn-more-arrow'>▸</span>");
var moreToggle = $("<button type='button' class='wiktku-dfn-chipfield-toggle wiktku-dfn-more-toggle'></button>")
.append(document.createTextNode(wiktkuUtil.msg.moreMenu + " ")).append(moreArrow);
var moreMenu = $("<div class='wiktku-dfn-more-menu'></div>").hide();
function closeMenu() {
moreMenu.hide();
moreArrow.html("▸");
}
moreToggle.on("click", function (e) {
e.preventDefault();
$(".wiktku-dfn-more-menu").not(moreMenu).hide();
moreMenu.toggle();
moreArrow.html(moreMenu.is(":visible") ? "▾" : "▸");
});
function menuEntry(label, cls, extraCls) {
var $link = $("<a href='#' class='wiktku-dfn-more-item'></a>").text(label);
if (cls) $link.attr("data-pick", cls);
if (extraCls) $link.addClass(extraCls);
moreMenu.append($link);
return $link;
}
// Group 1: the two label fields. Their toggle only joins the chip row once asked for.
$.each(CHIP_FIELD_DEFS, function (_, def) {
var $link = menuEntry("+ " + def.label, def.cls);
$link.on("click", function (e) {
e.preventDefault();
closeMenu();
def.field.wrap.show();
def.field.open();
$link.hide();
});
});
// Group 2: the word lists. wiktku-dfn-pick-word so a şablon sense hides these entries
// along with the groups themselves.
var firstWord = true;
$.each(PICK_DEFS, function (_, def) {
if (!def.word) return;
var $link = menuEntry("+ " + def.label, def.cls,
"wiktku-dfn-pick-word" + (firstWord ? " wiktku-dfn-more-group-start" : ""));
firstWord = false;
$link.on("click", function (e) {
e.preventDefault();
closeMenu();
revealGroup(def.cls, true);
});
});
// Group 3: no "Maneê rake" counterpart -- the tab row already carries one bin for the
// open sense, and a second delete in here would be a second way to do the same thing.
var $newSense = menuEntry(wiktkuUtil.msg.addAnotherSense, null, "wiktku-dfn-more-group-start");
$newSense.on("click", function (e) {
e.preventDefault();
closeMenu();
steps.dfn.addSense($("#wiktku-start-dfn"), block);
});
chipFieldRow.append($("<span class='wiktku-dfn-more-wrap'></span>").append(moreToggle, moreMenu));
bindMoreMenuCloser();
syncPicks();
updateFirstVisible();
}());
return block;
},
/** Shows only `block` among the sense list; hides every other one. */
setOpenSense: function (stepDiv, block) {
stepDiv.find("li.wiktku-dfn-sense").each(function () {
$(this).toggleClass("wiktku-sense-collapsed", !$(this).is(block));
});
steps.dfn.renderSenseTabs(stepDiv);
},
/** Rebuilds the "Mane 1 / Mane 2 / [trash] / + Maneyeke din" tab row from the current sense list. */
renderSenseTabs: function (stepDiv) {
var tabsDiv = stepDiv.find(".wiktku-dfn-sense-tabs");
if (!tabsDiv.length) return;
tabsDiv.empty();
var blocks = stepDiv.find("li.wiktku-dfn-sense");
var openLi = null;
blocks.each(function (i) {
var li = $(this);
var isOpen = !li.hasClass("wiktku-sense-collapsed");
if (isOpen) openLi = li;
var tab = $("<span class='wiktku-dfn-sense-tab'></span>")
.text("Mane " + (i + 1))
.toggleClass("wiktku-tab-active", isOpen);
tab.on("click", function () { steps.dfn.setOpenSense(stepDiv, li); });
tabsDiv.append(tab);
});
// One delete button for the whole bar — not one per tab — so
// there's only ever a single, deliberate hit-target for a
// destructive action, positioned away from the tabs people
// click to switch senses. Always keep at least one Mane.
if (blocks.length > 1 && openLi) {
var openIdx = blocks.index(openLi);
var trash = makeTrashButton("Vê maneyê jê bibe (Mane " + (openIdx + 1) + ")");
trash.on("click", function () {
if (!window.confirm("Tu dixwazî Mane " + (openIdx + 1) + " jê bibî? Ev tişt nayê vegerandin.")) return;
steps.dfn.removeSense(stepDiv, openLi);
});
tabsDiv.append(trash.$element);
}
var addBtn = $("<button type='button' class='wiktku-add-sense'></button>")
.append($("<span class='wiktku-add-sense-plus'>+</span>"))
.append(document.createTextNode(wiktkuUtil.msg.anotherSense));
addBtn.on("click", function (e) {
e.preventDefault();
steps.dfn.addSense(stepDiv);
});
tabsDiv.append(addBtn);
},
/** Adds a sense, opens it, and hides the others. With afterBlock, the new one lands directly
* under that sense rather than at the end — "Maneyeke din zêde bike" in a sense's own
* "Hevmane û Bêhtir" menu is asking for one next to *this* meaning. */
addSense: function (stepDiv, afterBlock) {
var list = stepDiv.find("ol.wiktku-dfn-list");
var newBlock = steps.dfn.buildSenseBlock(stepDiv);
if (afterBlock && afterBlock.length) newBlock.insertAfter(afterBlock);
else list.append(newBlock);
initSelects(newBlock);
steps.dfn.setOpenSense(stepDiv, newBlock);
newBlock.find("textarea.wiktku-dfn-mane").first().focus();
refresh();
return newBlock;
},
/** Removes one sense block. Assumes the caller already confirmed
* and already checked more than one sense remains. If the
* removed sense was the open one, opens whichever sense ends up
* taking its place (or the last one, if it was the last). */
removeSense: function (stepDiv, li) {
var list = stepDiv.find("ol.wiktku-dfn-list");
var blocks = list.find("li.wiktku-dfn-sense");
var removedIdx = blocks.index(li);
var wasOpen = !li.hasClass("wiktku-sense-collapsed");
li.remove();
if (wasOpen) {
var remaining = list.find("li.wiktku-dfn-sense");
var next = remaining.eq(removedIdx);
if (!next.length) next = remaining.last();
if (next.length) steps.dfn.setOpenSense(stepDiv, next);
}
steps.dfn.renderSenseTabs(stepDiv);
refresh();
},
/** Grows a sub-list (nimûne/formên alternatîv/hevmane/dijmane/termên koordîne) on input. */
onSubInput: function (list, input, rows, narrow, modifiers) {
var val = $.trim($(input).val());
var item = $(input).parent();
var isLast = item.is(list.children("li:last-child"));
if (val && isLast) {
var next = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-sub-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", rows)
.attr("class", "wiktku-dfn-subfield" + (narrow ? " wiktku-start-narrow" : ""));
next.on("input", function () {
steps.dfn.onSubInput(list, this, rows, narrow, modifiers);
});
list.append(modifiers ? makeModifierLi(next) : $("<li></li>").append(next));
// New field starts with no placeholder — give it (and any
// others in the list) the current one.
var updatePlaceholders = list.data("updatePlaceholders");
if (typeof updatePlaceholders === "function") updatePlaceholders();
}
},
// Populated from data-main.json (tplDefs, jederDefs).
tplDefs: {},
jederDefs: {},
build: function (stepDiv) {
if (stepDiv.find("ol.wiktku-dfn-list").length) return null;
var tabsDiv = $("<div class='wiktku-dfn-sense-tabs'></div>");
stepDiv.append(tabsDiv);
var list = $("<ol class='wiktku-dfn-list'></ol>");
list.append(steps.dfn.buildSenseBlock(stepDiv));
stepDiv.append(list);
steps.dfn.renderSenseTabs(stepDiv);
return null;
},
/** Reads all sense blocks from the DOM and returns an array of sense objects. */
getSenses: function (stepDiv) {
var senses = [];
stepDiv.find("li.wiktku-dfn-sense").each(function () {
var block = $(this);
var tplSel = block.find("select.wiktku-dfn-tpl-select");
var tpl = tplSel.val() || "";
// noLang: template already encodes the language — don't add |lang| param.
var tplNoLang = tpl
? tplSel.find("option[value='" + tpl + "']").attr("data-nolang") === "1"
: false;
var tplTitle = $.trim(block.find("input.wiktku-dfn-tpl-title").val());
// The mane box and the îzah box are one gloss on the page: "tercime {{îzah|…}}". Joined
// here so everything downstream (getOutput, the linkify pass, the cache) keeps seeing a
// single string, exactly as it did when the pair was two boxes.
var mane = $.trim(block.find("textarea.wiktku-dfn-mane").val());
var izah = $.trim(block.find("textarea.wiktku-dfn-izah").val());
if (izah) {
mane = $.trim(mane + " {{îzah|" + izah + "}}");
}
function readList(cls) {
var vals = [];
// Matches only the canonical field, not the visible word-input mirror makeModifierLi() adds alongside it.
block.find("." + cls + "-list textarea.wiktku-dfn-subfield").each(function () {
var v = $.trim($(this).val());
if (v) vals.push(v);
});
return vals;
}
// Nimûne items are compound: { text, tr?, t? }
function readNimuneList() {
var items = [];
block.find(".wiktku-dfn-nimune-list li.wiktku-dfn-nimune-li").each(function () {
var text = $.trim($(this).find(".wiktku-dfn-nimune-text").val());
if (!text) return;
var obj = { text: text };
var tr = $.trim($(this).find(".wiktku-dfn-nimune-tr").val());
var t = $.trim($(this).find(".wiktku-dfn-nimune-t").val());
if (tr) obj.tr = tr;
if (t) obj.t = t;
items.push(obj);
});
return items;
}
// Jêder is a growing list of citations, one per li — mirrors readNimuneList().
function readJederList() {
var items = [];
block.find(".wiktku-dfn-jeder-list li.wiktku-dfn-jeder-li").each(function () {
var li = $(this);
var quote = $.trim(li.find(".wiktku-dfn-nimune-quote").val());
if (!quote) return;
var obj = { quote: quote };
var qtr = $.trim(li.find(".wiktku-dfn-nimune-quote-tr").val());
var qt = $.trim(li.find(".wiktku-dfn-nimune-quote-t").val());
if (qtr) obj.quoteTr = qtr;
if (qt) obj.quoteT = qt;
var tplName = li.find(".wiktku-dfn-quote-src-sel").val();
var url = tplName === "__raw__" || tplName === "__category__" ? ""
: $.trim(li.find(".wiktku-dfn-quote-url").val());
if (url) obj.url = url;
if (tplName === "__manual__") {
var manual = $.trim(li.find(".wiktku-dfn-quote-src-manual").val());
if (manual) obj.jederManual = manual;
} else if (tplName === "__raw__") {
// The quote box already holds a finished citation template; it is written
// out untouched, so there is nothing else on this row to read.
obj.raw = true;
} else if (tplName === "__category__") {
// A picked Kategorî:Çavkanî template IS the citation template, with its own
// TemplateData parameters -- rendered into the same markup the jêder-* branch
// below uses, so one loop reads both.
var catName = li.data("catTemplate");
if (catName) {
obj.jederCatTpl = catName;
var catFields = {};
li.find(".wiktku-dfn-quote-src-input").each(function () {
var ck = $(this).attr("data-field-key");
var cv = $.trim($(this).val());
if (ck && cv) catFields[ck] = cv;
});
// A restore in progress has no boxes yet -- its TemplateData is still on
// the way -- so the values it is restoring stand in until they exist.
// See restoreDfn, which parks them here and clears them once they land.
if (!Object.keys(catFields).length) {
catFields = li.data("catFields") || {};
}
obj.jederCatFields = catFields;
var catLang = li.data("catLangParam");
if (catLang) obj.jederCatLangParam = catLang;
}
} else if (tplName) {
obj.jederTpl = tplName;
var fields = {};
li.find(".wiktku-dfn-quote-src-input").each(function () {
var k = $(this).attr("data-field-key");
var v = $.trim($(this).val());
if (k && v) fields[k] = v;
});
obj.jêderFields = wiktkuCite.withLinkAsUrl(tplName, fields, url);
}
items.push(obj);
});
return items;
}
// Dever/Şîwe, Etîket and Kategorî are chip fields (buildChipField),
// not textareas, so readList() doesn't reach them — each chip's
// value lives in data-value, scoped by the field's wrap class.
function readChipList(wrapCls) {
var vals = [];
block.find("." + wrapCls + "-wrap .wiktku-dfn-chipfield-chip").each(function () {
var v = $.trim($(this).attr("data-value") || "");
if (v) vals.push(v);
});
return vals;
}
var sense = {
tpl: tpl,
tplNoLang: tplNoLang,
tplTitle: tplTitle,
mane: mane,
maneid: $.trim(block.find("input.wiktku-dfn-maneid-input").val() || ""),
etiket: readChipList("wiktku-dfn-etiket"),
kategori: readChipList("wiktku-dfn-kategori"),
nimune: readNimuneList(),
jeder: readJederList(),
dever: readChipList("wiktku-dfn-dever")
};
// alternativ, hevmane, dijmane, koordine, jornav, jernav — one property each, named by
// senseGroups' "field" so getOutput() and restoreDfn() find them under the same names.
$.each(wordGroups(), function (_, g) {
sense[g.field] = readList(g.cls);
});
senses.push(sense);
});
return senses;
},
getOutput: function (values, callback) {
var stepDiv = $("#wiktku-start-dfn");
var senses = steps.dfn.getSenses(stepDiv);
var lang = values.lang;
// A sense counts as "filled" if it has a mane, a chosen template + title,
// or any other content (etîket/hevmane/nimûne/jêder…) worth keeping.
var hasMane = senses.some(function (s) {
return s.mane !== "" || (s.tpl && s.tplTitle) || senseHasExtraContent(s);
});
var defLines = []; // one entry per kept sense (goes to linkify)
var citeLines = []; // parallel — nimûne+jêder for that same sense
var izahLines = []; // parallel — raw îzah gloss for that same sense
// True only once some sense's *actual* dfn text (post tercime/îzah
// split) is non-empty.
var hasLinkableMane = false;
$.each(senses, function (_, sense) {
// Declared once here rather than with `var` in each branch
// below — the template-sense branch returns early, so only
// one of the two assignments ever actually runs, but both
// are lexically in this same function scope.
var idx, cite;
// — Template sense (e.g. {{guharto|ku|somepage}}) —
if (sense.tpl) {
if (!sense.tplTitle) return; // skip empty template senses
var tplManeidTpl = buildManeidTpl(sense, lang);
var tplKatTpl = buildSenseTpl(sense, "kategori", lang, "K");
var tplDeverTpl = buildDeverTpl(sense, lang);
var tplLabelTpl = buildEtiketTpl(sense, lang);
var tplCall = sense.tplNoLang
? "{{" + sense.tpl + "|" + sense.tplTitle + "}}"
: "{{" + sense.tpl + "|" + lang + "|" + sense.tplTitle + "}}";
idx = defLines.length;
cite = buildCite(sense, lang);
defLines.push("# " + (tplManeidTpl ? tplManeidTpl + " " : "") + (tplKatTpl ? tplKatTpl + " " : "") + (tplDeverTpl ? tplDeverTpl + " " : "") +
(tplLabelTpl ? tplLabelTpl + " " : "") + tplCall +
(cite ? "{{wiktku-cite-" + idx + "}}" : ""));
citeLines.push(cite);
izahLines.push(""); // template senses have no îzah
return;
}
// — Normal mane sense —
var dfn = sense.mane;
if (!dfn && !senseHasExtraContent(sense)) return; // truly empty
// Pull the îzah gloss (if any) out of the combined tercime+îzah
// string — it's spliced back in separately below, after its
// own linkify pass (see izahLines / the second linkifyText call).
var izahRaw = null;
var tercimeIzah = splitTercimeIzah(dfn);
if (tercimeIzah) {
dfn = tercimeIzah.tercime;
izahRaw = stripWikilinks(tercimeIzah.izah);
}
if (dfn) {
dfn = stripWikilinks(dfn);
// Sentence-case + trailing period are for ku definitions,
// which are written as full glosses.
if (lang === "ku") {
if (dfn[0] !== dfn[0].toUpperCase()) dfn = capitalize(dfn);
if (!dfn.match(/[.…?!)\]]$/)) dfn += ".";
}
// Real prose — this is what makes the sense worth running
// through the main linkify pass.
hasLinkableMane = true;
} else if (izahRaw === null) {
// No mane at all — this is just the "{{mane?|lang}}"
// placeholder, a bare template call with no plain words
// in it. Do NOT count it as linkable: sending nothing but
// bracketed templates through the main linkify pass hangs
// the linkify gadget (see the hasLinkableMane/hasIzah
// check below).
//
// Every language, not just ku: a sense kept for its jêgirtin or word lists
// alone still needs the marker, or the line goes out as a bare "# ".
dfn = "{{mane?|" + lang + "}}";
}
var maneidTpl = buildManeidTpl(sense, lang);
var katTpl = buildSenseTpl(sense, "kategori", lang, "K");
var deverTpl = buildDeverTpl(sense, lang);
var labelTpl = buildEtiketTpl(sense, lang);
var out = "# " + (maneidTpl ? maneidTpl + " " : "") + (katTpl ? katTpl + " " : "") + (deverTpl ? deverTpl + " " : "") + (labelTpl ? labelTpl + " " : "") + dfn;
idx = defLines.length;
var hasThisIzah = izahRaw !== null && $.trim(izahRaw) !== "";
// Bare {{...}} token — like {{wiktku-cite-N}}, this survives
// linkify's first pass untouched (it treats template calls as
// opaque), then gets swapped for {{îzah|…}} once the actual
// gloss text has been linkified in its own separate pass.
if (hasThisIzah) {
out += (dfn ? " " : "") + "{{wiktku-izah-" + idx + "}}";
}
function wordLine(tpl, words) {
if (!words.length) return "";
return "\n#: {{" + tpl + "|" + lang + "|" + words.join("|") + "}}";
}
// Template name and sense property both come from senseGroups, so this and
// DefSideBoxes write the same "#:" lines.
$.each(SENSE_GROUPS, function (_, g) {
out += wordLine(g.key, sense[g.field] || []);
});
cite = buildCite(sense, lang);
defLines.push(out + (cite ? "{{wiktku-cite-" + idx + "}}" : ""));
citeLines.push(cite);
izahLines.push(hasThisIzah ? izahRaw : "");
});
// Splice citeLines back into a linkified string, replacing each
// {{wiktku-cite-N}} token with its stored nimûne/jêder block.
function restoreCites(t) {
return t.replace(/\{\{wiktku-cite-(\d+)\}\}/g, function (_, n) {
return citeLines[parseInt(n, 10)] || "";
});
}
// Runs text through the linkify gadget if it's loaded, otherwise
// just passes it through unchanged. Shared by both linkify passes
// below (main mane text, and — separately — îzah glosses).
var linkifyText = wiktkuUtil.linkifyText;
var hasIzah = izahLines.some(function (t) { return t && $.trim(t) !== ""; });
var text = defLines.join("\n") + "\n";
if (!hasMane) {
// Nothing written yet: the sense is still marked as needing one, but no example
// placeholder goes with it.
text = "# {{mane?|" + lang + "}}\n";
callback(text);
return;
}
// Runs the îzah-only linkify pass against `base` (either the raw
// text, when there's no linkable mane to run the main pass on
// first, or the already-linkified main text). Shared by both
// branches below so the token-splice logic only lives once.
function runIzahPass(base) {
var izahJoined = $.map(izahLines, function (raw, idx) {
if (!raw) return undefined;
return "{{wiktku-izah-open-" + idx + "}}" + raw + "{{wiktku-izah-close-" + idx + "}}";
}).join("\n");
linkifyText(izahJoined, function (izahLinked) {
var izahByIdx = {};
var re = /\{\{wiktku-izah-open-(\d+)\}\}([\s\S]*?)\{\{wiktku-izah-close-\1\}\}/g;
var m;
while ((m = re.exec(izahLinked)) !== null) {
izahByIdx[parseInt(m[1], 10)] = m[2];
}
var withIzah = base.replace(/\{\{wiktku-izah-(\d+)\}\}/g, function (_, n) {
var linked = izahByIdx[parseInt(n, 10)];
return linked !== undefined ? "{{îzah|" + linked + "}}" : "";
});
callback(restoreCites(withIzah));
});
}
if (!hasLinkableMane) {
if (!hasIzah) {
// All filled senses are template senses (or otherwise have
// no plain text at all) — nothing here needs linkify.
callback(restoreCites(text));
return;
}
// There's no plain mane text anywhere (e.g. tercime left blank
// with only îzah filled in), so every defLine is just a bare
// template call with nothing to word-link. Sending a line
// like "# {{wiktku-izah-0}}" through the main linkify pass
// gives it no plain words to work with, makes the linkify
// gadget hang — so skip that pass entirely and run only
// the îzah pass, splicing straight into the unlinkified text.
var loadIzahOnly = function () { runIzahPass(text); };
mw.loader.using("ext.gadget.linkify", loadIzahOnly, loadIzahOnly);
return;
}
var didTryToLoad = function () {
// First pass: the main definition text (mane), same as before. On failure the text
// comes back unchanged, so the save carries on with what the user wrote -- only the
// automatic linking is lost, and saying so is better than losing it silently.
linkifyText(text, function (mainLinked, error) {
if (error) {
mw.notify(error, { type: "warn" });
}
if (!hasIzah) {
callback(restoreCites(mainLinked));
return;
}
// Second pass: îzah glosses, linkified via their own
// separate call. Each gloss is wrapped in its own
// {{wiktku-izah-open/close-N}} pair; like the existing
// {{wiktku-cite-N}} tokens, these sit inside double
// braces so linkify leaves the tokens themselves alone —
// only the raw gloss text between each pair gets linked.
runIzahPass(mainLinked);
});
};
mw.loader.using("ext.gadget.linkify", didTryToLoad, didTryToLoad);
}
},
tewandin: {
id: "tewandin",
name: "tewandin",
question_fmt: 'Tewandina "' + pageName + '" binivîse',
visible: function (values) {
return values.lang && values.pos;
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-tewandin-wrap").length) return null;
var wrap = $("<div class='wiktku-tewandin-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-tewandin-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-tewandin-text")
.attr("rows", 4);
wrap.append(ta);
stepDiv.append(wrap);
function autoPopulate() {
if (!ta.length) return;
var vals = getValues();
var c = vals.cinsiyet || "";
var p = vals.pos || "";
if (!pageName.includes(" ") && c && GENDER_BASE_LABELS[c] &&
(p === "navdêr" || p === "serenav") && vals.lang === "ku" &&
c !== "mndudil") {
var label = (c === "mnmane") ? "mê û nêr" : GENDER_BASE_LABELS[c];
var out = "{{ku-tewîn-nav|" + label;
if (p === "serenav") out += "|y";
out += "}}";
if (!$.trim(ta.val())) ta.val(out);
}
}
// The example placeholder only fits Kurmancî nouns — showing it
// for a verb or another language would suggest the wrong template.
function updatePlaceholder() {
if (!ta.length) return;
var vals = getValues();
ta.attr("placeholder", (vals.lang === "ku" && vals.pos === "navdêr") ? "{{ku-tewîn-nav|nêr}}" : "");
}
setTimeout(function () {
autoPopulate();
updatePlaceholder();
$("#wiktku-start-form").on("change.tewandin",
"#wiktku-start-lang-field, #wiktku-start-cinsiyet-field, #wiktku-start-pos-field",
function () { autoPopulate(); updatePlaceholder(); });
}, 0);
return ta;
},
output: function () {
var manualText = $.trim($("#wiktku-start-tewandin .wiktku-tewandin-text").val());
if (manualText) {
return "\n==== Tewandin ====\n" + manualText + "\n";
}
return "";
}
},
// resim — one or more images
resim: {
id: "resim",
name: "resim",
question_fmt: "Resmekê lê zêde bike",
visible: function (values) { return !!(values.lang && values.pos); },
// The rows are wikt.edit's, shared with DefSideBoxes, which offers the same three boxes.
field: null,
build: function (stepDiv) {
if (stepDiv.find(".wiktku-resim-wrap").length) return null;
steps.resim.field = wiktkuUtil.buildImageListField({
prefix: "wiktku-",
rowClass: "wiktku-posparams-row",
inputClass: "wiktku-start-step-field wiktku-start-narrow",
pageName: pageName
});
stepDiv.append($("<div class='wiktku-resim-wrap'></div>").append(steps.resim.field.$element));
return null;
},
/** {file, caption, alt} per filled row, with the caption wrapped for a non-Kurdish entry. */
getImages: function (values) {
var lang = (values && values.lang) || getValues().lang || "";
if (!steps.resim.field) return [];
return $.map(steps.resim.field.values(), function (image) {
return {
file: image.file,
caption: wiktkuWikitext.captionForLang(image.caption, lang),
alt: image.alt,
size: image.size,
extra: image.extra
};
});
},
output: function (values) {
var images = steps.resim.getImages(values);
if (!images.length) return "";
return $.grep($.map(images, wiktkuWikitext.buildImageLine), Boolean).join("\n") + "\n";
}
},
// h-bnr — {{h-bnr|Page1|Page2|…}} prepended before the lang section (shared, not per-section)
bnrh: {
id: "bnrh",
name: "herwiha binêre (h-bnr)",
question_fmt: "Şablona h-bnr li qismê herî jor a rûpelê",
//visible: function (values) { return !!values.lang; },
multiple: true,
narrow: true,
placeholder: "Ji bo 'test' bnr 'Test', 'teşt'",
output: function (values) {
var call = wiktkuWikitext.buildHbnrCall(values.bnrh);
return call ? call + "\n" : "";
}
},
bikaranin: {
id: "bikaranin",
name: "Notên bikaranînê",
question_fmt: "Notên bikaranînê yên li ser vê kelîmeyê",
visible: function (values) {
return values.lang && values.pos;
},
multiple: false,
narrow: false,
output: function (values) {
var text = $.trim(values.bikaranin || "");
if (!text) return "";
return "\n==== Notên bikaranînê ====\n" + text + "\n";
},
},
xwendin: {
id: "xwendin",
name: "Bêhtir bixwîne",
question_fmt: "Lînkên derveyî eleqedarî vê madeyê",
visible: function (values) {
return values.lang && values.pos;
},
multiple: false,
narrow: false,
placeholder: function (values) {
if (!values || !values.lang) {
return `* {{wpedî}} ji bo ku meqaleya Wîkîpediyayê lînk bidî`;
}
const ziman = values.lang === "ku" ? "" : `|ziman=${values.lang}`;
return `* {{wpedî${ziman}}} ji bo ku meqaleya Wîkîpediyayê lînk bidî`;
},
output: function (values) {
var text = $.trim(values.xwendin || "");
if (!text) return "";
return "\n==== Bêhtir bixwîne ====\n" + text + "\n";
},
},
alfabeyendin: {
id: "alfabeyendin",
name: "Bi alfabeyên din",
question_fmt: function (values) {
var base = "Nivîsîna vê kelîmeyê bi alfabeyên din"
var langLabel = values.lang ? getLangLabel(values.lang) : "";
if (langLabel) {
return base + " ên " + langLabel;
}
return base;
},
// Latin-script Kurdish itself, and languages with only one script
// on record, don't need this — but any language metadata.json
// lists more than one script for (data-main.json's "sc") clearly does.
visible: function (values) {
if (!(values.lang && values.pos)) return false;
var lang = values.lang;
return lang === "ku" || ZARAVA_LANG_CODES.indexOf(lang) !== -1 || langHasMultipleScripts(lang);
},
multiple: false,
narrow: false,
placeholder: "Ji bo kurmancî: * {{ku-ar|şiklê alfaba erebî}}",
output: function (values) {
var text = $.trim(values.alfabeyendin || "");
if (!text) return "";
return "\n==== Bi alfabeyên din ====\n" + text + "\n";
},
},
formalt: makeWordListStep("formalt", "Formên alternatîv ên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
hevmane: makeWordListStep("hevmane", "Hevmaneyên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
dijmane: makeWordListStep("dijmane", "Dijmaneyên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
jornav: makeWordListStep("jornav", "Jornavên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
jernav: makeWordListStep("jernav", "Jêrnavên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
je: makeWordListStep("je", "Kelîmeyên jê çêbûyî binivîse"),
tekildar: makeWordListStep("tekildar", "Kelîmeyên ji aliyê etîmolojiyê ve têkildarî vê kelîmeyê binivîse"),
binereherwiha: makeWordListStep("binereherwiha", "Kelîmeyên din ên eleqedar binivîse"),
etym: {
id: "etym",
name: "etîmolojî",
question_fmt: "Etîmolojiya kelîmeyê binivîse",
// etymTpls populated from data-main.json
etymTpls: {},
visible: function (values) {
return values.lang && values.pos;
},
// Fills the insert-dropdown from the current etymTpls object.
refreshTplSelect: function () {
var sel = $("#wiktku-start-etym .wiktku-tpl-insert-select");
if (!sel.length) return;
sel.find("option[value!='']").remove();
$.each(steps.etym.etymTpls, function (name, tpl) {
sel.append($("<option></option>").val(tpl.snippet).text(name + " — " + tpl.desc));
});
markTemplateOptions(sel, function (opt) { return tplNameFromSnippet(opt.val()); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-etym-wrap").length) return null;
var wrap = $("<div class='wiktku-etym-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-etym-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-etym-text")
.attr("rows", 4)
.attr("placeholder", "Ji {{ji|en|ku|apple}}. yan rasterast binivîse…");
// Template insert dropdown — options also refreshed after data-main.json loads.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
$.each(steps.etym.etymTpls, function (name, tpl) {
sel.append($("<option></option>").val(tpl.snippet).text(name + " — " + tpl.desc));
});
sel.on("change", function () {
var snippet = $(this).val();
if (!snippet) return;
var lang = getValues().lang || "";
snippet = snippet.replace(/CODE/g, lang).replace(/SOURCELANG/g, "?");
$(this).val("");
sel.trigger("wiktku:refresh");
// Same param dialog DefSideBoxes' own Etîmolojî editor opens on a fresh insert: filled
// from the template's own TemplateData, with CODE/SOURCELANG already substituted above
// as the fields' starting values, so the user only has to replace the "?" placeholders
// rather than hunt for them in raw wikitext afterward.
wiktkuCite.openTemplateDialog(snippet).then(function (edited) {
if (!edited) return; // cancelled, or "Jê bibe" on a call that was never inserted
insertAtCursor(ta[0], edited);
ta.focus();
});
});
wrap.append(sel);
wrap.append(ta);
stepDiv.append(wrap);
initSelects(wrap);
return ta;
},
output: function (values) {
var text = $.trim($("#wiktku-start-etym .wiktku-etym-text").val());
if (text) {
return "\n==== Etîmolojî ====\n" + text + "\n";
}
// {{ji?}} (a redirect to {{bêetîmolojî}}) is only ever added automatically to Kurdish
// entries.
if (values.lang !== "ku") return "";
// Multi-word page names skip the {{ji?|lang}} placeholder entirely -- "source unknown?" doesn't apply to a phrase.
if (pageName.includes(" ")) return "";
// No etymology entered — emit {{ji?|lang}} as a placeholder.
// Skip it only when every filled sense is a şablon sense
// a completely empty entry with no şablon chosen still gets the placeholder
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasFilledTplSense = dfnSenses.some(function (s) {
return !!s.tpl && !!s.tplTitle;
});
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
if (hasFilledTplSense && !hasRegularMane) return "";
return "\n==== Etîmolojî ====\n{{ji?|" + values.lang + "}}\n";
} },
dunde: {
id: "dunde",
name: "dûnde",
question_fmt: "Eger ev kelîme ji vî zimanî ketibe zimanekî din, vî zimanî û kelîmeya wî binivîse",
visible: function (values) {
return !!values.pos;
},
disabledKeys: function (values) {
return {"navz": true, [values.lang]: true}
},
// Language dropdown + growing word-input list; a new row appends
// when the dropdown gets a value. Mirrors steps.trans, but flat.
buildRow: function (stepDiv) {
var row = $("<li class='wiktku-dunde-row wiktku-lang-row'></li>");
// Language selector — the searchable picker is applied after insertion into DOM.
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zimanekî bigere");
populateMenus(langSel, steps.dunde.keys || [], steps.dunde.disabledKeys);
// When a language is chosen, ensure a next empty row exists.
langSel.on("change", function () {
steps.dunde.onLangChange(stepDiv, row);
});
// Word list for this language.
var wordList = $("<ul class='wiktku-dunde-words wiktku-lang-row-words'></ul>");
var firstWord = steps.dunde.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
// The searchable picker is attached by initSelects() once the row
// is in the DOM (see onLangChange/build below).
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-dunde-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Kelîme…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-dunde-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.dunde.makeWordInput();
list.append($("<li></li>").append(next));
}
});
return input;
},
onLangChange: function (stepDiv, row) {
var list = stepDiv.find("ul.wiktku-dunde-list");
var isLast = row.is(list.children("li.wiktku-dunde-row:last-child"));
if (isLast) {
var newRow = steps.dunde.buildRow(stepDiv);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
// Keep disabled keys in sync across all lang selectors.
steps.dunde.syncDisabledKeys(stepDiv);
},
/** Disables already-selected languages in all other dropdowns. */
syncDisabledKeys: function (stepDiv) {
var taken = $.extend({}, steps.dunde.disabledKeys);
stepDiv.find("li.wiktku-dunde-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
stepDiv.find("li.wiktku-dunde-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
build: function (stepDiv) {
if (stepDiv.find("ul.wiktku-dunde-list").length) return null;
var list = $("<ul class='wiktku-dunde-list wiktku-lang-row-list'></ul>");
list.append(steps.dunde.buildRow(stepDiv));
stepDiv.append(list);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return null;
},
/** Reads all [langcode, words[]] rows from the DOM. */
getRows: function (stepDiv) {
var result = [];
stepDiv.find("li.wiktku-dunde-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-dunde-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
output: function () {
var stepDiv = $("#wiktku-start-dunde");
var rows = steps.dunde.getRows(stepDiv);
if (!rows.length) return "";
var lines = $.grep($.map(rows, function (row) {
return wiktkuWikitext.buildDundeLine(row[0], row[1]);
}), Boolean);
return "\n==== Dûnde ====\n" + lines.join("\n") + "\n";
}
},
zarava: {
id: "zarava",
name: "bi zaravayên din",
question_fmt: "'"+ pageName + "' di zaravayên din de çawa tê gotin?",
visible: function (values) {
return steps.trans.visible(values);
},
disabledKeys: {},
buildRow: function (group) {
var row = $("<li class='wiktku-zarava-row wiktku-lang-row'></li>");
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zaravayekî bigere");
populateMenus(langSel, steps.zarava.keys || [], steps.zarava.disabledKeys);
langSel.on("change", function () {
steps.zarava.onLangChange(group, row);
refresh();
});
var wordList = $("<ul class='wiktku-zarava-words wiktku-lang-row-words'></ul>");
var firstWord = steps.zarava.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-zarava-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Tercime…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-zarava-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.zarava.makeWordInput();
list.append($("<li></li>").append(next));
}
});
// Blur, not input -- refresh() re-syncs the whole form, same
// reasoning as the Mane textareas' blur-only refresh below.
input.on("blur", function () { refresh(); });
return input;
},
onLangChange: function (group, row) {
var list = group.find("ul.wiktku-zarava-list");
var isLast = row.is(list.children("li.wiktku-zarava-row:last-child"));
if (isLast) {
var newRow = steps.zarava.buildRow(group);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
steps.zarava.syncDisabledKeys(group);
},
/** Disables already-selected dialects in all other dropdowns within one group. */
syncDisabledKeys: function (group) {
var taken = $.extend({}, steps.zarava.disabledKeys);
group.find("li.wiktku-zarava-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
group.find("li.wiktku-zarava-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
/** Builds (or fetches) the .wiktku-zarava-group for one gloss. */
buildGroup: function (stepDiv, gi) {
var group = $("<div class='wiktku-zarava-group'></div>").attr("data-gloss-pos", gi);
var list = $("<ul class='wiktku-zarava-list wiktku-lang-row-list'></ul>");
list.append(steps.zarava.buildRow(group));
group.append(list);
stepDiv.append(group);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return group;
},
/** Syncs one .wiktku-zarava-group per current gloss (same logic as steps.trans.build). */
build: function (stepDiv) {
var glosses = steps.trans.glossList();
if (!glosses.length) {
stepDiv.find(".wiktku-zarava-group").remove();
return null;
}
var existingGroups = stepDiv.find(".wiktku-zarava-group");
if (existingGroups.length > glosses.length) {
existingGroups.slice(glosses.length).remove();
}
$.each(glosses, function (gi, gloss) {
var group = stepDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
if (!group.length) group = steps.zarava.buildGroup(stepDiv, gi);
var label = group.find(".wiktku-zarava-group-label");
if (glosses.length > 1) {
if (!label.length) {
label = $("<div class='wiktku-zarava-group-label'></div>");
group.prepend(label);
}
label.text(gloss.index + " " + gloss.text);
} else if (label.length) {
label.remove();
}
});
return null;
},
/** Reads the dialect rows for a single gloss group. */
getGroupTranslations: function (group) {
var result = [];
group.find("li.wiktku-zarava-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-zarava-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
/** Reads all rows from the DOM, grouped by gloss position. */
getTranslations: function (stepDiv) {
var result = [];
stepDiv.find(".wiktku-zarava-group").each(function () {
result.push(steps.zarava.getGroupTranslations($(this)));
});
return result;
},
output: function (values) {
if (values.lang !== "ku") return "";
var stepDiv = $("#wiktku-start-zarava");
var glosses = steps.trans.glossList();
if (!glosses.length) return "";
// A completely empty entry still gets a placeholder section
// to fill in later; an individual placeholder gloss is only
// skipped once some other sense already has real content.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
var blocks = $.map(glosses, function (gloss, gi) {
var group = stepDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
var rows = group.length ? steps.zarava.getGroupTranslations(group) : [];
if (!gloss.text && !rows.length && hasRegularMane) return undefined;
if (rows.length) {
rows.sort(function (a, b) {
var aLang = langsByCode[a[0]] || {};
var bLang = langsByCode[b[0]] || {};
return (aLang.sortKey || "").localeCompare(bLang.sortKey || "", "ku");
});
}
var wordLines = $.map(rows, function (row) {
var lang = row[0];
var words = row[1];
var wikiWords = words.map(function (w) {
return "{{W|" + lang + "|" + w + "}}";
}).join(", ");
return "* {{Z|" + lang + "}}: " + wikiWords;
}).join("\n");
var block = "{{werger-ser|" + (gloss.text || "?") + "|" + gloss.index + "}}";
if (wordLines) block += "\n" + wordLines;
block += "\n{{werger-bin}}";
return block;
});
if (!blocks.length) return "";
return "\n==== Bi zaravayên din ====\n" + blocks.join("\n") + "\n";
}
},
trans: {
id: "trans",
name: "werger",
question_fmt: "Tercimeyên vê maneyê bi zimanên din binivîse",
visible: function (values) {
if (!(values.lang === "ku" && values.pos)) return false;
// Hide only when every sense is a filled template sense (no plain
// gloss to attach translations to). A mix, or nothing filled in
// yet, still shows Werger.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRealSense = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
var hasFilledTplSense = dfnSenses.some(function (s) {
return s.tpl && s.tplTitle;
});
return hasRealSense || !hasFilledTplSense;
},
disabledKeys: {
"navz": true,
"ku": true,
"ckb": true,
"sdh": true,
"lki": true,
"hac": true,
"zza": true },
// Language dropdown + growing word-input list, scoped to one gloss's
// group (each gloss has its own group and row list).
buildRow: function (group) {
var row = $("<li class='wiktku-trans-row wiktku-lang-row'></li>");
// Language selector — the searchable picker is applied after insertion into DOM.
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zimanekî bigere");
populateMenus(langSel, steps.trans.keys || [], steps.trans.disabledKeys);
// When a language is chosen, ensure a next empty row exists.
langSel.on("change", function () {
steps.trans.onLangChange(group, row);
refresh();
});
// Word list for this language.
var wordList = $("<ul class='wiktku-trans-words wiktku-lang-row-words'></ul>");
var firstWord = steps.trans.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
// The searchable picker is attached by initSelects() once the row
// is in the DOM (see onLangChange/buildGroup below).
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-trans-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Tercime…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-trans-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.trans.makeWordInput();
list.append($("<li></li>").append(next));
}
});
// Blur, not input -- refresh() re-syncs the whole form, same
// reasoning as the Mane textareas' blur-only refresh below.
input.on("blur", function () { refresh(); });
return input;
},
onLangChange: function (group, row) {
var list = group.find("ul.wiktku-trans-list");
var isLast = row.is(list.children("li.wiktku-trans-row:last-child"));
if (isLast) {
var newRow = steps.trans.buildRow(group);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
// Keep disabled keys in sync across this gloss's lang selectors
// only — the same language can be used for different senses.
steps.trans.syncDisabledKeys(group);
},
/** Disables already-selected languages in all other dropdowns within one group. */
syncDisabledKeys: function (group) {
var taken = $.extend({}, steps.trans.disabledKeys);
group.find("li.wiktku-trans-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
group.find("li.wiktku-trans-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
// Builds (or fetches) the .wiktku-trans-group for one gloss.
buildGroup: function (stepDiv, gi) {
var group = $("<div class='wiktku-trans-group'></div>").attr("data-gloss-pos", gi);
var list = $("<ul class='wiktku-trans-list wiktku-lang-row-list'></ul>");
list.append(steps.trans.buildRow(group));
group.append(list);
stepDiv.append(group);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return group;
},
// Syncs one .wiktku-trans-group per current gloss, keyed by
// data-gloss-pos so groups stay matched across rebuilds.
build: function (stepDiv) {
var glosses = steps.trans.glossList();
if (!glosses.length) {
stepDiv.find(".wiktku-trans-group").remove();
return null;
}
// Drop groups for glosses that no longer exist (always from the
// tail — groups are only ever appended in gloss order below).
var existingGroups = stepDiv.find(".wiktku-trans-group");
if (existingGroups.length > glosses.length) {
existingGroups.slice(glosses.length).remove();
}
$.each(glosses, function (gi, gloss) {
var group = stepDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
if (!group.length) group = steps.trans.buildGroup(stepDiv, gi);
// Only label groups when there's more than one gloss to
// distinguish between — a single gloss needs no caption.
var label = group.find(".wiktku-trans-group-label");
if (glosses.length > 1) {
if (!label.length) {
label = $("<div class='wiktku-trans-group-label'></div>");
group.prepend(label);
}
label.text(gloss.index + " " + gloss.text);
} else if (label.length) {
label.remove();
}
});
return null;
},
/** Reads the translation rows for a single gloss group. */
getGroupTranslations: function (group) {
var result = [];
group.find("li.wiktku-trans-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-trans-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
// Reads all translation rows, grouped by gloss position.
getTranslations: function (stepDiv) {
var result = [];
stepDiv.find(".wiktku-trans-group").each(function () {
result.push(steps.trans.getGroupTranslations($(this)));
});
return result;
},
// Builds the numbered "{ index, gloss }" list, one per dfn sense
// with a real mane (skips template senses). index is 1-based and
// matches the sense's position in the "#" definition list.
// When nothing's filled in, returns one placeholder entry so
// Werger still has a group to attach to; when every sense is a
// filled template sense, stays empty (matches trans.visible()).
glossList: function () {
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var glosses = [];
var hasFilledTplSense = false;
$.each(dfnSenses, function (i, s) {
if (s.tpl) {
if (s.tplTitle) hasFilledTplSense = true;
return; // template sense — no gloss to caption with
}
var dfn = s.mane;
if (!dfn) {
// No mane, but other fields are filled — still gets a
// gloss group since dfn.getOutput() emits {{mane?}} for it.
if (senseHasExtraContent(s)) glosses.push({ index: i + 1, text: "" });
return;
}
if (dfn[0] !== dfn[0].toLowerCase()) dfn = decapitalize(dfn);
dfn = stripWikilinks(dfn).replace(/[.…?!)\]،؟]+$/, "");
glosses.push({ index: i + 1, text: dfn });
});
if (!glosses.length && !hasFilledTplSense) {
glosses.push({ index: 1, text: "" });
}
return glosses;
},
output: function (values) {
if (values.lang !== "ku") return "";
var stepDiv = $("#wiktku-start-trans");
var glosses = steps.trans.glossList();
// Every sense is a template sense — there's no real gloss
// anywhere and no group in the DOM either (glossList() only
// returns empty in that case), so skip the section entirely.
if (!glosses.length) return "";
// A completely empty entry (nothing filled in dfn at all, no
// şablon chosen either) still gets a placeholder Werger section
// to fill in later. Only
// skip an individual placeholder gloss below when some other
// sense actually has real content, making that particular
// placeholder redundant.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
// Each gloss now has its own group of translation rows —
// attach every gloss's own words under its own werger-ser/-bin
// pair instead of dumping everything under the first one.
var blocks = $.map(glosses, function (gloss, gi) {
var group = stepDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
var rows = group.length ? steps.trans.getGroupTranslations(group) : [];
// Placeholder gloss (no real mane entered yet) with nothing
// translated into it. If some other sense already has real
// content, this placeholder is redundant — skip it. If the
// whole entry is empty, keep it so Werger still gets a
// section to fill in.
if (!gloss.text && !rows.length && hasRegularMane) return undefined;
if (rows.length) {
// Sort by Kurdish sort key.
rows.sort(function (a, b) {
var aLang = langsByCode[a[0]] || {};
var bLang = langsByCode[b[0]] || {};
return (aLang.sortKey || "").localeCompare(bLang.sortKey || "", "ku");
});
}
var wordLines = $.map(rows, function (row) {
var lang = row[0];
var words = row[1];
var wikiWords = words.map(function (w) {
return "{{W|" + lang + "|" + w + "}}";
}).join(", ");
return "* {{Z|" + lang + "}}: " + wikiWords;
}).join("\n");
var block = "{{werger-ser|" + (gloss.text || "?") + "|" + gloss.index + "}}";
if (wordLines) block += "\n" + wordLines;
block += "\n{{werger-bin}}";
return block;
});
if (!blocks.length) return "";
return "\n==== Werger ====\n" + blocks.join("\n") + "\n";
}
},
cavkani: {
id: "cavkani",
name: "çavkanî",
question_fmt: "Çavkaniyên din (ne jêgirtin) lê zêde bike",
visible: function (values) {
return !!values.pos;
},
// Fills the insert-dropdown from the members of [[Kategorî:Çavkanî]].
refreshTplSelect: function () {
var sel = $("#wiktku-start-cavkani .wiktku-tpl-insert-select");
if (!sel.length) return;
sel.find("option[value!='']").remove();
$.each(catSrcTemplates || [], function (_, entry) {
sel.append($("<option></option>").val(entry.target).text(entry.label));
});
markTemplateOptions(sel, function (opt) { return opt.val(); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-cavkani-wrap").length) return null;
var wrap = $("<div class='wiktku-cavkani-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-cavkani-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-cavkani-text")
.attr("rows", 4)
.attr("placeholder", wiktkuUtil.msg.pickOrTypeTemplate);
// Template insert dropdown, populated from [[Kategorî:Çavkanî]] —
// options refreshed once the category members are fetched.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
var status = $("<span class='wiktku-cavkani-status'></span>");
// Same param dialog DefSideBoxes' own Çavkanî picker opens: the chosen template's
// TemplateData becomes a form, so "rûpel=", "sernav=" and the rest are filled in before
// anything lands in the box, instead of a bare {{Ç:…}} to go back and complete by hand.
sel.on("change", function () {
var name = $(this).val();
$(this).val("");
sel.trigger("wiktku:refresh");
if (!name) return;
wiktkuCite.openTemplateDialog("{{" + name + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or removed a call that was never inserted
}
insertAtCursor(ta[0], "* " + edited + "\n");
ta.trigger("focus");
});
});
wrap.append(sel);
wrap.append(status).append(ta);
stepDiv.append(wrap);
// build() runs again for every new beş (see the "+ Beşeke din"
// handler), after the initial page-wide pass — so this select needs
// its own init or it stays a plain, unsearchable native <select>.
initSelects(wrap);
if (catSrcTemplates) {
steps.cavkani.refreshTplSelect();
} else {
status.text(wiktkuUtil.msg.templatesLoading);
fetchCatSrcTemplates().then(function () {
steps.cavkani.refreshTplSelect();
status.text("");
}, function () {
status.text(wiktkuUtil.msg.templatesFailed);
});
}
return ta;
},
output: function () {
var text = $.trim($("#wiktku-start-cavkani .wiktku-cavkani-text").val());
if (!text) return "";
return "\n==== Çavkanî ====\n" + text + "\n";
}
},
};
steps.trans.keys = steps.lang.choices;
steps.dunde.keys = steps.lang.choices;
steps.zarava.keys = getZaravaKeys(steps.lang.choices);
// This array only controls DOM build order — nothing else keys off it
// (see the comment above stepIdToGroup). Mane tab order: Mane (senses,
// with malûmatên pêşketî nested inside each, see steps.dfn.build) →
// Wêne → Bi zaravayên din → Werger, so dfn/resim/zarava/trans sit
// adjacent to each other in that order.
var ordered_steps = [
steps.lang, steps.pos, steps.posparams,
steps.cinsiyet, steps.jimar, steps.lektur, steps.tr,
// Reading order: the senses, then what belongs to the entry as a whole. Etîmolojî follows
// "Bi alfabeyên din" and keeps a permanent slot; dunde and the three "eleqedar" lists are
// reached through the Beşên din picker like every other optional section.
steps.dfn, steps.tewandin, steps.alfabeyendin, steps.etym, steps.zarava, steps.trans,
steps.dunde, steps.je, steps.tekildar, steps.binereherwiha,
// Bilêvkirin isn't part of any STEP_GROUPS tab, but still needs a
// slot here because this array is what builds the DOM.
steps.pron,
// Beşên din fields — behind the picker.
steps.resim,
steps.bnrh, steps.bikaranin, steps.formalt, steps.hevmane, steps.dijmane, steps.jornav, steps.jernav,
steps.cavkani, steps.xwendin];
// ── Sections ─────────────────────────────────────────────────────────────
// One group, no tab bar. What is always on screen: the senses (whose own blocks carry Nimûne,
// Jêgirtin, Hevmane and Dijmane — see steps.dfn.allGroups), Tewandin, alfabeyendin, zarava, trans
// and Etîmolojî. Everything else is added from the "Beşên din" picker, which sits at the top of the
// form; a newly-picked section is inserted directly under it (see renderBesenDinPicker()), so the
// most recently added one is always the one nearest the picker.
//
// Tewandin keeps its own free-text {{ku-tewîn-nav}} field and does NOT absorb the Cins/zayend form
// parameters (m=/n=/p=), which stay on the headword template.
// Behind the "Beşên din" picker: shown only after an explicit open, the way DefSideBoxes' own
// "+ Beşek lê zêde bike" works -- nothing is on screen until it is asked for.
var BESENDIN_STEP_IDS = ["resim", "formalt", "hevmane", "dijmane", "jornav", "jernav", "bikaranin",
"cavkani", "xwendin", "bnrh", "dunde", "je", "tekildar", "binereherwiha"];
// One group, so no tab bar: the senses, the fields that go with them, and Etîmolojî, which is common
// enough to keep a permanent slot. Everything else is reached through the picker above.
var STEP_GROUPS = [
{ id: "mane", label: "Mane", steps: ["dfn", "tewandin", "alfabeyendin", "zarava", "trans", "etym"] }
];
// Step id -> index into STEP_GROUPS; undefined for the always-visible ungrouped steps.
var stepIdToGroup = {};
$.each(STEP_GROUPS, function (gi, g) {
$.each(g.steps, function (_, sid) { stepIdToGroup[sid] = gi; });
});
// Step ids that belong to each section (not shared across sections).
var PER_SECTION_STEP_IDS = [
"pos", "posparams", "cinsiyet", "jimar", "lektur", "tr",
"resim", "dfn", "tewandin", "formalt", "bikaranin", "alfabeyendin", "hevmane", "dijmane", "jornav", "jernav", "je", "tekildar", "etym", "dunde", "binereherwiha", "zarava", "trans", "cavkani", "xwendin"
];
// ── Multi-section state ──────────────────────────────────────────────────
// Each entry is a plain snapshot: { pos, cinsiyet, lektur, tr, dfn (senses[]),
// je, tekildar, binereherwiha, trans (rows[]) }.
var sections = [{}]; // section 0 starts empty (DOM is the truth)
var activeSection = 0;
// ── Bilêvkirin as a standalone section ───────────────────────────────
// Pronunciation belongs to the word, not to one part of speech, so it
// sits beside Navdêr/Lêker in the tab bar rather than inside any of them.
// Modelled as a flag rather than an entry in `sections` since
// activeSection is used as an array index in many places — viewing
// Bilêvkirin leaves activeSection pointing at the POS section you were on.
var viewingPron = false; // is it the tab currently on screen
// Which STEP_GROUPS tab is currently shown. Lives out here (like
// activeSection above) so refresh() — which is not nested inside build()
// — can read it and so renderGroupTabs()/updateGroupTabIndicators() below
// can be called from refresh() too. Both operate on the tab bar via its
// #wiktku-group-tabs id rather than a closured element reference, for the
// same reason.
var activeGroup = 0;
// Unsaved-changes / autosave state. Lives out here (same reason as
// activeGroup above) so destroy() — which runs on final submit and on
// switching to the source editor, and is not nested inside build() —
// can clear the timers and the beforeunload warning. build() assigns
// into these; it doesn't re-declare them.
var formDirty = false;
var autosaveDebounceTimer = null;
var autosaveInterval = null;
/** Re-renders the step-group tab bar to reflect the current activeGroup. */
// Scrolls the given tab bar to the top of the viewport, below the sticky offset, so a switched-to tab
// does not open off-screen. Deferred one frame: switching can shorten the page, and the browser may
// clamp the scroll position before the measurement runs.
function scrollTabBarIntoView(barId) {
window.requestAnimationFrame(function () {
var bar = document.getElementById(barId);
if (!bar) return;
var form = document.getElementById("wiktku-start-form");
var stabsH = form ? (parseFloat(form.style.getPropertyValue("--wiktku-stabs-h")) || 0) : 0;
var siteHeaderH = form ? (parseFloat(form.style.getPropertyValue("--wiktku-siteheader-h")) || 0) : 0;
var top = bar.getBoundingClientRect().top + window.pageYOffset - stabsH - siteHeaderH;
$("html, body").animate({ scrollTop: Math.max(0, top) }, 200);
});
}
function renderGroupTabs() {
var groupTabsDiv = $("#wiktku-group-tabs");
if (!groupTabsDiv.length) return;
// One group is nothing to switch between. Done here rather than only where the bar is built,
// because the form is rebuilt on language/part-of-speech changes and a bar left over from an
// earlier build would otherwise sit there until refresh() next got round to hiding it.
if (STEP_GROUPS.length < 2) {
groupTabsDiv.remove();
return;
}
groupTabsDiv.empty();
$.each(STEP_GROUPS, function (i, g) {
var tab = $("<span></span>")
.addClass("wiktku-group-tab")
.attr("data-group-idx", i)
.text(g.label)
.toggleClass("wiktku-tab-active", i === activeGroup);
(function (idx) {
tab.on("click", function () {
if (idx === activeGroup) return;
activeGroup = idx;
renderGroupTabs();
renderBesenDinPicker();
refresh();
scrollTabBarIntoView("wiktku-group-tabs");
});
}(i));
groupTabsDiv.append(tab);
});
}
/**
* Toggles a "needs attention" dot on any group tab that still has a
* required, unfilled field hidden inside it — so switching tabs doesn't
* leave the person wondering why "Bişîne" stays disabled.
*/
function updateGroupTabIndicators(values) {
var tabs = $("#wiktku-group-tabs .wiktku-group-tab");
if (!tabs.length) return;
$.each(STEP_GROUPS, function (i, g) {
var missing = false;
$.each(g.steps, function (_, sid) {
var step = steps[sid];
if (!step || !step.required) return;
var val = values[sid];
if (val === "" || (step.multiple && $.isArray(val) && val.join("") === "")) {
missing = true;
}
});
tabs.eq(i).toggleClass("wiktku-tab-missing", missing && i !== activeGroup);
});
}
// "Beşên din" reveals one field at a time via a picker rather than
// stacking them all. Per-section state, snapshotted in
// saveCurrentSection() and restored in restoreSection() below, and
// auto-seeded from any field that already has content (so a returning
// draft doesn't hide something already filled in).
var openedOtherSections = {};
var BESENDIN_SET = {};
$.each(BESENDIN_STEP_IDS, function (_, sid) { BESENDIN_SET[sid] = true; });
// The picker sits at the top of the form, above the senses, and is on screen whenever the form is.
/** Re-renders the "Beşên din" picker to list only the not-yet-opened sections. */
function renderBesenDinPicker() {
var wrap = $("#wiktku-besendin-picker");
if (!wrap.length) return;
wrap.empty();
var pickerValues = getValues();
// Every section it offers belongs to a part-of-speech section, so there is nothing to add until
// one is chosen. Checked here rather than only in refresh(): this function is also called on form
// build and on section changes, and each of those would otherwise show the picker on its own.
if (!pickerValues.pos) {
wrap.hide();
return;
}
var unopened = $.grep(BESENDIN_STEP_IDS, function (sid) {
if (openedOtherSections[sid]) return false;
// Don't offer a section that would open onto nothing: several of
// these are gated (Bi zaravayên din / Werger need lang=ku, most
// need a pos). refresh() would hide it again straight away, so
// picking it would read as a broken dropdown.
var step = steps[sid];
if (step && typeof step.visible === "function") {
try {
if (!step.visible(pickerValues)) return false;
} catch (e) { /* a step whose visible() needs DOM we lack yet — offer it */ }
}
return true;
});
if (!unopened.length) {
wrap.hide(); // everything's open — nothing left to pick
return;
}
// Same voice and styling as a step's own question line, not a section heading -- this picker sits
// between cards rather than being one.
wrap.append($("<label class='wiktku-start-step-question'></label>").text("Binebeşek lê zêde bike"));
var sel = $("<select class='wiktku-besendin-select'></select>");
sel.attr("data-placeholder", "— binebeşên din hilbijêre —");
sel.append($("<option value=''>— binebeşên din hilbijêre —</option>"));
$.each(unopened, function (_, sid) {
var step = steps[sid];
if (!step) return;
sel.append($("<option></option>").val(sid).text(capitalize(step.name)));
});
sel.on("change", function () {
var sid = $(this).val();
if (!sid) return;
openedOtherSections[sid] = true;
// Newly opened section moves right under the picker, ahead of
// whatever's already open, instead of wherever it happened to
// sit in the fixed step order.
$("#wiktku-start-" + sid).insertAfter(wrap);
renderBesenDinPicker();
refresh();
});
wrap.append(sel);
initSelects(wrap);
wrap.show();
}
/** Marks any BESENDIN_STEP_IDS field that already has real content as
* "opened", so restoring a draft doesn't bury filled-in sections behind
* the picker. Reuses each step's own output() as the emptiness check —
* it already knows what "empty" means per field, and it reads the DOM
* the restore just populated. Call this AFTER the DOM has the restored
* values in it. */
function seedOpenedFromContent() {
var values = getValues();
$.each(BESENDIN_STEP_IDS, function (_, sid) {
if (openedOtherSections[sid]) return;
var step = steps[sid];
if (!step || typeof step.output !== "function") return;
try {
if (step.output(values)) openedOtherSections[sid] = true;
} catch (e) { /* a step whose output() needs more state than we have yet — skip it */ }
});
}
function snapshotDfn() {
return steps.dfn.getSenses($("#wiktku-start-dfn"));
}
// Rebuilds the dfn DOM from a previously snapshotted senses array.
function restoreDfn(senses) {
var stepDiv = $("#wiktku-start-dfn");
// Tear down the existing tab bar + list so build() recreates them cleanly.
stepDiv.find(".wiktku-dfn-sense-tabs").remove();
stepDiv.find("ol.wiktku-dfn-list").remove();
steps.dfn.build(stepDiv);
var list = stepDiv.find("ol.wiktku-dfn-list");
$.each(senses, function (i, sense) {
// Ensure enough blocks exist (build() seeds one).
var blocks = list.find("li.wiktku-dfn-sense");
var block = blocks.eq(i);
if (!block.length) {
var nb = steps.dfn.buildSenseBlock(stepDiv);
list.append(nb);
block = list.find("li.wiktku-dfn-sense").last();
}
// Template selector.
block.find("select.wiktku-dfn-tpl-select").val(sense.tpl || "");
if (sense.tpl) {
block.find("textarea.wiktku-dfn-mane").hide();
block.find("input.wiktku-dfn-tpl-title").val(sense.tplTitle || "").show();
// The picker is collapsed behind a link by default; a restored
// sense that already uses a template has to show it, or the
// chosen template is invisible and looks lost.
var reveal = block.data("revealTplRow");
if (typeof reveal === "function") reveal();
} else {
block.find("textarea.wiktku-dfn-mane").val(sense.mane || "").show();
block.find("input.wiktku-dfn-tpl-title").hide().val("");
}
// dfnArea is the only thing actually saved, so split it back into
// tercime/îzah for editing (mirrors syncNonKuToMane's join format).
var maneParts = splitTercimeIzah(sense.mane);
block.find("textarea.wiktku-dfn-mane").val(maneParts ? $.trim(maneParts.tercime) : (sense.mane || ""));
block.find(".wiktku-dfn-izah").val(maneParts ? $.trim(maneParts.izah) : "");
var setIzahOpen = block.data("setIzahOpen");
if (setIzahOpen) setIzahOpen(!!(maneParts && $.trim(maneParts.izah)));
// Re-sync the formên alternatîv/hevmane/… hide state now that
// tplSelect's value was set (its own "change"
// handler doesn't fire from .val(), so this doesn't happen otherwise).
var updateVis = block.data("updateFieldVisibility");
if (updateVis) updateVis();
// Same for the mane-box mode (plain vs tercime+îzah), since this
// also depends on tplSelect's (and lang's) value.
var updateMane = block.data("updateManeMode");
if (updateMane) updateMane();
// Restore a sub-list (etiket, formên alternatîv, hevmane …) from a values array.
function restoreSubList(cls, values) {
if (!values || !values.length) return;
var groupDiv = block.find("." + cls + "-group");
var subList = groupDiv.find("ol, ul").first();
if (!subList.length) return; // group not found
var useModifiers = isWordGroupCls(cls);
// Fill existing items, append new ones as needed.
$.each(values, function (j, val) {
var items = subList.find("li");
if (j < items.length) {
// useModifiers rows keep the word and its modifier tags in
// separate elements (see makeModifierLi) — setRawValue()
// re-splits val() the same way parsing it fresh would,
// instead of dumping the raw "word<key:value>" string into
// the visible word box.
var setRaw = items.eq(j).data("setRawValue");
if (useModifiers && typeof setRaw === "function") {
setRaw(val);
} else {
items.eq(j).find("textarea").val(val);
}
} else {
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-restore-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.val(val);
ta.on("input", function () {
steps.dfn.onSubInput(subList, this, 1, true, useModifiers);
});
subList.append(useModifiers ? makeModifierLi(ta) : $("<li></li>").append(ta));
}
});
if (!useModifiers) return;
// A fresh empty row goes on the end so there's always somewhere
// to add the next word without first having to click into a
// filled one.
var filled = subList.find("li.wiktku-word-li");
if (filled.last().is(":last-child")) {
var nextTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-restore-next-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow");
nextTa.on("input", function () {
steps.dfn.onSubInput(subList, this, 1, true, useModifiers);
});
subList.append(makeModifierLi(nextTa));
}
}
// Populate the word lists that the draft has something for. senseGroups' "field" is both the
// property getSenses() read them into and the tail of the box's own class.
$.each(wordGroups(), function (_, g) {
var vals = sense[g.field];
if (vals && vals.length) restoreSubList(g.cls, vals);
});
// Dever/Şîwe, Etîket, Kategorî — chip fields, not textarea
// sub-lists, restored via the adders buildSenseBlock stashed on
// the block.
block.find("input.wiktku-dfn-maneid-input").val(sense.maneid || "");
var chipRestoreMap = {
addDeverChip: sense.dever,
addEtiketChip: sense.etiket,
addKategoriChip: sense.kategori
};
$.each(chipRestoreMap, function (dataKey, vals) {
var addChip = block.data(dataKey);
if (addChip && vals && vals.length) {
$.each(vals, function (_, v) { addChip(v); });
}
});
// Nimûne items are objects { text, tr?, t? } — restore using the builder.
if (sense.nimune && sense.nimune.length) {
var nimuneGroupDiv = block.find(".wiktku-dfn-nimune-group");
var nimuneList = nimuneGroupDiv.find("ol.wiktku-dfn-nimune-list");
var buildFn = nimuneGroupDiv.data("buildNimuneLi");
nimuneList.empty();
$.each(sense.nimune, function (_, n) {
var text = typeof n === "object" ? (n.text || "") : n;
if (!text) return;
var li = buildFn ? buildFn() : $("<li class='wiktku-dfn-nimune-li'></li>");
li.find(".wiktku-dfn-nimune-text").val(text).each(function () { highlightLinebreak(this); });
if (n.tr) li.find(".wiktku-dfn-nimune-tr").val(n.tr);
if (n.t) li.find(".wiktku-dfn-nimune-t").val(n.t);
nimuneList.append(li);
});
// Always leave a blank trailing item.
if (buildFn) nimuneList.append(buildFn());
}
// Restore jêder entries — accepts current array shape or a legacy
// single object (older saved sections) for backwards compat.
var jederArr = $.isArray(sense.jeder) ? sense.jeder : (sense.jeder ? [sense.jeder] : []);
if (jederArr.length) {
var jederGroupDiv = block.find(".wiktku-dfn-jeder-group");
var jederList = jederGroupDiv.find("ol.wiktku-dfn-jeder-list");
var buildJederFn = jederGroupDiv.data("buildJederLi");
jederList.empty();
$.each(jederArr, function (_, j) {
if (!j || !j.quote) return;
var li = buildJederFn ? buildJederFn() : $("<li class='wiktku-dfn-jeder-li'></li>");
li.find(".wiktku-dfn-nimune-quote").val(j.quote).each(function () { highlightLinebreak(this); });
if (j.quoteTr) li.find(".wiktku-dfn-nimune-quote-tr").val(j.quoteTr);
if (j.quoteT) li.find(".wiktku-dfn-nimune-quote-t").val(j.quoteT);
if (j.url) li.find(".wiktku-dfn-quote-url").val(j.url);
if (j.jederManual) {
var manualSel = li.find(".wiktku-dfn-quote-src-sel");
manualSel.val("__manual__").trigger("change");
li.find(".wiktku-dfn-quote-src-manual").val(j.jederManual);
} else if (j.raw) {
li.find(".wiktku-dfn-quote-src-sel").val("__raw__").trigger("change");
} else if (j.jederCatTpl) {
// Reopen the category picker on the saved template and put its fields back. The
// fields arrive asynchronously (TemplateData), so the saved values are applied
// once they exist rather than straight away.
var catSelEl = li.find(".wiktku-dfn-quote-src-sel");
catSelEl.val("__category__").trigger("change");
li.data("catTemplate", j.jederCatTpl);
if (j.jederCatLangParam) li.data("catLangParam", j.jederCatLangParam);
li.find(".wiktku-dfn-quote-src-cat-input").val(j.jederCatTpl);
// Kept on the row from the moment the restore starts. Submitting walks every section
// -- restore, then read -- and the read is synchronous while the boxes below only
// exist once TemplateData comes back, so without this the values are read from boxes
// that are not there yet and every parameter but the quote is lost. readJederList()
// falls back to these until the boxes catch up.
li.data("catFields", j.jederCatFields || {});
wiktkuCite.fieldSpecs(j.jederCatTpl).then(function (spec) {
var srcFields = li.find(".wiktku-dfn-quote-src-fields");
li.data("catLangParam", j.jederCatLangParam || spec.langParamKey || null);
$.each(spec.fields, function (_, field) {
srcFields.append(li.data("buildSrcFieldRow")(field));
});
$.each(j.jederCatFields || {}, function (key, val) {
li.find(".wiktku-dfn-quote-src-input[data-field-key='" + key + "']").val(val);
});
// The boxes hold them now, so they are the truth again -- clearing this keeps a
// field the user empties from being refilled from the stale copy.
li.removeData("catFields");
});
} else if (j.jederTpl) {
var srcSel = li.find(".wiktku-dfn-quote-src-sel");
srcSel.val(j.jederTpl).trigger("change");
if (j.jêderFields) {
$.each(j.jêderFields, function (key, val) {
li.find(".wiktku-dfn-quote-src-input[data-field-key='" + key + "']").val(val);
});
}
}
jederList.append(li);
});
// Always leave a blank trailing item, same as Nimûne.
if (buildJederFn) jederList.append(buildJederFn());
}
});
// Unlike addSense(), build()/buildSenseBlock() above don't wrap their own <select>s -- do it once for the whole restored list (idempotent, so already-wrapped selects are skipped).
initSelects(list);
// Everything above filled boxes that are still behind the chip row, so each block is asked to
// show whichever of its sub-fields the draft actually had something in.
list.find("li.wiktku-dfn-sense").each(function () {
var syncPicks = $(this).data("syncSensePicks");
if (syncPicks) syncPicks();
});
// Restoring a draft opens the first sense's tab by default.
steps.dfn.setOpenSense(stepDiv, list.find("li.wiktku-dfn-sense").first());
}
// Snapshots simple (non-dfn) per-section steps from the DOM into obj.
function saveCurrentSection() {
var snap = {};
$.each(PER_SECTION_STEP_IDS, function (_, id) {
if (id === "dfn") {
snap.dfn = snapshotDfn();
return;
}
if (id === "etym") {
snap.etym = $.trim($("#wiktku-start-etym .wiktku-etym-text").val());
return;
}
if (id === "cavkani") {
snap.cavkani = $.trim($("#wiktku-start-cavkani .wiktku-cavkani-text").val());
return;
}
if (id === "posparams") {
snap.posparams = $.trim($("#wiktku-start-posparams .wiktku-posparams-text").val());
return;
}
if (id === "resim") {
// One entry per row, including blank ones, so a mid-edit draft
// (e.g. a filename typed but no caption yet) survives a cache
// restore instead of being silently dropped.
snap.resim = $("#wiktku-start-resim li.wiktku-resim-item").map(function () {
return {
file: $.trim($(this).find(".wiktku-resim-file").val()),
caption: $.trim($(this).find(".wiktku-resim-caption").val()),
alt: $.trim($(this).find(".wiktku-resim-alt").val())
};
}).get();
return;
}
var step = steps[id];
if (!step) return;
var stepDiv = $("#wiktku-start-" + id);
if (id === "trans") {
snap.trans = steps.trans.getTranslations(stepDiv);
return;
}
if (id === "zarava") {
snap.zarava = steps.zarava.getTranslations(stepDiv);
return;
}
if (id === "dunde") {
snap.dunde = steps.dunde.getRows(stepDiv);
return;
}
// Generic: read all step fields.
var fields = stepDiv.find(".wiktku-start-step-field");
if (step.multiple) {
snap[id] = fields.map(function () { return $(this).val(); }).get();
} else {
snap[id] = fields.val();
}
});
// Which "Beşên din" sub-sections this section has already opened
// via the picker — per-section, like everything else here.
snap.openedOtherSections = $.extend({}, openedOtherSections);
sections[activeSection] = snap;
}
// Restores a previously saved snapshot into the form DOM.
function restoreSection(snap) {
$.each(PER_SECTION_STEP_IDS, function (_, id) {
if (id === "dfn") {
restoreDfn(snap.dfn || []);
return;
}
if (id === "etym") {
$("#wiktku-start-etym .wiktku-etym-wrap").remove();
steps.etym.build($("#wiktku-start-etym"));
$("#wiktku-start-etym .wiktku-etym-text").val(snap.etym || "");
return;
}
if (id === "cavkani") {
$("#wiktku-start-cavkani .wiktku-cavkani-wrap").remove();
steps.cavkani.build($("#wiktku-start-cavkani"));
$("#wiktku-start-cavkani .wiktku-cavkani-text").val(snap.cavkani || "");
return;
}
if (id === "posparams") {
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-step").remove();
steps.posparams.build(ppDiv);
var savedTa = snap.posparams || "";
ppDiv.find(".wiktku-posparams-text").val(savedTa);
// Re-parse saved textarea to restore helper widget states.
if (savedTa) {
// Read with the shared parser, so a nested "c=" is never taken for the call's own, and a
// saved "z=" restores the gender the same as "c=".
var savedC = wiktkuWikitext.callParamValue(savedTa, "c");
var savedJ = wiktkuWikitext.callParamValue(savedTa, "j");
var savedTr = wiktkuWikitext.callParamValue(savedTa, "tr");
// Write into the shared hidden field (not the visible
// select, which has no <option>s yet) — syncHelpers()
// below reads it back to restore the dropdown once the
// gender/number options for this language are populated.
if (savedC) $("#wiktku-start-cinsiyet-field").val(savedC);
if (savedJ) $("#wiktku-start-jimar-field").val(savedJ);
if (savedTr) ppDiv.find(".wiktku-posparams-field[data-pp-key='tr']").val(savedTr);
// For lêker, c= holds the verb-type code instead of a
// gender — split it back across the two dropdowns it is
// now assembled from ("x-gh" → form "x" + base "gh").
if (savedC && /^lêker/.test(snap.pos || "")) {
var lekturParts = /^(?:([xphb])-)?(n?gh)$/.exec(savedC);
if (lekturParts) {
ppDiv.find(".wiktku-posparams-lektur-sel").val(lekturParts[2]);
ppDiv.find(".wiktku-posparams-lekturform-sel").val(lekturParts[1] || "");
}
}
// Restore checked optional params via the same param/value overrides rebuildTa uses.
ppDiv.find(".wiktku-posparams-opt-panel input[type='checkbox']").each(function () {
var key = $(this).val();
var override = wiktkuPosparams.OPT_PARAM_OVERRIDES[key];
var needle = override ? "|" + override.param + "=" + override.value : "|" + key + "=1";
$(this).prop("checked", savedTa.indexOf(needle) !== -1);
});
}
steps.posparams.syncHelpers(ppDiv);
return;
}
if (id === "resim") {
var resimDiv = $("#wiktku-start-resim");
resimDiv.find(".wiktku-resim-wrap").remove();
steps.resim.build(resimDiv);
// Accept both the new array-of-rows format and the older
// single {file, caption} object saved before multi-image
// support existed.
var resimSnap = snap.resim;
var rows = $.isArray(resimSnap) ? resimSnap : (resimSnap ? [resimSnap] : []);
// setValues() keeps a blank trailing row of its own, same as Nimûne/Dûnde.
if (rows.length && steps.resim.field) steps.resim.field.setValues(rows);
return;
}
if (id === "trans") {
// Rebuild trans groups (dfn must already be restored by this
// point in the $.each loop, so glossList() reflects it).
var transDiv = $("#wiktku-start-trans");
transDiv.find(".wiktku-trans-group").remove();
steps.trans.build(transDiv);
var groupsSnap = snap.trans || [];
$.each(groupsSnap, function (gi, rows) {
var group = transDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
if (!group.length) return; // gloss no longer exists — drop its rows
var list = group.find("ul.wiktku-trans-list");
$.each(rows, function (j, row) {
var lang = row[0], words = row[1];
var lastRow = list.find("li.wiktku-trans-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-trans-words");
$.each(words, function (k, word) {
var wordItems = wordList.find("li");
if (k < wordItems.length) {
wordItems.eq(k).find("textarea").val(word);
} else {
// .val() below doesn't fire "input", so the normal
// auto-grow never runs — append a matching item
// ourselves instead of silently dropping the word.
var extraInput = steps.trans.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
// Leave one trailing empty field so the row can still grow
// when the user types, matching a freshly-built row.
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.trans.makeWordInput()));
}
});
});
return;
}
if (id === "zarava") {
// Rebuild zarava groups (dfn must already be restored by this
// point in the $.each loop, so glossList() reflects it).
var zaravaDiv = $("#wiktku-start-zarava");
zaravaDiv.find(".wiktku-zarava-group").remove();
steps.zarava.build(zaravaDiv);
var zaravaGroupsSnap = snap.zarava || [];
$.each(zaravaGroupsSnap, function (gi, rows) {
var group = zaravaDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
if (!group.length) return; // gloss no longer exists — drop its rows
var list = group.find("ul.wiktku-zarava-list");
$.each(rows, function (j, row) {
var lang = row[0], words = row[1];
var lastRow = list.find("li.wiktku-zarava-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-zarava-words");
$.each(words, function (k, word) {
var wordItems = wordList.find("li");
if (k < wordItems.length) {
wordItems.eq(k).find("textarea").val(word);
} else {
var extraInput = steps.zarava.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.zarava.makeWordInput()));
}
});
});
return;
}
if (id === "dunde") {
// Rebuild dunde rows.
var dundeDiv = $("#wiktku-start-dunde");
dundeDiv.find("ul.wiktku-dunde-list").remove();
steps.dunde.build(dundeDiv);
var dundeRows = snap.dunde || [];
$.each(dundeRows, function (i, row) {
var lang = row[0], words = row[1];
var list = dundeDiv.find("ul.wiktku-dunde-list");
var lastRow = list.find("li.wiktku-dunde-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-dunde-words");
$.each(words, function (j, word) {
var wordItems = wordList.find("li");
if (j < wordItems.length) {
wordItems.eq(j).find("textarea").val(word);
} else {
// .val() below doesn't fire "input", so the normal
// auto-grow never runs — append a matching item
// ourselves instead of silently dropping the word.
var extraInput = steps.dunde.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
// Leave one trailing empty field so the row can still grow
// when the user types, matching a freshly-built row.
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.dunde.makeWordInput()));
}
});
return;
}
var step = steps[id];
if (!step) return;
var stepDiv = $("#wiktku-start-" + id);
var fields = stepDiv.find(".wiktku-start-step-field");
var val = snap[id];
if (val === undefined || val === null) return;
if (step.multiple) {
var arr = val;
fields.each(function (i) {
if (i < arr.length) $(this).val(arr[i]);
});
} else {
fields.val(val);
}
// Tell the pickers about the programmatic value change.
fields.filter("select").trigger("wiktku:refresh");
});
// Restore which "Beşên din" sub-sections were already opened for
// this section, then seed in anything the DOM restore above just
// filled in but that wasn't marked opened yet.
openedOtherSections = $.extend({}, snap.openedOtherSections || {});
seedOpenedFromContent();
renderBesenDinPicker();
}
function getWikitextForCurrentSection(callback) {
var values = getValues();
var entry = steps.pos.output(values);
// Bilêvkirin is NOT emitted here — it is one shared field for the
// whole word, so emitting it per section duplicated it. getWikitext()
// prints it once, above the POS sections.
steps.dfn.getOutput(values, function (output) {
entry += output;
entry += steps.tewandin.output(values);
entry += steps.bikaranin.output(values);
entry += steps.alfabeyendin.output(values);
entry += steps.formalt.output(values);
entry += steps.hevmane.output(values);
entry += steps.dijmane.output(values);
entry += steps.jornav.output(values);
entry += steps.jernav.output(values);
entry += steps.je.output(values);
entry += steps.tekildar.output(values);
entry += steps.etym.output(values);
entry += steps.dunde.output(values);
entry += steps.binereherwiha.output(values);
entry += steps.zarava.output(values);
entry += steps.trans.output(values);
entry += steps.cavkani.output(values);
entry += steps.xwendin.output(values);
callback(entry, values);
});
}
// Two sections of the same part of speech would otherwise both be "=== Rengdêr ===", which makes them
// indistinguishable on the page, in the table of contents and in every link that targets one. Run over
// the finished sections rather than guessed from the pos alone, so "Rengdêr" and "Formeke rengdêrê"
// -- different headings from the same pos -- are correctly left alone.
function numberRepeatedHeadings(results) {
var headingRE = /^(===\s*)([^=\n]+?)(\s*===)$/m;
var counts = {};
$.each(results, function (_, text) {
var m = headingRE.exec(text || "");
if (m) counts[m[2]] = (counts[m[2]] || 0) + 1;
});
var seen = {};
$.each(results, function (i, text) {
var m = headingRE.exec(text || "");
if (!m || counts[m[2]] < 2) {
return;
}
seen[m[2]] = (seen[m[2]] || 0) + 1;
results[i] = text.replace(headingRE, m[1] + m[2] + " " + seen[m[2]] + m[3]);
});
}
function getWikitextAllSections(callback) {
saveCurrentSection();
var results = new Array(sections.length);
var sharedValues = getValues(); // lang/pron are shared
function processSection(i) {
// Each hop through here can be reached asynchronously: the dfn
// step waits on mw.loader.using("ext.gadget.linkify") before
// calling back, so from that point on the submit handler's own
// try/catch is no longer on the stack. Guard every hop instead,
// or a throw after linkify loads vanishes into an unhandled
// rejection and the form just sits there looking busy.
try {
if (i >= sections.length) {
// All done — restore original active section.
restoreSection(sections[activeSection]);
refresh();
numberRepeatedHeadings(results);
callback(results, sharedValues);
return;
}
restoreSection(sections[i]);
refresh();
getWikitextForCurrentSection(function (sectionText) {
results[i] = sectionText;
processSection(i + 1);
});
} catch (e) {
if (typeof onSubmitError === "function") onSubmitError(e);
else throw e;
}
}
processSection(0);
}
// Assembles wikitext for all sections plus the shared lang header.
function getWikitext(callback) {
getWikitextAllSections(function (sectionTexts, sharedValues) {
var bnrh = steps.bnrh.output(sharedValues);
var entry = steps.lang.output(sharedValues);
// One pronunciation per word, printed once between the language
// header and the POS sections — see steps.pron.output.
entry += steps.pron.output(sharedValues);
entry += sectionTexts.join("\n");
// Reconstruction-namespace pages (ns 108) mark the language
// section with {{jinûvesazkirî|langcode}} directly above it —
// only prepended when generating fresh wikitext, same as h-bnr
// below (addLangMode handles an existing page separately).
if (!addLangMode && ns === 108) entry = "{{jinûvesazkirî|" + sharedValues.lang + "}}\n" + entry;
// h-bnr goes before the language section — only prepended when
// generating fresh wikitext (not addLangMode, which handles it separately).
if (!addLangMode && bnrh) entry = bnrh + entry;
// Edit summary uses section 0's values. Read from the saved snapshot
// (not the live DOM) so it doesn't mix section 0's pos with whichever
// section happens to be active/restored at this point.
var dfnSenses = (sections[0] && sections[0].dfn) || [];
var dfnSummary = dfnSenses.map(function (s) {
if (s.mane) return s.mane;
// Template sense (e.g. {{guharto|ku|somepage}}) — same
// construction as the tplCall built in getOutput, so the
// summary shows what actually gets written to the entry
// instead of going blank.
if (s.tpl && s.tplTitle) {
return s.tplNoLang
? "{{" + s.tpl + "|" + s.tplTitle + "}}"
: "{{" + s.tpl + "|" + sharedValues.lang + "|" + s.tplTitle + "}}";
}
return "";
}).filter(Boolean).join("; ");
var summary = "([[Mediawiki:Gadget-start.js|WF:NÛ]] v.2.0) +" +
sharedValues.lang + ":" + sections[0].pos + ":" + dfnSummary;
var summaryField = document.getElementById("wpSummary");
if (addLangMode && summaryField.value) {
summary = summaryField.value + " " + summary;
}
summaryField.value = summary;
callback(entry);
});
}
var continueBtn;
// Set by build() to its submitFailed(). Lives out here because the
// wikitext-assembly functions below are outside build()'s closure but
// still need a way to report a failure to the person rather than dying
// silently in the console.
var onSubmitError = null;
// Whether Mane/Bi zaravayên din/Werger has anything in it — kept
// outside refresh() so the click handler can read the latest value.
function getValuesForStep(stepId) {
var fields = $("#wiktku-start-" + stepId + " .wiktku-start-step-field");
var step = steps[stepId];
if (step.keys) {
var values = {};
fields.each(function (i, field) {
var key = $(field).prevAll(".wiktku-start-step-keyfield").val();
if (key) values[key] = $(field).val();
});
return values;
}
else if (step.multiple) {
return fields.map(function () {
return $(this).val();
}).get();
}
return fields.val();
}
function getValues() {
var values = {};
$.each(steps, function (stepId) {
values[stepId] = getValuesForStep(stepId);
});
return values;
}
function populateMenus(inputs, choices, disabledValues, placeholderText) {
if (!disabledValues) disabledValues = {};
var vals = inputs.map(function (i, input) {
return $(input).val();
});
inputs.empty();
inputs.append($("<option></option>").text(placeholderText || ""));
$.each(choices, function (j, choice) {
// Append the language code in parentheses so that typing a code
// (e.g. "ku") in the search box still finds the language name.
var displayText = capitalize(choice.label);
if (choice.sortKey !== undefined) {
// This is a language choice {code, label, sortKey} — show code.
displayText = capitalize(choice.label) + " (" + choice.code + ")";
}
var option = $(mw.html.element("option", {
value: choice.code }, displayText));
if (disabledValues[choice.code]) option.prop("disabled", true);
inputs.append(option);
});
inputs.val(function (i) {
return vals[i];
});
}
// Keys to disable for a key menu: already-taken or explicitly disabled.
function getDisabledKeys(step) {
var occupiedKeys = {};
$("#wiktku-start-" + step.id + " .wiktku-start-step-keyfield").each(function () {
occupiedKeys[$(this).val()] = true;
});
if (step.disabledKeys) $.extend(occupiedKeys, step.disabledKeys);
return occupiedKeys;
}
function buildKeyInput(step) {
if (!step.keys) return undefined;
var input = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
input.attr("data-placeholder", "Hilbijêre " + step.keyName);
populateMenus(input, step.keys, getDisabledKeys(step));
// Keep the key menus mutually exclusive: whatever one row selects is
// disabled in every other row. Recomputed from the DOM on each change.
input.on("change", function () {
var all = $("#wiktku-start-" + step.id + " .wiktku-start-step-keyfield");
var taken = {};
all.each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
all.not(this).each(function () {
var self = $(this), own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (code) $(this).prop("disabled", !!taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
});
// Return the wrapper; the select lives inside it and still fires change.
return wrapNativeSelect(input);
}
// Adds another text field to the list if the last one now has text.
function refreshForTextField(currentInput, step) {
currentInput = $(currentInput);
var value = currentInput.val();
var item = currentInput.parent();
if (value && !item.next("li").length) {
var nextInput = $("<textarea></textarea>")
.attr("name", "wiktku-field-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", currentInput.attr("class"))
.attr("rows", currentInput.attr("rows") || 1);
if (step.placeholder) {
nextInput.attr("placeholder", typeof step.placeholder === "function" ? step.placeholder() : step.placeholder);
}
nextInput.on("input", function (evt) {
refreshForTextField(evt.target, step);
});
var nextKeyInput = buildKeyInput(step);
var nextItem = step.modifiers
? makeModifierLi(nextInput).prepend(nextKeyInput || "")
: $("<li></li>").append(nextKeyInput).append(nextInput);
item.parent().append(nextItem);
if (nextKeyInput) initSelects(nextItem);
}
var canContinue = !continueBtn.isDisabled();
if ((value && !canContinue) || (!value && canContinue)) {
refresh(step);
}
}
function prepareInput(input, step) {
if (!input) return;
if (step.required) input.prop('required', true);
input.change(step, refresh);
}
// Shows/hides steps based on whether their prerequisites are met, and
// updates placeholders where applicable.
function refresh(evt) {
var values = getValues();
var changedStep = evt && evt.data;
var canContinue = true;
// Re-filter template options in every sense block whenever lang changes.
$("#wiktku-start-dfn .wiktku-dfn-tpl-select").each(function () {
var fn = $(this).data("updateOptions");
if (typeof fn === "function") fn();
});
// Re-filter the pronunciation template dropdown when language changes -- etym/cavkani's lists don't depend on lang, so they're only built once at data load instead of on every refresh().
steps.pron.refreshTplSelect();
// Re-render any step's question label that depends on current values
// (e.g. dfn's Werger-section reminder, which only applies to ku).
$.each(steps, function (stepId, step) {
if (typeof step.question_fmt === "function") {
$("#wiktku-start-" + stepId + " .wiktku-start-step-question")
.text(step.question_fmt(values));
}
});
// Pos's definition/example text, right under its dropdown — see
// steps.pos.example and the .wiktku-start-pos-example build above.
$("#wiktku-start-pos .wiktku-start-pos-example").text(steps.pos.example(values));
// Sync every sense block's mane-box mode (plain mane vs tercime+îzah)
// to the current language, in case lang changes after blocks exist.
// Same pass also re-syncs Dever/Şîwe's default open/closed state
// (open for ku, closed otherwise — see updateDeverOpenState) and
// reveals any sub-field group that has ended up with content in it
// while still behind the chip row (a draft, a restore).
$("#wiktku-start-dfn li.wiktku-dfn-sense").each(function () {
var updateMane = $(this).data("updateManeMode");
if (updateMane) updateMane();
var updateDeverOpen = $(this).data("updateDeverOpenState");
if (updateDeverOpen) updateDeverOpen();
var syncPicks = $(this).data("syncSensePicks");
if (syncPicks) syncPicks();
});
// Sync tr/t sub-fields in every nimûne item to the current language.
// tr shown when language needs transliteration; t once a language is chosen.
(function () {
var lang = values.lang || "";
var showTr = needsTransliteration(lang);
var showT = !!lang;
$("#wiktku-start-dfn .wiktku-dfn-nimune-list li.wiktku-dfn-nimune-li").each(function () {
var li = $(this);
// Ensure the sub-wrapper exists when we need it.
var subWrap = li.find(".wiktku-dfn-nimune-sub");
// tr field
var trTa = li.find(".wiktku-dfn-nimune-tr");
if (showTr && !trTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
li.append(subWrap);
}
trTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-tr")
.attr("placeholder", "Transliterasyon…");
subWrap.prepend(trTa);
} else if (!showTr && trTa.length) {
trTa.remove();
}
// t field
var tTa = li.find(".wiktku-dfn-nimune-t");
if (showT && !tTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
li.append(subWrap);
}
tTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-t");
subWrap.append(tTa);
} else if (!showT && tTa.length) {
tTa.remove();
}
tTa.attr("placeholder", nimuneTranslationPrompt(lang));
// Remove the wrapper if it ended up empty.
subWrap = li.find(".wiktku-dfn-nimune-sub");
if (subWrap.length && !subWrap.children().length) subWrap.remove();
});
}());
// Re-sync the "<label> bi <ziman>…" placeholder on every always-visible
// sub-field list (etîket, kategorî, hevmane, …), since the language may
// not have been chosen yet when these lists were first built, or may
// have changed since.
$("#wiktku-start-dfn .wiktku-dfn-sublist").each(function () {
var upd = $(this).data("updatePlaceholders");
if (typeof upd === "function") upd();
});
// Same idea for word-list steps (jê, têkildar, binêre herwiha, …)
// whose placeholder is a function of the current language.
$.each(steps, function (stepId, step) {
if (typeof step.placeholder === "function") {
$("#wiktku-start-" + stepId + " textarea.wiktku-start-step-field")
.attr("placeholder", step.placeholder());
}
});
// Sync tr/t sub-fields in every jêder citation to the current language.
// tr shown when language needs transliteration; t shown when lang !== "ku".
(function () {
var lang = values.lang || "";
var showTr = needsTransliteration(lang);
var showT = lang && lang !== "ku";
$("#wiktku-start-dfn .wiktku-dfn-jeder-list li.wiktku-dfn-jeder-li").each(function () {
var li = $(this);
// Ensure the sub-wrapper exists when we need it. It must sit right
// after the quote textarea and before the source selector.
var subWrap = li.find(".wiktku-dfn-quote-sub");
// tr field
var trTa = li.find(".wiktku-dfn-nimune-quote-tr");
if (showTr && !trTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
// Insert after the quote textarea's resize-wrap (if the manual
// resize handle has already been attached to it), not after the
// bare textarea -- otherwise this lands *inside* that wrap,
// between the textarea and its own grip, breaking jêgirtin's
// handle and nesting the tr/t boxes inside it.
var quoteTa = li.find(".wiktku-dfn-nimune-quote");
var quoteResizeWrap = quoteTa.closest(".wiktku-resize-wrap");
(quoteResizeWrap.length ? quoteResizeWrap : quoteTa).after(subWrap);
}
trTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-tr")
.attr("placeholder", "Transliterasyona jêgirtinê…");
subWrap.prepend(trTa);
} else if (!showTr && trTa.length) {
trTa.remove();
}
// t field
var tTa = li.find(".wiktku-dfn-nimune-quote-t");
if (showT && !tTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
// See the tr-field branch above for why this targets the
// resize-wrap instead of the bare textarea.
var quoteTa2 = li.find(".wiktku-dfn-nimune-quote");
var quoteResizeWrap2 = quoteTa2.closest(".wiktku-resize-wrap");
(quoteResizeWrap2.length ? quoteResizeWrap2 : quoteTa2).after(subWrap);
}
tTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-t")
.attr("placeholder", "Tercimeya jêgirtinê bi kurmancî…");
subWrap.append(tTa);
} else if (!showT && tTa.length) {
tTa.remove();
}
// Remove the wrapper if it ended up empty.
subWrap = li.find(".wiktku-dfn-quote-sub");
if (subWrap.length && !subWrap.children().length) subWrap.remove();
});
}());
$.each(steps, function (stepId, step) {
var groupIdx = stepIdToGroup[stepId];
var hiddenByTab = (groupIdx !== undefined && groupIdx !== activeGroup) ||
// Beşên din picker: stays hidden even while its tab is
// active until explicitly opened from the dropdown.
(BESENDIN_SET[stepId] && !openedOtherSections[stepId]);
// Bilêvkirin is its own section now, so the two views are
// mutually exclusive: on it, only pron shows (plus Ziman, which
// is shared and stays above every section); off it, pron is
// hidden no matter which tab is active. pron is deliberately
// ungrouped — stepIdToGroup has no entry for it — so without
// this branch it would show on all four tabs at once.
if (stepId === "pron") {
hiddenByTab = hiddenByTab || !viewingPron;
} else if (viewingPron && stepId !== "lang") {
hiddenByTab = true;
}
if (hiddenByTab || (typeof(step.visible) === "function" && !step.visible(values))) {
$("#wiktku-start-" + stepId).hide();
}
else {
var stepDiv = $("#wiktku-start-" + stepId);
if (changedStep !== step && typeof(step.build) === "function") {
prepareInput(step.build(stepDiv, values), step);
}
// After build, sync the posparams helper widgets whenever pos or lang changes.
if (step.id === "posparams" && typeof step.syncHelpers === "function") {
step.syncHelpers(stepDiv);
}
var input = stepDiv.find(".wiktku-start-step-field");
if (typeof(step.example) === "function") {
// .find() above already reaches nested fields (e.g. the dfn
// step's mane textareas inside each sense block), so one
// assignment covers every step; dfn keeps its own fallback.
var exampleText = step.example(values) || (step.id === "dfn" ? "Mane…" : "");
input.attr("placeholder", exampleText);
}
stepDiv.slideDown();
}
if (step.required &&
(values[stepId] === "" ||
(step.multiple && values[stepId].join("") === ""))) {
canContinue = false;
}
});
// One group means no bar to show; it only appears again if STEP_GROUPS ever grows back.
$("#wiktku-group-tabs").toggle(STEP_GROUPS.length > 1 && !!values.pos && !viewingPron);
// Bilêvkirin hides the picker outright; otherwise renderBesenDinPicker() decides, showing itself
// only once a part of speech is chosen and there are sections left to offer. The decision lives
// there rather than here because that function has other callers, which would each need the same
// test.
var besenDinActive = !viewingPron;
if (!besenDinActive) {
$("#wiktku-besendin-picker").hide();
}
// The option list is filtered by each step's visible(), which depends
// on lang/pos — so it has to be rebuilt when those change, not just
// when a section is opened. Safe to call from here: rendering the
// picker doesn't itself call refresh(), only picking an option does.
if (besenDinActive) renderBesenDinPicker();
updateGroupTabIndicators(values);
// Ziman and Cureyê rêzimanî are all it takes: an entry with a heading and a template but no
// definition yet is a skeleton somebody means to come back to, not a mistake to be blocked.
continueBtn.setDisabled(!canContinue);
}
function destroy() {
if (autosaveDebounceTimer) clearTimeout(autosaveDebounceTimer);
if (autosaveInterval) clearInterval(autosaveInterval);
formDirty = false;
$(window).off("beforeunload.wiktkuUnsaved");
$("#wiktku-start-form, .noafterwizard").slideUp(function () {
$(this).remove();
});
$(".noarticletext .nowizard").slideDown();
// #editform was kept laid-out but off-screen (see build()) instead of
// display:none, precisely so the editor never had to size itself
// against a zero-width/height box. Just bring it back on-screen —
// no slideDown needed since it was never actually collapsed.
$("#editform").removeClass("wiktku-offscreen");
// Belt-and-braces: if WikiEditor/CodeMirror/mobile's auto-grow still
// cached a stale size from some earlier point, nudge them to
// recalculate now that the box is genuinely visible.
$(window).trigger("resize");
if (window.CodeMirror) {
var cm = $("#wpTextbox1").data("CodeMirror") || $("#wpTextbox1").data("cmInstance");
if (cm && typeof cm.refresh === "function") cm.refresh();
}
$("#ca-edit").removeClass("selected");
$("#ca-wiktku-nowizard").addClass("selected");
}
// Priority order for Kurdish dialects and navz.
// Codes listed here come before all other languages; unlisted codes
// are sorted alphabetically by their display name via localeCompare.
var LANG_PRIORITY = ["navz", "ku", "hac", "sdh", "lki", "ckb", "zza"];
// Inserts a new "== {{ziman|code}} ==" section at the correct sorted
// position: navz, then ku/hac/sdh/lki/ckb/zza, then alphabetical by name.
function insertNewLangSection(existing, wikitext) {
var newCodeMatch = wikitext.match(new RegExp("^\n?" + langHeaderSource("[^}]+")));
var newCode = newCodeMatch ? newCodeMatch[1] : "";
var sections = [];
var preamble = "";
var anyHeaderSrc = "\n?" + langHeaderSource("[^}]+");
var firstMatch = new RegExp("^" + anyHeaderSrc, "m").exec(existing);
if (!firstMatch) {
// No existing lang sections — just write the new section.
// Add \n\n only when there is real non-whitespace content above.
var trimmed = existing.replace(/\s+$/, "");
return trimmed ? trimmed + "\n\n" + wikitext : wikitext;
}
// Everything before the first == {{ziman|...}} == is the preamble.
var firstSectionStart = firstMatch.index;
// Walk back to the start of that line.
var lineStart = existing.lastIndexOf("\n", firstSectionStart - 1);
var preambleEnd = lineStart !== -1 ? lineStart + 1 : 0;
preamble = existing.slice(0, preambleEnd);
// Parse each language section: header line + everything until the next header.
var body = existing.slice(preambleEnd);
// (?![\s\S]) matches only true end-of-string, since a bare "$" with
// /m would stop at the section's first line break instead.
var langSectionRe = new RegExp(
"^(" + anyHeaderSrc + "\\n?)([\\s\\S]*?)(?=\\n" + anyHeaderSrc + "|(?![\\s\\S]))",
"gm"
);
var m;
while ((m = langSectionRe.exec(body)) !== null) {
sections.push({
code: m[2],
content: m[0] // full text of this section including header
});
}
// Determine sort rank: priority list first (by index), then alphabetical.
function getLangName(code) {
var entry = langsByCode[code];
return entry ? entry.label : code;
}
function sortRank(code) {
var idx = LANG_PRIORITY.indexOf(code);
return idx !== -1 ? idx : LANG_PRIORITY.length;
}
function compareLangs(a, b) {
var ra = sortRank(a), rb = sortRank(b);
if (ra !== rb) return ra - rb;
// Both are outside priority list — sort by display name.
return getLangName(a).localeCompare(getLangName(b), userLang);
}
// Insert the new section into the sorted list.
var newSection = { code: newCode, content: wikitext };
var inserted = false;
for (var i = 0; i < sections.length; i++) {
if (compareLangs(newCode, sections[i].code) < 0) {
sections.splice(i, 0, newSection);
inserted = true;
break;
}
}
if (!inserted) sections.push(newSection);
// Re-join: preamble + sections, each separated by exactly \n\n.
// Trim trailing whitespace off each section's content first so we
// control the spacing precisely.
var joined = sections.map(function (s) {
return s.content.replace(/\s+$/, "");
}).join("\n\n");
// Only add a separator after the preamble when it has real content.
var preambleTrimmed = preamble.replace(/\s+$/, "");
var prefix = preambleTrimmed ? preambleTrimmed + "\n\n" : "";
return prefix + joined + "\n";
}
function build() {
$(".nowizard, .nowizard .noafterwizard").hide();
var callToAction = $("#wiktku-calltoaction");
var origCallToActionText = callToAction.text();
$(mw.util.addPortletLink("p-views", "#", "Edîtora çavkaniyê veke",
"ca-wiktku-nowizard", "Vê rûpelê bi edîtora çavkaniyê çêke",
undefined, "#ca-watch")).click(function (evt) {
evt.preventDefault();
callToAction.text(origCallToActionText);
destroy();
});
if (callToAction.length) {
callToAction.text(origCallToActionText +
" Forma li jêr ji bo çêkirina madeyeke nû bi kar bîne.");
}
var startForm = $("<form id='wiktku-start-form'></form>");
var clearPageCheck = null;
if (addLangMode) {
startForm.append($("<p class='wiktku-addlang-notice'></p>").html(
"Bi vê formê tu yê zimanekî nû li rûpelê îlawe bikî. Eger ev ziman jixwe hebe, beşa nû li dawiya vî zimanî dê were îlawekirin. " +
"Ji kerema xwe netîceya dawî kontrol bike, eger problemek derkeve, li <a href='" +
mw.util.getUrl('Mediawiki talk:Gadget-start.js') +
"'>nîqaşa vê amûrê</a> problemê ragihîne."
));
clearPageCheck = $("<input type='checkbox' id='wiktku-clear-page-check'>");
var clearPageLabel = $("<label for='wiktku-clear-page-check'></label>")
.text(" Muhtewaya rûpelê paqij bike (her tiştî jê bibe û rûpelê ji nû ve binivîse)");
startForm.append(
$("<p class='wiktku-addlang-notice'></p>")
.append(clearPageCheck)
.append(clearPageLabel)
);
// Re-evaluate step visibility (e.g. the pron step) whenever this
// is toggled, since it changes what counts as "existing content".
clearPageCheck.on("change", function () {
refresh();
});
}
var spinner;
// Tab bar for switching between POS sections.
var sectionTabsDiv = $("<div id='wiktku-section-tabs'></div>");
// Tab bar for switching between step-groups (topics) within a section.
// renderGroupTabs()/updateGroupTabIndicators() (which populate and
// maintain it) live outside build() — see near "var activeSection".
var groupTabsDiv = $("<div id='wiktku-group-tabs'></div>");
// Both tab bars are sticky. group-tabs sits below section-tabs' rendered height, which varies
// when the bar wraps, so it is measured and published as a CSS custom property.
// Vector 2022's own sticky header would cover section-tabs, so its occupied height goes into
// --wiktku-siteheader-h. It slides with scroll, hence the re-measure on scroll as well as resize.
function siteHeaderOffset() {
var el = document.querySelector(".vector-sticky-header");
if (!el) return 0;
var rect = el.getBoundingClientRect();
// Present in the DOM at all times on Vector 2022, but only
// actually occupying screen space once scrolled into place —
// before that it sits translated above the viewport (negative/
// zero rect), which must not add a phantom gap under our bars.
if (rect.bottom <= 0 || rect.top > 4) return 0;
return Math.max(0, rect.bottom);
}
var stickyOffsetQueued = false;
function syncStickyTabsOffset() {
var h = sectionTabsDiv.is(":visible") ? (sectionTabsDiv.outerHeight() || 0) : 0;
startForm.css("--wiktku-stabs-h", h + "px");
startForm.css("--wiktku-siteheader-h", siteHeaderOffset() + "px");
}
function queueStickyTabsOffsetSync() {
if (stickyOffsetQueued) return;
stickyOffsetQueued = true;
window.requestAnimationFrame(function () {
stickyOffsetQueued = false;
syncStickyTabsOffset();
});
}
$(window).on("resize.wiktkuStickyTabs scroll.wiktkuStickyTabs", queueStickyTabsOffsetSync);
// "Beşên din" picker — the search box for adding a section, at the top of the form;
// renderBesenDinPicker() fills it.
var besenDinPickerDiv = $("<div id='wiktku-besendin-picker' class='wiktku-besendin-picker'></div>");
/** Puts the form back in a usable state after a failed submit and
* says what happened. Without this the button stays disabled and
* the spinner spins forever on any exception — the person is left
* staring at a form that looks busy but is dead, with the reason
* visible only in the browser console. */
function submitFailed(err) {
spinner.hide();
continueBtn.setDisabled(false);
continueBtn.$element.removeClass("wiktku-btn-sending");
var detail = (err && (err.message || err.toString())) || "sedem nediyar";
mw.notify(
"Şandin bi ser neket: " + detail +
" — tiştê ku te nivîsî winda nebûye. Heke ev dubare bibe, " +
"bi 'Edîtora çavkaniyê veke' berdewam bike.",
{ type: "error", tag: "wiktku-submit", autoHide: false }
);
// Deliberately console.error as well: the notification is for the
// person, this is for whoever they report the bug to.
if (window.console && console.error) console.error("[wiktku] submit failed", err);
}
onSubmitError = submitFailed;
startForm.submit(function (evt) {
evt.preventDefault();
continueBtn.setDisabled(true);
continueBtn.$element.addClass("wiktku-btn-sending");
spinner.show();
try {
getWikitext(function (wikitext) {
try {
if (addLangMode && clearPageCheck && clearPageCheck.prop("checked")) {
// User asked to wipe the page — write fresh content.
$("#wpTextbox1").val(wikitext);
} else if (addLangMode) {
var textbox = $("#wpTextbox1");
var existing = textbox.val() || "";
// ── h-bnr merge ──────────────────────────────────────────
// If the user filled bnr-h inputs, either merge into the
// existing {{h-bnr|…}} call or prepend a new one.
var bnrhOut = steps.bnrh.output(getValues());
if (bnrhOut) {
var newPages = bnrhOut.replace(/^\{\{h-bnr\|/, "").replace(/\}\}\n?$/, "").split("|");
var bnrhRe = /\{\{h-bnr\|([^}]*)\}\}/i;
if (bnrhRe.test(existing)) {
existing = existing.replace(bnrhRe, function (_, inner) {
var existingPages = inner.split("|").map(function (p) { return p.trim(); });
$.each(newPages, function (_, p) {
p = p.trim();
if (p && existingPages.indexOf(p) === -1) existingPages.push(p);
});
return "{{h-bnr|" + existingPages.join("|") + "}}";
});
} else {
existing = bnrhOut + existing;
}
}
// If this language's section already exists (whitespace
// differences aside), merge into it; otherwise insert new.
var langHeaderMatch = wikitext.match(new RegExp("^(\n?" + langHeaderSource("[^}]+") + "\\n)"));
if (langHeaderMatch) {
var langHeader = langHeaderMatch[1]; // e.g. "== {{ziman|ku}} ==\n"
var newCode = langHeaderMatch[2];
var existingHeader = findLangHeader(existing, newCode);
if (existingHeader) {
// Strip the header from the new output.
var newContent = wikitext.slice(langHeader.length);
// Find the insertion point: end of the existing lang section.
// A new L2 heading marks the start of the next language block.
var searchFrom = existingHeader.index + existingHeader.match.length;
// Find the next == … == heading after this section's start.
var nextL2 = existing.indexOf("\n== ", searchFrom);
var insertAt = nextL2 !== -1 ? nextL2 : existing.length;
// Ensure there's a blank line before the inserted block.
var before = existing.slice(0, insertAt);
var after = existing.slice(insertAt);
var sep = /\n\s*$/.test(before) ? "" : "\n";
var merged = before + sep + newContent + after;
// The section just added may repeat a part of speech the page already had. Both
// are renumbered across the whole language section, so a lone "Rengdêr" that was
// there before becomes "Rengdêr 1" and the new one "Rengdêr 2" -- leaving the old
// one unnumbered would make the pair unreadable on the page.
var secStart = existingHeader.index;
var nextLang = merged.indexOf("\n== ", secStart + existingHeader.match.length);
var secEnd = nextLang === -1 ? merged.length : nextLang;
textbox.val(
merged.slice(0, secStart) +
wiktkuEdit.wt.numberRepeatedPosHeadings(merged.slice(secStart, secEnd)) +
merged.slice(secEnd)
);
} else {
// New language — insert at the correct sorted position.
textbox.val(insertNewLangSection(existing, wikitext));
}
} else {
// Unexpected format — insert at sorted position anyway.
textbox.val(insertNewLangSection(existing, wikitext));
}
} else {
$("#wpTextbox1").val(wikitext);
}
$("html, body").animate({scrollTop: 0});
callToAction.text("Hindik ma!");
// The draft is now live in the editor, submitted or not — keeping
// it cached would just offer to restore already-used content
// the next time the wizard opens on this page.
try { formCache.Set({}); } catch (e) { /* storage unavailable — harmless, the content is already in the editor */ }
destroy();
} catch (e) {
// Thrown while writing the result into #wpTextbox1.
submitFailed(e);
}
});
} catch (e) {
// Catches anything thrown synchronously inside getWikitext.
submitFailed(e);
}
});
// The step the "Beşên din" picker is inserted directly above.
var PICKER_BEFORE_STEP = "tewandin";
// Tracks whether groupTabsDiv has been inserted yet.
var groupTabsInserted = false;
$.each(ordered_steps, function (i, step) {
var stepDiv = $(mw.html.element("div", {
"class": "wiktku-start-step",
id: "wiktku-start-" + step.id }));
var name = capitalize(step.name);
var nameH3 = $("<h3></h3>").addClass("wiktku-start-step-name");
var nameLabel = $("<label></label>")
.attr("for", "wiktku-start-" + step.id + "-field")
.text(name);
nameH3.append(nameLabel);
stepDiv.append(nameH3);
if (step.question_fmt) {
// question_fmt can be a plain string, or a function of the
// current values (used by steps whose question text depends
// on e.g. the chosen language) — refresh() keeps it in sync
// afterwards via the .wiktku-start-step-question class.
var questionText = typeof step.question_fmt === "function"
? step.question_fmt(getValues())
: step.question_fmt;
var questionLabel = $(mw.html.element("label", {
"class": "wiktku-start-step-question",
"for": "wiktku-start-" + step.id + "-field" }, questionText));
stepDiv.append(questionLabel);
}
var input;
if (step.choices) {
input = $(mw.html.element("select", {
"class": "wiktku-start-step-field",
id: "wiktku-start-" + step.id + "-field" }));
input.attr("data-placeholder",
capitalize(step.name) + " hilbijêre ");
populateMenus(input, step.choices);
// The language step ships ~300 languages; if a search comes up empty, fetch the other ~3400
// and refill this select before telling the user there is no such language.
if (step.id === "lang") {
input.data("wiktkuOnNoMatch", function (query, refresh) {
if (!wiktkuMergeLanguages || wiktkuLangs.isFull()) {
return;
}
wiktkuLangs.ensureFull().then(function (all) {
populateMenus(input, wiktkuMergeLanguages(all));
refresh();
});
});
}
if (step.multiple) input.attr("multiple", true);
// initSelects() runs over the whole form once it is in the DOM.
stepDiv.append(input);
// Pos-specific: the definition/example for whichever cureyê
// rêzimanî is currently chosen, as plain text right under the
// dropdown — kept in sync by refresh() via .wiktku-start-pos-example.
if (step.id === "pos") {
stepDiv.append(
$("<p class='wiktku-step-hint wiktku-start-pos-example'></p>").text(step.example(getValues()))
);
}
} else if (typeof(step.build) === "function") {
input = step.build(stepDiv);
} else {
input = $("<textarea></textarea>")
.attr("class", "wiktku-start-step-field")
.attr("id", "wiktku-start-" + step.id + "-field")
.attr("rows", step.rows || 2);
if (step.narrow) input.addClass("wiktku-start-narrow");
if (step.placeholder) {
input.attr("placeholder", typeof step.placeholder === "function" ? step.placeholder() : step.placeholder);
}
if (step.multiple) {
input.on("input", function (evt) {
refreshForTextField(evt.target, step);
});
var keyInput = buildKeyInput(step);
// Same one-box-per-word layout as the per-sense ⊕-modifier
// groups (see makeModifierLi) when this step has modifiers.
var list = $(step.ordered ? "<ol></ol>" : "<ul></ul>")
.append(step.modifiers
? makeModifierLi(input)
: $("<li></li>").append(keyInput).append(input));
stepDiv.append(list);
}
else stepDiv.append(input);
}
prepareInput(input, step);
stepDiv.hide();
// Section tabs (switch between POS sections) sit above the
// per-section fields, starting at "pos".
if (step.id === "pos") {
startForm.append(sectionTabsDiv);
}
// Step-group tabs sit just before the first grouped step in
// ordered_steps. Derived from stepIdToGroup so this keeps
// working no matter how STEP_GROUPS/ordered_steps get reordered.
if (!groupTabsInserted && stepIdToGroup[step.id] !== undefined) {
// With a single group there is nothing to switch between, so the bar is never built --
// rather than built and then hidden, which left it on screen whenever the form was
// rebuilt before refresh() next ran.
if (STEP_GROUPS.length > 1) {
startForm.append(groupTabsDiv);
}
groupTabsInserted = true;
}
// The picker sits between the senses and Tewandin: sections are added underneath it, so a
// newly picked one lands right here and pushes the rest of the form down.
if (step.id === PICKER_BEFORE_STEP) {
startForm.append(besenDinPickerDiv);
}
startForm.append(stepDiv);
});
// ── Section tab helpers ──────────────────────────────────────────────
// ── Add-section button — lives in the tab bar, disabled until pos chosen ──
var addSectionBtn = new OO.ui.ButtonWidget({
label: "+ Beşeke din",
disabled: true,
classes: [ "wiktku-add-sense-btn" ]
});
/** Adds a new, empty POS section and switches to it. */
function addPosSection() {
saveCurrentSection();
viewingPron = false;
sections.push({});
activeSection = sections.length - 1;
$.each(PER_SECTION_STEP_IDS, function (_, id) {
// Declared once here rather than with `var` in the "dfn"
// branch and again in the fallback branch below — each
// branch returns before the other runs, but both are
// lexically in this same function scope.
var stepDiv;
if (id === "dfn") {
stepDiv = $("#wiktku-start-dfn");
stepDiv.find(".wiktku-dfn-sense-tabs").remove();
stepDiv.find("ol.wiktku-dfn-list").remove();
steps.dfn.build(stepDiv);
return;
}
if (id === "etym") {
$("#wiktku-start-etym .wiktku-etym-wrap").remove();
steps.etym.build($("#wiktku-start-etym"));
return;
}
if (id === "cavkani") {
$("#wiktku-start-cavkani .wiktku-cavkani-wrap").remove();
steps.cavkani.build($("#wiktku-start-cavkani"));
return;
}
if (id === "posparams") {
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-step").remove();
return;
}
if (id === "resim") {
$("#wiktku-start-resim .wiktku-resim-wrap").remove();
return;
}
if (id === "trans") {
var transDiv = $("#wiktku-start-trans");
transDiv.find(".wiktku-trans-group").remove();
return;
}
if (id === "zarava") {
var zaravaDiv = $("#wiktku-start-zarava");
zaravaDiv.find(".wiktku-zarava-group").remove();
return;
}
if (id === "dunde") {
var dundeDiv = $("#wiktku-start-dunde");
dundeDiv.find("ul.wiktku-dunde-list").remove();
return;
}
stepDiv = $("#wiktku-start-" + id);
stepDiv.find(".wiktku-start-step-field").val("");
stepDiv.find("select.wiktku-start-step-field").trigger("wiktku:refresh");
});
// Fresh section — no "Beşên din" sub-section has been opened yet.
openedOtherSections = {};
renderBesenDinPicker();
renderSectionTabs();
refresh();
}
addSectionBtn.on("click", addPosSection);
/** Removes one POS section. Assumes the caller already confirmed and
* already checked more than one section remains. If the removed
* section was the one on screen, its in-progress DOM edits are
* discarded along with it (never saved back into sections[]) and a
* neighboring section is restored in its place. */
function removePosSection(idx) {
sections.splice(idx, 1);
if (idx === activeSection) {
viewingPron = false;
activeSection = Math.min(idx, sections.length - 1);
restoreSection(sections[activeSection]);
refresh();
} else if (idx < activeSection) {
activeSection -= 1;
}
renderSectionTabs();
}
/** Re-render the tab bar to reflect current sections[] and activeSection. */
function renderSectionTabs() {
// Detach (not remove) the button first: .empty() strips bound
// event data from any children left inside it, which would
// silently kill addSectionBtn's click handler on every re-render.
addSectionBtn.$element.detach();
sectionTabsDiv.empty();
var lang = getValues().lang || "";
if (!lang) {
sectionTabsDiv.hide();
syncStickyTabsOffset();
return;
}
sectionTabsDiv.show();
// Bilêvkirin leads the bar, ahead of the POS sections.
var pronTab = $("<span></span>")
.addClass("wiktku-section-tab wiktku-section-tab-pron")
.text("Bilêvkirin")
.toggleClass("wiktku-tab-active", viewingPron);
pronTab.on("click", function () {
if (viewingPron) return;
saveCurrentSection();
viewingPron = true;
refresh();
renderSectionTabs();
scrollTabBarIntoView("wiktku-section-tabs");
});
sectionTabsDiv.append(pronTab);
$.each(sections, function (i) {
var posVal = sections[i].pos || "";
// Numbered while there is more than one section of this part of speech, so two tabs are
// never both "Rengdêr" -- the same numbering the headings get on save.
var samePos = $.grep(sections, function (s) { return (s.pos || "") === posVal; });
var nth = 0;
$.each(sections, function (j, s) {
if ((s.pos || "") === posVal && j <= i) nth++;
});
var tabLabel = posVal
? capitalize(posVal) + (samePos.length > 1 ? " " + nth : "")
: ("Beş " + (i + 1));
var tab = $("<span></span>")
.addClass("wiktku-section-tab")
.text(tabLabel)
.toggleClass("wiktku-tab-active", i === activeSection && !viewingPron);
(function (idx) {
tab.on("click", function () {
if (idx === activeSection && !viewingPron) return;
// Leaving Bilêvkirin doesn't need a save — its field
// is shared and never part of a section snapshot.
if (!viewingPron) saveCurrentSection();
viewingPron = false;
activeSection = idx;
restoreSection(sections[activeSection]);
refresh();
renderSectionTabs();
scrollTabBarIntoView("wiktku-section-tabs");
});
}(i));
sectionTabsDiv.append(tab);
});
// One delete button for the whole bar — not one per tab — same
// reasoning as the Mane tabs' trash button: deletes whichever
// Beş is currently open, so there's only one thing a click on it
// can possibly remove. Not shown on Bilêvkirin (nothing to
// delete there) or when it's the only Beş left.
if (sections.length > 1 && !viewingPron) {
var activeTabLabel = sections[activeSection] && sections[activeSection].pos
? capitalize(sections[activeSection].pos)
: ("Beş " + (activeSection + 1));
var sectionTrash = makeTrashButton("Vê beşê jê bibe (" + activeTabLabel + ")");
sectionTrash.on("click", function () {
if (!window.confirm("Tu dixwazî \"" + activeTabLabel + "\" jê bibî? Ev tişt nayê vegerandin.")) return;
removePosSection(activeSection);
});
sectionTabsDiv.append(sectionTrash.$element);
}
// Enable "+ Beşeke din" only when the active section has a pos chosen.
var activePos = sections[activeSection] && sections[activeSection].pos
? sections[activeSection].pos
: getValues().pos;
addSectionBtn.setDisabled(!activePos);
sectionTabsDiv.append(addSectionBtn.$element);
syncStickyTabsOffset();
}
renderSectionTabs();
/**
* Re-populates a growing "multiple" field list (bnrh, je, tekildar, …) from a saved array.
* Values are set with .val() plus an "input" event so refreshForTextField() grows the rows,
* exactly as typing into the last row does.
*/
function restoreMultiField(id, values) {
var container = $("#wiktku-start-" + id);
$.each(values || [], function (i, v) {
var field = container.find(".wiktku-start-step-field").eq(i);
if (!field.length || !v) return;
field.val(v).trigger("input");
});
}
/**
* Restores a full cached draft: shared lang/pron/bnrh fields, then
* every section (via the same restoreSection()/refresh()/
* renderSectionTabs() sequence already used when switching tabs).
* @param cached {{lang: string, pron: string, bnrh: Array, sections: Array, activeSection: number}}
*/
function restoreFromCache(cached) {
if (cached.lang) {
$("#wiktku-start-lang-field").val(cached.lang);
$("#wiktku-start-lang-field").trigger("wiktku:refresh");
}
// Rebuild dependent steps (pos, pron wrap, etc.) now that lang is set.
refresh();
if (cached.pron !== undefined) {
$("#wiktku-start-pron .wiktku-pron-text").val(cached.pron);
}
if (cached.bnrh) {
restoreMultiField("bnrh", cached.bnrh);
}
sections = (cached.sections && cached.sections.length) ? cached.sections : [{}];
activeSection = (typeof cached.activeSection === "number" &&
cached.activeSection >= 0 && cached.activeSection < sections.length)
? cached.activeSection : 0;
restoreSection(sections[activeSection]);
refresh();
renderSectionTabs();
updateLastSavedIndicator(cached.savedAt || null);
formDirty = false;
wiktkuNotify("Danûstandina berê ya qeydkirî ji cache hate barkirin.",
{ type: "info", tag: "wiktku-cache-restore" });
}
// Update the active tab's label whenever pos changes.
startForm.on("change", "#wiktku-start-pos-field", function () {
// renderSectionTabs() labels each tab from sections[i].pos, which
// is only synced from the DOM by saveCurrentSection() (on tab
// switch/section add). Without this, the active tab's own label
// stays on the previous pos (or "Beş N") until the user switches
// away and back. Update it directly so the rename is immediate.
if (sections[activeSection]) sections[activeSection].pos = $(this).val();
renderSectionTabs();
// A new cureyê rêzimanî means any previously chosen cinsiyet/
// cureyê lêkerê no longer applies — clear both selections (and
// the shared hidden cinsiyet field, since rebuildTa()/syncHelpers()
// fall back to it) before wiping the textarea, so nothing stale
// gets carried over into the rebuilt template.
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-cinsiyet-sel").val("");
ppDiv.find(".wiktku-posparams-jimar-sel").val("");
ppDiv.find(".wiktku-posparams-lektur-sel").prop("selectedIndex", 0);
ppDiv.find(".wiktku-posparams-lekturform-sel").prop("selectedIndex", 0);
$("#wiktku-start-cinsiyet-field").val("");
$("#wiktku-start-jimar-field").val("");
ppDiv.find(".wiktku-posparams-text").val("");
// refresh() runs steps.posparams.syncHelpers(), which toggles the
// param rows for the new pos and — since the textarea is now
// empty — calls rebuildTa() to repopulate it.
refresh();
});
// Show the tab bar as soon as a language is selected.
startForm.on("change", "#wiktku-start-lang-field", function () {
renderSectionTabs();
});
var footer = $("<div id='wiktku-start-footer'></div>");
spinner = $(mw.html.element("img", {
id: "wiktku-start-spinner",
src: "//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif",
alt: "Tê barkirin…",
title: "Tê barkirin…" }));
footer.append(spinner.hide());
continueBtn = new OO.ui.ButtonWidget({
id: "wiktku-unhide-editform",
label: "Bişîne",
flags: [ "primary", "progressive" ]
});
continueBtn.on("click", function () {
// Immediate feedback that the click registered
continueBtn.$element.addClass("wiktku-btn-flash");
setTimeout(function () {
continueBtn.$element.removeClass("wiktku-btn-flash");
}, 500);
wiktkuNotify("Tê şandin…", { type: "info", tag: "wiktku-submit" });
startForm.trigger("submit");
});
continueBtn.$element.addClass("wiktku-footer-primary");
footer.append(continueBtn.$element);
// "Edîtora çavkaniyê veke" button — inline fallback for the portlet link
var sourceEditorBtn = new OO.ui.ButtonWidget({
label: "Edîtora çavkaniyê veke",
flags: [ "destructive" ],
});
sourceEditorBtn.on("click", function () {
destroy();
});
sourceEditorBtn.$element.addClass("wiktku-footer-primary");
footer.append(sourceEditorBtn.$element);
// ── Cache save/clear buttons ─────────────────────────────────────────
// Manual, page-scoped save of the entire wizard (all sections, not
// just the active one) via [[MediaWiki:Gadget-StorageUtils.js]]'s
// ObjectStorage. Restoring happens silently on load at the end of
// build(), once the full language list is available.
//
// Saving also happens automatically (debounced while typing, plus a
// periodic fallback) — see "Autosave" below — since relying on the
// person to remember to click "Li cache qeyd bike" before an
// accidental tab close/reload defeats the point of having a cache
// at all.
function buildCacheSnapshot() {
return {
lang: getValues().lang || "",
pron: $("#wiktku-start-pron .wiktku-pron-text").val() || "",
// bnrh is shared (page-level), same as lang/pron above — not
// per-section, so it doesn't live in sections[].
bnrh: getValuesForStep("bnrh"),
sections: sections,
activeSection: activeSection,
savedAt: Date.now()
};
}
// ── Last-saved indicator ─────────────────────────────────────────────
var lastSavedEl = $("<span id='wiktku-last-saved'></span>");
function formatSavedTime(ts) {
try {
return new Date(ts).toLocaleTimeString(mw.config.get("wgUserLanguage"),
{ hour: "2-digit", minute: "2-digit" });
} catch (e) {
return new Date(ts).toLocaleTimeString();
}
}
function updateLastSavedIndicator(ts) {
lastSavedEl.text(ts ? ("Cache: qeydkirî di saet " + formatSavedTime(ts) + " de") : "");
}
// ── Dirty-state tracking + autosave ──────────────────────────────────
// formDirty mirrors "does the live form differ from what's in cache".
// Anything the person types/selects sets it; a successful save (auto
// or manual) clears it; destroy() (final submit, or switching to the
// source editor) also clears it since the wizard's job is over at
// that point either way. formDirty/autosaveDebounceTimer/autosaveInterval
// are declared in the outer scope (near activeGroup) — assigned here,
// not re-declared — so destroy() can reach them too.
var AUTOSAVE_DEBOUNCE_MS = 8000; // save shortly after typing pauses
var AUTOSAVE_INTERVAL_MS = 60000; // …and at least this often regardless
function performAutosave() {
if (!formDirty) return;
saveCurrentSection();
try {
formCache.Set(buildCacheSnapshot());
formDirty = false;
updateLastSavedIndicator(Date.now());
} catch (e) {
// Silent on autosave failure (storage blocked/full, etc.) — the
// manual button still surfaces a proper error if the person
// tries it directly, no need to interrupt typing with a toast.
}
}
function scheduleAutosave() {
formDirty = true;
if (autosaveDebounceTimer) clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = setTimeout(performAutosave, AUTOSAVE_DEBOUNCE_MS);
}
startForm.on("input change", "input, textarea, select", scheduleAutosave);
autosaveInterval = setInterval(performAutosave, AUTOSAVE_INTERVAL_MS);
// Warn on accidental tab close/reload while there's anything unsaved.
$(window).on("beforeunload.wiktkuUnsaved", function (e) {
if (!formDirty) return undefined;
e.preventDefault();
e.returnValue = "";
return "";
});
var saveCacheBtn = new OO.ui.ButtonWidget({
label: "Li cache qeyd bike",
title: "Halê vê formê yê niha (ziman, bilêvkirin û hemû beş) li cache (localStorage) qeyd bike, da ku eger rûpel bêyî xwestin were girtin an ji nû ve were barkirin, tu karibî berdewam bikî."
});
saveCacheBtn.on("click", function () {
saveCurrentSection();
// ObjectStorage.Set() doesn't return a success/failure value, so
// the only way to detect a real failure (storage blocked, quota
// exceeded, JSON.stringify choking on something unexpected) is to
// catch it here rather than inspect a (nonexistent) return value.
try {
var snapshot = buildCacheSnapshot();
formCache.Set(snapshot);
formDirty = false;
updateLastSavedIndicator(snapshot.savedAt);
wiktkuNotify("Halê formê li cache hate qeydkirin.", { type: "success", tag: "wiktku-cache-save" });
} catch (e) {
mw.notify("Qeydkirina li cache bi ser neket.", { type: "error", tag: "wiktku-cache-save" });
}
});
var clearCacheBtn = new OO.ui.ButtonWidget({
label: "Cache paqij bike",
flags: [ "destructive" ],
title: "Datayên qeydkirî yên vê rûpelê ji cache jê bibe (forma ku niha xuya dike naguherîne)."
});
clearCacheBtn.on("click", function () {
formCache.Set({});
// Cancel the scheduled autosave, or it would rewrite the cache from the still-filled form.
// The form is left clean, so autosave resumes only once something is edited again.
if (autosaveDebounceTimer) {
clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = null;
}
formDirty = false;
updateLastSavedIndicator(null);
wiktkuNotify("Cache hate paqijkirin.", { type: "success", tag: "wiktku-cache-clear" });
});
// Everything sits on the bar: no overflow menu, nothing a click away.
// Settings button — opens the Tercîh dialog. Plain OOUI icon names depend on which icon-pack
// module happens to be loaded, so this uses the actual Codex "settings" (color-subtle) SVG from
// Commons directly, the same way the spinner above hotlinks its gif.
var prefsBtn = $("<button></button>", {
type: "button",
"class": "wiktku-prefs-btn",
title: "Tercîh — sazkariyên vê amûrê biguherîne",
"aria-label": "Tercîh"
});
prefsBtn.on("click", function (e) {
e.preventDefault();
wiktkuOpenPrefsDialog();
});
clearCacheBtn.$element.addClass("wiktku-btn-danger");
saveCacheBtn.$element.addClass("wiktku-footer-cache");
clearCacheBtn.$element.addClass("wiktku-footer-cache");
footer.append(saveCacheBtn.$element)
.append(clearCacheBtn.$element)
.append(prefsBtn)
.append($("<span class='wiktku-cache-note'></span>").append(lastSavedEl));
startForm.append(footer);
// Moved off-screen rather than display:none. WikiEditor and the mobile editor measure
// #wpTextbox1 while the wizard is up, and a hidden ancestor reports zero, which they then keep.
// Off-screen keeps it laid out and measurable.
$("#editform").addClass("wiktku-offscreen").before(startForm);
// renderGroupTabs() looks up #wiktku-group-tabs via a live DOM
// selector, so it must run after startForm is actually in the document.
renderGroupTabs();
renderBesenDinPicker();
// One pass over the whole form: every select that hasn't already been
// initialised, and isn't opted out with .wiktku-plain-select, gets
// the searchable picker.
initSelects(startForm);
refresh();
// Give every draggable textarea a custom resize grip (works
// consistently on Safari/iOS, unlike the native corner grip — see
// the "Manual drag-to-resize handle" comment in styles.css), and
// keep applying it to textareas added later on (new sense blocks,
// quote-source fields, nimûne rows, …).
wiktkuInitManualResize(startForm);
new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
$(mutation.addedNodes).each(function () {
if (this.nodeType !== 1) return;
wiktkuInitManualResize(this);
});
});
}).observe(startForm[0], { childList: true, subtree: true });
// Now that the full language list is loaded, silently restore any
// draft the editor previously saved for this page.
var cachedDraft = formCache.Get();
if (cachedDraft && cachedDraft.sections && cachedDraft.sections.length) {
restoreFromCache(cachedDraft);
}
}
// Only oojs-ui-core blocks the initial render now — icons-moderation
// (the trash glyph) is fetched lazily by makeTrashButton() instead, so
// slow connections aren't held up loading it before anyone needs it.
mw.loader.using(["oojs-ui-core"], function () {
function wiktkuProceedWithData(wiktkuData) {
var d = wiktkuData || {};
// Populate metadata.
if (d.metadata) $.extend(metadata, d.metadata);
// Populate gender labels.
if (d.genderLabels) $.extend(GENDER_BASE_LABELS, d.genderLabels);
// Jimar (number) code definitions
if (d.numberLabels) $.extend(NUMBER_LABELS, d.numberLabels);
if (d.senseGroups) SENSE_GROUPS = d.senseGroups;
// Headings for the page-level word lists, from the same table DefSideBoxes reads.
$.each(d.pageSubsections || [], function (_, sub) {
if (sub.kind !== "wordlist" || !steps[sub.id]) return;
steps[sub.id].header = sub.header;
steps[sub.id].name = decapitalize(sub.header);
});
if (d.wordlistTemplates && d.wordlistTemplates.length) WORDLIST_TEMPLATE = d.wordlistTemplates[0];
if (d.deverQuickPicks) DEVER_QUICK_PICKS = d.deverQuickPicks;
if (d.lekturChoices) LEKTUR_CHOICES = d.lekturChoices;
if (d.lekturFormChoices) LEKTUR_FORM_CHOICES = d.lekturFormChoices;
// Populate lang choices (pinned list).
if (d.langChoices) steps.lang.choices = choicesFromObject(d.langChoices, true);
// Populate langsByCode and merge full language list from d.ziman.
// d.ziman is a {code: name} object with all known languages.
$.each(steps.lang.choices, function (i, choice) {
if (choice.sortKey === undefined) choice.sortKey = choice.label;
langsByCode[choice.code] = choice;
});
// Only the ~300 languages of ziman-core.json are merged in at load; the other ~3400 arrive
// through wiktkuMergeLanguages() the first time somebody searches for one of them. The pinned
// choices are kept aside so a second merge rebuilds the list instead of appending to it.
var wiktkuPinnedChoices = steps.lang.choices;
wiktkuMergeLanguages = function (zimanNames) {
var pinnedCodes = {};
$.each(wiktkuPinnedChoices, function (i, c) { pinnedCodes[c.code] = true; });
var extraChoices = [];
$.each(zimanNames || {}, function (code, name) {
// A proto-language is never the language of an ordinary entry, but it is the whole point
// of a reconstruction page, so ns 108 keeps them in the list.
if (!isReconstruction && /-pro$/i.test(code)) return;
if (/ziman[eê]\s+destan/i.test(name)) return;
if (pinnedCodes[code]) return;
var choice = { code: code, label: name, sortKey: name };
extraChoices.push(choice);
langsByCode[code] = choice;
});
// One Intl.Collator built once and reused for the whole sort, instead of localeCompare() rebuilding a collator on every pair -- same order, far faster over ~3700 entries.
var langCollator = new Intl.Collator(userLang);
extraChoices.sort(function (a, b) {
return langCollator.compare(a.sortKey, b.sortKey);
});
steps.lang.choices = wiktkuPinnedChoices.concat(extraChoices);
// The translation/dialect steps reference any language, so they follow the same list.
steps.trans.keys = steps.lang.choices;
steps.dunde.keys = steps.lang.choices;
steps.zarava.keys = getZaravaKeys(steps.lang.choices);
return steps.lang.choices;
};
wiktkuMergeLanguages(wiktkuLangs.names());
// Keep the full merged list around for trans/dunde/zarava — those
// should still be able to reference any language even when the
// Tercîh panel restricts the main "ziman" step below — then, if
// the user set a restriction there, narrow steps.lang.choices
// itself to just those codes.
var wiktkuFullLangChoices = steps.lang.choices;
if (wiktkuPrefs.langCodes && wiktkuPrefs.langCodes.length) {
var wiktkuAllowedLangSet = {};
$.each(wiktkuPrefs.langCodes, function (i, code) { wiktkuAllowedLangSet[code] = true; });
var wiktkuRestrictedChoices = $.grep(wiktkuFullLangChoices, function (c) {
return !!wiktkuAllowedLangSet[c.code];
});
// Fall back to the full list instead of showing an empty
// dropdown if none of the saved codes matched anything
// (typo'd code, data-main.json changed since, …).
steps.lang.choices = wiktkuRestrictedChoices.length ? wiktkuRestrictedChoices : wiktkuFullLangChoices;
}
// Populate POS choices — also a plain {code: label} object, from data-sections.json.
if (d.posChoices) steps.pos.choices = choicesFromObject(d.posChoices, false);
// Populate template definitions for the dfn sense-block builder.
if (d.tplDefs) steps.dfn.tplDefs = d.tplDefs;
// Populate pos placeholder examples.
if (d.posExamples) steps.pos.posExamples = d.posExamples;
// Populate citation template definitions for the quote widget.
if (d.jederDefs) steps.dfn.jederDefs = d.jederDefs;
// Populate pronunciation and etymology template insert lists.
if (d.pronTpls) {
steps.pron.pronTpls = d.pronTpls;
steps.pron.refreshTplSelect();
}
if (d.etymTpls) {
steps.etym.etymTpls = d.etymTpls;
steps.etym.refreshTplSelect();
}
// Sync trans/dunde/zarava keys with the full language list
// (not the possibly Tercîh-restricted steps.lang.choices).
steps.trans.keys = wiktkuFullLangChoices;
steps.dunde.keys = wiktkuFullLangChoices;
steps.zarava.keys = getZaravaKeys(wiktkuFullLangChoices);
build();
}
// The data ships inside wikt.edit, so it is already parsed and in memory.
wiktkuProceedWithData(wiktkuEdit.data);
});
});
//</nowiki>
gdsp3f3wjz6q3dyimv1psnpc4oa9xb4
Modul:ckb-headword
828
1358794
6531785
6341545
2026-09-16T07:27:31Z
Wikihez
7344
no Sorani forms or transliteration on Latin-script entries: the endings and stems are Arabic-script
6531785
Scribunto
text/plain
-- Binêre [[Şablon:ckb-rengdêr]], [[Şablon:ckb-navdêr]], [[Şablon:ckb-lêker]]
--
-- Nivîskar: [[:Bikarhêner:Şêr|Bikarhêner:Şêr]]
-- Armanc: Rêkxistina serwaje, tîpguhêzî, kategorî û etîketên ACCEL ji bo soranî (ckb)
-- Guherto: 2026-08-23
local export = {}
local pos_functions = {}
local lang = require("Module:languages").getByCode("ckb")
local full_headword = require("Module:headword").full_headword
local scripts = require("Module:scripts")
local gsub = mw.ustring.gsub
local match = mw.ustring.match
-- =========================================================================
-- LÎSTEYA 100 RENGDÊRÊN NEQIYASBAR (KU NAYÊN BERAWIRDKIRIN)
-- =========================================================================
local neqiyasbar_list = {
["مردوو"] = true, ["زیندوو"] = true, ["دووگیان"] = true, ["سووتاو"] = true,
["کوژراو"] = true, ["کراوە"] = true, ["داخراو"] = true, ["شکاو"] = true,
["سوورکراوە"] = true, ["پێگەیشتوو"] = true, ["خنکاو"] = true, ["کوێر"] = true,
["کەڕ"] = true, ["ڵاڵ"] = true, ["نەخۆش"] = true, ["ساغ"] = true,
["بریندار"] = true, ["پەککەوتوو"] = true, ["تەواو"] = true, ["نیوە"] = true,
["چوارگۆشە"] = true, ["سێگۆشە"] = true, ["بازنەیی"] = true, ["ڕاست"] = true,
["ئاسۆیی"] = true, ["ستوونی"] = true, ["داری"] = true, ["ئاسنی"] = true,
["بەردی"] = true, ["زێڕی"] = true, ["زیوی"] = true, ["شوشەیی"] = true,
["پلاستیکی"] = true, ["پۆڵایی"] = true, ["یەکەم"] = true, ["دووەم"] = true,
["سێیەم"] = true, ["چوارەم"] = true, ["پێنجەم"] = true, ["شەشەم"] = true,
["حەوتەم"] = true, ["هەشتەم"] = true, ["نۆیەم"] = true, ["دەیەم"] = true,
["تاک"] = true, ["جووت"] = true, ["تەنیا"] = true, ["دووانە"] = true,
["کوردی"] = true, ["ئەڵمانی"] = true, ["ئینگلیزی"] = true, ["فەرەنسی"] = true,
["عەرەبی"] = true, ["فارسی"] = true, ["تورکی"] = true, ["ئەمریکی"] = true,
["ژاپۆنی"] = true, ["چینی"] = true, ["ڕۆژئاوایی"] = true, ["ڕۆژهەڵاتی"] = true,
["باکووری"] = true, ["باشووری"] = true, ["ساڵانە"] = true, ["مانگانە"] = true,
["حەفتانە"] = true, ["ڕۆژانە"] = true, ["شەوانە"] = true, ["پێشوو"] = true,
["داهاتوو"] = true, ["ئێستایی"] = true, ["مێژوویی"] = true, ["نێر"] = true,
["مێ"] = true, ["مرۆڤیی"] = true, ["ئاژەڵی"] = true, ["ڕووەکی"] = true,
["کیمیایی"] = true, ["فیزیکی"] = true, ["پزیشکی"] = true, ["یاسایی"] = true,
["نایاسایی"] = true, ["سیاسی"] = true, ["ئابووری"] = true, ["ئایینی"] = true,
["کۆمەڵایەتی"] = true, ["فەرمی"] = true, ["نافەرمی"] = true, ["گشتی"] = true,
["تایبەت"] = true, ["ڕەها"] = true, ["ڕێژەیی"] = true, ["سەرەکی"] = true,
["لاوەکی"] = true, ["زانستی"] = true, ["ئەدەبی"] = true, ["هونەری"] = true,
["وەرزشی"] = true, ["جیهانی"] = true, ["ناوخۆیی"] = true, ["دەرەکی"] = true,
["دیجیتاڵی"] = true, ["ئۆتۆماتیکی"] = true, ["دەستی"] = true, ["ئەتۆمی"] = true,
}
local ugsub = mw.ustring.gsub
local umatch = mw.ustring.match
local function fixBizroke(text)
if not text or text == "" then return "" end
-- Heke berê dengdêra 'i' an dengdêrên din tê de hebin, ducar nake û dest lê nade!
if match(text, "[iîeêaouûIÎEÊAOUÛ]") then
return text
end
local C = "[^aeêiîouûAEÊIÎOUÛ%s%p%d]"
-- 1. Destpêk: du dengdar bêyî sp/st/sk/şt
if not (umatch(text, "^s[ptk]") or umatch(text, "^ş[ptk]")) then
text = ugsub(text, "^(" .. C .. ")(" .. C .. ")", "%1i%2")
end
-- 2. Nav: sê dengdar li pey hev (mînak: şkr -> şkir)
text = ugsub(text, "(" .. C .. ")(" .. C .. ")(" .. C .. ")", "%1%2i%3")
-- 3. Dawî: du dengdar li dawiya peyvê
text = ugsub(text, "(" .. C .. ")(" .. C .. ")$", "%1i%2")
return text
end
-- =========================================================================
-- FONKSIYONÊN CEŞNÊN PEYVAN
-- =========================================================================
-- 1. RENGDÊR
pos_functions["rengdêr"] = function(args, data, trBase, is_form, is_latn)
if is_form then return end
-- The endings below are Arabic-script Sorani.
if is_latn then return end
local word = data.pagename
-- Heke '-' hatibe nivîsandin an di lîsteyê de be, dibe neqiyasbar
if neqiyasbar_list[word] or args[1] == "-" or args[2] == "-" or args["komp"] == "-" then
table.insert(data.inflections, {label = "neqiyasbar"})
table.insert(data.categories, "Rengdêrên neqiyasbar bi soranî")
else
local comp = word .. "تر"
local sup = word .. "ترین"
local trComp = trBase ~= "" and (trBase .. "tir") or nil
local trSup = trBase ~= "" and (trBase .. "tirîn") or nil
table.insert(data.inflections, {
label = "komparatîv",
accel = {form = "comparative"},
{term = comp, translit = trComp}
})
table.insert(data.inflections, {
label = "sûperlatîv",
accel = {form = "superlative"},
{term = sup, translit = trSup}
})
end
end
-- 2. NAVDÊR
pos_functions["navdêr"] = function(args, data, trBase, is_form, is_latn)
if is_form then return end
local word = data.pagename
local pl = args["pl"] or args[2]
if not pl then
-- The plural endings below are Arabic-script Sorani; on a Latin entry only an explicit pl= shows.
if is_latn then return end
local lastLetter = mw.ustring.sub(word, -1)
if match(lastLetter, "[اێیۆ]") then
pl = word .. "یان"
elseif lastLetter == "ە" then
pl = gsub(word, "ە$", "") .. "ان"
else
pl = word .. "ان"
end
end
if pl ~= "-" then
local trPl
if not is_latn then
local rawPlTr = lang:transliterate(pl) or ""
rawPlTr = gsub(rawPlTr, "%(î%)", "(y)")
trPl = fixBizroke(rawPlTr)
end
table.insert(data.inflections, {
label = "pirjimar",
accel = {form = "p"},
{term = pl, translit = trPl}
})
end
end
-- 3. LÊKER
pos_functions["lêker"] = function(args, data, trBase, is_form, is_latn)
if is_form then return end
local word = data.pagename
local presStem = args["reh"] or args[2]
-- The past stem is made by dropping Arabic-script ن, so on a Latin entry only an explicit reh= shows.
if not is_latn then
local pastStem = gsub(word, "ن$", "")
local trPast = fixBizroke(lang:transliterate(pastStem) or "")
table.insert(data.inflections, {label = "reha borî", {term = pastStem, translit = trPast}})
end
if presStem and presStem ~= "" then
local trPres = not is_latn and fixBizroke(lang:transliterate(presStem) or "") or nil
table.insert(data.inflections, {label = "reha niha", {term = presStem, translit = trPres}})
end
end
-- 4. HOKER
pos_functions["hoker"] = function(args, data, trBase, is_form, is_latn)
if is_form then return end
if is_latn then return end
if args[2] == "+" then
local comp = data.pagename .. "تر"
local trComp = trBase ~= "" and (trBase .. "tir") or nil
table.insert(data.inflections, {
label = "komparatîv",
accel = {form = "comparative"},
{term = comp, translit = trComp}
})
end
end
-- =========================================================================
-- DERGEHÊ SEREKE (MAIN ENTRY POINT)
-- =========================================================================
function export.show(frame)
local raw_pos = frame.args["nav"] or frame.args[1] or frame.args["pos"] or "rengdêr"
local args = frame:getParent() and frame:getParent().args or frame.args
local pagename = args["serwaje"] or args["head"] or mw.title.getCurrentTitle().subpageText
local tr = args["tr"]
-- Latin-script entries get no Sorani forms or transliteration: both are Arabic-script. ckb is
-- registered with ku-Arab only, so lang:findBestScript() never reports Latn; judge by sc= and
-- by the characters of the page name itself.
local check_sc = scripts.findBestScriptWithoutLang(pagename)
local is_latn = args["sc"] == "Latn" or (check_sc and check_sc:getCode() == "Latn")
-- Kontrol dike ka formeke tewandî ye (form=erê)
local is_form = false
if args["form"] == "erê" or frame.args["form"] == "erê" or raw_pos:find("^form") then
is_form = true
end
-- Tîpguhêziya biaqil a bizrokê
local translit = nil
if tr and tr ~= "" then
translit = tr
elseif not is_latn then
local rawTr = lang:transliterate(pagename) or ""
rawTr = gsub(rawTr, "%(î%)", "(y)")
local fixed = fixBizroke(rawTr)
if fixed ~= "" and fixed ~= pagename then
translit = fixed
end
end
local translits_list = translit and {translit} or nil
local custom_heads = (args["serwaje"] or args["head"]) and {args["serwaje"] or args["head"]} or nil
local data = {
lang = lang,
heads = custom_heads,
translits = translits_list,
inflections = {},
pagename = pagename,
no_redundant_head_cat = true,
}
if is_form then
data.pos_category = "formên " .. raw_pos .. "an"
data.categories = {
"Formên " .. raw_pos .. "an bi soranî"
}
else
data.pos_category = raw_pos
data.categories = {
"Lema bi soranî",
raw_pos:gsub("^%l", mw.ustring.upper) .. " bi soranî"
}
end
if pos_functions[raw_pos] then
pos_functions[raw_pos](args, data, translit or "", is_form, is_latn)
end
return full_headword(data)
end
return export
6075mp51lfersnddby74qzwd28bvweo
Bikarhêner:Wikihez/Gadget-test/wikt-core.js
2
1358903
6531663
6424904
2026-09-15T17:27:11Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none
6531663
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.core.js]]
*
* What a page view needs: the helpers that find headings and definition lines, and the settings the
* launcher reads. Widgets, wikitext parsing, language lists and the full data tables are in wikt.edit.
*
* A package module's files are private to it, so this main file is what other gadgets reach:
* var sections = require("ext.gadget.wikt.core").sections;
*/
// Every label these gadgets show that more than one of them shows, so the wording is written once and
// reads the same everywhere. Here rather than in wikt.edit because the view path shows some of them too.
var MESSAGES = {
// actions
add: "Lê zêde bike",
cancel: "Betal bike",
edit: "Biguhêre",
loading: "Tê barkirin…",
// words and their modifiers
addModifier: "Xisûsiyetekê lê zêde bike",
removeModifier: "Vê xisûsiyetê jê bibe",
removeWord: "Vê kelîmeyê jê bibe",
// senses
anotherSense: "Maneyeke din",
addAnotherSense: "Maneyeke din zêde bike",
moreMenu: "Hevmane û Bêhtir",
// templates
templatesLoading: "Şablon tên barkirin…",
templatesFailed: "Şablon nehatin barkirin.",
searchTemplate: "Li navê şablonê bigere…",
addTemplate: "Şablonekê lê zêde bike…",
pickOrTypeTemplate: "Şablonekî ji lîsteyê hilbijêre an rasterast binivîse…",
openTemplatePage: "Rûpela şablonê veke",
// citations
citoidFill: "Bi otomatîk tijî bike",
citoidAuto: "Bi Citoidê otomatîk zêde bike",
citoidFilled: "✓ Malûmat hatin dagirtin — li jêr kontrol bike.",
citoidNeedsUrl: "Ji kerema xwe re pêşî URL, DOI an ISBNekê binivîse.",
sourceByHand: "Bi destê xwe binivîsim",
sourceFromCategory: "Ji Kategorî:Çavkanî şablonekê hilbijêre",
sourceRaw: "Şablonê wek xwe biparêze (jixwe nivîsandî)",
// glosses
tercimePrompt: "Tercimeyên rasterast ên kurmancî… Ji bo school-a îngilîzî li vir 'mekteb, medrese' binivîse",
// errors
saveFailed: "Nikare rûpelê xilas bike: ",
sectionMissing: "Ev beş li vir tune: "
};
module.exports = {
// Package file names as Gadgets-definition lists them: the files live under "wikt.core/".
sections: require("./wikt.core/sections.js"),
prefs: require("./wikt.core/prefs.js"),
msg: MESSAGES
};
// </nowiki>
1962rrk1wqouyllpj1ex8pe5myuvl77
Bikarhêner:Wikihez/Gadget-test/dsb.js
2
1358907
6531636
6513961
2026-09-15T14:43:26Z
Wikihez
7344
Pencils reveal on hover only on devices with a real pointer, so a first tap on iOS Safari opens the link
6531636
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.dsb.js]]
*
* The part of DefSideBoxes that runs on a page view: finds the definition lines and hangs a pencil on
* each. Forms, dialogs and saves are in wikt.dsb-panels, fetched on the first click (never on hover).
*
* The "Nimûne/jêgirtin" item drives add-examples, which wikt.dsb-panels depends on -- so it is loaded by
* the time any panel exists, and never on a plain page view.
*/
(function () {
"use strict";
var core = require("ext.gadget.wikt.core");
var sections = core.sections;
var MANE_SECTION = sections.MANE_SECTION;
var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS;
var msg = core.msg;
var headingOuter = sections.headingOuter;
var headingDepth = sections.headingDepth;
var headingText = sections.headingText;
var isManeHeading = sections.isManeHeading;
var sectionTypeOf = sections.sectionTypeOf;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var headingTextOrdinalWithinLanguage = sections.headingTextOrdinalWithinLanguage;
var languageCodeForOl = sections.languageCodeForOl;
var eachLeafDefinition = sections.eachLeafDefinition;
if (mw.config.get("wgAction") !== "view") {
return;
}
// An entry, a reconstruction, or a sandbox -- see sections.isEntryPage().
if (!sections.isEntryPage()) {
return;
}
if (/&printable=yes|&diff=|&oldid=/.test(window.location.search)) {
return;
}
window.wikt = window.wikt || {};
if (window.wikt.defSideBoxesLoaded) {
return;
}
window.wikt.defSideBoxesLoaded = true;
// A save from the queue box reloads with core's post-edit flag set (see dsb-panels.js). Core loads the
// module that reads the flag only after a form save, so it is loaded here; it shows the toast and clears the flag.
if (mw.storage.session.get("mw-PostEdit" + mw.config.get("wgPageName"))) {
mw.loader.load("mediawiki.action.view.postEdit");
}
// The panels are a separate module so their ~100 KB never lands on a page view. Fetched once, on the
// first click that actually needs a form -- never on hover; every later call reuses the same promise.
var panelsPromise = null;
var panelsApi = null;
// A panel is built once and merely shown again after that, so the reveal cannot live in the builders.
// Only click handlers call this -- hover must never pull the panels module.
function revealQueue() {
withPanels(function (panels) {
panels.openQueueBox();
});
}
function withPanels(fn) {
if (!panelsPromise) {
panelsPromise = mw.loader.using("ext.gadget.wikt.dsb-panels").then(function (require) {
panelsApi = require("ext.gadget.wikt.dsb-panels");
return panelsApi;
});
}
panelsPromise.then(fn, function () {
mw.notify("Nikare amûrên guhartinê bar bike.", { type: "error" });
});
}
// The element holding an example's last line of text: a quotation's source line and its quote are
// blocks, so a pencil after them would sit on a line of its own.
function lastTextLine(el) {
var last = el.lastElementChild;
while (last && /^(DIV|DL|DD|UL|OL|LI)$/.test(last.tagName)) {
el = last;
last = el.lastElementChild;
}
return el;
}
// A pencil on every example and quotation under a sense, opening add-examples' form on that one line.
function attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3) {
// A queued change redraws the sense (dsb-panels.js raises this); its examples get their pencils again.
if (!$li.data("dsbExampleRedraw")) {
$li.data("dsbExampleRedraw", true);
$li.on("dsbSenseRedrawn", function () {
attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3);
});
}
sections.exampleItemsOf($li).each(function () {
var item = this;
if ($(item).hasClass("dsb-has-example-toolbar")) {
return;
}
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", "Nimûneyê biguhêre");
var $toolbar = $("<span class='dsb-example-toolbar'></span>").append($toggle);
$toggle.on("click", function (e) {
e.preventDefault();
e.stopPropagation();
withPanels(function (panels) {
panels.editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, item);
});
});
// Touch has no hover: a tap on the example shows its pencil, as a tap on a sense does.
$(lastTextLine(item)).append($toolbar);
$(item).addClass("dsb-has-example-toolbar").on("click.dsbReveal", function () {
$toolbar.addClass("dsb-revealed");
});
});
}
// How many rows the pencil is currently holding open. A pinned row means somebody is part-way through
// an edit, and opening another one under the pointer moves the page: the content below shifts, the
// pointer ends up over something else, that fires its own hover, and the page jumps back and forth.
// So while anything is pinned, hovering opens nothing.
var pinnedRows = 0;
var canHover = !window.matchMedia || window.matchMedia("(hover: hover)").matches;
function attachToolbar($li, language, sectionType, sectionOrdinal, definitionLevel, $h3, isLastSense) {
if ($li.children(".dsb-toolbar").length) {
return;
}
var $toolbar = $("<span class='dsb-toolbar'></span>");
// The real InterlanguageLinks pencil icon (same asset defEditButton uses); image swap is in the CSS.
var $toggle = $("<a href='#' class='dsb-toggle' title='Guhartina pêşketî'></a>");
// Revealed by $toggle -- an indented panel under the sense, not always visible. Built lazily on
// first open rather than eagerly for every sense on the page.
var $quickRow = $("<div class='dsb-quick-row'></div>").hide();
var pinned = false; // the pencil pins the row open; hovering alone does not
var hovering = false;
// Builds the row -- fetching the panels module the first time -- then calls back. Asynchronous now,
// so the callback re-checks that the row is still wanted rather than showing it unconditionally.
function ensureBuilt(then) {
if ($quickRow.data("built")) {
then();
return;
}
withPanels(function (panels) {
if (!$quickRow.data("built")) {
panels.buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense);
$quickRow.data("built", true);
}
then();
});
}
// Anything the user has started here: an open field box, the open dropdown, or focus inside the row.
function engaged() {
return pinned ||
$quickRow.find(".dsb-open-boxes").children().length > 0 ||
$quickRow.find(".dsb-more-menu").is(":visible") ||
$.contains($quickRow.get(0), document.activeElement);
}
function setPinned(next) {
if (pinned === next) {
return;
}
pinned = next;
pinnedRows += next ? 1 : -1;
}
$toggle.on("click", function (e) {
e.preventDefault();
// Through their own cancel handler rather than a bare hide(), so a row this closes also drops
// its pinned state -- hiding it alone would leave pinnedRows counting a row nobody can see.
$(".dsb-quick-row").not($quickRow).trigger("dsbCancel");
setPinned(!pinned);
if (pinned) {
ensureBuilt(function () {
if (pinned) {
$quickRow.show();
}
});
} else {
$quickRow.hide();
}
});
// Hover reveals the row and takes it away again, the way en.wikt's does -- but never while something
// on it is in use, or a half-typed box would vanish on the way to the mouse leaving.
$li.on("mouseenter.dsbHover", function () {
// iOS plays a tap as mouseenter first, and a row shown here would swallow that tap's click.
if (!canHover) {
return;
}
hovering = true;
// A row the pencil is holding open is already open, and no row opens on hover while another
// one is: that is the case where the page is being edited and must stay still.
if (pinned || pinnedRows) {
return;
}
// Hover reveals a row that already exists; it never fetches the panels module. Otherwise
// simply moving the pointer across an entry would pull ~100 KB on every page view, which is
// the whole cost the split exists to avoid. The pencil is what asks for it.
if ($quickRow.data("built")) {
$quickRow.show();
}
});
// Raised by the panel's "Betal bike" (see dsb-panels.js): clear the pinned state as well as
// hiding, so one click on the pencil opens it again.
$quickRow.on("dsbCancel", function () {
setPinned(false);
hovering = false;
$quickRow.hide();
});
$li.on("mouseleave.dsbHover", function () {
hovering = false;
if (!engaged()) {
$quickRow.hide();
}
});
$toolbar.append($toggle);
// The class positions the <li>, so the toolbar's gutter slot is measured from the line itself and
// not from wherever it happens to sit in the child order -- ShowIDs prepends its id pill to this
// same <li>.
$li.addClass("dsb-def").prepend($toolbar);
// Touch has no :hover -- a tap anywhere on the line reveals the pencil the same way hovering does on
// desktop (CSS gates this to touch devices; harmless if it also fires on a mouse click).
$li.on("click.dsbReveal", function () {
$toolbar.addClass("dsb-revealed");
});
// Land $quickRow right after the sense's own gloss text -- before any existing "Hevmane: ..." <dl>,
// citation <ul>, or the Nimûne/jêgirtin proxy link's own <ul>, not appended after all of that.
var $firstBlock = $li.children("dl, ul, ol").first();
if ($firstBlock.length) {
$firstBlock.before($quickRow);
} else {
$li.append($quickRow);
}
}
// A section the data marks hidden is left alone: Werger waits for the translation tools it belongs
// to, and a pencil that opens a box those cannot yet fill is a promise this gadget does not keep.
function isHiddenSection(header) {
return PAGE_SUBSECTIONS.some(function (s) {
return s.hidden && s.header === header;
});
}
// Every subsection under a part of speech, whether or not the data knows the header: one it does not
// know opens as wikitext, or as a word list when that is what it holds -- see buildSubsectionPanel.
function attachSubsectionEditors($h3, language, sectionType, sectionOrdinal, posDepth) {
posDepth = posDepth || 3;
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var depth = headingDepth($cursor);
if (depth && depth <= posDepth) {
break;
}
if (depth !== posDepth + 1) {
continue;
}
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
var header = headingText($hh);
if (!header || isHiddenSection(header) || $hh.data("dsbAttached")) {
continue;
}
$hh.data("dsbAttached", true);
attachSubsectionEditor($hh, $cursor, $h3, header, language, sectionType, sectionOrdinal);
}
}
// Where the pencil goes depends on what the skin does with its own edit links. Desktop skins put
// "[biguhêre]" right after the heading text, so the pencil follows it and the two read as one group.
// Minerva parks its edit icon at the far right of the heading row instead, a screen-width away from the
// text, so there the pencil goes inside the heading rather than chasing it.
function placeHeadingToolbar($heading, $toolbar) {
if (mw.config.get("skin") !== "minerva") {
var $editsection = headingOuter($heading).find(".mw-editsection").first();
if ($editsection.length) {
$editsection.after($toolbar);
return;
}
}
$heading.append($toolbar);
}
// Touch has no hover, so a heading's pencil is shown by tapping the heading itself. Taps that mean
// something else are left alone: the skin's own edit links, and the pencil once it is showing.
function revealHeadingToolbarOnTap($heading, $toolbar) {
headingOuter($heading).on("click", function (e) {
if ($(e.target).closest("a, .dsb-toolbar").length) {
return;
}
$toolbar.toggleClass("dsb-revealed");
});
}
// One pencil's worth of behaviour, shared by the "====" headings and by an image's own caption: the
// panel is built the first time it is opened, never before, and the pencil stays visible while it is.
// free, when given, pins the panel to a heading located by its own text rather than through a part of
// speech: {header, ordinal}.
function bindPanelToggle($toggle, $toolbar, $panel, $h3, header, language, sectionType, sectionOrdinal, free) {
$toggle.on("click", function (e) {
e.preventDefault();
$panel.toggle();
// Stays put while its panel is open, so it does not vanish from under the pointer.
$toolbar.toggleClass("dsb-revealed", $panel.is(":visible"));
if (!$panel.is(":visible")) {
return;
}
if (!$panel.data("built")) {
$panel.data("built", true);
$panel.text(msg.loading);
withPanels(function (panels) {
$panel.empty();
panels.buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, header, free);
});
}
revealQueue();
});
}
function attachSubsectionEditor($hh, $heading, $h3, header, language, sectionType, sectionOrdinal) {
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", header + " biguhêre");
var $panel = $("<div class='dsb-subsection-panel'></div>").hide();
$toolbar.append($toggle);
placeHeadingToolbar($hh, $toolbar);
revealHeadingToolbarOnTap($hh, $toolbar);
$heading.after($panel);
bindPanelToggle($toggle, $toolbar, $panel, $h3, header, language, sectionType, sectionOrdinal);
}
// The "place" subsection that owns the thumbnails under a part-of-speech heading -- named in the data,
// not spelled out here, so renaming it there renames it everywhere.
var IMAGE_HEADER = (function () {
var found = null;
PAGE_SUBSECTIONS.forEach(function (s) {
if (s.place === "pos-top") {
found = s.header;
}
});
return found;
}());
// A pencil on the image's own caption. A wrong caption is noticed while looking at the picture, and
// this is where the writer reaches for it -- the same editor the section's own list offers, opened on
// the image that is already there.
// Every thumbnail between the heading and the first subsection, which is the run "pos-top" covers.
// Anything else in that run is stepped over: a headword line, a declension table or the stylesheet a
// template brings with it all sit before the pictures on plenty of entries.
function attachImageEditors($h3, language, sectionType, sectionOrdinal) {
if (!IMAGE_HEADER) {
return;
}
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if (headingDepth($cursor)) {
break;
}
if ($cursor.is(".thumb, figure, .floatright, .floatleft")) {
attachImageEditor($cursor, $h3, language, sectionType, sectionOrdinal);
}
}
}
// A "===" section that is not a part of speech but is one of ours: Bilêvkirin, which sits above the
// part-of-speech blocks and belongs to the whole language.
function placeSubsectionFor($h3) {
var text = headingText($h3);
var found = null;
PAGE_SUBSECTIONS.forEach(function (s) {
if (s.place === "pron" && s.header === text) {
found = s;
}
});
return found;
}
// The first part-of-speech heading under the same language. The writer works from a section, so
// without one there is nothing to anchor the box to.
function firstPosHeadingAfter($h3) {
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is(".mw-heading2, h2")) {
return null; // the next language starts
}
var depth = headingDepth($cursor);
if (depth !== 3 && depth !== 4) {
continue;
}
var $hh = $cursor.is("h3, h4") ? $cursor : $cursor.find("h3, h4").first();
if (sectionTypeOf($hh) || isManeHeading($hh)) {
return $hh;
}
}
return null;
}
// Its own pencil, opening the same box the section list offers -- a heading nobody can edit from the
// page is a heading people edit by hand.
function attachPlaceHeadingEditor($h3, subsection) {
if ($h3.data("dsbAttached")) {
return;
}
var $pos = firstPosHeadingAfter($h3);
if (!$pos) {
return;
}
var $ol = null;
for (var $cursor = headingOuter($pos).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("ol")) {
$ol = $cursor;
break;
}
if ($cursor.is(".mw-heading2, .mw-heading3, h2, h3")) {
break;
}
}
var language = $ol && languageCodeForOl($ol);
if (!language) {
return;
}
$h3.data("dsbAttached", true);
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", subsection.header + " " + msg.edit);
var $panel = $("<div class='dsb-subsection-panel'></div>").hide();
$toolbar.append($toggle);
placeHeadingToolbar($h3, $toolbar);
revealHeadingToolbarOnTap($h3, $toolbar);
headingOuter($h3).after($panel);
bindPanelToggle($toggle, $toolbar, $panel, $pos, subsection.header, language,
sectionTypeOf($pos) || MANE_SECTION, sectionOrdinalWithinLanguage($pos));
}
// Every other heading inside a language: the ones that sit beside the parts of speech rather than
// under one ("=== Etîmolojî ===", "=== Çavkanî ==="), and the ones nobody planned for. Each opens
// the wikitext under its own heading, or the {{kol3}} form when that is what it holds.
function attachFreeSectionEditors() {
$("#mw-content-text").find("h3, h4, h5, h6").each(function () {
var $h = $(this);
// A heading that already carries one of this gadget's pencils is spoken for.
if ($h.data("dsbAttached") || headingOuter($h).find(".dsb-toolbar").length) {
return;
}
var header = headingText($h);
if (!header || isHiddenSection(header)) {
return;
}
var language = languageCodeForOl($h);
if (!language) {
return;
}
$h.data("dsbAttached", true);
attachFreeSectionEditor($h, header, language);
});
}
function attachFreeSectionEditor($h, header, language) {
var free = { header: header, ordinal: headingTextOrdinalWithinLanguage($h) };
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", header + " " + msg.edit);
var $panel = $("<div class='dsb-subsection-panel'></div>").hide();
$toolbar.append($toggle);
placeHeadingToolbar($h, $toolbar);
revealHeadingToolbarOnTap($h, $toolbar);
headingOuter($h).after($panel);
bindPanelToggle($toggle, $toolbar, $panel, $h, header, language, null, 0, free);
}
function attachImageEditor($fig, $h3, language, sectionType, sectionOrdinal) {
if ($fig.data("dsbAttached")) {
return;
}
var $caption = $fig.find("figcaption, .thumbcaption").first();
if (!$caption.length) {
return;
}
$fig.data("dsbAttached", true);
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-caption'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", IMAGE_HEADER + " biguhêre");
// After the figure, not inside it: the panel is full width, and a floated thumbnail is not.
var $panel = $("<div class='dsb-subsection-panel dsb-panel-after-image'></div>").hide();
$toolbar.append($toggle);
$caption.append($toolbar);
revealHeadingToolbarOnTap($caption, $toolbar);
$fig.after($panel);
bindPanelToggle($toggle, $toolbar, $panel, $h3, IMAGE_HEADER, language, sectionType, sectionOrdinal);
}
function attachSubsectionTrigger($h3, $ol, language, sectionType, sectionOrdinal) {
if ($h3.data("dsbAttached")) {
return;
}
$h3.data("dsbAttached", true);
var $anchor = $ol;
for (var $cursor = $ol.next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is(".mw-heading2, .mw-heading3, .mw-heading4, .mw-heading5, .mw-heading6, h2, h3, h4, h5, h6")) {
break;
}
$anchor = $cursor;
}
var $trigger = $("<a href='#' class='dsb-add-subsection'>+ Beşek lê zêde bike</a>");
var $panel = $("<div class='dsb-subsection-panel'></div>").hide();
$trigger.on("click", function (e) {
e.preventDefault();
$panel.toggle();
if (!$panel.is(":visible")) {
return;
}
if (!$panel.data("built")) {
$panel.data("built", true);
$panel.text(msg.loading);
withPanels(function (panels) {
$panel.empty();
panels.buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal);
});
}
revealQueue();
});
$anchor.after($panel);
$anchor.after($trigger);
}
// Attaches the "choose a part of speech" trigger under a heading whose part of speech is unsettled: an
// unclassified "=== Mane ===" placeholder, or a "=== Biwêj ===" that is really a noun, verb, ... .
function attachConvertPosTrigger($h3, sectionType) {
if ($h3.data("dsbConvertAttached")) {
return;
}
$h3.data("dsbConvertAttached", true);
// languageCodeForOl() is really just "nearest preceding .besazimani[id] before this element in
// document order" -- nothing ol-specific about it, so it works unchanged for a heading too.
var language = languageCodeForOl($h3);
if (!language) {
return;
}
var sectionOrdinal = sectionOrdinalWithinLanguage($h3);
// The same pencil the "====" subsections get, so reclassifying reads as an edit affordance rather
// than a line of body text.
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", "Cureyê rêzimanî hilbijêre");
var $panel = $("<div class='dsb-convert-pos-panel'></div>").hide();
$toolbar.append($toggle);
$toggle.on("click", function (e) {
e.preventDefault();
$panel.toggle();
// Stays put while its panel is open, so it does not vanish from under the pointer.
$toolbar.toggleClass("dsb-revealed", $panel.is(":visible"));
if (!$panel.is(":visible")) {
return;
}
if (!$panel.data("built")) {
$panel.data("built", true);
$panel.text(msg.loading);
withPanels(function (panels) {
$panel.empty();
panels.buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal);
});
}
revealQueue();
});
// Raised by the panel's "Betal bike": closes it and drops what was typed, so the pencil reopens it fresh.
$panel.on("dsbCancel", function () {
$panel.hide().empty().data("built", false);
$toolbar.removeClass("dsb-revealed");
});
placeHeadingToolbar($h3, $toolbar);
revealHeadingToolbarOnTap($h3, $toolbar);
headingOuter($h3).after($panel);
}
// ---- driver ----
function init() {
$("h3, h4").each(function () {
var $h3 = $(this);
var posDepth = headingDepth($h3) || 3;
var sectionType = sectionTypeOf($h3);
if (!sectionType) {
var place = placeSubsectionFor($h3);
if (place) {
attachPlaceHeadingEditor($h3, place);
return;
}
if (!isManeHeading($h3)) {
return; // not a recognized word-sense section (Etîmolojî, Çavkanî, ...)
}
// An unclassified placeholder: offer the POS picker, then carry on and wire up the same
// per-sense pencils, so the POS choice and the sense edits can be queued and saved together.
sectionType = MANE_SECTION;
attachConvertPosTrigger($h3, sectionType);
} else {
// Every part of speech, not just the ones most often mistaken: any section can turn out to
// be another, and the picker is what changes it.
attachConvertPosTrigger($h3, sectionType);
}
var sectionOrdinal = sectionOrdinalWithinLanguage($h3);
var $ol = null;
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("ol")) {
$ol = $cursor;
break;
}
if (headingDepth($cursor)) {
break;
}
}
if (!$ol) {
return;
}
var language = languageCodeForOl($ol);
if (!language) {
return;
}
var lastTopIndex = $ol.children("li").not(".mw-empty-elt").length - 1;
eachLeafDefinition($ol, function ($li) {
var definitionLevel = [];
var $definitionItem = $li;
while ($definitionItem.prop("tagName") === "LI") {
definitionLevel.splice(0, 0, $definitionItem.prevAll("li:not(.mw-empty-elt)").length);
$definitionItem = $definitionItem.parent().parent();
}
definitionLevel.splice(0, 0, sectionType + "|" + sectionOrdinal);
var isLastSense = definitionLevel.length === 2 && definitionLevel[1] === lastTopIndex;
attachToolbar($li, language, sectionType, sectionOrdinal, definitionLevel, $h3, isLastSense);
attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3);
});
attachSubsectionTrigger($h3, $ol, language, sectionType, sectionOrdinal, posDepth);
attachSubsectionEditors($h3, language, sectionType, sectionOrdinal, posDepth);
attachImageEditors($h3, language, sectionType, sectionOrdinal);
});
// Last, so a heading one of the walks above claimed keeps the pencil that knows more about it.
attachFreeSectionEditors();
}
// One listener for every "Hevmane û Bêhtir" dropdown on the page.
$(document).on("click.dsbMoreMenu", function (e) {
$(".dsb-more-menu:visible").each(function () {
var $menu = $(this);
if ($(e.target).closest($menu).length || $(e.target).closest($menu.prev(".dsb-more-toggle")).length) {
return;
}
$menu.slideUp();
var $arrow = $menu.data("dsbArrow");
if ($arrow) {
$arrow.text("▸");
}
});
});
// Closes every open panel/preview this gadget can leave in the DOM, so bfcache never freezes one mid-open.
function closeAllOpenUi() {
if (panelsApi) {
panelsApi.clearQueue();
panelsApi.closeAllPanels();
return;
}
// Before the panels arrive there is nothing built to tear down, only the view-path rows.
$(".dsb-quick-row, .dsb-more-menu, .dsb-preview-inline, .dsb-preview-block").hide();
}
$(window).on("pagehide", closeAllOpenUi);
// Belt-and-suspenders: also close on restore-from-bfcache, in case pagehide's own hide didn't stick.
$(window).on("pageshow", function (e) {
if (e.originalEvent && e.originalEvent.persisted) {
closeAllOpenUi();
}
});
$(init);
// </nowiki>
}());
// </nowiki>
io2uo7kwzyuwjjqdfoopvlib4u4s4bj
Bikarhêner:Wikihez/Gadget-test/dsb.css
2
1358908
6531635
6513699
2026-09-15T14:43:16Z
Wikihez
7344
Pencils reveal on hover only on devices with a real pointer, so a first tap on iOS Safari opens the link
6531635
css
text/css
/* <nowiki> */
/*
* Page: [[MediaWiki:Gadget-wikt.dsb.css]]
*
* Styles for the view path only: the pencil and its toolbar, which is all wikt.dsb draws before someone
* clicks. Everything the panels need is in wikt.dsb-panels.css, loaded with them.
* Colour and spacing tokens come from wikt.core.css.
*/
/* position:absolute with top/left auto keeps the static position but drops the box out of flow; the negative margin then slides the pencil into the list gutter, in front of the marker (en.wikt's .defEditButton trick). */
/* Anchors the gutter the pencil sits in. Without it the toolbar is placed from its static position, which
moves as soon as anything else is prepended to the line. */
li.dsb-def {
position: relative;
}
/* A sense line is positioned (its pencil hangs off it), so its own background -- the :target highlight
after clicking "mane 1" in a caption, or a queued edit's preview -- would paint over a picture floated
beside it. The pictures sit one layer up instead. */
.mw-parser-output figure,
.mw-parser-output .thumb {
position: relative;
z-index: 1;
}
/* {{bênimûne}}'s own "Nimûneyekê lê zêde bike" line: the sense's "+ Nimûne/jêgirtin" item offers the same.
Here rather than with add-examples, which only loads once a panel is opened. */
.mw-parser-output li:has(> .benimune) {
display: none;
}
.dsb-toolbar {
position: absolute;
left: -40px;
top: 0;
width: 40px;
height: 26px;
font-family: var(--wikt-sans);
}
/* A heading's pencil is inline, so it keeps the flow and needs none of the above. */
/* On an image's caption: inline after the caption text, and always visible -- an entry carries a
picture or two, not a column of headings, so there is nothing here for a hidden icon to spare.
The z-index lifts it over the sense lines: a sense's <li> is positioned and its box runs the full
width of the column, right under a floated picture, so it would otherwise take the click. */
.dsb-toolbar-caption {
position: relative;
left: auto;
top: auto;
z-index: 1;
display: inline-block;
width: auto;
height: auto;
margin-left: 0.35em;
vertical-align: middle;
white-space: nowrap;
}
.dsb-toolbar-caption .dsb-toggle {
display: inline-block;
margin: 0;
vertical-align: middle;
opacity: 0.55;
transition: opacity 0.12s ease;
}
.dsb-toolbar-caption .dsb-toggle:focus,
.dsb-toolbar-caption.dsb-revealed .dsb-toggle {
opacity: 1;
}
/* Beside the picture rather than below it: the thumbnails sit above the definitions, so a panel that
cleared them dropped past every float and took the whole definition list down with it. */
.dsb-panel-after-image {
display: flow-root;
}
.dsb-toolbar-heading {
left: auto;
top: auto;
}
.dsb-toggle {
display: block;
margin-top: 5px; /* sits on the "1." marker; a pencil reads low, so it is lifted off centre */
width: 13px;
height: 13px;
background-image: url(//upload.wikimedia.org/wikipedia/commons/d/d3/InterlanguageLinks-Asset-Pencil.gif);
background-repeat: no-repeat;
background-position: center;
background-size: contain;
text-decoration: none;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease;
}
.dsb-toggle:hover,
.dsb-toggle:focus {
background-image: url(//upload.wikimedia.org/wikipedia/commons/d/d1/InterlanguageLinks-Asset-Pencil-Hover.gif);
}
/* Same pencil on a "====" or "===" heading: no list gutter to sit in, so it stays inline after the text. */
.dsb-toolbar-heading {
position: static;
display: inline-block;
width: auto;
height: auto;
margin-left: 0;
vertical-align: middle;
}
.dsb-toolbar-heading .dsb-toggle {
display: inline-block;
margin: 0 0 0 6px;
vertical-align: middle;
/* Hidden until the heading is hovered, like the per-sense pencil: a page with several subsections
otherwise carries an icon on every heading, all competing for the eye at once. */
opacity: 0;
}
/* Revealed by hovering anywhere on the heading, by keyboard focus, and while its own panel is open
(dsb-revealed, set in dsb.js) so it never disappears from under the pointer mid-edit. */
.dsb-toolbar-heading .dsb-toggle:focus,
.dsb-toolbar-heading.dsb-revealed .dsb-toggle {
opacity: 1;
}
/* Reveal rules, in ascending specificity so a later one never loses to an earlier one. */
.dsb-toggle:focus {
opacity: 1;
}
/* Touch has no hover, so a toolbar marks itself revealed on tap instead. A heading's pencil is revealed
by a tap on the heading (see revealHeadingToolbarOnTap in dsb.js), so a page of subsections is not a
column of icons before anything has been asked for. */
@media (hover: none), (pointer: coarse) {
.dsb-toolbar.dsb-revealed .dsb-toggle {
opacity: 1;
}
}
/* The glyph stays 13px -- the asset is only 10px, so scaling it up just blurs it. The tap target grows
invisibly instead, to the 44px minimum. */
@media (max-width: 720px), (pointer: coarse) {
.dsb-toggle {
position: relative;
}
.dsb-toggle::after {
content: "";
position: absolute;
inset: -10px;
}
}
/* Hover reveals only where there is a real pointer. On iOS Safari a tap first plays :hover, and when that
makes something visible the tap is spent on it and the link underneath needs a second tap. Touch gets
the pencils from dsb-revealed instead. */
@media (hover: hover) {
li:hover .dsb-toolbar .dsb-toggle,
.dsb-has-example-toolbar:hover .dsb-example-toolbar .dsb-toggle,
.mw-heading:hover .dsb-toolbar-heading .dsb-toggle,
h2:hover .dsb-toolbar-heading .dsb-toggle,
h3:hover .dsb-toolbar-heading .dsb-toggle,
h4:hover .dsb-toolbar-heading .dsb-toggle,
figure:hover .dsb-toolbar-caption .dsb-toggle,
.thumb:hover .dsb-toolbar-caption .dsb-toggle {
opacity: 1;
}
}
/* The pencil on each example and quotation: at the end of its last line, shown while that example is
hovered. */
.dsb-example-toolbar {
display: inline-block;
margin-left: 6px;
vertical-align: middle;
}
.dsb-example-toolbar .dsb-toggle {
display: inline-block;
margin: 0;
opacity: 0;
transition: opacity 0.12s ease;
}
.dsb-example-toolbar .dsb-toggle:focus,
.dsb-example-toolbar.dsb-revealed .dsb-toggle {
opacity: 1;
}
/* ── Built on the view path ───────────────────────────────────────────
dsb.js creates these before wikt.dsb-panels is fetched, so their styles have to travel with it rather
than with the panels: the "+ Beşek lê zêde bike" link is visible from the start, and the panel
containers are on screen showing "Tê barkirin…" while the module is still on its way. Everything
inside the panels stays in wikt.dsb-panels.css -- that markup does not exist until it has loaded. */
/* Matched to the add-examples gadget's own "Nimûneyeke bikaranînê lê zêde bike" link: a quiet subtle-grey
secondary action at the page's own font and size, not an accent-coloured one. Deliberately no --wikt-sans,
so it inherits the page font the way that link does. */
.dsb-add-subsection {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 8px 0;
font-size: inherit;
font-weight: 400;
color: var( --color-subtle, #54595d );
text-decoration: none;
border-bottom: 0;
cursor: pointer;
}
.dsb-add-subsection:hover,
.dsb-add-subsection:focus {
color: var( --color-base, #202122 );
text-decoration: underline;
}
.dsb-subsection-panel,
.dsb-convert-pos-panel {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
box-sizing: border-box;
max-width: 520px;
margin: 6px 0 14px;
padding: 14px 16px;
font-family: var(--wikt-sans);
font-size: 13px;
color: var(--wikt-ink-2);
background: var(--wikt-card-sunken);
border: 1px solid var(--wikt-card-edge);
border-radius: var(--wikt-r-card);
}
/* Mobile: the containers above lose the desktop width cap. */
.skin-minerva .dsb-quick-row,
.skin-minerva .dsb-subsection-panel,
.skin-minerva .dsb-convert-pos-panel {
max-width: 100%;
}
/* </nowiki> */
i21vicqk3axx1pgm4sf0paexandbq7v
Bikarhêner:Wikihez/Gadget-test/cite.js
2
1358913
6531662
6514256
2026-09-15T17:27:00Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none
6531662
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.cite.js]]
*
* Citations for both add-examples and the wizard: jêder-* templates, [[Kategorî:Çavkanî]] templates,
* Citoid, and the "#*" line they become. fieldSpecs() returns plain field descriptions because the two
* callers draw them differently (OOUI vs the wizard's own grid); everything else is shared. Also the
* template-param dialog (openTemplateDialog), used the same way by DefSideBoxes and start.js.
*
* The only caller of window.wikt.citoid, which the citoid-helper gadget owns.
*/
(function () {
"use strict";
var edit = require("ext.gadget.wikt.edit");
var util = edit.util;
var wt = edit.wt;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
var tdText = wt.tdText;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var jederDefs = edit.data.jederDefs || {};
var api = new mw.Api();
// Sentinel _currentJederKey / dropdown value for "pick a template from Kategorî:Çavkanî". Not a real
// jederDefs key. Both gadgets use this same string, so a draft saved by one is read by the other.
var CATEGORY_KEY = "__category__";
// Sentinel for "the example box holds a finished template; write it out untouched".
var RAW_KEY = "__raw__";
// Covered by the form's own dedicated boxes, so they are never drawn again as generic template fields.
var COVERED_PARAMS = ["jêgirtin", "jg", "t", "tr"];
// A Kategorî:Çavkanî template whose TemplateData labels a parameter exactly this is fed the language
// from the language dropdown instead of showing a box for it. Unlike jêder-*, an arbitrary template has
// no "parameter 1 is the language" convention, so nothing is assumed positionally.
var LANG_LABEL = "koda zimanî";
// Field key -> {span, multiline}, taken from whichever jêder-* template defines it first. Most
// Kategorî:Çavkanî templates reuse the same bibliographic parameter names, so "sernav" comes out
// full-width and multiline there too rather than as a generic one-line box.
var SIZE_BY_KEY = {};
Object.keys(jederDefs).forEach(function (jederKey) {
(jederDefs[jederKey].fields || []).forEach(function (field) {
if (!SIZE_BY_KEY[field.key]) {
SIZE_BY_KEY[field.key] = {
span: field.span || 4,
multiline: !!field.multiline,
width: field.width || ""
};
}
});
});
// Field pairs Modul:quote refuses together ("Divê tenê yek ji |tarîx= an |sal= were dayîn"): a full
// date and a bare year say the same thing twice. Both are offered because either may be all a source
// gives, so the forms clear one when the other is filled rather than letting the citation break.
var EXCLUSIVE_PAIRS = [["sal", "tarîx"]];
function exclusivePartner(key) {
var found = null;
EXCLUSIVE_PAIRS.forEach(function (pair) {
if (pair[0] === key) {
found = pair[1];
} else if (pair[1] === key) {
found = pair[0];
}
});
return found;
}
function fieldSize(key) {
return SIZE_BY_KEY[key] || { span: 4, multiline: false, width: "" };
}
// ---- text helpers -------------------------------------------------------------------------------
// The entry name as a pattern. A Kurdish word ending in -î loses the circumflex once something is
// attached to it ("safî" is written "safiye"), so a title ending that way matches either spelling --
// otherwise the name is simply never found in the quotation.
function titlePattern(title) {
var escaped = escapeRegExp(String(title || ""));
return /î$/i.test(escaped) ? escaped.slice(0, -1) + "[îi]" : escaped;
}
// Wraps the entry's own name in bold wherever it appears in the quotation, which is how quotations are
// written here, and reports whether the name ends up bold so a caller can say so.
//
// The name may carry an attached suffix ("celbê"), so the match runs on to the end of the word. A word
// ends at whitespace, punctuation or markup: excluding markup is what keeps "celb<br>me’şûq" from
// bolding as one lump. The typographic apostrophe stays a word character -- it is one inside Kurdish
// words like "me’şûq".
function boldenPageTitle(text, title) {
var source = String(text || "");
var escapedTitle = titlePattern(title);
var suffixPattern = "([^\\s.,;:?!()'\"<>\\[\\]{}|=/\\\\«»„“”–—]+)?";
var titleRe = new RegExp("(" + escapedTitle + suffixPattern + ")", "ig");
var titleAnywhere = new RegExp(escapedTitle, "i");
// Whatever already sits between ''' marks is copied through untouched, so a quotation somebody
// bolded by hand -- or one an earlier run bolded -- never collects a second pair.
var boldSpan = /'''[\s\S]*?'''/g;
var out = "";
var last = 0;
var alreadyBold = false;
var span;
while ((span = boldSpan.exec(source)) !== null) {
out += source.slice(last, span.index).replace(titleRe, "'''$1'''");
if (titleAnywhere.test(span[0])) {
alreadyBold = true;
}
out += span[0];
last = span.index + span[0].length;
}
out += source.slice(last).replace(titleRe, "'''$1'''");
return { text: out, bolded: out !== source || alreadyBold };
}
// A literal newline would end the single-line "#*" wikitext, so display text keeps its break as <br>...
function nlToBr(text) {
return String(text || "").replace(/\r\n|\r|\n/g, "<br>");
}
// ...while a plain parameter value (an author, a title) just loses it.
function nlToSpace(text) {
return String(text || "").replace(/\s*(?:\r\n|\r|\n)\s*/g, " ");
}
// Bolds the entry name inside a pasted citation's jêgirtin=/jg= value only, so a book title or author
// containing it is left alone. With no such parameter nothing is bolded: running the plain-text bolder
// over wikitext would wrap the braces too.
function boldQuoteParam(text, title) {
var match = /(\|\s*(?:jêgirtin|jg)\s*=)([\s\S]*?)(?=\|[^|=]*=|\}\}\s*$)/.exec(text);
if (!match) {
return text;
}
var bolded = boldenPageTitle(match[2], title).text;
return text.slice(0, match.index) + match[1] + bolded + text.slice(match.index + match[0].length);
}
// "name — description" for each Kategorî:Çavkanî option in a <select>, from the description half of
// the same TemplateData every citation picker already fetches -- so a search box built over these
// options can also match on what a template documents itself as doing, not only its name. An
// undescribed option, or a failed fetch, just keeps its plain label; never blocks on this.
// A self-contained "search Kategorî:Çavkanî" combo: a text input plus its own results list, matching
// add-examples' own jêgirtin picker -- each row shows its label, a " → target" arrow when a redirect's
// label differs from its target, and a 🔗 link to the canonical template's own page. opts.onPick(entry)
// fires when a row is chosen; the input is then left holding the plain label, not "label → target", so
// re-focusing it to search again doesn't start from that arrow text. opts.describe: true also fills in
// each visible row's own TemplateData description once it loads (batched, so scrolling through matches
// costs a handful of requests, not one per row).
function buildCategoryTemplateSearch(opts) {
opts = opts || {};
var shell = util.buildSearchList({
placeholder: opts.placeholder || util.msg.searchTemplate
});
var $input = shell.$input;
var $results = shell.$results;
var $status = $("<span class='wiktku-cat-tpl-status'></span>").text(util.msg.templatesLoading);
var list = [];
function decorate($text, target, wanted) {
if (!wanted) {
return;
}
edit.wt.fetchTemplateData(target).then(function (doc) {
var desc = doc && edit.wt.tdText(doc.description);
if (desc && $text.closest("body").length) {
$text.append(document.createTextNode(" — " + edit.wt.shortDesc(desc)));
}
});
}
function showResults(query) {
var q = $.trim(query || "").toLowerCase();
$results.empty();
var matched = q ? list.filter(function (entry) {
return entry.label.toLowerCase().indexOf(q) !== -1;
}) : list;
if (!matched.length) {
shell.close();
return;
}
var shown = matched.slice(0, 50);
// Only what a search has narrowed to: an empty box is the whole category, and describing its
// first fifty would ask the wiki about templates nobody has looked for.
var describing = !!opts.describe && !!q;
if (describing) {
edit.wt.fetchTemplateDataBatch(shown.map(function (entry) { return entry.target; }));
}
shown.forEach(function (entry) {
var $li = $("<li class='wiktku-cat-tpl-row'></li>");
var $text = $("<span class='wiktku-cat-tpl-text'></span>").text(entry.label);
if (entry.label !== entry.target) {
$text.append($("<span class='wiktku-cat-tpl-arrow'></span>").text(" → " + entry.target));
}
decorate($text, entry.target, describing);
var $link = $("<a class='wiktku-cat-tpl-link' target='_blank' rel='noopener'>🔗</a>")
.attr("title", util.msg.openTemplatePage)
.attr("href", util.tplPageUrl(entry.target));
// stopPropagation only -- preventDefault would stop the link from actually navigating.
$link.on("mousedown click", function (e) { e.stopPropagation(); });
$li.append($text, $link);
shell.bindPick($li, function () {
shell.close();
$input.val(entry.label);
opts.onPick(entry);
});
$results.append($li);
});
shell.open();
}
$input.on("input focus", function () {
showResults($(this).val());
});
$input.on("blur", function () {
setTimeout(shell.close, 200); // lets the pick handler fire first
});
fetchCategoryTemplates().then(function (l) {
list = l;
$status.text("");
}, function () {
$status.text(util.msg.templatesFailed);
});
return {
$element: $("<span class='wiktku-native-select-wrap wiktku-cat-tpl-picker'></span>").append($input, $results, $status),
$input: $input,
$status: $status
};
}
// ---- Kategorî:Çavkanî -----------------------------------------------------------------------------
// The category's template members plus their redirects, each redirect its own searchable entry pointing
// at the same canonical target, so a template is findable by any of its names. Cached for the session.
var catTemplatesPromise = null;
function fetchCategoryTemplates() {
if (catTemplatesPromise) {
return catTemplatesPromise;
}
catTemplatesPromise = api.get({
action: "query",
generator: "categorymembers",
gcmtitle: "Kategorî:Çavkanî",
gcmnamespace: 10,
gcmlimit: "max",
prop: "redirects",
rdnamespace: 10,
rdlimit: "max",
formatversion: 2
}).then(function (data) {
var list = [];
((data.query && data.query.pages) || []).forEach(function (page) {
var target = page.title.replace(/^[^:]+:/, "");
list.push({ label: target, target: target });
(page.redirects || []).forEach(function (redirect) {
list.push({ label: redirect.title.replace(/^[^:]+:/, ""), target: target });
});
});
list.sort(function (a, b) {
return a.label.localeCompare(b.label, mw.config.get("wgUserLanguage"));
});
return list;
}, function () {
return [];
});
return catTemplatesPromise;
}
// ---- template-param dialog: fills a {{Name|...}} call from its own TemplateData -------------------
// Shared by DefSideBoxes (a fresh insert from Etîmolojî/Çavkanî, or double-clicking an existing chip
// to edit it) and start.js's own Etîmolojî/Çavkanî pickers -- one dialog, so all of them read the same
// TemplateData the same way.
// OOUI's own dialog stack -- the same widgetry VisualEditor's template dialog is built on.
var citeWindowManager = null;
function templateDialogManager() {
if (!citeWindowManager) {
citeWindowManager = new OO.ui.WindowManager();
$(document.body).append(citeWindowManager.$element);
}
return citeWindowManager;
}
// Opens one template call in a modal built from its TemplateData. Resolves with the edited wikitext, or
// null when the user cancels.
function openTemplateDialog(callWikitext) {
var done = $.Deferred();
var call = parseTemplateCalls(callWikitext).calls[0];
if (!call) {
return done.resolve(null).promise();
}
$.when(
mw.loader.using(["oojs-ui-core", "oojs-ui-windows"]),
edit.wt.fetchTemplateData(call.name)
).then(function (_loaded, td) {
var result = null;
// The parent is captured rather than reached through TplDialog.super: "super" is a reserved word,
// and OOjs has moved between .super and .parent for it over the years.
var ParentDialog = OO.ui.ProcessDialog;
function TplDialog(config) {
ParentDialog.call(this, config);
}
OO.inheritClass(TplDialog, ParentDialog);
TplDialog.static.name = "citeTemplateDialog";
TplDialog.static.title = "{{" + call.name + "}}";
TplDialog.static.actions = [
{ action: "save", label: "Biguhêre", flags: ["primary", "progressive"] },
{ action: "remove", label: "Jê bibe", flags: "destructive" },
{ label: util.msg.cancel, flags: "safe" }
];
TplDialog.prototype.initialize = function () {
ParentDialog.prototype.initialize.apply(this, arguments);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.form = buildTemplateParamForm(call, td);
this.panel.$element.append(this.form.$element);
this.$body.append(this.panel.$element);
};
TplDialog.prototype.getBodyHeight = function () {
return this.panel.$element.outerHeight(true);
};
TplDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === "save" || action === "remove") {
return new OO.ui.Process(function () {
result = action === "remove" ? "" : dialog.form.values();
dialog.close();
});
}
return ParentDialog.prototype.getActionProcess.call(this, action);
};
var manager = templateDialogManager();
var dialog = new TplDialog({ size: "medium" });
manager.addWindows([dialog]);
manager.openWindow(dialog).closed.then(function () {
manager.clearWindows();
done.resolve(result);
});
}, function () {
done.resolve(null);
});
return done.promise();
}
// TemplateData descriptions are plain text, but authors write wikilinks in them; render those as links.
function tdDescription(text) {
var $out = $("<span></span>");
var re = /\[\[([^\[\]|]+)(?:\|([^\[\]]*))?\]\]/g;
var last = 0;
var m;
while ((m = re.exec(text)) !== null) {
if (m.index > last) {
$out.append(document.createTextNode(text.slice(last, m.index)));
}
$out.append($("<a></a>").attr("href", mw.util.getUrl(m[1])).text(m[2] || m[1]));
last = re.lastIndex;
}
if (last < text.length) {
$out.append(document.createTextNode(text.slice(last)));
}
return $out;
}
// A parameter whose documentation says so takes "kelîme<t:...>" inline modifiers, so it gets the pill
// widget instead of a plain box. TemplateData has no field for this, hence the marker in the description.
var INLINE_MODIFIER_MARKER = "WF:Guherkerên rêzê";
function acceptsInlineModifiers(doc) {
return tdText(doc && doc.description).indexOf(INLINE_MODIFIER_MARKER) !== -1;
}
// A language-code parameter, per its own label.
function isLanguageParam(doc) {
return /ziman/i.test(tdText(doc && doc.label));
}
// 3693 languages is far too many to rebuild per dialog (~190ms), and a datalist can be shared by id, so
// this is built once and reused by every language box on the page.
var LANG_DATALIST_ID = "dsb-language-list";
function fillLanguageDatalist($list) {
var names = langs.names();
$list.empty();
Object.keys(names).forEach(function (code) {
$list.append($("<option></option>").val(code).attr("label", names[code]));
});
}
function languageDatalistId() {
if (!document.getElementById(LANG_DATALIST_ID)) {
var $list = $("<datalist></datalist>").attr("id", LANG_DATALIST_ID);
fillLanguageDatalist($list);
$(document.body).append($list);
// Refilled in place if the long list ever arrives, so every box using it grows at once.
langs.onFull(function () {
fillLanguageDatalist($("#" + LANG_DATALIST_ID));
});
}
return LANG_DATALIST_ID;
}
function buildTemplateParamForm(call, td) {
var parsed = splitCallParams(call.params);
var docParams = (td && td.params) || {};
var keys = (td && td.paramOrder) ? td.paramOrder.slice() : Object.keys(docParams);
parsed.used.forEach(function (k) {
if (keys.indexOf(k) === -1) {
keys.push(k);
}
});
var $element = $("<div class='dsb-tpl-form'></div>");
var readers = {}; // key -> () => the parameter's current text
$element.append($("<div class='dsb-tpl-name'></div>").append(
$("<a></a>").attr("href", mw.util.getUrl("Şablon:" + call.name))
.attr("target", "_blank").text("{{" + call.name + "}}")
));
var desc = tdText(td && td.description);
if (desc) {
$element.append($("<div class='dsb-tpl-desc'></div>").append(tdDescription(desc)));
}
keys.forEach(function (key) {
var doc = docParams[key] || {};
var value = parsed.values[key] || "";
var $field;
if (acceptsInlineModifiers(doc)) {
// The same word row the sense lists use: plain text plus removable "<t:...>" pills.
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: tdText(doc.label) || key,
modTitle: util.msg.addModifier,
removeTitle: "Vê nirxê jê bibe",
removeModTitle: util.msg.removeModifier
});
row.setRaw(value);
// A <ul> so the row's own "close other pickers" lookup has something to scope to.
$field = $("<ul class='dsb-wordlist dsb-tpl-modifier-list'></ul>").append(row.$li);
readers[key] = row.getRaw;
} else {
var $input = $("<input type='text' class='dsb-input'>").val(value);
if (isLanguageParam(doc)) {
$input.attr("list", languageDatalistId()).attr("placeholder", "koda zimanî…");
// Typing a language the short list does not have is the cue to fetch the rest.
$input.on("input", function () {
if (langs.missingFromCore($input.val())) {
langs.ensureFull();
}
});
} else {
var suggested = doc.suggestedvalues || [];
if (suggested.length) {
var listId = "dsb-sv-" + call.name.replace(/[^A-Za-z0-9]/g, "") + "-" + key;
var $list = $("<datalist></datalist>").attr("id", listId);
suggested.forEach(function (v) {
$list.append($("<option></option>").val(v));
});
$input.attr("list", listId);
$element.append($list);
}
}
$field = $input;
readers[key] = function () {
return $input.val();
};
}
var $label = $("<label class='dsb-adv-label'></label>").text(tdText(doc.label) || key);
if (!docParams[key]) {
$label.append($("<span class='dsb-tpl-undocumented'></span>").text(" (ne di belgeyê de)"));
}
var $row = $("<div class='dsb-tpl-param-row'></div>").append($label, $field);
var help = tdText(doc.description);
if (help) {
$row.append($("<div class='dsb-tpl-help'></div>").append(tdDescription(help)));
}
if (doc.deprecated) {
$row.addClass("dsb-tpl-deprecated");
}
$element.append($row);
});
return {
$element: $element,
values: function () {
var vals = {};
keys.forEach(function (k) {
vals[k] = $.trim(readers[k]() || "");
});
return buildTemplateCall(call.name, vals, keys);
}
};
}
// The documented parameters of one template (name without the "Şablon:" prefix), in its own
// paramOrder; [] when it has no TemplateData. The fetch, the shaping and the cache all live in
// wikt.edit, so the wizard, DefSideBoxes and this module share one request and one copy per template.
//
// The key stands in for a missing label, because these become form fields and a field needs something
// written on it.
// A template's parameters: its TemplateData, or -- for one with none, like most Kategorî:Çavkanî
// templates -- every {{{name}}} its own source reads.
function templateParams(name) {
return edit.wt.templateDoc(name).then(function (td) {
var params = ((td && td.params) || []).map(function (p) {
return {
key: p.key,
label: p.label || p.key,
required: p.required,
deprecated: p.deprecated
};
});
return params.length ? params : sourceParams(name);
});
}
var sourceParamsCache = {};
function sourceParams(name) {
if (!sourceParamsCache[name]) {
sourceParamsCache[name] = new mw.Api().get({
action: "query", prop: "revisions", titles: "Template:" + name, rvprop: "content", rvslots: "main",
redirects: 1, formatversion: 2
}).then(function (data) {
var page = data.query.pages[0];
var source = page.revisions ? page.revisions[0].slots.main.content : "";
var keys = [];
var re = /\{\{\{\s*([^{}|]+?)\s*[|}]/g;
var m;
while ((m = re.exec(source)) !== null) {
if (!/^\d+$/.test(m[1]) && keys.indexOf(m[1]) === -1) {
keys.push(m[1]);
}
}
return keys.map(function (key) {
return { key: key, label: key, required: false, deprecated: false };
});
}, function () {
return [];
});
}
return sourceParamsCache[name];
}
// alias -> the name a template's TemplateData documents it under ({r: "rûpel"} for jêder-kitêb). Asked
// of the wiki itself: the parameter lists baked into data-main.json carry no aliases.
var aliasCache = {};
function templateAliases(name) {
if (!aliasCache[name]) {
aliasCache[name] = new mw.Api().get({
action: "templatedata", titles: "Template:" + name, redirects: 1, formatversion: 2
}).then(function (data) {
var map = {};
$.each(data.pages || {}, function (_, page) {
$.each(page.params || {}, function (key, p) {
(p.aliases || []).forEach(function (alias) {
map[alias] = key;
});
});
});
return map;
}, function () {
return {};
});
}
return aliasCache[name];
}
// What to draw for one Kategorî:Çavkanî template: a field per documented parameter, minus the ones the
// form already has boxes for and minus the language parameter, which is filled from the dropdown.
// A template with no TemplateData at all falls back to a lone "Rûpel" box, which is what such citations
// almost always need ({{Şablon|rûpel=12|jêgirtin=…}}).
function fieldSpecs(name) {
return templateParams(name).then(function (params) {
var langParamKey = null;
(params || []).forEach(function (p) {
if ((p.label || "").trim().toLowerCase() === LANG_LABEL) {
langParamKey = p.key;
}
});
var filtered = (params || []).filter(function (p) {
return COVERED_PARAMS.indexOf(p.key) === -1 && p.key !== langParamKey;
});
var list = filtered.length ? filtered : [{ key: "rûpel", label: "Rûpel", required: false }];
return {
langParamKey: langParamKey,
fields: list.map(function (p) {
var size = fieldSize(p.key);
return {
key: p.key,
label: p.label,
required: p.required,
span: size.span,
width: size.width,
multiline: size.multiline
};
})
};
});
}
// The same list for a jêder-* template, straight out of the shared table.
function jederFieldSpecs(jederKey) {
var def = jederDefs[jederKey];
return def ? (def.fields || []).slice() : [];
}
// ---- Citoid --------------------------------------------------------------------------------------
// Today, as Modul:quote wants it (it renders "2024-05-05" as "5 gulan 2024").
function today() {
var d = new Date();
function pad(n) { return (n < 10 ? "0" : "") + n; }
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
}
// Carries across the parts of a Citoid result that citoid-helper's own mapping drops: the access date
// and the identifiers for the fields the forms have only just gained. Applied here rather than in
// citoid-helper so it reaches both gadgets without an edit to the MediaWiki: gadget page.
function enrichCitoidFields(fields, raw) {
fields = fields || {};
if (fields.url && !fields["tarîxa-gihiştinê"]) {
// Citoid reports when it actually fetched the page ("accessDate": "2026-09-03"), so use that
// rather than the clock -- they differ whenever the answer came from a cache. today() is only
// the fallback for a result that carries no accessDate.
var accessed = raw && (raw.accessDate || raw.accessdate);
fields["tarîxa-gihiştinê"] = (accessed && String(accessed).slice(0, 10)) || today();
}
if (raw) {
var issn = Array.isArray(raw.ISSN) ? raw.ISSN[0] : raw.ISSN;
if (issn && !fields.issn) {
fields.issn = issn;
}
var doi = Array.isArray(raw.DOI) ? raw.DOI[0] : raw.DOI;
if (doi && !fields.doi) {
fields.doi = doi;
}
if (raw.volume && !fields.cild) {
fields.cild = raw.volume;
}
}
return fields;
}
// One URL/DOI/ISBN -> {jederKey, fields}. The lookup itself lives in the citoid-helper gadget; this
// wrapper exists so neither caller has to know that, and so a missing gadget fails in one place.
function fetchFromCitoid(query) {
if (!(window.wikt && window.wikt.citoid && window.wikt.citoid.fetchJederFields)) {
return $.Deferred().reject("Amûra Citoidê ne barkirî ye.").promise();
}
return window.wikt.citoid.fetchJederFields(query).then(function (result) {
result.fields = enrichCitoidFields(result.fields, result.raw);
return result;
});
}
// Re-derives the fields for a different jêder-* template than the one citoid detected, enriched the
// same way -- used when the person had already chosen a template and we keep their choice.
// The form's link box stands in for a jêder-* template's own url= when that is left empty.
function withLinkAsUrl(jederKey, fields, link) {
var def = jederDefs[jederKey];
var hasUrl = !!def && (def.fields || []).some(function (field) {
return field.key === "url";
});
if (hasUrl && !fields.url && /^https?:\/\//.test(link || "")) {
fields.url = link;
}
return fields;
}
function citoidFieldsFor(jederKey, raw, query) {
if (!(window.wikt && window.wikt.citoid && window.wikt.citoid.dataToJederFields)) {
return {};
}
return enrichCitoidFields(window.wikt.citoid.dataToJederFields(jederKey, raw, query), raw);
}
// ---- the "#*" line ------------------------------------------------------------------------------
// One finished citation line. Modes:
// "jeder" {{jêder-kitêb|ku|sernav=…|jêgirtin=…}}
// "category" the Kategorî:Çavkanî template as the citation template: {{Ç:Kitêb|rûpel=12|…}}
// "plain" free-text source, or none: {{jêder|ku|2=…|jêgirtin=…}} / {{nimûne|ku|…}}
// "raw" a finished template, passed through with only the entry name bolded
// opts: {mode, template, language, fields, langParamKey, quote, translation, transcription,
// source, sourceURL, title, marker, keepTranslation, extra}
function buildCitation(opts) {
var marker = opts.marker || "#*";
var title = opts.title || mw.config.get("wgTitle");
var quote = boldenPageTitle(String(opts.quote || ""), title).text;
if (opts.mode === "raw") {
// Written as given: the point is to paste a citation built elsewhere and have it survive intact.
// A line copied off another entry usually still carries its own "#*"/"#:" -- the marker is added
// back below, so strip it rather than emitting "#* #* {{...}}".
var pasted = String(opts.quote || "").replace(LIST_MARKER, "");
return marker + " " + nlToBr(boldQuoteParam(pasted, title)).trim();
}
var isCategory = opts.mode === "category";
var isTemplate = isCategory || opts.mode === "jeder";
var name = isTemplate ? opts.template : (opts.source || opts.sourceURL ? "jêder" : "nimûne");
var line = marker + " {{" + name;
// jêder-*, jêder and nimûne all take the language as parameter 1. A Kategorî:Çavkanî template only
// gets one when its own TemplateData names it, under its own key -- never assumed positionally.
if (!isCategory) {
line += "|" + opts.language;
}
if (isTemplate) {
var fields = opts.fields || {};
Object.keys(fields).forEach(function (key) {
var value = nlToSpace(String(fields[key] || "").trim());
if (value) {
line += "|" + key + "=" + value;
}
});
if (isCategory && opts.langParamKey) {
line += "|" + opts.langParamKey + "=" + opts.language;
}
// "Ç:" templates are quotation-equivalents rather than usage examples (see Modul:quote).
if (isCategory && /^Ç:/.test(name)) {
line += "|wekjêgirtin=1";
}
} else {
if (opts.source) {
// A positional value containing "=" would be read as its own key=value pair, so name it.
line += (String(opts.source).indexOf("=") !== -1 ? "|2=" : "|") + opts.source;
}
if (opts.sourceURL) {
line += "|url=" + opts.sourceURL;
}
}
var isUsex = name === "nimûne";
var translation = nlToBr(String(opts.translation || "").trim());
// {{nimûne}} always takes one, a Kurmancî example's being its explanation; a quotation's is left out
// for Kurmancî unless the edited line already had one (keepTranslation).
var writeTranslation = !!translation && (isUsex || opts.language !== "ku" || opts.keepTranslation);
// {{nimûne}}'s example is parameter 2 and its translation parameter 3, both positional. A value
// containing "=" would be read as key=value, so then both are named: naming only the example would
// slide the translation into slot 2.
var namedSlots = isUsex && (quote.indexOf("=") !== -1 || (writeTranslation && translation.indexOf("=") !== -1));
line += "|" + (isUsex ? (namedSlots ? "2=" : "") : "jêgirtin=") + nlToBr(quote);
if (writeTranslation) {
// Kategorî:Çavkanî templates route through Modul:quote and take its "t="; jêder-* uses "tercime=".
line += "|" + (isUsex ? (namedSlots ? "3=" : "") : isCategory ? "t=" : "tercime=") + translation;
}
var transcription = nlToBr(String(opts.transcription || "").trim());
if (transcription) {
line += "|tr=" + transcription;
}
// Parameters the form has no box for, carried through from the line being edited.
(opts.extra || []).forEach(function (param) {
line += "|" + param;
});
// A short example and its translation read best on one line, which {{nimûne}} does only when told.
// Short is Modul:usex's own measure for "rêz=auto": at most 100 characters shown between them.
if (isUsex && writeTranslation && !hasInlineParam(opts.extra) &&
shownLength(quote) + shownLength(translation) <= 100) {
line += "|rêz=1";
}
return line + "}}";
}
// True when the parameters already say inline or not: {{nimûne}}'s rêz=, under any of its names.
function hasInlineParam(params) {
return (params || []).some(function (param) {
return /^\s*(?:rêz|birêz|inline)\s*=/.test(param);
});
}
// How many characters a bit of wikitext shows: link targets, bold/italic marks and tags don't count.
function shownLength(text) {
return String(text || "")
.replace(/\[\[(?:[^\]|]*\|)?([^\]]*)\]\]/g, "$1")
.replace(/'{2,}/g, "")
.replace(/<[^>]*>/g, " ")
.length;
}
// A leading "#*" / "#:" / "##*" ... list marker.
var LIST_MARKER = /^\s*#+[*:]\s*/;
// True when the text is a whole citation line copied from somewhere else: its own list marker followed
// by a template call. That is the shape of every line this family writes -- {{jêder}}, {{jêder-kitêb}},
// {{nm}}, {{nimûne}}, {{mk}}, any Kategorî:Çavkanî template -- so the template name is not checked
// against a list, which could never cover every citation template on the wiki.
//
// Used to switch the form into raw mode by itself: pasting a finished line and forgetting to change the
// dropdown would otherwise wrap it in a second {{nimûne}}.
function looksLikeCitationLine(text) {
var trimmed = String(text || "");
if (!LIST_MARKER.test(trimmed)) {
return false;
}
return trimmed.replace(LIST_MARKER, "").slice(0, 2) === "{{";
}
// True when the example box holds nothing but one finished template call -- the cue to offer, or to
// switch to, the "write it out as-is" mode.
function looksLikeTemplate(text) {
var trimmed = String(text || "").trim();
if (trimmed.slice(0, 2) !== "{{" || trimmed.slice(-2) !== "}}") {
return false;
}
var parsed = edit.wt.parseTemplateCalls(trimmed);
return parsed.calls.length === 1 && !parsed.otherContent;
}
// ---- reading a finished line back --------------------------------------------------------------
// [[Şablon:nimûne]] and every redirect to it.
var USEX_NAMES = ["nimûne", "mk", "ux", "mînak", "nim", "nm"];
var USEX_TRANSLATION = ["3", "t", "tercime", "translation", "w", "werger"];
var USEX_TRANSCRIPTION = ["tr", "transliteration"];
// A quotation template's translation, under the name Modul:quote documents and its aliases.
var QUOTE_TRANSLATION = ["t", "tercime", "werger", "w"];
// [[Şablon:bênimûne]] and its redirects: the "no example yet" placeholder, with or without a language.
var EMPTY_EXAMPLE_MARKER = /^#+\*\s*\{\{\s*(?:bênimûne|bêmînak|mînak\?|nimûne\?)\s*(?:\|[^{}]*)?\}\}\s*$/i;
// The text of a line written as one italic run ("''…''", bold inside allowed -- "'''''X''' …''" opens
// with both), or null.
function italicRun(text) {
var t = $.trim(text || "");
var opening = /^'*/.exec(t)[0].length;
var m = /^''([\s\S]+)''$/.exec(t);
if ((opening !== 2 && opening !== 5) || !m || /''/.test(m[1].replace(/'''/g, ""))) {
return null;
}
return m[1];
}
function brToNl(text) {
return String(text || "").replace(/<br\s*\/?>/gi, "\n");
}
/**
* One "#:"/"#*" block -- its line plus any deeper "#::"/"#*:" lines -- as the form's values, or null when
* it is not an example: a word list, the "no example yet" placeholder, a plain note.
* {mode: "nimune"|"jeder"|"source"|"raw", marker, template, text, translation, transcription,
* source, sourceURL, fields, extra}
* extra is every parameter the form has no box for, to be written back as it was. A "raw" block is
* edited as written: text is the whole block, list markers included.
*/
function parseCitationLine(block) {
var all = String(block || "").split("\n");
var k = 1;
while (k < all.length && !/^#/.test(all[k])) {
k++;
}
var head = /^#+([:*])\s*([\s\S]*)$/.exec(all.slice(0, k).join("\n"));
var sub = all.slice(k);
if (!head || EMPTY_EXAMPLE_MARKER.test(all[0])) {
return null;
}
var out = { mode: "raw", marker: head[1], template: "", text: "", translation: "", transcription: "",
source: "", sourceURL: "", fields: {}, extra: [] };
var body = $.trim(head[2]);
var parsed = edit.wt.parseTemplateCalls(body);
var call = parsed.calls.length === 1 && !parsed.otherContent ? parsed.calls[0] : null;
var name = call ? edit.wt.canonicalTemplateKey(call.name) : "";
name = name.charAt(0).toLowerCase() + name.slice(1);
var isUsex = USEX_NAMES.indexOf(name) !== -1;
var def = call && jederDefs[name];
if (call && !sub.length && (isUsex || def || name === "jêder")) {
var split = edit.wt.splitCallParams(call.params);
var fieldKeys = def ? def.fields.map(function (f) { return f.key; }) : [];
out.mode = isUsex ? "nimune" : def ? "jeder" : "source";
out.template = def ? name : "";
split.used.forEach(function (key, i) {
var value = brToNl(split.values[key]);
if (key === "1") {
return; // the language, which the section already says
}
if (isUsex ? key === "2" : (key === "jêgirtin" || key === "jg")) {
out.text = value;
} else if ((isUsex ? USEX_TRANSLATION : QUOTE_TRANSLATION).indexOf(key) !== -1) {
out.translation = value;
} else if (USEX_TRANSCRIPTION.indexOf(key) !== -1) {
out.transcription = value;
} else if (def && fieldKeys.indexOf(key) !== -1) {
out.fields[key] = split.values[key];
} else if (name === "jêder" && key === "2") {
out.source = split.values[key];
} else if (name === "jêder" && key === "url") {
out.sourceURL = split.values[key];
} else {
// Named when it was positional, so it cannot shift into a slot the form now fills.
out.extra.push(/^\d+$/.test(key) && !/=/.test(call.params[i]) ? key + "=" + split.values[key] : $.trim(call.params[i]));
}
});
return out;
}
// Any other citation template on a "#*" line -- the Kategorî:Çavkanî ones, {{J:…}} and {{Ç:…}} --
// reads into the category mode: quote, translation and transcription in their boxes, every other
// named parameter as a field. Which of those get a box is known once the template's parameters
// are fetched (see applyAliases).
if (call && !sub.length && out.marker === "*" &&
(/^[JÇ]:/.test($.trim(call.name)) || /(?:^|\|)\s*(?:jêgirtin|jg)\s*=/.test(call.params.join("|")))) {
var catSplit = edit.wt.splitCallParams(call.params);
out.mode = "category";
out.template = $.trim(call.name);
catSplit.used.forEach(function (key, i) {
var value = catSplit.values[key];
if (key === "jêgirtin" || key === "jg") {
out.text = brToNl(value);
} else if (QUOTE_TRANSLATION.indexOf(key) !== -1) {
out.translation = brToNl(value);
} else if (USEX_TRANSCRIPTION.indexOf(key) !== -1) {
out.transcription = brToNl(value);
} else if (key === "wekjêgirtin" && /^Ç:/.test(out.template)) {
return; // buildCitation() writes it for every "Ç:" template
} else if (/^\d+$/.test(key)) {
out.extra.push(!/=/.test(call.params[i]) ? key + "=" + value : $.trim(call.params[i]));
} else {
out.fields[key] = value;
}
});
return out;
}
if (out.marker === ":") {
// The hand-written kind: "#: ''…''", its translation, if any, on a "#::" line under it.
var italic = italicRun(body);
var translationLine = sub.length === 1 ? /^#+::\s*([\s\S]*)$/.exec(sub[0]) : null;
if (italic !== null && (!sub.length || translationLine)) {
out.mode = "nimune";
out.text = brToNl(italic);
out.translation = translationLine ? brToNl(italicRun(translationLine[1]) || $.trim(translationLine[1])) : "";
return out;
}
if (!isUsex) {
return null;
}
}
out.text = all.join("\n");
return out;
}
/**
* Settles which parameters of a read line get a box. aliases maps an alias to its documented name
* (templateAliases); fieldKeys are the boxes the form drew. A parameter written under an alias -- "r="
* for "rûpel=" -- goes to its box; one with no box goes to extra, written back as it was.
*/
function applyAliases(example, aliases, fieldKeys) {
var pending = example.extra.slice();
Object.keys(example.fields).forEach(function (key) {
if (fieldKeys.indexOf(key) === -1) {
pending.push(key + "=" + example.fields[key]);
delete example.fields[key];
}
});
example.extra = [];
pending.forEach(function (param) {
var m = /^\s*([^=]+?)\s*=([\s\S]*)$/.exec(param);
var key = m ? (aliases[m[1]] || m[1]) : null;
var value = m ? $.trim(m[2]) : "";
if (m && fieldKeys.indexOf(key) !== -1 && !example.fields[key]) {
example.fields[key] = value;
} else if (m && key === "jêgirtin" && !example.text) {
example.text = brToNl(value);
} else if (m && QUOTE_TRANSLATION.indexOf(key) !== -1 && !example.translation) {
example.translation = brToNl(value);
} else {
example.extra.push(param);
}
});
return example;
}
/**
* newLine with its parameters in the order originalLine had them, so an edit's diff shows only what
* changed. Parameters match by name -- an alias counting as the name it stands for (aliases, from
* templateAliases) -- and positional ones by position; any the original lacked follow in newLine's own
* order. A line that is not one template call on either side comes back as it was.
*/
function keepOrder(newLine, originalLine, aliases) {
aliases = aliases || {};
var marker = /^#+[:*]\s*/.exec(newLine || "");
var oldBody = /^#+[:*]\s*([\s\S]*)$/.exec(originalLine || "");
if (!marker || !oldBody) {
return newLine;
}
var fresh = edit.wt.parseTemplateCalls(newLine.slice(marker[0].length));
var old = edit.wt.parseTemplateCalls($.trim(oldBody[1]));
if (fresh.calls.length !== 1 || fresh.otherContent || old.calls.length !== 1 || old.otherContent) {
return newLine;
}
function keyed(params) {
var positional = 0;
return params.map(function (raw) {
var m = /^\s*([^|={}\n]+?)\s*=/.exec(raw);
var key = m ? m[1] : String(++positional);
return { key: aliases[key] || key, raw: raw };
});
}
var rest = keyed(fresh.calls[0].params);
var ordered = [];
keyed(old.calls[0].params).forEach(function (o) {
for (var i = 0; i < rest.length; i++) {
if (rest[i].key === o.key) {
ordered.push(rest.splice(i, 1)[0]);
return;
}
}
});
return marker[0] + "{{" + fresh.calls[0].name + ordered.concat(rest).map(function (p) {
return "|" + p.raw;
}).join("") + "}}";
}
// Where the {{ … }} opened on lines[start] closes, so a template written over several lines stays whole.
function braceEnd(lines, start) {
var depth = 0;
for (var i = start; i < lines.length; i++) {
depth += (lines[i].match(/\{\{/g) || []).length - (lines[i].match(/\}\}/g) || []).length;
if (depth <= 0) {
return i;
}
}
return lines.length - 1;
}
// The "#:"/"#*" blocks right under the definition on lines[defIndex], each {start, end, text, example}:
// example is parseCitationLine's reading, or null for a block that is not an example.
function exampleBlocks(lines, defIndex) {
var depth = /^#+/.exec(lines[defIndex])[0].length;
var own = new RegExp("^#{" + depth + "}[:*]");
var deeper = new RegExp("^#{" + depth + "}[:*][:*]");
var blocks = [];
var i = defIndex + 1;
while (i < lines.length && own.test(lines[i])) {
var start = i;
var end = braceEnd(lines, i);
while (end + 1 < lines.length && deeper.test(lines[end + 1])) {
end = braceEnd(lines, end + 1);
}
var text = lines.slice(start, end + 1).join("\n");
blocks.push({ start: start, end: end, text: text, example: parseCitationLine(text) });
i = end + 1;
}
return blocks;
}
/**
* Replaces the index-th example under a definition with code, or removes it when code is null.
* original is the block as it read when the form opened. When the index no longer holds it (an earlier
* queued change moved it) the one block still reading exactly that is used; otherwise nothing is touched.
*/
function replaceExample(pageContent, language, sectionType, sectionOrdinal, definitionLevel, index, original, code) {
var loc = edit.wt.locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defIndex = edit.wt.findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var examples = exampleBlocks(lines, defIndex).filter(function (b) {
return b.example;
});
var block = examples[index] && examples[index].text === original ? examples[index] : null;
if (!block) {
var same = examples.filter(function (b) {
return b.text === original;
});
block = same.length === 1 ? same[0] : null;
}
if (!block) {
throw new edit.util.SubmitAbort("Ev nimûne ji dema vekirina formê ve hatiye guhartin — rûpelê nû bike û dîsa biceribîne.");
}
var replacement = code === null ? [] : String(code).split("\n");
lines.splice.apply(lines, [block.start, block.end - block.start + 1].concat(replacement));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Words only, lower-cased: links, templates, tags and quote marks out, so the page's drawn text and the
// line's wikitext can be compared.
function normalizeWords(text) {
return String(text || "")
.replace(/\[\[(?:[^\]|]*\|)?([^\]]*)\]\]/g, "$1")
.replace(/\{\{[^{}]*\}\}/g, " ")
.replace(/<[^>]*>/g, " ")
.replace(/'{2,}/g, "")
.toLowerCase()
.replace(/[\s.,;:!?"'“”‘’«»()\[\]\-–—…\/]+/g, "");
}
// True when the text a page draws for an example comes from this line.
function exampleMatches(drawnText, example) {
var source = example.text;
if (example.mode === "raw") {
var quote = /\|\s*(?:jêgirtin|jg)\s*=([^|}]*)/.exec(source);
source = quote ? quote[1] : source.replace(/^#+[:*]\s*/, "");
}
var drawn = normalizeWords(drawnText);
var written = normalizeWords(source);
if (!drawn || !written) {
return false;
}
return drawn === written || written.indexOf(drawn.slice(0, 24)) !== -1;
}
module.exports = {
CATEGORY_KEY: CATEGORY_KEY,
RAW_KEY: RAW_KEY,
COVERED_PARAMS: COVERED_PARAMS,
LANG_LABEL: LANG_LABEL,
jederDefs: jederDefs,
fieldSize: fieldSize,
exclusivePartner: exclusivePartner,
boldenPageTitle: boldenPageTitle,
nlToBr: nlToBr,
nlToSpace: nlToSpace,
fetchCategoryTemplates: fetchCategoryTemplates,
buildCategoryTemplateSearch: buildCategoryTemplateSearch,
openTemplateDialog: openTemplateDialog,
buildTemplateParamForm: buildTemplateParamForm,
templateParams: templateParams,
fieldSpecs: fieldSpecs,
jederFieldSpecs: jederFieldSpecs,
fetchFromCitoid: fetchFromCitoid,
citoidFieldsFor: citoidFieldsFor,
withLinkAsUrl: withLinkAsUrl,
today: today,
buildCitation: buildCitation,
looksLikeTemplate: looksLikeTemplate,
looksLikeCitationLine: looksLikeCitationLine,
USEX_NAMES: USEX_NAMES,
EMPTY_EXAMPLE_MARKER: EMPTY_EXAMPLE_MARKER,
parseCitationLine: parseCitationLine,
applyAliases: applyAliases,
keepOrder: keepOrder,
templateAliases: templateAliases,
exampleBlocks: exampleBlocks,
replaceExample: replaceExample,
exampleMatches: exampleMatches
};
}());
// </nowiki>
9fzshq4mz2rzhlxch6aui022w229y9e
Modul:parameters/data/ceribandin
828
1359050
6531682
6362512
2026-09-15T18:11:38Z
Wikihez
7344
etymon: crz as the Kurdish name of pos, Kurdish pos values (reh, kelîme, pêşgir, paşgir, navgir), root/word categories in Kurdish (rehê ... yê, kelîmeya ... ya)
6531682
Scribunto
text/plain
local list_to_set = require("Module:table").listToSet
local function process_set_param(param)
param.set = list_to_set(param.set)
param.converted_set = true
return param
end
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 m = {}
-- [[Module:anchors]]
m["anchor"] = {
[1] = {required = true, list = true, disallow_holes = true},
}
m["senseid"] = {
[1] = required_lang_default_und,
[2] = required_default_,
tag = process_set_param{set = {"li", "p"}, default = "li"},
}
m["etymid"] = {
[1] = required_lang_default_und,
[2] = required_default_
}
-- [[Module:etymon]]
m["etymon"] = {
[1] = required_lang_default_und,
[2] = {list = true, disallow_holes = true},
id = true,
title = true,
tree = boolean,
text = true,
-- Kurdish names for the same two parameters.
dar = {alias_of = "tree"},
metn = {alias_of = "text"},
exnihilo = boolean,
etydate = true,
doublet = {sublist = "comma without whitespace"},
-- Kurdish name for the same parameter, cf. Şablon:cotreh.
cotreh = {alias_of = "doublet"},
rfe = true,
etystub = true,
nl = boolean,
pos = true,
-- Kurdish name for the same parameter; Modul:etymon maps its Kurdish values (reh, kelîme, ...).
crz = {alias_of = "pos"},
notree = {default = false, type = "boolean"},
nocat = {default = false, type = "boolean"},
nodot = {default = false, type = "boolean"},
dot = true,
json = {default = false, type = "boolean"},
}
-- [[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,
}
m["t-needed"] = {
[1] = required_lang_default_und,
[2] = process_set_param{set = {"usex", "quote"}},
["nocat"] = boolean,
["sort"] = true,
}
m["trans-top"] = {
[1] = true,
["id"] = true,
["column-width"] = true,
}
m["trans-top-also"] = {
[1] = required,
[2] = list,
["id"] = list_allow_holes_separate_no_index,
["column-width"] = true,
}
m["checktrans-top"] = {
[1] = true,
["column-width"] = true,
}
m["trans-bottom"] = empty_list
m["trans-see"] = {
[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
698tqojgwgm6vmgyss5ua5qdnh9630p
MediaWiki:Gadget-wikt.core.js
8
1359152
6531675
6426094
2026-09-15T17:59:17Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none; fill button in the wizard's pill style
6531675
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.core.js]]
*
* What a page view needs: the helpers that find headings and definition lines, and the settings the
* launcher reads. Widgets, wikitext parsing, language lists and the full data tables are in wikt.edit.
*
* A package module's files are private to it, so this main file is what other gadgets reach:
* var sections = require("ext.gadget.wikt.core").sections;
*/
// Every label these gadgets show that more than one of them shows, so the wording is written once and
// reads the same everywhere. Here rather than in wikt.edit because the view path shows some of them too.
var MESSAGES = {
// actions
add: "Lê zêde bike",
cancel: "Betal bike",
edit: "Biguhêre",
loading: "Tê barkirin…",
// words and their modifiers
addModifier: "Xisûsiyetekê lê zêde bike",
removeModifier: "Vê xisûsiyetê jê bibe",
removeWord: "Vê kelîmeyê jê bibe",
// senses
anotherSense: "Maneyeke din",
addAnotherSense: "Maneyeke din zêde bike",
moreMenu: "Hevmane û Bêhtir",
// templates
templatesLoading: "Şablon tên barkirin…",
templatesFailed: "Şablon nehatin barkirin.",
searchTemplate: "Li navê şablonê bigere…",
addTemplate: "Şablonekê lê zêde bike…",
pickOrTypeTemplate: "Şablonekî ji lîsteyê hilbijêre an rasterast binivîse…",
openTemplatePage: "Rûpela şablonê veke",
// citations
citoidFill: "Bi otomatîk tijî bike",
citoidAuto: "Bi Citoidê otomatîk zêde bike",
citoidFilled: "✓ Malûmat hatin dagirtin — li jêr kontrol bike.",
citoidNeedsUrl: "Ji kerema xwe re pêşî URL, DOI an ISBNekê binivîse.",
sourceByHand: "Bi destê xwe binivîsim",
sourceFromCategory: "Ji Kategorî:Çavkanî şablonekê hilbijêre",
sourceRaw: "Şablonê wek xwe biparêze (jixwe nivîsandî)",
// glosses
tercimePrompt: "Tercimeyên rasterast ên kurmancî… Ji bo school-a îngilîzî li vir 'mekteb, medrese' binivîse",
// errors
saveFailed: "Nikare rûpelê xilas bike: ",
sectionMissing: "Ev beş li vir tune: "
};
module.exports = {
// Package file names as Gadgets-definition lists them: the files live under "wikt.core/".
sections: require("./wikt.core/sections.js"),
prefs: require("./wikt.core/prefs.js"),
msg: MESSAGES
};
// </nowiki>
1962rrk1wqouyllpj1ex8pe5myuvl77
MediaWiki:Gadget-wikt.cite.js
8
1359166
6531674
6529963
2026-09-15T17:59:08Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none; fill button in the wizard's pill style
6531674
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.cite.js]]
*
* Citations for both add-examples and the wizard: jêder-* templates, [[Kategorî:Çavkanî]] templates,
* Citoid, and the "#*" line they become. fieldSpecs() returns plain field descriptions because the two
* callers draw them differently (OOUI vs the wizard's own grid); everything else is shared. Also the
* template-param dialog (openTemplateDialog), used the same way by DefSideBoxes and start.js.
*
* The only caller of window.wikt.citoid, which the citoid-helper gadget owns.
*/
(function () {
"use strict";
var edit = require("ext.gadget.wikt.edit");
var util = edit.util;
var wt = edit.wt;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
var tdText = wt.tdText;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var jederDefs = edit.data.jederDefs || {};
var api = new mw.Api();
// Sentinel _currentJederKey / dropdown value for "pick a template from Kategorî:Çavkanî". Not a real
// jederDefs key. Both gadgets use this same string, so a draft saved by one is read by the other.
var CATEGORY_KEY = "__category__";
// Sentinel for "the example box holds a finished template; write it out untouched".
var RAW_KEY = "__raw__";
// Covered by the form's own dedicated boxes, so they are never drawn again as generic template fields.
var COVERED_PARAMS = ["jêgirtin", "jg", "t", "tr"];
// A Kategorî:Çavkanî template whose TemplateData labels a parameter exactly this is fed the language
// from the language dropdown instead of showing a box for it. Unlike jêder-*, an arbitrary template has
// no "parameter 1 is the language" convention, so nothing is assumed positionally.
var LANG_LABEL = "koda zimanî";
// Field key -> {span, multiline}, taken from whichever jêder-* template defines it first. Most
// Kategorî:Çavkanî templates reuse the same bibliographic parameter names, so "sernav" comes out
// full-width and multiline there too rather than as a generic one-line box.
var SIZE_BY_KEY = {};
Object.keys(jederDefs).forEach(function (jederKey) {
(jederDefs[jederKey].fields || []).forEach(function (field) {
if (!SIZE_BY_KEY[field.key]) {
SIZE_BY_KEY[field.key] = {
span: field.span || 4,
multiline: !!field.multiline,
width: field.width || ""
};
}
});
});
// Field pairs Modul:quote refuses together ("Divê tenê yek ji |tarîx= an |sal= were dayîn"): a full
// date and a bare year say the same thing twice. Both are offered because either may be all a source
// gives, so the forms clear one when the other is filled rather than letting the citation break.
var EXCLUSIVE_PAIRS = [["sal", "tarîx"]];
function exclusivePartner(key) {
var found = null;
EXCLUSIVE_PAIRS.forEach(function (pair) {
if (pair[0] === key) {
found = pair[1];
} else if (pair[1] === key) {
found = pair[0];
}
});
return found;
}
function fieldSize(key) {
return SIZE_BY_KEY[key] || { span: 4, multiline: false, width: "" };
}
// ---- text helpers -------------------------------------------------------------------------------
// The entry name as a pattern. A Kurdish word ending in -î loses the circumflex once something is
// attached to it ("safî" is written "safiye"), so a title ending that way matches either spelling --
// otherwise the name is simply never found in the quotation.
function titlePattern(title) {
var escaped = escapeRegExp(String(title || ""));
return /î$/i.test(escaped) ? escaped.slice(0, -1) + "[îi]" : escaped;
}
// Wraps the entry's own name in bold wherever it appears in the quotation, which is how quotations are
// written here, and reports whether the name ends up bold so a caller can say so.
//
// The name may carry an attached suffix ("celbê"), so the match runs on to the end of the word. A word
// ends at whitespace, punctuation or markup: excluding markup is what keeps "celb<br>me’şûq" from
// bolding as one lump. The typographic apostrophe stays a word character -- it is one inside Kurdish
// words like "me’şûq".
function boldenPageTitle(text, title) {
var source = String(text || "");
var escapedTitle = titlePattern(title);
var suffixPattern = "([^\\s.,;:?!()'\"<>\\[\\]{}|=/\\\\«»„“”–—]+)?";
var titleRe = new RegExp("(" + escapedTitle + suffixPattern + ")", "ig");
var titleAnywhere = new RegExp(escapedTitle, "i");
// Whatever already sits between ''' marks is copied through untouched, so a quotation somebody
// bolded by hand -- or one an earlier run bolded -- never collects a second pair.
var boldSpan = /'''[\s\S]*?'''/g;
var out = "";
var last = 0;
var alreadyBold = false;
var span;
while ((span = boldSpan.exec(source)) !== null) {
out += source.slice(last, span.index).replace(titleRe, "'''$1'''");
if (titleAnywhere.test(span[0])) {
alreadyBold = true;
}
out += span[0];
last = span.index + span[0].length;
}
out += source.slice(last).replace(titleRe, "'''$1'''");
return { text: out, bolded: out !== source || alreadyBold };
}
// A literal newline would end the single-line "#*" wikitext, so display text keeps its break as <br>...
function nlToBr(text) {
return String(text || "").replace(/\r\n|\r|\n/g, "<br>");
}
// ...while a plain parameter value (an author, a title) just loses it.
function nlToSpace(text) {
return String(text || "").replace(/\s*(?:\r\n|\r|\n)\s*/g, " ");
}
// Bolds the entry name inside a pasted citation's jêgirtin=/jg= value only, so a book title or author
// containing it is left alone. With no such parameter nothing is bolded: running the plain-text bolder
// over wikitext would wrap the braces too.
function boldQuoteParam(text, title) {
var match = /(\|\s*(?:jêgirtin|jg)\s*=)([\s\S]*?)(?=\|[^|=]*=|\}\}\s*$)/.exec(text);
if (!match) {
return text;
}
var bolded = boldenPageTitle(match[2], title).text;
return text.slice(0, match.index) + match[1] + bolded + text.slice(match.index + match[0].length);
}
// "name — description" for each Kategorî:Çavkanî option in a <select>, from the description half of
// the same TemplateData every citation picker already fetches -- so a search box built over these
// options can also match on what a template documents itself as doing, not only its name. An
// undescribed option, or a failed fetch, just keeps its plain label; never blocks on this.
// A self-contained "search Kategorî:Çavkanî" combo: a text input plus its own results list, matching
// add-examples' own jêgirtin picker -- each row shows its label, a " → target" arrow when a redirect's
// label differs from its target, and a 🔗 link to the canonical template's own page. opts.onPick(entry)
// fires when a row is chosen; the input is then left holding the plain label, not "label → target", so
// re-focusing it to search again doesn't start from that arrow text. opts.describe: true also fills in
// each visible row's own TemplateData description once it loads (batched, so scrolling through matches
// costs a handful of requests, not one per row).
function buildCategoryTemplateSearch(opts) {
opts = opts || {};
var shell = util.buildSearchList({
placeholder: opts.placeholder || util.msg.searchTemplate
});
var $input = shell.$input;
var $results = shell.$results;
var $status = $("<span class='wiktku-cat-tpl-status'></span>").text(util.msg.templatesLoading);
var list = [];
function decorate($text, target, wanted) {
if (!wanted) {
return;
}
edit.wt.fetchTemplateData(target).then(function (doc) {
var desc = doc && edit.wt.tdText(doc.description);
if (desc && $text.closest("body").length) {
$text.append(document.createTextNode(" — " + edit.wt.shortDesc(desc)));
}
});
}
function showResults(query) {
var q = $.trim(query || "").toLowerCase();
$results.empty();
var matched = q ? list.filter(function (entry) {
return entry.label.toLowerCase().indexOf(q) !== -1;
}) : list;
if (!matched.length) {
shell.close();
return;
}
var shown = matched.slice(0, 50);
// Only what a search has narrowed to: an empty box is the whole category, and describing its
// first fifty would ask the wiki about templates nobody has looked for.
var describing = !!opts.describe && !!q;
if (describing) {
edit.wt.fetchTemplateDataBatch(shown.map(function (entry) { return entry.target; }));
}
shown.forEach(function (entry) {
var $li = $("<li class='wiktku-cat-tpl-row'></li>");
var $text = $("<span class='wiktku-cat-tpl-text'></span>").text(entry.label);
if (entry.label !== entry.target) {
$text.append($("<span class='wiktku-cat-tpl-arrow'></span>").text(" → " + entry.target));
}
decorate($text, entry.target, describing);
var $link = $("<a class='wiktku-cat-tpl-link' target='_blank' rel='noopener'>🔗</a>")
.attr("title", util.msg.openTemplatePage)
.attr("href", util.tplPageUrl(entry.target));
// stopPropagation only -- preventDefault would stop the link from actually navigating.
$link.on("mousedown click", function (e) { e.stopPropagation(); });
$li.append($text, $link);
shell.bindPick($li, function () {
shell.close();
$input.val(entry.label);
opts.onPick(entry);
});
$results.append($li);
});
shell.open();
}
$input.on("input focus", function () {
showResults($(this).val());
});
$input.on("blur", function () {
setTimeout(shell.close, 200); // lets the pick handler fire first
});
fetchCategoryTemplates().then(function (l) {
list = l;
$status.text("");
}, function () {
$status.text(util.msg.templatesFailed);
});
return {
$element: $("<span class='wiktku-native-select-wrap wiktku-cat-tpl-picker'></span>").append($input, $results, $status),
$input: $input,
$status: $status
};
}
// ---- Kategorî:Çavkanî -----------------------------------------------------------------------------
// The category's template members plus their redirects, each redirect its own searchable entry pointing
// at the same canonical target, so a template is findable by any of its names. Cached for the session.
var catTemplatesPromise = null;
function fetchCategoryTemplates() {
if (catTemplatesPromise) {
return catTemplatesPromise;
}
catTemplatesPromise = api.get({
action: "query",
generator: "categorymembers",
gcmtitle: "Kategorî:Çavkanî",
gcmnamespace: 10,
gcmlimit: "max",
prop: "redirects",
rdnamespace: 10,
rdlimit: "max",
formatversion: 2
}).then(function (data) {
var list = [];
((data.query && data.query.pages) || []).forEach(function (page) {
var target = page.title.replace(/^[^:]+:/, "");
list.push({ label: target, target: target });
(page.redirects || []).forEach(function (redirect) {
list.push({ label: redirect.title.replace(/^[^:]+:/, ""), target: target });
});
});
list.sort(function (a, b) {
return a.label.localeCompare(b.label, mw.config.get("wgUserLanguage"));
});
return list;
}, function () {
return [];
});
return catTemplatesPromise;
}
// ---- template-param dialog: fills a {{Name|...}} call from its own TemplateData -------------------
// Shared by DefSideBoxes (a fresh insert from Etîmolojî/Çavkanî, or double-clicking an existing chip
// to edit it) and start.js's own Etîmolojî/Çavkanî pickers -- one dialog, so all of them read the same
// TemplateData the same way.
// OOUI's own dialog stack -- the same widgetry VisualEditor's template dialog is built on.
var citeWindowManager = null;
function templateDialogManager() {
if (!citeWindowManager) {
citeWindowManager = new OO.ui.WindowManager();
$(document.body).append(citeWindowManager.$element);
}
return citeWindowManager;
}
// Opens one template call in a modal built from its TemplateData. Resolves with the edited wikitext, or
// null when the user cancels.
function openTemplateDialog(callWikitext) {
var done = $.Deferred();
var call = parseTemplateCalls(callWikitext).calls[0];
if (!call) {
return done.resolve(null).promise();
}
$.when(
mw.loader.using(["oojs-ui-core", "oojs-ui-windows"]),
edit.wt.fetchTemplateData(call.name)
).then(function (_loaded, td) {
var result = null;
// The parent is captured rather than reached through TplDialog.super: "super" is a reserved word,
// and OOjs has moved between .super and .parent for it over the years.
var ParentDialog = OO.ui.ProcessDialog;
function TplDialog(config) {
ParentDialog.call(this, config);
}
OO.inheritClass(TplDialog, ParentDialog);
TplDialog.static.name = "citeTemplateDialog";
TplDialog.static.title = "{{" + call.name + "}}";
TplDialog.static.actions = [
{ action: "save", label: "Biguhêre", flags: ["primary", "progressive"] },
{ action: "remove", label: "Jê bibe", flags: "destructive" },
{ label: util.msg.cancel, flags: "safe" }
];
TplDialog.prototype.initialize = function () {
ParentDialog.prototype.initialize.apply(this, arguments);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.form = buildTemplateParamForm(call, td);
this.panel.$element.append(this.form.$element);
this.$body.append(this.panel.$element);
};
TplDialog.prototype.getBodyHeight = function () {
return this.panel.$element.outerHeight(true);
};
TplDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === "save" || action === "remove") {
return new OO.ui.Process(function () {
result = action === "remove" ? "" : dialog.form.values();
dialog.close();
});
}
return ParentDialog.prototype.getActionProcess.call(this, action);
};
var manager = templateDialogManager();
var dialog = new TplDialog({ size: "medium" });
manager.addWindows([dialog]);
manager.openWindow(dialog).closed.then(function () {
manager.clearWindows();
done.resolve(result);
});
}, function () {
done.resolve(null);
});
return done.promise();
}
// TemplateData descriptions are plain text, but authors write wikilinks in them; render those as links.
function tdDescription(text) {
var $out = $("<span></span>");
var re = /\[\[([^\[\]|]+)(?:\|([^\[\]]*))?\]\]/g;
var last = 0;
var m;
while ((m = re.exec(text)) !== null) {
if (m.index > last) {
$out.append(document.createTextNode(text.slice(last, m.index)));
}
$out.append($("<a></a>").attr("href", mw.util.getUrl(m[1])).text(m[2] || m[1]));
last = re.lastIndex;
}
if (last < text.length) {
$out.append(document.createTextNode(text.slice(last)));
}
return $out;
}
// A parameter whose documentation says so takes "kelîme<t:...>" inline modifiers, so it gets the pill
// widget instead of a plain box. TemplateData has no field for this, hence the marker in the description.
var INLINE_MODIFIER_MARKER = "WF:Guherkerên rêzê";
function acceptsInlineModifiers(doc) {
return tdText(doc && doc.description).indexOf(INLINE_MODIFIER_MARKER) !== -1;
}
// A language-code parameter, per its own label.
function isLanguageParam(doc) {
return /ziman/i.test(tdText(doc && doc.label));
}
// 3693 languages is far too many to rebuild per dialog (~190ms), and a datalist can be shared by id, so
// this is built once and reused by every language box on the page.
var LANG_DATALIST_ID = "dsb-language-list";
function fillLanguageDatalist($list) {
var names = langs.names();
$list.empty();
Object.keys(names).forEach(function (code) {
$list.append($("<option></option>").val(code).attr("label", names[code]));
});
}
function languageDatalistId() {
if (!document.getElementById(LANG_DATALIST_ID)) {
var $list = $("<datalist></datalist>").attr("id", LANG_DATALIST_ID);
fillLanguageDatalist($list);
$(document.body).append($list);
// Refilled in place if the long list ever arrives, so every box using it grows at once.
langs.onFull(function () {
fillLanguageDatalist($("#" + LANG_DATALIST_ID));
});
}
return LANG_DATALIST_ID;
}
function buildTemplateParamForm(call, td) {
var parsed = splitCallParams(call.params);
var docParams = (td && td.params) || {};
var keys = (td && td.paramOrder) ? td.paramOrder.slice() : Object.keys(docParams);
parsed.used.forEach(function (k) {
if (keys.indexOf(k) === -1) {
keys.push(k);
}
});
var $element = $("<div class='dsb-tpl-form'></div>");
var readers = {}; // key -> () => the parameter's current text
$element.append($("<div class='dsb-tpl-name'></div>").append(
$("<a></a>").attr("href", mw.util.getUrl("Şablon:" + call.name))
.attr("target", "_blank").text("{{" + call.name + "}}")
));
var desc = tdText(td && td.description);
if (desc) {
$element.append($("<div class='dsb-tpl-desc'></div>").append(tdDescription(desc)));
}
keys.forEach(function (key) {
var doc = docParams[key] || {};
var value = parsed.values[key] || "";
var $field;
if (acceptsInlineModifiers(doc)) {
// The same word row the sense lists use: plain text plus removable "<t:...>" pills.
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: tdText(doc.label) || key,
modTitle: util.msg.addModifier,
removeTitle: "Vê nirxê jê bibe",
removeModTitle: util.msg.removeModifier
});
row.setRaw(value);
// A <ul> so the row's own "close other pickers" lookup has something to scope to.
$field = $("<ul class='dsb-wordlist dsb-tpl-modifier-list'></ul>").append(row.$li);
readers[key] = row.getRaw;
} else {
var $input = $("<input type='text' class='dsb-input'>").val(value);
if (isLanguageParam(doc)) {
$input.attr("list", languageDatalistId()).attr("placeholder", "koda zimanî…");
// Typing a language the short list does not have is the cue to fetch the rest.
$input.on("input", function () {
if (langs.missingFromCore($input.val())) {
langs.ensureFull();
}
});
} else {
var suggested = doc.suggestedvalues || [];
if (suggested.length) {
var listId = "dsb-sv-" + call.name.replace(/[^A-Za-z0-9]/g, "") + "-" + key;
var $list = $("<datalist></datalist>").attr("id", listId);
suggested.forEach(function (v) {
$list.append($("<option></option>").val(v));
});
$input.attr("list", listId);
$element.append($list);
}
}
$field = $input;
readers[key] = function () {
return $input.val();
};
}
var $label = $("<label class='dsb-adv-label'></label>").text(tdText(doc.label) || key);
if (!docParams[key]) {
$label.append($("<span class='dsb-tpl-undocumented'></span>").text(" (ne di belgeyê de)"));
}
var $row = $("<div class='dsb-tpl-param-row'></div>").append($label, $field);
var help = tdText(doc.description);
if (help) {
$row.append($("<div class='dsb-tpl-help'></div>").append(tdDescription(help)));
}
if (doc.deprecated) {
$row.addClass("dsb-tpl-deprecated");
}
$element.append($row);
});
return {
$element: $element,
values: function () {
var vals = {};
keys.forEach(function (k) {
vals[k] = $.trim(readers[k]() || "");
});
return buildTemplateCall(call.name, vals, keys);
}
};
}
// The documented parameters of one template (name without the "Şablon:" prefix), in its own
// paramOrder; [] when it has no TemplateData. The fetch, the shaping and the cache all live in
// wikt.edit, so the wizard, DefSideBoxes and this module share one request and one copy per template.
//
// The key stands in for a missing label, because these become form fields and a field needs something
// written on it.
// A template's parameters: its TemplateData, or -- for one with none, like most Kategorî:Çavkanî
// templates -- every {{{name}}} its own source reads.
function templateParams(name) {
return edit.wt.templateDoc(name).then(function (td) {
var params = ((td && td.params) || []).map(function (p) {
return {
key: p.key,
label: p.label || p.key,
required: p.required,
deprecated: p.deprecated
};
});
return params.length ? params : sourceParams(name);
});
}
var sourceParamsCache = {};
function sourceParams(name) {
if (!sourceParamsCache[name]) {
sourceParamsCache[name] = new mw.Api().get({
action: "query", prop: "revisions", titles: "Template:" + name, rvprop: "content", rvslots: "main",
redirects: 1, formatversion: 2
}).then(function (data) {
var page = data.query.pages[0];
var source = page.revisions ? page.revisions[0].slots.main.content : "";
var keys = [];
var re = /\{\{\{\s*([^{}|]+?)\s*[|}]/g;
var m;
while ((m = re.exec(source)) !== null) {
if (!/^\d+$/.test(m[1]) && keys.indexOf(m[1]) === -1) {
keys.push(m[1]);
}
}
return keys.map(function (key) {
return { key: key, label: key, required: false, deprecated: false };
});
}, function () {
return [];
});
}
return sourceParamsCache[name];
}
// alias -> the name a template's TemplateData documents it under ({r: "rûpel"} for jêder-kitêb). Asked
// of the wiki itself: the parameter lists baked into data-main.json carry no aliases.
var aliasCache = {};
function templateAliases(name) {
if (!aliasCache[name]) {
aliasCache[name] = new mw.Api().get({
action: "templatedata", titles: "Template:" + name, redirects: 1, formatversion: 2
}).then(function (data) {
var map = {};
$.each(data.pages || {}, function (_, page) {
$.each(page.params || {}, function (key, p) {
(p.aliases || []).forEach(function (alias) {
map[alias] = key;
});
});
});
return map;
}, function () {
return {};
});
}
return aliasCache[name];
}
// What to draw for one Kategorî:Çavkanî template: a field per documented parameter, minus the ones the
// form already has boxes for and minus the language parameter, which is filled from the dropdown.
// A template with no TemplateData at all falls back to a lone "Rûpel" box, which is what such citations
// almost always need ({{Şablon|rûpel=12|jêgirtin=…}}).
function fieldSpecs(name) {
return templateParams(name).then(function (params) {
var langParamKey = null;
(params || []).forEach(function (p) {
if ((p.label || "").trim().toLowerCase() === LANG_LABEL) {
langParamKey = p.key;
}
});
var filtered = (params || []).filter(function (p) {
return COVERED_PARAMS.indexOf(p.key) === -1 && p.key !== langParamKey;
});
var list = filtered.length ? filtered : [{ key: "rûpel", label: "Rûpel", required: false }];
return {
langParamKey: langParamKey,
fields: list.map(function (p) {
var size = fieldSize(p.key);
return {
key: p.key,
label: p.label,
required: p.required,
span: size.span,
width: size.width,
multiline: size.multiline
};
})
};
});
}
// The same list for a jêder-* template, straight out of the shared table.
function jederFieldSpecs(jederKey) {
var def = jederDefs[jederKey];
return def ? (def.fields || []).slice() : [];
}
// ---- Citoid --------------------------------------------------------------------------------------
// Today, as Modul:quote wants it (it renders "2024-05-05" as "5 gulan 2024").
function today() {
var d = new Date();
function pad(n) { return (n < 10 ? "0" : "") + n; }
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
}
// Carries across the parts of a Citoid result that citoid-helper's own mapping drops: the access date
// and the identifiers for the fields the forms have only just gained. Applied here rather than in
// citoid-helper so it reaches both gadgets without an edit to the MediaWiki: gadget page.
function enrichCitoidFields(fields, raw) {
fields = fields || {};
if (fields.url && !fields["tarîxa-gihiştinê"]) {
// Citoid reports when it actually fetched the page ("accessDate": "2026-09-03"), so use that
// rather than the clock -- they differ whenever the answer came from a cache. today() is only
// the fallback for a result that carries no accessDate.
var accessed = raw && (raw.accessDate || raw.accessdate);
fields["tarîxa-gihiştinê"] = (accessed && String(accessed).slice(0, 10)) || today();
}
if (raw) {
var issn = Array.isArray(raw.ISSN) ? raw.ISSN[0] : raw.ISSN;
if (issn && !fields.issn) {
fields.issn = issn;
}
var doi = Array.isArray(raw.DOI) ? raw.DOI[0] : raw.DOI;
if (doi && !fields.doi) {
fields.doi = doi;
}
if (raw.volume && !fields.cild) {
fields.cild = raw.volume;
}
}
return fields;
}
// One URL/DOI/ISBN -> {jederKey, fields}. The lookup itself lives in the citoid-helper gadget; this
// wrapper exists so neither caller has to know that, and so a missing gadget fails in one place.
function fetchFromCitoid(query) {
if (!(window.wikt && window.wikt.citoid && window.wikt.citoid.fetchJederFields)) {
return $.Deferred().reject("Amûra Citoidê ne barkirî ye.").promise();
}
return window.wikt.citoid.fetchJederFields(query).then(function (result) {
result.fields = enrichCitoidFields(result.fields, result.raw);
return result;
});
}
// Re-derives the fields for a different jêder-* template than the one citoid detected, enriched the
// same way -- used when the person had already chosen a template and we keep their choice.
// The form's link box stands in for a jêder-* template's own url= when that is left empty.
function withLinkAsUrl(jederKey, fields, link) {
var def = jederDefs[jederKey];
var hasUrl = !!def && (def.fields || []).some(function (field) {
return field.key === "url";
});
if (hasUrl && !fields.url && /^https?:\/\//.test(link || "")) {
fields.url = link;
}
return fields;
}
function citoidFieldsFor(jederKey, raw, query) {
if (!(window.wikt && window.wikt.citoid && window.wikt.citoid.dataToJederFields)) {
return {};
}
return enrichCitoidFields(window.wikt.citoid.dataToJederFields(jederKey, raw, query), raw);
}
// ---- the "#*" line ------------------------------------------------------------------------------
// One finished citation line. Modes:
// "jeder" {{jêder-kitêb|ku|sernav=…|jêgirtin=…}}
// "category" the Kategorî:Çavkanî template as the citation template: {{Ç:Kitêb|rûpel=12|…}}
// "plain" free-text source, or none: {{jêder|ku|2=…|jêgirtin=…}} / {{nimûne|ku|…}}
// "raw" a finished template, passed through with only the entry name bolded
// opts: {mode, template, language, fields, langParamKey, quote, translation, transcription,
// source, sourceURL, title, marker, keepTranslation, extra}
function buildCitation(opts) {
var marker = opts.marker || "#*";
var title = opts.title || mw.config.get("wgTitle");
var quote = boldenPageTitle(String(opts.quote || ""), title).text;
if (opts.mode === "raw") {
// Written as given: the point is to paste a citation built elsewhere and have it survive intact.
// A line copied off another entry usually still carries its own "#*"/"#:" -- the marker is added
// back below, so strip it rather than emitting "#* #* {{...}}".
var pasted = String(opts.quote || "").replace(LIST_MARKER, "");
return marker + " " + nlToBr(boldQuoteParam(pasted, title)).trim();
}
var isCategory = opts.mode === "category";
var isTemplate = isCategory || opts.mode === "jeder";
var name = isTemplate ? opts.template : (opts.source || opts.sourceURL ? "jêder" : "nimûne");
var line = marker + " {{" + name;
// jêder-*, jêder and nimûne all take the language as parameter 1. A Kategorî:Çavkanî template only
// gets one when its own TemplateData names it, under its own key -- never assumed positionally.
if (!isCategory) {
line += "|" + opts.language;
}
if (isTemplate) {
var fields = opts.fields || {};
Object.keys(fields).forEach(function (key) {
var value = nlToSpace(String(fields[key] || "").trim());
if (value) {
line += "|" + key + "=" + value;
}
});
if (isCategory && opts.langParamKey) {
line += "|" + opts.langParamKey + "=" + opts.language;
}
// "Ç:" templates are quotation-equivalents rather than usage examples (see Modul:quote).
if (isCategory && /^Ç:/.test(name)) {
line += "|wekjêgirtin=1";
}
} else {
if (opts.source) {
// A positional value containing "=" would be read as its own key=value pair, so name it.
line += (String(opts.source).indexOf("=") !== -1 ? "|2=" : "|") + opts.source;
}
if (opts.sourceURL) {
line += "|url=" + opts.sourceURL;
}
}
var isUsex = name === "nimûne";
var translation = nlToBr(String(opts.translation || "").trim());
// {{nimûne}} always takes one, a Kurmancî example's being its explanation; a quotation's is left out
// for Kurmancî unless the edited line already had one (keepTranslation).
var writeTranslation = !!translation && (isUsex || opts.language !== "ku" || opts.keepTranslation);
// {{nimûne}}'s example is parameter 2 and its translation parameter 3, both positional. A value
// containing "=" would be read as key=value, so then both are named: naming only the example would
// slide the translation into slot 2.
var namedSlots = isUsex && (quote.indexOf("=") !== -1 || (writeTranslation && translation.indexOf("=") !== -1));
line += "|" + (isUsex ? (namedSlots ? "2=" : "") : "jêgirtin=") + nlToBr(quote);
if (writeTranslation) {
// Kategorî:Çavkanî templates route through Modul:quote and take its "t="; jêder-* uses "tercime=".
line += "|" + (isUsex ? (namedSlots ? "3=" : "") : isCategory ? "t=" : "tercime=") + translation;
}
var transcription = nlToBr(String(opts.transcription || "").trim());
if (transcription) {
line += "|tr=" + transcription;
}
// Parameters the form has no box for, carried through from the line being edited.
(opts.extra || []).forEach(function (param) {
line += "|" + param;
});
// A short example and its translation read best on one line, which {{nimûne}} does only when told.
// Short is Modul:usex's own measure for "rêz=auto": at most 100 characters shown between them.
if (isUsex && writeTranslation && !hasInlineParam(opts.extra) &&
shownLength(quote) + shownLength(translation) <= 100) {
line += "|rêz=1";
}
return line + "}}";
}
// True when the parameters already say inline or not: {{nimûne}}'s rêz=, under any of its names.
function hasInlineParam(params) {
return (params || []).some(function (param) {
return /^\s*(?:rêz|birêz|inline)\s*=/.test(param);
});
}
// How many characters a bit of wikitext shows: link targets, bold/italic marks and tags don't count.
function shownLength(text) {
return String(text || "")
.replace(/\[\[(?:[^\]|]*\|)?([^\]]*)\]\]/g, "$1")
.replace(/'{2,}/g, "")
.replace(/<[^>]*>/g, " ")
.length;
}
// A leading "#*" / "#:" / "##*" ... list marker.
var LIST_MARKER = /^\s*#+[*:]\s*/;
// True when the text is a whole citation line copied from somewhere else: its own list marker followed
// by a template call. That is the shape of every line this family writes -- {{jêder}}, {{jêder-kitêb}},
// {{nm}}, {{nimûne}}, {{mk}}, any Kategorî:Çavkanî template -- so the template name is not checked
// against a list, which could never cover every citation template on the wiki.
//
// Used to switch the form into raw mode by itself: pasting a finished line and forgetting to change the
// dropdown would otherwise wrap it in a second {{nimûne}}.
function looksLikeCitationLine(text) {
var trimmed = String(text || "");
if (!LIST_MARKER.test(trimmed)) {
return false;
}
return trimmed.replace(LIST_MARKER, "").slice(0, 2) === "{{";
}
// True when the example box holds nothing but one finished template call -- the cue to offer, or to
// switch to, the "write it out as-is" mode.
function looksLikeTemplate(text) {
var trimmed = String(text || "").trim();
if (trimmed.slice(0, 2) !== "{{" || trimmed.slice(-2) !== "}}") {
return false;
}
var parsed = edit.wt.parseTemplateCalls(trimmed);
return parsed.calls.length === 1 && !parsed.otherContent;
}
// ---- reading a finished line back --------------------------------------------------------------
// [[Şablon:nimûne]] and every redirect to it.
var USEX_NAMES = ["nimûne", "mk", "ux", "mînak", "nim", "nm"];
var USEX_TRANSLATION = ["3", "t", "tercime", "translation", "w", "werger"];
var USEX_TRANSCRIPTION = ["tr", "transliteration"];
// A quotation template's translation, under the name Modul:quote documents and its aliases.
var QUOTE_TRANSLATION = ["t", "tercime", "werger", "w"];
// [[Şablon:bênimûne]] and its redirects: the "no example yet" placeholder, with or without a language.
var EMPTY_EXAMPLE_MARKER = /^#+\*\s*\{\{\s*(?:bênimûne|bêmînak|mînak\?|nimûne\?)\s*(?:\|[^{}]*)?\}\}\s*$/i;
// The text of a line written as one italic run ("''…''", bold inside allowed -- "'''''X''' …''" opens
// with both), or null.
function italicRun(text) {
var t = $.trim(text || "");
var opening = /^'*/.exec(t)[0].length;
var m = /^''([\s\S]+)''$/.exec(t);
if ((opening !== 2 && opening !== 5) || !m || /''/.test(m[1].replace(/'''/g, ""))) {
return null;
}
return m[1];
}
function brToNl(text) {
return String(text || "").replace(/<br\s*\/?>/gi, "\n");
}
/**
* One "#:"/"#*" block -- its line plus any deeper "#::"/"#*:" lines -- as the form's values, or null when
* it is not an example: a word list, the "no example yet" placeholder, a plain note.
* {mode: "nimune"|"jeder"|"source"|"raw", marker, template, text, translation, transcription,
* source, sourceURL, fields, extra}
* extra is every parameter the form has no box for, to be written back as it was. A "raw" block is
* edited as written: text is the whole block, list markers included.
*/
function parseCitationLine(block) {
var all = String(block || "").split("\n");
var k = 1;
while (k < all.length && !/^#/.test(all[k])) {
k++;
}
var head = /^#+([:*])\s*([\s\S]*)$/.exec(all.slice(0, k).join("\n"));
var sub = all.slice(k);
if (!head || EMPTY_EXAMPLE_MARKER.test(all[0])) {
return null;
}
var out = { mode: "raw", marker: head[1], template: "", text: "", translation: "", transcription: "",
source: "", sourceURL: "", fields: {}, extra: [] };
var body = $.trim(head[2]);
var parsed = edit.wt.parseTemplateCalls(body);
var call = parsed.calls.length === 1 && !parsed.otherContent ? parsed.calls[0] : null;
var name = call ? edit.wt.canonicalTemplateKey(call.name) : "";
name = name.charAt(0).toLowerCase() + name.slice(1);
var isUsex = USEX_NAMES.indexOf(name) !== -1;
var def = call && jederDefs[name];
if (call && !sub.length && (isUsex || def || name === "jêder")) {
var split = edit.wt.splitCallParams(call.params);
var fieldKeys = def ? def.fields.map(function (f) { return f.key; }) : [];
out.mode = isUsex ? "nimune" : def ? "jeder" : "source";
out.template = def ? name : "";
split.used.forEach(function (key, i) {
var value = brToNl(split.values[key]);
if (key === "1") {
return; // the language, which the section already says
}
if (isUsex ? key === "2" : (key === "jêgirtin" || key === "jg")) {
out.text = value;
} else if ((isUsex ? USEX_TRANSLATION : QUOTE_TRANSLATION).indexOf(key) !== -1) {
out.translation = value;
} else if (USEX_TRANSCRIPTION.indexOf(key) !== -1) {
out.transcription = value;
} else if (def && fieldKeys.indexOf(key) !== -1) {
out.fields[key] = split.values[key];
} else if (name === "jêder" && key === "2") {
out.source = split.values[key];
} else if (name === "jêder" && key === "url") {
out.sourceURL = split.values[key];
} else {
// Named when it was positional, so it cannot shift into a slot the form now fills.
out.extra.push(/^\d+$/.test(key) && !/=/.test(call.params[i]) ? key + "=" + split.values[key] : $.trim(call.params[i]));
}
});
return out;
}
// Any other citation template on a "#*" line -- the Kategorî:Çavkanî ones, {{J:…}} and {{Ç:…}} --
// reads into the category mode: quote, translation and transcription in their boxes, every other
// named parameter as a field. Which of those get a box is known once the template's parameters
// are fetched (see applyAliases).
if (call && !sub.length && out.marker === "*" &&
(/^[JÇ]:/.test($.trim(call.name)) || /(?:^|\|)\s*(?:jêgirtin|jg)\s*=/.test(call.params.join("|")))) {
var catSplit = edit.wt.splitCallParams(call.params);
out.mode = "category";
out.template = $.trim(call.name);
catSplit.used.forEach(function (key, i) {
var value = catSplit.values[key];
if (key === "jêgirtin" || key === "jg") {
out.text = brToNl(value);
} else if (QUOTE_TRANSLATION.indexOf(key) !== -1) {
out.translation = brToNl(value);
} else if (USEX_TRANSCRIPTION.indexOf(key) !== -1) {
out.transcription = brToNl(value);
} else if (key === "wekjêgirtin" && /^Ç:/.test(out.template)) {
return; // buildCitation() writes it for every "Ç:" template
} else if (/^\d+$/.test(key)) {
out.extra.push(!/=/.test(call.params[i]) ? key + "=" + value : $.trim(call.params[i]));
} else {
out.fields[key] = value;
}
});
return out;
}
if (out.marker === ":") {
// The hand-written kind: "#: ''…''", its translation, if any, on a "#::" line under it.
var italic = italicRun(body);
var translationLine = sub.length === 1 ? /^#+::\s*([\s\S]*)$/.exec(sub[0]) : null;
if (italic !== null && (!sub.length || translationLine)) {
out.mode = "nimune";
out.text = brToNl(italic);
out.translation = translationLine ? brToNl(italicRun(translationLine[1]) || $.trim(translationLine[1])) : "";
return out;
}
if (!isUsex) {
return null;
}
}
out.text = all.join("\n");
return out;
}
/**
* Settles which parameters of a read line get a box. aliases maps an alias to its documented name
* (templateAliases); fieldKeys are the boxes the form drew. A parameter written under an alias -- "r="
* for "rûpel=" -- goes to its box; one with no box goes to extra, written back as it was.
*/
function applyAliases(example, aliases, fieldKeys) {
var pending = example.extra.slice();
Object.keys(example.fields).forEach(function (key) {
if (fieldKeys.indexOf(key) === -1) {
pending.push(key + "=" + example.fields[key]);
delete example.fields[key];
}
});
example.extra = [];
pending.forEach(function (param) {
var m = /^\s*([^=]+?)\s*=([\s\S]*)$/.exec(param);
var key = m ? (aliases[m[1]] || m[1]) : null;
var value = m ? $.trim(m[2]) : "";
if (m && fieldKeys.indexOf(key) !== -1 && !example.fields[key]) {
example.fields[key] = value;
} else if (m && key === "jêgirtin" && !example.text) {
example.text = brToNl(value);
} else if (m && QUOTE_TRANSLATION.indexOf(key) !== -1 && !example.translation) {
example.translation = brToNl(value);
} else {
example.extra.push(param);
}
});
return example;
}
/**
* newLine with its parameters in the order originalLine had them, so an edit's diff shows only what
* changed. Parameters match by name -- an alias counting as the name it stands for (aliases, from
* templateAliases) -- and positional ones by position; any the original lacked follow in newLine's own
* order. A line that is not one template call on either side comes back as it was.
*/
function keepOrder(newLine, originalLine, aliases) {
aliases = aliases || {};
var marker = /^#+[:*]\s*/.exec(newLine || "");
var oldBody = /^#+[:*]\s*([\s\S]*)$/.exec(originalLine || "");
if (!marker || !oldBody) {
return newLine;
}
var fresh = edit.wt.parseTemplateCalls(newLine.slice(marker[0].length));
var old = edit.wt.parseTemplateCalls($.trim(oldBody[1]));
if (fresh.calls.length !== 1 || fresh.otherContent || old.calls.length !== 1 || old.otherContent) {
return newLine;
}
function keyed(params) {
var positional = 0;
return params.map(function (raw) {
var m = /^\s*([^|={}\n]+?)\s*=/.exec(raw);
var key = m ? m[1] : String(++positional);
return { key: aliases[key] || key, raw: raw };
});
}
var rest = keyed(fresh.calls[0].params);
var ordered = [];
keyed(old.calls[0].params).forEach(function (o) {
for (var i = 0; i < rest.length; i++) {
if (rest[i].key === o.key) {
ordered.push(rest.splice(i, 1)[0]);
return;
}
}
});
return marker[0] + "{{" + fresh.calls[0].name + ordered.concat(rest).map(function (p) {
return "|" + p.raw;
}).join("") + "}}";
}
// Where the {{ … }} opened on lines[start] closes, so a template written over several lines stays whole.
function braceEnd(lines, start) {
var depth = 0;
for (var i = start; i < lines.length; i++) {
depth += (lines[i].match(/\{\{/g) || []).length - (lines[i].match(/\}\}/g) || []).length;
if (depth <= 0) {
return i;
}
}
return lines.length - 1;
}
// The "#:"/"#*" blocks right under the definition on lines[defIndex], each {start, end, text, example}:
// example is parseCitationLine's reading, or null for a block that is not an example.
function exampleBlocks(lines, defIndex) {
var depth = /^#+/.exec(lines[defIndex])[0].length;
var own = new RegExp("^#{" + depth + "}[:*]");
var deeper = new RegExp("^#{" + depth + "}[:*][:*]");
var blocks = [];
var i = defIndex + 1;
while (i < lines.length && own.test(lines[i])) {
var start = i;
var end = braceEnd(lines, i);
while (end + 1 < lines.length && deeper.test(lines[end + 1])) {
end = braceEnd(lines, end + 1);
}
var text = lines.slice(start, end + 1).join("\n");
blocks.push({ start: start, end: end, text: text, example: parseCitationLine(text) });
i = end + 1;
}
return blocks;
}
/**
* Replaces the index-th example under a definition with code, or removes it when code is null.
* original is the block as it read when the form opened. When the index no longer holds it (an earlier
* queued change moved it) the one block still reading exactly that is used; otherwise nothing is touched.
*/
function replaceExample(pageContent, language, sectionType, sectionOrdinal, definitionLevel, index, original, code) {
var loc = edit.wt.locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defIndex = edit.wt.findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var examples = exampleBlocks(lines, defIndex).filter(function (b) {
return b.example;
});
var block = examples[index] && examples[index].text === original ? examples[index] : null;
if (!block) {
var same = examples.filter(function (b) {
return b.text === original;
});
block = same.length === 1 ? same[0] : null;
}
if (!block) {
throw new edit.util.SubmitAbort("Ev nimûne ji dema vekirina formê ve hatiye guhartin — rûpelê nû bike û dîsa biceribîne.");
}
var replacement = code === null ? [] : String(code).split("\n");
lines.splice.apply(lines, [block.start, block.end - block.start + 1].concat(replacement));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Words only, lower-cased: links, templates, tags and quote marks out, so the page's drawn text and the
// line's wikitext can be compared.
function normalizeWords(text) {
return String(text || "")
.replace(/\[\[(?:[^\]|]*\|)?([^\]]*)\]\]/g, "$1")
.replace(/\{\{[^{}]*\}\}/g, " ")
.replace(/<[^>]*>/g, " ")
.replace(/'{2,}/g, "")
.toLowerCase()
.replace(/[\s.,;:!?"'“”‘’«»()\[\]\-–—…\/]+/g, "");
}
// True when the text a page draws for an example comes from this line.
function exampleMatches(drawnText, example) {
var source = example.text;
if (example.mode === "raw") {
var quote = /\|\s*(?:jêgirtin|jg)\s*=([^|}]*)/.exec(source);
source = quote ? quote[1] : source.replace(/^#+[:*]\s*/, "");
}
var drawn = normalizeWords(drawnText);
var written = normalizeWords(source);
if (!drawn || !written) {
return false;
}
return drawn === written || written.indexOf(drawn.slice(0, 24)) !== -1;
}
module.exports = {
CATEGORY_KEY: CATEGORY_KEY,
RAW_KEY: RAW_KEY,
COVERED_PARAMS: COVERED_PARAMS,
LANG_LABEL: LANG_LABEL,
jederDefs: jederDefs,
fieldSize: fieldSize,
exclusivePartner: exclusivePartner,
boldenPageTitle: boldenPageTitle,
nlToBr: nlToBr,
nlToSpace: nlToSpace,
fetchCategoryTemplates: fetchCategoryTemplates,
buildCategoryTemplateSearch: buildCategoryTemplateSearch,
openTemplateDialog: openTemplateDialog,
buildTemplateParamForm: buildTemplateParamForm,
templateParams: templateParams,
fieldSpecs: fieldSpecs,
jederFieldSpecs: jederFieldSpecs,
fetchFromCitoid: fetchFromCitoid,
citoidFieldsFor: citoidFieldsFor,
withLinkAsUrl: withLinkAsUrl,
today: today,
buildCitation: buildCitation,
looksLikeTemplate: looksLikeTemplate,
looksLikeCitationLine: looksLikeCitationLine,
USEX_NAMES: USEX_NAMES,
EMPTY_EXAMPLE_MARKER: EMPTY_EXAMPLE_MARKER,
parseCitationLine: parseCitationLine,
applyAliases: applyAliases,
keepOrder: keepOrder,
templateAliases: templateAliases,
exampleBlocks: exampleBlocks,
replaceExample: replaceExample,
exampleMatches: exampleMatches
};
}());
// </nowiki>
9fzshq4mz2rzhlxch6aui022w229y9e
MediaWiki:Gadget-wikt.dsb.js
8
1359171
6531638
6529974
2026-09-15T14:48:49Z
Wikihez
7344
Pencils reveal on hover only on devices with a real pointer, so a first tap on iOS Safari opens the link
6531638
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.dsb.js]]
*
* The part of DefSideBoxes that runs on a page view: finds the definition lines and hangs a pencil on
* each. Forms, dialogs and saves are in wikt.dsb-panels, fetched on the first click (never on hover).
*
* The "Nimûne/jêgirtin" item drives add-examples, which wikt.dsb-panels depends on -- so it is loaded by
* the time any panel exists, and never on a plain page view.
*/
(function () {
"use strict";
var core = require("ext.gadget.wikt.core");
var sections = core.sections;
var MANE_SECTION = sections.MANE_SECTION;
var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS;
var msg = core.msg;
var headingOuter = sections.headingOuter;
var headingDepth = sections.headingDepth;
var headingText = sections.headingText;
var isManeHeading = sections.isManeHeading;
var sectionTypeOf = sections.sectionTypeOf;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var headingTextOrdinalWithinLanguage = sections.headingTextOrdinalWithinLanguage;
var languageCodeForOl = sections.languageCodeForOl;
var eachLeafDefinition = sections.eachLeafDefinition;
if (mw.config.get("wgAction") !== "view") {
return;
}
// An entry, a reconstruction, or a sandbox -- see sections.isEntryPage().
if (!sections.isEntryPage()) {
return;
}
if (/&printable=yes|&diff=|&oldid=/.test(window.location.search)) {
return;
}
window.wikt = window.wikt || {};
if (window.wikt.defSideBoxesLoaded) {
return;
}
window.wikt.defSideBoxesLoaded = true;
// A save from the queue box reloads with core's post-edit flag set (see dsb-panels.js). Core loads the
// module that reads the flag only after a form save, so it is loaded here; it shows the toast and clears the flag.
if (mw.storage.session.get("mw-PostEdit" + mw.config.get("wgPageName"))) {
mw.loader.load("mediawiki.action.view.postEdit");
}
// The panels are a separate module so their ~100 KB never lands on a page view. Fetched once, on the
// first click that actually needs a form -- never on hover; every later call reuses the same promise.
var panelsPromise = null;
var panelsApi = null;
// A panel is built once and merely shown again after that, so the reveal cannot live in the builders.
// Only click handlers call this -- hover must never pull the panels module.
function revealQueue() {
withPanels(function (panels) {
panels.openQueueBox();
});
}
function withPanels(fn) {
if (!panelsPromise) {
panelsPromise = mw.loader.using("ext.gadget.wikt.dsb-panels").then(function (require) {
panelsApi = require("ext.gadget.wikt.dsb-panels");
return panelsApi;
});
}
panelsPromise.then(fn, function () {
mw.notify("Nikare amûrên guhartinê bar bike.", { type: "error" });
});
}
// The element holding an example's last line of text: a quotation's source line and its quote are
// blocks, so a pencil after them would sit on a line of its own.
function lastTextLine(el) {
var last = el.lastElementChild;
while (last && /^(DIV|DL|DD|UL|OL|LI)$/.test(last.tagName)) {
el = last;
last = el.lastElementChild;
}
return el;
}
// A pencil on every example and quotation under a sense, opening add-examples' form on that one line.
function attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3) {
// A queued change redraws the sense (dsb-panels.js raises this); its examples get their pencils again.
if (!$li.data("dsbExampleRedraw")) {
$li.data("dsbExampleRedraw", true);
$li.on("dsbSenseRedrawn", function () {
attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3);
});
}
sections.exampleItemsOf($li).each(function () {
var item = this;
if ($(item).hasClass("dsb-has-example-toolbar")) {
return;
}
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", "Nimûneyê biguhêre");
var $toolbar = $("<span class='dsb-example-toolbar'></span>").append($toggle);
$toggle.on("click", function (e) {
e.preventDefault();
e.stopPropagation();
withPanels(function (panels) {
panels.editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, item);
});
});
// Touch has no hover: a tap on the example shows its pencil, as a tap on a sense does.
$(lastTextLine(item)).append($toolbar);
$(item).addClass("dsb-has-example-toolbar").on("click.dsbReveal", function () {
$toolbar.addClass("dsb-revealed");
});
});
}
// How many rows the pencil is currently holding open. A pinned row means somebody is part-way through
// an edit, and opening another one under the pointer moves the page: the content below shifts, the
// pointer ends up over something else, that fires its own hover, and the page jumps back and forth.
// So while anything is pinned, hovering opens nothing.
var pinnedRows = 0;
var canHover = !window.matchMedia || window.matchMedia("(hover: hover)").matches;
function attachToolbar($li, language, sectionType, sectionOrdinal, definitionLevel, $h3, isLastSense) {
if ($li.children(".dsb-toolbar").length) {
return;
}
var $toolbar = $("<span class='dsb-toolbar'></span>");
// The real InterlanguageLinks pencil icon (same asset defEditButton uses); image swap is in the CSS.
var $toggle = $("<a href='#' class='dsb-toggle' title='Guhartina pêşketî'></a>");
// Revealed by $toggle -- an indented panel under the sense, not always visible. Built lazily on
// first open rather than eagerly for every sense on the page.
var $quickRow = $("<div class='dsb-quick-row'></div>").hide();
var pinned = false; // the pencil pins the row open; hovering alone does not
var hovering = false;
// Builds the row -- fetching the panels module the first time -- then calls back. Asynchronous now,
// so the callback re-checks that the row is still wanted rather than showing it unconditionally.
function ensureBuilt(then) {
if ($quickRow.data("built")) {
then();
return;
}
withPanels(function (panels) {
if (!$quickRow.data("built")) {
panels.buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense);
$quickRow.data("built", true);
}
then();
});
}
// Anything the user has started here: an open field box, the open dropdown, or focus inside the row.
function engaged() {
return pinned ||
$quickRow.find(".dsb-open-boxes").children().length > 0 ||
$quickRow.find(".dsb-more-menu").is(":visible") ||
$.contains($quickRow.get(0), document.activeElement);
}
function setPinned(next) {
if (pinned === next) {
return;
}
pinned = next;
pinnedRows += next ? 1 : -1;
}
$toggle.on("click", function (e) {
e.preventDefault();
// Through their own cancel handler rather than a bare hide(), so a row this closes also drops
// its pinned state -- hiding it alone would leave pinnedRows counting a row nobody can see.
$(".dsb-quick-row").not($quickRow).trigger("dsbCancel");
setPinned(!pinned);
if (pinned) {
ensureBuilt(function () {
if (pinned) {
$quickRow.show();
}
});
} else {
$quickRow.hide();
}
});
// Hover reveals the row and takes it away again, the way en.wikt's does -- but never while something
// on it is in use, or a half-typed box would vanish on the way to the mouse leaving.
$li.on("mouseenter.dsbHover", function () {
// iOS plays a tap as mouseenter first, and a row shown here would swallow that tap's click.
if (!canHover) {
return;
}
hovering = true;
// A row the pencil is holding open is already open, and no row opens on hover while another
// one is: that is the case where the page is being edited and must stay still.
if (pinned || pinnedRows) {
return;
}
// Hover reveals a row that already exists; it never fetches the panels module. Otherwise
// simply moving the pointer across an entry would pull ~100 KB on every page view, which is
// the whole cost the split exists to avoid. The pencil is what asks for it.
if ($quickRow.data("built")) {
$quickRow.show();
}
});
// Raised by the panel's "Betal bike" (see dsb-panels.js): clear the pinned state as well as
// hiding, so one click on the pencil opens it again.
$quickRow.on("dsbCancel", function () {
setPinned(false);
hovering = false;
$quickRow.hide();
});
$li.on("mouseleave.dsbHover", function () {
hovering = false;
if (!engaged()) {
$quickRow.hide();
}
});
$toolbar.append($toggle);
// The class positions the <li>, so the toolbar's gutter slot is measured from the line itself and
// not from wherever it happens to sit in the child order -- ShowIDs prepends its id pill to this
// same <li>.
$li.addClass("dsb-def").prepend($toolbar);
// Touch has no :hover -- a tap anywhere on the line reveals the pencil the same way hovering does on
// desktop (CSS gates this to touch devices; harmless if it also fires on a mouse click).
$li.on("click.dsbReveal", function () {
$toolbar.addClass("dsb-revealed");
});
// Land $quickRow right after the sense's own gloss text -- before any existing "Hevmane: ..." <dl>,
// citation <ul>, or the Nimûne/jêgirtin proxy link's own <ul>, not appended after all of that.
var $firstBlock = $li.children("dl, ul, ol").first();
if ($firstBlock.length) {
$firstBlock.before($quickRow);
} else {
$li.append($quickRow);
}
}
// A section the data marks hidden is left alone: Werger waits for the translation tools it belongs
// to, and a pencil that opens a box those cannot yet fill is a promise this gadget does not keep.
function isHiddenSection(header) {
return PAGE_SUBSECTIONS.some(function (s) {
return s.hidden && s.header === header;
});
}
// Every subsection under a part of speech, whether or not the data knows the header: one it does not
// know opens as wikitext, or as a word list when that is what it holds -- see buildSubsectionPanel.
function attachSubsectionEditors($h3, language, sectionType, sectionOrdinal, posDepth) {
posDepth = posDepth || 3;
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var depth = headingDepth($cursor);
if (depth && depth <= posDepth) {
break;
}
if (depth !== posDepth + 1) {
continue;
}
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
var header = headingText($hh);
if (!header || isHiddenSection(header) || $hh.data("dsbAttached")) {
continue;
}
$hh.data("dsbAttached", true);
attachSubsectionEditor($hh, $cursor, $h3, header, language, sectionType, sectionOrdinal);
}
}
// Where the pencil goes depends on what the skin does with its own edit links. Desktop skins put
// "[biguhêre]" right after the heading text, so the pencil follows it and the two read as one group.
// Minerva parks its edit icon at the far right of the heading row instead, a screen-width away from the
// text, so there the pencil goes inside the heading rather than chasing it.
function placeHeadingToolbar($heading, $toolbar) {
if (mw.config.get("skin") !== "minerva") {
var $editsection = headingOuter($heading).find(".mw-editsection").first();
if ($editsection.length) {
$editsection.after($toolbar);
return;
}
}
$heading.append($toolbar);
}
// Touch has no hover, so a heading's pencil is shown by tapping the heading itself. Taps that mean
// something else are left alone: the skin's own edit links, and the pencil once it is showing.
function revealHeadingToolbarOnTap($heading, $toolbar) {
headingOuter($heading).on("click", function (e) {
if ($(e.target).closest("a, .dsb-toolbar").length) {
return;
}
$toolbar.toggleClass("dsb-revealed");
});
}
// One pencil's worth of behaviour, shared by the "====" headings and by an image's own caption: the
// panel is built the first time it is opened, never before, and the pencil stays visible while it is.
// free, when given, pins the panel to a heading located by its own text rather than through a part of
// speech: {header, ordinal}.
function bindPanelToggle($toggle, $toolbar, $panel, $h3, header, language, sectionType, sectionOrdinal, free) {
$toggle.on("click", function (e) {
e.preventDefault();
$panel.toggle();
// Stays put while its panel is open, so it does not vanish from under the pointer.
$toolbar.toggleClass("dsb-revealed", $panel.is(":visible"));
if (!$panel.is(":visible")) {
return;
}
if (!$panel.data("built")) {
$panel.data("built", true);
$panel.text(msg.loading);
withPanels(function (panels) {
$panel.empty();
panels.buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, header, free);
});
}
revealQueue();
});
}
function attachSubsectionEditor($hh, $heading, $h3, header, language, sectionType, sectionOrdinal) {
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", header + " biguhêre");
var $panel = $("<div class='dsb-subsection-panel'></div>").hide();
$toolbar.append($toggle);
placeHeadingToolbar($hh, $toolbar);
revealHeadingToolbarOnTap($hh, $toolbar);
$heading.after($panel);
bindPanelToggle($toggle, $toolbar, $panel, $h3, header, language, sectionType, sectionOrdinal);
}
// The "place" subsection that owns the thumbnails under a part-of-speech heading -- named in the data,
// not spelled out here, so renaming it there renames it everywhere.
var IMAGE_HEADER = (function () {
var found = null;
PAGE_SUBSECTIONS.forEach(function (s) {
if (s.place === "pos-top") {
found = s.header;
}
});
return found;
}());
// A pencil on the image's own caption. A wrong caption is noticed while looking at the picture, and
// this is where the writer reaches for it -- the same editor the section's own list offers, opened on
// the image that is already there.
// Every thumbnail between the heading and the first subsection, which is the run "pos-top" covers.
// Anything else in that run is stepped over: a headword line, a declension table or the stylesheet a
// template brings with it all sit before the pictures on plenty of entries.
function attachImageEditors($h3, language, sectionType, sectionOrdinal) {
if (!IMAGE_HEADER) {
return;
}
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if (headingDepth($cursor)) {
break;
}
if ($cursor.is(".thumb, figure, .floatright, .floatleft")) {
attachImageEditor($cursor, $h3, language, sectionType, sectionOrdinal);
}
}
}
// A "===" section that is not a part of speech but is one of ours: Bilêvkirin, which sits above the
// part-of-speech blocks and belongs to the whole language.
function placeSubsectionFor($h3) {
var text = headingText($h3);
var found = null;
PAGE_SUBSECTIONS.forEach(function (s) {
if (s.place === "pron" && s.header === text) {
found = s;
}
});
return found;
}
// The first part-of-speech heading under the same language. The writer works from a section, so
// without one there is nothing to anchor the box to.
function firstPosHeadingAfter($h3) {
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is(".mw-heading2, h2")) {
return null; // the next language starts
}
var depth = headingDepth($cursor);
if (depth !== 3 && depth !== 4) {
continue;
}
var $hh = $cursor.is("h3, h4") ? $cursor : $cursor.find("h3, h4").first();
if (sectionTypeOf($hh) || isManeHeading($hh)) {
return $hh;
}
}
return null;
}
// Its own pencil, opening the same box the section list offers -- a heading nobody can edit from the
// page is a heading people edit by hand.
function attachPlaceHeadingEditor($h3, subsection) {
if ($h3.data("dsbAttached")) {
return;
}
var $pos = firstPosHeadingAfter($h3);
if (!$pos) {
return;
}
var $ol = null;
for (var $cursor = headingOuter($pos).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("ol")) {
$ol = $cursor;
break;
}
if ($cursor.is(".mw-heading2, .mw-heading3, h2, h3")) {
break;
}
}
var language = $ol && languageCodeForOl($ol);
if (!language) {
return;
}
$h3.data("dsbAttached", true);
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", subsection.header + " " + msg.edit);
var $panel = $("<div class='dsb-subsection-panel'></div>").hide();
$toolbar.append($toggle);
placeHeadingToolbar($h3, $toolbar);
revealHeadingToolbarOnTap($h3, $toolbar);
headingOuter($h3).after($panel);
bindPanelToggle($toggle, $toolbar, $panel, $pos, subsection.header, language,
sectionTypeOf($pos) || MANE_SECTION, sectionOrdinalWithinLanguage($pos));
}
// Every other heading inside a language: the ones that sit beside the parts of speech rather than
// under one ("=== Etîmolojî ===", "=== Çavkanî ==="), and the ones nobody planned for. Each opens
// the wikitext under its own heading, or the {{kol3}} form when that is what it holds.
function attachFreeSectionEditors() {
$("#mw-content-text").find("h3, h4, h5, h6").each(function () {
var $h = $(this);
// A heading that already carries one of this gadget's pencils is spoken for.
if ($h.data("dsbAttached") || headingOuter($h).find(".dsb-toolbar").length) {
return;
}
var header = headingText($h);
if (!header || isHiddenSection(header)) {
return;
}
var language = languageCodeForOl($h);
if (!language) {
return;
}
$h.data("dsbAttached", true);
attachFreeSectionEditor($h, header, language);
});
}
function attachFreeSectionEditor($h, header, language) {
var free = { header: header, ordinal: headingTextOrdinalWithinLanguage($h) };
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", header + " " + msg.edit);
var $panel = $("<div class='dsb-subsection-panel'></div>").hide();
$toolbar.append($toggle);
placeHeadingToolbar($h, $toolbar);
revealHeadingToolbarOnTap($h, $toolbar);
headingOuter($h).after($panel);
bindPanelToggle($toggle, $toolbar, $panel, $h, header, language, null, 0, free);
}
function attachImageEditor($fig, $h3, language, sectionType, sectionOrdinal) {
if ($fig.data("dsbAttached")) {
return;
}
var $caption = $fig.find("figcaption, .thumbcaption").first();
if (!$caption.length) {
return;
}
$fig.data("dsbAttached", true);
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-caption'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", IMAGE_HEADER + " biguhêre");
// After the figure, not inside it: the panel is full width, and a floated thumbnail is not.
var $panel = $("<div class='dsb-subsection-panel dsb-panel-after-image'></div>").hide();
$toolbar.append($toggle);
$caption.append($toolbar);
revealHeadingToolbarOnTap($caption, $toolbar);
$fig.after($panel);
bindPanelToggle($toggle, $toolbar, $panel, $h3, IMAGE_HEADER, language, sectionType, sectionOrdinal);
}
function attachSubsectionTrigger($h3, $ol, language, sectionType, sectionOrdinal) {
if ($h3.data("dsbAttached")) {
return;
}
$h3.data("dsbAttached", true);
var $anchor = $ol;
for (var $cursor = $ol.next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is(".mw-heading2, .mw-heading3, .mw-heading4, .mw-heading5, .mw-heading6, h2, h3, h4, h5, h6")) {
break;
}
$anchor = $cursor;
}
var $trigger = $("<a href='#' class='dsb-add-subsection'>+ Beşek lê zêde bike</a>");
var $panel = $("<div class='dsb-subsection-panel'></div>").hide();
$trigger.on("click", function (e) {
e.preventDefault();
$panel.toggle();
if (!$panel.is(":visible")) {
return;
}
if (!$panel.data("built")) {
$panel.data("built", true);
$panel.text(msg.loading);
withPanels(function (panels) {
$panel.empty();
panels.buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal);
});
}
revealQueue();
});
$anchor.after($panel);
$anchor.after($trigger);
}
// Attaches the "choose a part of speech" trigger under a heading whose part of speech is unsettled: an
// unclassified "=== Mane ===" placeholder, or a "=== Biwêj ===" that is really a noun, verb, ... .
function attachConvertPosTrigger($h3, sectionType) {
if ($h3.data("dsbConvertAttached")) {
return;
}
$h3.data("dsbConvertAttached", true);
// languageCodeForOl() is really just "nearest preceding .besazimani[id] before this element in
// document order" -- nothing ol-specific about it, so it works unchanged for a heading too.
var language = languageCodeForOl($h3);
if (!language) {
return;
}
var sectionOrdinal = sectionOrdinalWithinLanguage($h3);
// The same pencil the "====" subsections get, so reclassifying reads as an edit affordance rather
// than a line of body text.
var $toolbar = $("<span class='dsb-toolbar dsb-toolbar-heading'></span>");
var $toggle = $("<a href='#' class='dsb-toggle'></a>").attr("title", "Cureyê rêzimanî hilbijêre");
var $panel = $("<div class='dsb-convert-pos-panel'></div>").hide();
$toolbar.append($toggle);
$toggle.on("click", function (e) {
e.preventDefault();
$panel.toggle();
// Stays put while its panel is open, so it does not vanish from under the pointer.
$toolbar.toggleClass("dsb-revealed", $panel.is(":visible"));
if (!$panel.is(":visible")) {
return;
}
if (!$panel.data("built")) {
$panel.data("built", true);
$panel.text(msg.loading);
withPanels(function (panels) {
$panel.empty();
panels.buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal);
});
}
revealQueue();
});
// Raised by the panel's "Betal bike": closes it and drops what was typed, so the pencil reopens it fresh.
$panel.on("dsbCancel", function () {
$panel.hide().empty().data("built", false);
$toolbar.removeClass("dsb-revealed");
});
placeHeadingToolbar($h3, $toolbar);
revealHeadingToolbarOnTap($h3, $toolbar);
headingOuter($h3).after($panel);
}
// ---- driver ----
function init() {
$("h3, h4").each(function () {
var $h3 = $(this);
var posDepth = headingDepth($h3) || 3;
var sectionType = sectionTypeOf($h3);
if (!sectionType) {
var place = placeSubsectionFor($h3);
if (place) {
attachPlaceHeadingEditor($h3, place);
return;
}
if (!isManeHeading($h3)) {
return; // not a recognized word-sense section (Etîmolojî, Çavkanî, ...)
}
// An unclassified placeholder: offer the POS picker, then carry on and wire up the same
// per-sense pencils, so the POS choice and the sense edits can be queued and saved together.
sectionType = MANE_SECTION;
attachConvertPosTrigger($h3, sectionType);
} else {
// Every part of speech, not just the ones most often mistaken: any section can turn out to
// be another, and the picker is what changes it.
attachConvertPosTrigger($h3, sectionType);
}
var sectionOrdinal = sectionOrdinalWithinLanguage($h3);
var $ol = null;
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("ol")) {
$ol = $cursor;
break;
}
if (headingDepth($cursor)) {
break;
}
}
if (!$ol) {
return;
}
var language = languageCodeForOl($ol);
if (!language) {
return;
}
var lastTopIndex = $ol.children("li").not(".mw-empty-elt").length - 1;
eachLeafDefinition($ol, function ($li) {
var definitionLevel = [];
var $definitionItem = $li;
while ($definitionItem.prop("tagName") === "LI") {
definitionLevel.splice(0, 0, $definitionItem.prevAll("li:not(.mw-empty-elt)").length);
$definitionItem = $definitionItem.parent().parent();
}
definitionLevel.splice(0, 0, sectionType + "|" + sectionOrdinal);
var isLastSense = definitionLevel.length === 2 && definitionLevel[1] === lastTopIndex;
attachToolbar($li, language, sectionType, sectionOrdinal, definitionLevel, $h3, isLastSense);
attachExampleEditors($li, language, sectionType, sectionOrdinal, definitionLevel, $h3);
});
attachSubsectionTrigger($h3, $ol, language, sectionType, sectionOrdinal, posDepth);
attachSubsectionEditors($h3, language, sectionType, sectionOrdinal, posDepth);
attachImageEditors($h3, language, sectionType, sectionOrdinal);
});
// Last, so a heading one of the walks above claimed keeps the pencil that knows more about it.
attachFreeSectionEditors();
}
// One listener for every "Hevmane û Bêhtir" dropdown on the page.
$(document).on("click.dsbMoreMenu", function (e) {
$(".dsb-more-menu:visible").each(function () {
var $menu = $(this);
if ($(e.target).closest($menu).length || $(e.target).closest($menu.prev(".dsb-more-toggle")).length) {
return;
}
$menu.slideUp();
var $arrow = $menu.data("dsbArrow");
if ($arrow) {
$arrow.text("▸");
}
});
});
// Closes every open panel/preview this gadget can leave in the DOM, so bfcache never freezes one mid-open.
function closeAllOpenUi() {
if (panelsApi) {
panelsApi.clearQueue();
panelsApi.closeAllPanels();
return;
}
// Before the panels arrive there is nothing built to tear down, only the view-path rows.
$(".dsb-quick-row, .dsb-more-menu, .dsb-preview-inline, .dsb-preview-block").hide();
}
$(window).on("pagehide", closeAllOpenUi);
// Belt-and-suspenders: also close on restore-from-bfcache, in case pagehide's own hide didn't stick.
$(window).on("pageshow", function (e) {
if (e.originalEvent && e.originalEvent.persisted) {
closeAllOpenUi();
}
});
$(init);
// </nowiki>
}());
// </nowiki>
io2uo7kwzyuwjjqdfoopvlib4u4s4bj
MediaWiki:Gadget-wikt.dsb.css
8
1359172
6531637
6529976
2026-09-15T14:48:38Z
Wikihez
7344
Pencils reveal on hover only on devices with a real pointer, so a first tap on iOS Safari opens the link
6531637
css
text/css
/* <nowiki> */
/*
* Page: [[MediaWiki:Gadget-wikt.dsb.css]]
*
* Styles for the view path only: the pencil and its toolbar, which is all wikt.dsb draws before someone
* clicks. Everything the panels need is in wikt.dsb-panels.css, loaded with them.
* Colour and spacing tokens come from wikt.core.css.
*/
/* position:absolute with top/left auto keeps the static position but drops the box out of flow; the negative margin then slides the pencil into the list gutter, in front of the marker (en.wikt's .defEditButton trick). */
/* Anchors the gutter the pencil sits in. Without it the toolbar is placed from its static position, which
moves as soon as anything else is prepended to the line. */
li.dsb-def {
position: relative;
}
/* A sense line is positioned (its pencil hangs off it), so its own background -- the :target highlight
after clicking "mane 1" in a caption, or a queued edit's preview -- would paint over a picture floated
beside it. The pictures sit one layer up instead. */
.mw-parser-output figure,
.mw-parser-output .thumb {
position: relative;
z-index: 1;
}
/* {{bênimûne}}'s own "Nimûneyekê lê zêde bike" line: the sense's "+ Nimûne/jêgirtin" item offers the same.
Here rather than with add-examples, which only loads once a panel is opened. */
.mw-parser-output li:has(> .benimune) {
display: none;
}
.dsb-toolbar {
position: absolute;
left: -40px;
top: 0;
width: 40px;
height: 26px;
font-family: var(--wikt-sans);
}
/* A heading's pencil is inline, so it keeps the flow and needs none of the above. */
/* On an image's caption: inline after the caption text, and always visible -- an entry carries a
picture or two, not a column of headings, so there is nothing here for a hidden icon to spare.
The z-index lifts it over the sense lines: a sense's <li> is positioned and its box runs the full
width of the column, right under a floated picture, so it would otherwise take the click. */
.dsb-toolbar-caption {
position: relative;
left: auto;
top: auto;
z-index: 1;
display: inline-block;
width: auto;
height: auto;
margin-left: 0.35em;
vertical-align: middle;
white-space: nowrap;
}
.dsb-toolbar-caption .dsb-toggle {
display: inline-block;
margin: 0;
vertical-align: middle;
opacity: 0.55;
transition: opacity 0.12s ease;
}
.dsb-toolbar-caption .dsb-toggle:focus,
.dsb-toolbar-caption.dsb-revealed .dsb-toggle {
opacity: 1;
}
/* Beside the picture rather than below it: the thumbnails sit above the definitions, so a panel that
cleared them dropped past every float and took the whole definition list down with it. */
.dsb-panel-after-image {
display: flow-root;
}
.dsb-toolbar-heading {
left: auto;
top: auto;
}
.dsb-toggle {
display: block;
margin-top: 5px; /* sits on the "1." marker; a pencil reads low, so it is lifted off centre */
width: 13px;
height: 13px;
background-image: url(//upload.wikimedia.org/wikipedia/commons/d/d3/InterlanguageLinks-Asset-Pencil.gif);
background-repeat: no-repeat;
background-position: center;
background-size: contain;
text-decoration: none;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease;
}
.dsb-toggle:hover,
.dsb-toggle:focus {
background-image: url(//upload.wikimedia.org/wikipedia/commons/d/d1/InterlanguageLinks-Asset-Pencil-Hover.gif);
}
/* Same pencil on a "====" or "===" heading: no list gutter to sit in, so it stays inline after the text. */
.dsb-toolbar-heading {
position: static;
display: inline-block;
width: auto;
height: auto;
margin-left: 0;
vertical-align: middle;
}
.dsb-toolbar-heading .dsb-toggle {
display: inline-block;
margin: 0 0 0 6px;
vertical-align: middle;
/* Hidden until the heading is hovered, like the per-sense pencil: a page with several subsections
otherwise carries an icon on every heading, all competing for the eye at once. */
opacity: 0;
}
/* Revealed by hovering anywhere on the heading, by keyboard focus, and while its own panel is open
(dsb-revealed, set in dsb.js) so it never disappears from under the pointer mid-edit. */
.dsb-toolbar-heading .dsb-toggle:focus,
.dsb-toolbar-heading.dsb-revealed .dsb-toggle {
opacity: 1;
}
/* Reveal rules, in ascending specificity so a later one never loses to an earlier one. */
.dsb-toggle:focus {
opacity: 1;
}
/* Touch has no hover, so a toolbar marks itself revealed on tap instead. A heading's pencil is revealed
by a tap on the heading (see revealHeadingToolbarOnTap in dsb.js), so a page of subsections is not a
column of icons before anything has been asked for. */
@media (hover: none), (pointer: coarse) {
.dsb-toolbar.dsb-revealed .dsb-toggle {
opacity: 1;
}
}
/* The glyph stays 13px -- the asset is only 10px, so scaling it up just blurs it. The tap target grows
invisibly instead, to the 44px minimum. */
@media (max-width: 720px), (pointer: coarse) {
.dsb-toggle {
position: relative;
}
.dsb-toggle::after {
content: "";
position: absolute;
inset: -10px;
}
}
/* Hover reveals only where there is a real pointer. On iOS Safari a tap first plays :hover, and when that
makes something visible the tap is spent on it and the link underneath needs a second tap. Touch gets
the pencils from dsb-revealed instead. */
@media (hover: hover) {
li:hover .dsb-toolbar .dsb-toggle,
.dsb-has-example-toolbar:hover .dsb-example-toolbar .dsb-toggle,
.mw-heading:hover .dsb-toolbar-heading .dsb-toggle,
h2:hover .dsb-toolbar-heading .dsb-toggle,
h3:hover .dsb-toolbar-heading .dsb-toggle,
h4:hover .dsb-toolbar-heading .dsb-toggle,
figure:hover .dsb-toolbar-caption .dsb-toggle,
.thumb:hover .dsb-toolbar-caption .dsb-toggle {
opacity: 1;
}
}
/* The pencil on each example and quotation: at the end of its last line, shown while that example is
hovered. */
.dsb-example-toolbar {
display: inline-block;
margin-left: 6px;
vertical-align: middle;
}
.dsb-example-toolbar .dsb-toggle {
display: inline-block;
margin: 0;
opacity: 0;
transition: opacity 0.12s ease;
}
.dsb-example-toolbar .dsb-toggle:focus,
.dsb-example-toolbar.dsb-revealed .dsb-toggle {
opacity: 1;
}
/* ── Built on the view path ───────────────────────────────────────────
dsb.js creates these before wikt.dsb-panels is fetched, so their styles have to travel with it rather
than with the panels: the "+ Beşek lê zêde bike" link is visible from the start, and the panel
containers are on screen showing "Tê barkirin…" while the module is still on its way. Everything
inside the panels stays in wikt.dsb-panels.css -- that markup does not exist until it has loaded. */
/* Matched to the add-examples gadget's own "Nimûneyeke bikaranînê lê zêde bike" link: a quiet subtle-grey
secondary action at the page's own font and size, not an accent-coloured one. Deliberately no --wikt-sans,
so it inherits the page font the way that link does. */
.dsb-add-subsection {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 8px 0;
font-size: inherit;
font-weight: 400;
color: var( --color-subtle, #54595d );
text-decoration: none;
border-bottom: 0;
cursor: pointer;
}
.dsb-add-subsection:hover,
.dsb-add-subsection:focus {
color: var( --color-base, #202122 );
text-decoration: underline;
}
.dsb-subsection-panel,
.dsb-convert-pos-panel {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
box-sizing: border-box;
max-width: 520px;
margin: 6px 0 14px;
padding: 14px 16px;
font-family: var(--wikt-sans);
font-size: 13px;
color: var(--wikt-ink-2);
background: var(--wikt-card-sunken);
border: 1px solid var(--wikt-card-edge);
border-radius: var(--wikt-r-card);
}
/* Mobile: the containers above lose the desktop width cap. */
.skin-minerva .dsb-quick-row,
.skin-minerva .dsb-subsection-panel,
.skin-minerva .dsb-convert-pos-panel {
max-width: 100%;
}
/* </nowiki> */
i21vicqk3axx1pgm4sf0paexandbq7v
MediaWiki:Gadget-wikt.start-wizard.js
8
1359175
6531672
6529979
2026-09-15T17:58:48Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none; fill button in the wizard's pill style
6531672
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.start-wizard.js]]
*/
// Li ser [[vi:MediaWiki:Gadget-start.js]] hatiye refaktorkirin.
// implicit dependencies: StorageUtils (ObjectStorage), Gadget-citoid-helper.js (wikt.citoid)
/* global ObjectStorage, wikt */
$(function () {
if (window.location.href.includes("editintro=User:Wikihez/creation/intro")) {
return; // Do not run the script wextê WF:Bilez dişuxile
}
// The same gate DefSideBoxes, add-examples and this wizard's own launcher use, so a page one of them
// works on is never a page this one quietly refuses to open on.
var ns = mw.config.get("wgNamespaceNumber");
if (!require("ext.gadget.wikt.core").sections.isEntryPage()) {
return;
}
// Reconstructed entries (ns 108) are unattested proto-forms: there is no recorded usage to give an
// example of or quote, and no dialects to mark, so the sense block leaves out Nimûne, Jêgirtin and
// Dever/Şîwe there.
var isReconstruction = ns === 108;
var addLangMode = mw.util.getParamValue("wiktkuaddlang") === "1";
// ── Tercîh (user settings) panel stored via mw.storage (localStorage), globally
var WIKTKU_PREFS_KEY = "wiktku_start_prefs";
var WIKTKU_PREFS_DEFAULTS = {
// [] = show the full language list in the "ziman" step; otherwise
// only these codes are shown there (data-main.json codes, not names).
langCodes: [],
// true = never auto-open on a new/empty page — only via the
// "+Ziman/beşeke din" portlet link.
onlyOnClick: false,
// "auto" | "p-cactions" | "p-tb" | "p-views" — where the
// "+Ziman/beşeke din" portlet link is added.
portlet: "auto",
// true = suppress routine/informational mw.notify toasts (page
// loading, cache auto-restored, cache saved/cleared, "tê şandin…").
// Errors and direct results of an explicit action (like saving
// Tercîh itself) always show regardless of this setting.
quiet: false
};
// Defaults and storage live in wikt.core/prefs.js, because the launcher reads them on every page view.
var wiktkuPrefsApi = require("ext.gadget.wikt.core").prefs;
function wiktkuLoadPrefs() { return wiktkuPrefsApi.load(); }
function wiktkuSavePrefs(prefs) { return wiktkuPrefsApi.save(prefs); }
var wiktkuPrefs = wiktkuLoadPrefs();
// Routine/informational notices go through here so "quiet mode" can
// suppress them; errors and explicit-action confirmations (like the
// Tercîh save toast) call mw.notify directly instead, and always show.
function wiktkuNotify(message, options) {
if (wiktkuPrefs.quiet) return;
mw.notify(message, options);
}
// Clears every draft cache and resize height this script writes, for all pages. Tercîh itself is
// left alone: it is the setting being edited when this button is reached.
// The state it touches is assigned by build(), which has always run by the time this is callable.
function wiktkuClearAllScriptCache() {
// Cancel the pending autosave, or it would rewrite the cache from the still-filled form.
if (autosaveDebounceTimer) {
clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = null;
}
formDirty = false;
$("#wiktku-last-saved").text("");
// Keeps formCache's in-memory copy in step with the localStorage sweep below.
try { formCache.Set({}); } catch (e) { /* storage unavailable — the raw sweep below still runs */ }
if (!window.localStorage) return;
var toRemove = [];
try {
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (!key) continue;
// ObjectStorage wraps the draft key as "enwikt/ObjectStorage/<name>?v=<n>&Type=…", so it
// is matched as a substring; the mw.storage keys below are plain prefixes.
if (key.indexOf("wiktku_start_draft_") !== -1 || key.indexOf("wiktku_resize_height_") === 0 ||
key.indexOf(WIKTKU_SUGGESTIONS_KEY_PREFIX) === 0) {
toRemove.push(key);
}
}
} catch (e) { return; }
$.each(toRemove, function (_, key) {
try { localStorage.removeItem(key); } catch (e) { /* skip this key, keep clearing the rest */ }
});
}
// Lazily built the first time it's opened; reused after that (see
// wiktkuOpenPrefsDialog below, defined once we know OOUI is available).
var wiktkuPrefsWindowManager = null;
var wiktkuPrefsDialog = null;
function wiktkuOpenPrefsDialog() {
mw.loader.using(["oojs-ui-core", "oojs-ui-windows"], function () {
function WiktkuPrefsDialog(config) {
WiktkuPrefsDialog.super.call(this, config);
}
if (!wiktkuPrefsDialog) {
OO.inheritClass(WiktkuPrefsDialog, OO.ui.ProcessDialog);
WiktkuPrefsDialog.static.name = "wiktkuPrefsDialog";
WiktkuPrefsDialog.static.title = "Tercîhên min";
WiktkuPrefsDialog.static.actions = [
{ action: "save", label: "Qeyd bike", flags: ["primary", "progressive"] },
{ action: "cancel", label: wiktkuUtil.msg.cancel, flags: "safe" },
{ action: "clearcache", label: "Hemû cache paqij bike", flags: "destructive" }
];
WiktkuPrefsDialog.prototype.initialize = function () {
WiktkuPrefsDialog.super.prototype.initialize.call(this);
this.langCodesInput = new OO.ui.TextInputWidget({
placeholder: "ku, zza, ckb (vala bihêle ji bo hemû zimanan)"
});
this.onlyOnClickCheckbox = new OO.ui.CheckboxInputWidget({});
this.quietCheckbox = new OO.ui.CheckboxInputWidget({});
this.portletDropdown = new OO.ui.DropdownInputWidget({
options: [
{ data: "auto", label: "Otomatîk (li gorî rûyê wîkiyê)" },
{ data: "p-cactions", label: "p-cactions - Li qismê Amûran" },
{ data: "left-navigation", label: "left-navigation - Li qismê Gotar-Gotûbêj" },
{ data: "p-tb", label: "p-tb - Li qismê Giştî" },
{ data: "p-views", label: "p-views - Li qismê Bixwîne" }
]
});
// Two named groups rather than one "Eyar" heap: which languages the wizard offers, then
// how it behaves. The dialog title already says these are preferences.
var langSet = new OO.ui.FieldsetLayout({ label: "Ziman" });
langSet.addItems([
new OO.ui.FieldLayout(this.langCodesInput, {
label: "Kodên zimanan ên ku tê xuyakirin li qismê \"ziman\"",
align: "top",
help: "Kodan bi virgulê (,) ji hev veqetîne. Vala bihêle da ku hemû ziman werin nîşandan.",
helpInline: true
})
]);
var behaviourSet = new OO.ui.FieldsetLayout({ label: "Wextê xebitînê" });
behaviourSet.addItems([
new OO.ui.FieldLayout(this.onlyOnClickCheckbox, {
label: "Tenê veke wextê \"+Ziman/beşeke din\" were tikandin",
align: "inline",
help: "Eger were hilbijartin, ev amûr êdî otomatîk li ser rûpelên nû dest pê nake — divê meriv her carê pêl \"+Ziman/beşeke din\" bike ku bişixule.",
helpInline: true
}),
new OO.ui.FieldLayout(this.portletDropdown, {
label: "Cihê lînka \"+Ziman/beşeke din\"",
align: "top",
help: "\"Otomatîk\" li gorî rûyê wîkiyê hildibijêre.",
helpInline: true
}),
new OO.ui.FieldLayout(this.quietCheckbox, {
label: "Moda bêdeng (kêmtir agahdarî nîşan bide)",
align: "inline",
help: "Agahdariyên normal nîşan nede. Xeta her tim tên xuyakirin.",
helpInline: true
})
]);
this.$element.addClass("wiktku-prefs-dialog");
this.$body.addClass("wiktku-prefs-dialog-body")
.append(langSet.$element, behaviourSet.$element);
};
WiktkuPrefsDialog.prototype.getSetupProcess = function (data) {
return WiktkuPrefsDialog.super.prototype.getSetupProcess.call(this, data)
.next(function () {
this.langCodesInput.setValue(wiktkuPrefs.langCodes.join(", "));
this.onlyOnClickCheckbox.setSelected(!!wiktkuPrefs.onlyOnClick);
this.portletDropdown.setValue(wiktkuPrefs.portlet || "auto");
this.quietCheckbox.setSelected(!!wiktkuPrefs.quiet);
}, this);
};
WiktkuPrefsDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === "save") {
return new OO.ui.Process(function () {
var codes = dialog.langCodesInput.getValue()
.split(/[,\s]+/)
.map(function (c) { return c.trim().toLowerCase(); })
.filter(function (c) { return !!c; });
wiktkuPrefs = {
langCodes: codes,
onlyOnClick: dialog.onlyOnClickCheckbox.isSelected(),
portlet: dialog.portletDropdown.getValue() || "auto",
quiet: dialog.quietCheckbox.isSelected()
};
wiktkuSavePrefs(wiktkuPrefs);
mw.notify(
"Tercîh hatin qeydkirin. Rûpelê ji nû ve bar bike (F5).",
{ type: "success", tag: "wiktku-prefs-save" }
);
dialog.close({ action: "save" });
});
}
if (action === "cancel") {
return new OO.ui.Process(function () {
dialog.close({ action: "cancel" });
});
}
if (action === "clearcache") {
return new OO.ui.Process(function () {
if (!window.confirm(
"Tu dixwazî hemû cache-ya vê amûrê ji her rûpelê paqij bikî " +
"(danûstandinên qeydkirî yên hemû rûpelan, bilindahiyên qutiyan)? " +
"Tercîhên li jor nayên jêbirin. Ev tişt nayê vegerandin."
)) return;
wiktkuClearAllScriptCache();
mw.notify("Hemû cache hate paqijkirin.", { type: "success", tag: "wiktku-cache-clear-all" });
dialog.close({ action: "clearcache" });
});
}
return WiktkuPrefsDialog.super.prototype.getActionProcess.call(this, action);
};
wiktkuPrefsWindowManager = new OO.ui.WindowManager();
$(document.body).append(wiktkuPrefsWindowManager.$element);
wiktkuPrefsDialog = new WiktkuPrefsDialog();
wiktkuPrefsWindowManager.addWindows([wiktkuPrefsDialog]);
}
wiktkuPrefsWindowManager.openWindow(wiktkuPrefsDialog);
});
}
// The launcher gadget adds the portlet link and decides whether this page wants the wizard before
// loading it at all. Both are repeated here only when this file was loaded on its own.
var wiktkuLaunched = !!(window.wikt && window.wikt.start && window.wikt.start.launched);
if (!wiktkuLaunched && !document.getElementById("ca-wiktku-addlang")) {
wiktkuPrefsApi.addLauncherLink(wiktkuPrefs);
// If "onlyOnClick" is set, the wizard should ONLY ever open via the
// "+Ziman/beşeke din" link — never automatically
var wiktkuNewPageDetected = $(".mw-newarticletext, .mw-newarticletextanon").length > 0;
if (wiktkuPrefs.onlyOnClick ? !addLangMode : (!addLangMode && !wiktkuNewPageDetected)) {
return;
}
}
// ── Shared helpers ───────────────────────────────────────────────────
// Generic text/UI utilities live in their own package file now — see the
// header of util.js for why. Aliased to bare names here so the ~400 call
// sites below read exactly as they did when these were local functions.
var wiktkuEdit = require("ext.gadget.wikt.edit");
var wiktkuUtil = wiktkuEdit.util;
var wiktkuWikitext = wiktkuEdit.wt;
var wiktkuLangs = wiktkuEdit.langs;
// Citations (jêder-*, Kategorî:Çavkanî, Citoid) all go through one module -- see wikt.cite.js.
var wiktkuCite = require("ext.gadget.wikt.cite");
var wiktkuPosparams = wiktkuEdit.posparams;
// Assigned once data-main.json is in: re-merges the language choices, and is called a second time if the
// full language list has to be fetched. Declared here so the step renderer can reach it.
var wiktkuMergeLanguages = null;
var capitalize = wiktkuUtil.capitalize;
var decapitalize = wiktkuUtil.decapitalize;
var escapeRegExp = wiktkuUtil.escapeRegExp;
var stripWikilinks = wiktkuUtil.stripWikilinks;
var splitTercimeIzah = wiktkuUtil.splitTercimeIzah;
var insertAtCursor = wiktkuUtil.insertAtCursor;
var wrapNativeSelect = wiktkuUtil.wrapNativeSelect;
var initSelects = wiktkuUtil.initSelects;
var wiktkuInitManualResize = wiktkuUtil.initManualResize;
var langHeaderSource = wiktkuWikitext.langHeaderSource;
// Inserts a line-style template snippet ("* {{…}}") at the cursor,
// prefixing it with a newline unless it's going at the very start of the
// box or right after one already — otherwise picking a second template
// from the pron dropdown right after the first lands them both on the
// same line: "* {{ku-IPA}}* {{ku-kîte}}".
function insertLineSnippet(el, snippet) {
var before = el.value.slice(0, el.selectionStart);
if (before && !/\n$/.test(before)) snippet = "\n" + snippet;
insertAtCursor(el, snippet);
}
// One delete button per tab bar, acting on the tab in view. The trash glyph
// (oojs-ui.styles.icons-moderation) is fetched here rather than at startup: it is only needed once a
// second Mane or Beş exists. The button is built immediately and the icon appears when the CSS lands.
function makeTrashButton(title) {
mw.loader.using("oojs-ui.styles.icons-moderation");
return new OO.ui.ButtonWidget({
icon: "trash",
flags: ["destructive"],
framed: false,
invisibleLabel: true,
label: title,
title: title,
classes: ["wiktku-trash-btn"]
});
}
function findLangHeader(text, code) {
var m = new RegExp("\n?" + langHeaderSource(escapeRegExp(code))).exec(text);
return m ? { index: m.index, match: m[0] } : null;
}
// The category's members come from wikt.cite, which also lists each template's redirects so it can be
// found by any of its names. Entries are {label, target}: label is what was typed/shown, target the
// canonical template to transclude.
var catSrcTemplates = null;
function fetchCatSrcTemplates() {
return wiktkuCite.fetchCategoryTemplates().then(function (list) {
catSrcTemplates = list;
return list;
});
}
var pageName = mw.config.get("wgPageName").replaceAll(/_/g, " ");
if (ns) pageName = pageName.replace(/.*\//, "");
var userLang = mw.config.get("wgUserLanguage");
var formCache = new ObjectStorage("wiktku_start_draft_" + mw.config.get("wgPageName"), 1);
var langsByCode = {};
// Looks up the display label for a language code, falling back to the
// code itself if it isn't known yet (e.g. langsByCode not loaded).
function getLangLabel(code) {
var entry = langsByCode[code];
return entry ? entry.label : code;
}
// The mane box holds a Kurdish definition for a Kurdish entry, and a direct translation for every
// other language -- the {{îzah}} box under it carries the explanation.
function manePlaceholder(code) {
return code && code !== "ku" ? wiktkuUtil.msg.tercimePrompt : "Maneya vê kelîmeyê binivîse…";
}
// A Kurmancî example gets an explanation, any other a translation.
function nimuneTranslationPrompt(code) {
return code === "ku" ? "Îzaheke nimûneyê bi kurmancî…" : "Tercimeya nimûneyê bi kurmancî…";
}
// Populated from data-main.json at runtime.
var metadata = {};
var ZARAVA_LANG_CODES = ["ckb", "sdh", "lki", "hac", "zza"];
// Converts a {code: label} object (langChoices, posChoices)
// into the {code, label[, sortKey]} array the rest of the script uses.
// isLang: true adds a sortKey and shows "(code)" after the label —
// language choices only.
function choicesFromObject(obj, isLang) {
var result = [];
$.each(obj || {}, function (code, label) {
var choice = { code: code, label: label };
if (isLang) choice.sortKey = label;
result.push(choice);
});
return result;
}
function getZaravaKeys(choices) {
var byCode = {};
$.each(choices || [], function (_, c) { byCode[c.code] = c; });
var result = [];
$.each(ZARAVA_LANG_CODES, function (_, code) {
if (byCode[code]) result.push(byCode[code]);
});
return result;
}
function needsTransliteration(langCode) {
var meta = metadata[langCode];
if (!meta || !meta.sc) return false;
var scripts = Array.isArray(meta.sc) ? meta.sc : [meta.sc];
return scripts.length > 0 && scripts[0] !== "Latn";
}
// metadata[lang].sc is either a single script string or an array of
// several — only the latter means the language is actually written in
// more than one script.
function langHasMultipleScripts(langCode) {
var meta = metadata[langCode];
return !!(meta && Array.isArray(meta.sc) && meta.sc.length > 1);
}
// Kodên cinsiyetê yên di metadata.g de — ji data-main.json tê dagirtin
var GENDER_BASE_LABELS = {};
// Number codes (y, d, p, p+, tp, j, j+, j/nj, nj, nj+, hj!, hj?) from
// data-main.json's "numberLabels"
var NUMBER_LABELS = {};
var SENSE_GROUPS = [];
var WORDLIST_TEMPLATE = "kol3"; // replaced by data-main.json's first wordlistTemplates entry
// The pills the Dever/Şîwe picker opens with; everything else comes from Modul:labels by typing.
var DEVER_QUICK_PICKS = [];
var LEKTUR_CHOICES = [];
var LEKTUR_FORM_CHOICES = [];
// Distinguishes the Jimar dropdown (j= param) from the Cîns dropdown
// (c= param): a code that's a key in NUMBER_LABELS is a number code,
// everything else is a gender code. Compound codes like "m-p"/"n-p" are
// gender+plural pairs and correctly stay on the gender side.
function isNumberCode(code) {
return Object.prototype.hasOwnProperty.call(NUMBER_LABELS, code);
}
// Builds the Jimar dropdown's <option> text for one code: "j — jimarbar".
function numberOptionLabel(code) {
var def = NUMBER_LABELS[code];
if (!def) return code;
var text = def.display;
return text ? (code + " — " + text) : code;
}
// Sub-lists that only make sense for a plain (non-şablon) sense — hidden
// when a şablon (e.g. {{guharto|…}}) is chosen for that sense
function genderLabel(code) {
if (code === "p") return "Pirjimar";
var pluralMatch = code.match(/^(.+)-p$/);
if (pluralMatch && GENDER_BASE_LABELS[pluralMatch[1]]) {
return GENDER_BASE_LABELS[pluralMatch[1]] + " (pirjimar)";
}
return GENDER_BASE_LABELS[code] || code;
}
// ── Template explanations ────────────────────────────────────────────
// Every place a template can be picked shows the same row: a status
// dot, the name, a link to the template page, and an ⓘ. This is the ⓘ.
//
// Text comes from the template's own TemplateData where it has any, so
// the explanation stays right without anyone maintaining a second copy
// by hand — with the desc already in data-main.json as the fallback for
// templates that have none.
// {desc, params} for the ⓘ popup, from wikt.edit -- the fetch, the shaping and the cache are all
// there, shared with wikt.cite and DefSideBoxes, so a template is asked for once however many pickers
// show it. Null for a template with no TemplateData, and that answer is kept too, so clicking ⓘ on
// an undocumented template twice is one request.
var fetchTemplateData = wiktkuEdit.wt.templateDoc;
var prefetchTemplateData = wiktkuEdit.wt.templateDocBatch;
/** Rewrites each tagged <option> in `sel` as "name — description", using
* TemplateData. Only fills rows that have no description yet, so the
* hand-written ones in data-main.json (which carry usage examples the API
* does not) are left exactly as they are. */
function applyTplDescriptions(sel) {
if (!sel || !sel.length) return;
var changed = false;
sel.find("option[data-tpl]").each(function () {
var opt = $(this);
var name = opt.attr("data-tpl");
if (opt.text().indexOf(" — ") !== -1) return; // already described
var td = wiktkuEdit.wt.templateDocNow(name);
var desc = (td && td.desc) || localTplDesc(name);
if (!desc) return;
opt.text(name + " — " + wiktkuEdit.wt.shortDesc(desc));
changed = true;
});
// The picker caches option text for searching, so it has to re-read.
if (changed) sel.trigger("wiktku:refresh");
}
/** Description from data-main.json's "desc", for templates with no TemplateData on the wiki. Works for any
* pool: pronTpls, etymTpls, tplDefs, jederDefs. */
function localTplDesc(name) {
var pools = [steps.pron.pronTpls, steps.etym.etymTpls, steps.dfn.tplDefs, steps.dfn.jederDefs];
var found = "";
$.each(pools, function (_, pool) {
if (pool && pool[name] && pool[name].desc) { found = pool[name].desc; return false; }
});
return found;
}
// Under the ⓘ it was opened from, kept inside the viewport, and flipped above it when there is no
// room below. Narrow screens get the width they have rather than a fixed card off the edge.
//
// box is where that ⓘ was when it was clicked. The row it sits in is closed by then, so it can no
// longer be measured; the last known box is kept for the re-placements after the text arrives.
function positionTplInfo(panel, anchor, box) {
var el = $(anchor).get(0);
var rect = box || (el && el.getBoundingClientRect ? el.getBoundingClientRect() : null);
if (rect && !rect.width && !rect.height) {
rect = null;
}
if (rect) {
panel.data("wiktkuAnchorBox", rect);
} else {
rect = panel.data("wiktkuAnchorBox") || null;
}
var margin = 8;
var arrow = 8;
var width = Math.min(360, $(window).width() - margin * 2);
panel.css({ width: width + "px", top: "0px", left: "0px" });
var height = panel.outerHeight();
// Above the dropdown, pointing down at it, the way MediaWiki's own filter tips sit; below it only
// when there is no room above.
var above = !rect || rect.top - height - arrow >= margin;
var top = !rect ? margin : (above ? rect.top - height - arrow : rect.bottom + arrow);
var left = rect ? Math.min(Math.max(margin, rect.left), $(window).width() - width - margin) : margin;
// The arrow points at the start of the box it explains, kept clear of the tip's corners.
var arrowX = rect ? Math.min(Math.max(16, rect.left + 24 - left), width - 16) : 16;
panel.toggleClass("wiktku-tpl-info-above", above).toggleClass("wiktku-tpl-info-below", !above);
panel.css({ top: top + "px", left: left + "px" });
panel.get(0).style.setProperty("--wiktku-arrow-x", arrowX + "px");
}
/** Opens the explanation for `name` as a tip above the dropdown it was asked from. */
function showTemplateInfo(name, anchorWrap, anchorEl, anchorBox) {
$(".wiktku-tpl-info").remove();
var panel = $("<div class='wiktku-tpl-info'></div>");
panel.append($("<div class='wiktku-tpl-info-name'></div>").text("{{" + name + "}}"));
var body = $("<div class='wiktku-tpl-info-desc'></div>").text(wiktkuUtil.msg.loading);
panel.append(body);
var params = $("<div class='wiktku-tpl-info-params'></div>");
panel.append(params);
panel.append($("<a class='wiktku-tpl-info-link' target='_blank' rel='noopener'></a>")
.attr("href", wiktkuUtil.tplPageUrl(name))
.text(wiktkuUtil.msg.openTemplatePage + " →"));
function closePanel() {
panel.remove();
$(document).off(".wiktkuTplInfo");
$(window).off(".wiktkuTplInfo");
}
var close = $("<button type='button' class='wiktku-tpl-info-close' title='Bigire'>✕</button>");
close.on("click", closePanel);
panel.prepend(close);
$(document).on("keydown.wiktkuTplInfo", function (e) {
if (e.which === 27) {
closePanel();
}
});
$(document).on("mousedown.wiktkuTplInfo", function (e) {
if (!$(e.target).closest(".wiktku-tpl-info").length) {
closePanel();
}
});
// A panel that stayed put while the page moved under it would point at nothing.
$(window).on("resize.wiktkuTplInfo scroll.wiktkuTplInfo", function () {
positionTplInfo(panel, anchorWrap);
});
// Over the form, not inside it: as a block in the flow it pushed every field below it down the
// page for as long as it was open, and the picker it explains moved with them.
$(document.body).append(panel);
positionTplInfo(panel, anchorWrap);
fetchTemplateData(name).then(function (td) {
var desc = (td && td.desc) || localTplDesc(name);
body.text(desc || "Ji bo vê şablonê îzahek nehatiye nivîsandin.");
if (td && td.params && td.params.length) {
params.append($("<div class='wiktku-tpl-info-params-title'></div>").text("Parametre:"));
var listEl = $("<ul></ul>");
$.each(td.params, function (_, p) {
var line = p.key + (p.label ? " — " + p.label : "") + (p.required ? " (mecbûrî)" : "");
listEl.append($("<li></li>").text(line));
});
params.append(listEl);
}
// The panel is as tall as its text, which only arrives now.
positionTplInfo(panel, anchorWrap);
});
}
/** Marks a picker's <option>s as template rows so the searchable widget
* renders them with the dot / 🔗 / ⓘ affordances, and wires the ⓘ
* handler for that picker. `nameOf` maps an <option> to a template
* name — the pickers store different things in value= (a snippet for
* the insert dropdowns, the bare name for the sense selector). */
function markTemplateOptions(sel, nameOf) {
if (!sel || !sel.length) return;
var names = [];
sel.find("option").each(function () {
var opt = $(this);
var name = nameOf(opt);
if (!name) return;
opt.attr("data-tpl", name);
// Deprecated/typo templates are the "special cases only" ones.
opt.attr("data-tpl-status", /kevnbûyî|şaşnivîs|kêmpeyda/i.test(opt.text()) ? "yellow" : "green");
names.push(name);
});
sel.data("wiktkuTplInfo", showTemplateInfo);
// Fill in the descriptions. Matters most for the Mane picker, whose
// entries in data-main.json are names with no desc at all — "rastnivîs",
// "ku-Latn", "guharto" on their own. One request covers every
// template in the picker, cached for the per-row ⓘ as well.
prefetchTemplateData(names).always(function () {
applyTplDescriptions(sel);
});
}
/** Pulls the template name out of a snippet like "* {{ku-IPA}}" or
* "{{ji|SOURCELANG|CODE|…}}". */
function tplNameFromSnippet(snippet) {
var m = /\{\{\s*([^|}\n]+)/.exec(snippet || "");
return m ? $.trim(m[1]) : "";
}
// True if a sense has content beyond mane worth keeping (etîket, word
// lists, nimûne, jêder) — so it's still emitted with {{mane?}} if empty.
function senseHasExtraContent(s) {
if (s.maneid) return true;
if (s.etiket && $.grep(s.etiket, function (e) { return $.trim(e) !== ""; }).length) return true;
if (s.kategori && $.grep(s.kategori, function (k) { return $.trim(k) !== ""; }).length) return true;
var anyWords = false;
$.each(wordGroups(), function (_, g) {
if ((s[g.field] || []).length) anyWords = true;
});
if (anyWords || (s.dever && s.dever.length)) return true;
if (s.nimune && $.grep(s.nimune, function (n) {
return (typeof n === "object" ? n.text : n) !== "";
}).length > 0) return true;
if (senseHasJeder(s)) return true;
return false;
}
// A technically-"complete" entry (Ziman + Cureyê rêzimanî filled) with
// nothing in Mane, Bi zaravayên din or Werger is still meaningfully
// empty, so this is checked separately at submit-click time with its
// own message.
// Converts literal line breaks typed into a nimûne/jêgirtin box into
// wikitext <br> tags,
function nlToBr(text) {
return text ? wiktkuCite.nlToBr(text) : text;
}
// Line breaks in a field save as "<br>"; this marks them so that is visible while typing.
var highlightLinebreak = wiktkuUtil.makeLinebreakHighlighter({ prefix: "wiktku" });
// Each nimûne item can be { text, tr, t }: text, transliteration, translation.
function formatNimune(nimunes, lang) {
var hasNimune = nimunes && $.grep(nimunes, function (n) {
return (typeof n === "object" ? n.text : n) !== "";
}).length > 0;
if (!hasNimune) {
// A sense with no example is left as it is, with no "{{nimûne?}}" placeholder.
return "";
}
return $.map(nimunes, function (n) {
var text = typeof n === "object" ? n.text : n;
if (!text) return undefined;
// The builder DefSideBoxes and add-examples use: the translation in slot 3, rêz=1 for a short pair.
return "\n" + wiktkuCite.buildCitation({
mode: "plain",
marker: "#:",
language: lang,
title: pageName,
quote: text,
translation: typeof n === "object" ? n.t : "",
transcription: typeof n === "object" ? n.tr : ""
});
}).join("");
}
// Formats jêder (citation) entries into "#* {{jêder…|…}}" lines; skips
// entries with no quote text. Falls back to the generic {{jêder}}
// template when no specific template was chosen.
function formatJeder(jederList, lang) {
if (!jederList || !jederList.length) return "";
return $.map(jederList, function (jeder) {
if (!jeder || !jeder.quote) return undefined;
var mode = jeder.raw ? "raw"
: jeder.jederTpl ? "jeder"
: jeder.jederCatTpl ? "category"
: "plain";
return "\n" + wiktkuCite.buildCitation({
mode: mode,
marker: "#*",
language: lang,
template: jeder.jederTpl || jeder.jederCatTpl,
fields: jeder.jederTpl ? (jeder.jêderFields || {}) : (jeder.jederCatFields || {}),
langParamKey: jeder.jederCatLangParam,
quote: jeder.quote,
transcription: jeder.quoteTr,
translation: jeder.quoteT,
source: jeder.jederManual || (mode === "plain" && !jeder.url ? "çavkanî?" : ""),
sourceURL: jeder.url
});
}).join("");
}
// True if a sense has at least one jêder entry with quote text.
function senseHasJeder(sense) {
return !!(sense.jeder && $.grep(sense.jeder, function (j) {
return j && j.quote;
}).length > 0);
}
// Builds "{{tplName|lang|part1|part2|…}}" from a sense field (kategori,
// etiket), wrapped inline (" {{…}} ") or on its own line ("\n#: {{…}}").
function buildSenseTpl(sense, field, lang, tplName) {
if (!sense[field] || !sense[field].length) return "";
var parts = sense[field].map(function (p) { return $.trim(p); })
.filter(function (p) { return p !== ""; });
if (!parts.length) return "";
var call = "{{" + tplName + "|" + lang + "|" + parts.join("|") + "}}";
return call
}
// Dever/Şîwe values only ever get their own {{dever|…}} call for ku —
// no |lang| param, since the template is Kurdish-dialect-specific. For
// every other language there's no dever template at all; see
// buildEtiketTpl() below, which folds them into {{e|}} instead.
function buildDeverTpl(sense, lang) {
if (lang !== "ku" || !sense.dever || !sense.dever.length) return "";
var parts = sense.dever.map(function (p) { return $.trim(p); })
.filter(function (p) { return p !== ""; });
if (!parts.length) return "";
return "{{dever|" + parts.join("|") + "}}";
}
// Builds the {{e|lang|…}} etîket call. For non-ku senses, Dever/Şîwe
// values are appended after the regular etîket values instead of going
// into their own {{dever|}} call (see buildDeverTpl() above).
function buildEtiketTpl(sense, lang) {
var etiket = (sense.etiket || []).slice();
if (lang !== "ku" && sense.dever && sense.dever.length) {
etiket = etiket.concat(sense.dever);
}
return buildSenseTpl({ etiket: etiket }, "etiket", lang, "e");
}
/**
* Assembles the nimûne + jêder wikitext block for a sense. Shared by
* both the template-sense and normal-mane-sense branches below.
*/
function buildCite(sense, lang) {
return formatNimune(sense.nimune, lang) +
formatJeder(sense.jeder, lang);
}
// ── Inline modifier system ───────────────────────────────────────────────
// "{{maneid|lang|x}}", the anchor id a sense can be linked by. First of the leading calls, which is
// data-main.json's leadingTplOrder.
function buildManeidTpl(sense, lang) {
var id = $.trim(sense.maneid || "");
return id ? "{{maneid|" + lang + "|" + id + "}}" : "";
}
// Quick-pick suggestions for the Dever/Şîwe, Etîket and Kategorî chip pickers on each sense, taken
// from [[Modul:labels]] rather than kept by hand here. Free text is still always accepted; this only
// saves typing, and keeps the lists honest as the module changes.
//
// Fetched on the first time one of the three panels is opened, never on load: the wizard opens on
// pages that never touch these fields. Once it lands, the open panel re-renders itself.
var wiktkuLabelSets = null;
var wiktkuLabelsPending = null;
var wiktkuLabelListeners = [];
function wiktkuEnsureLabels() {
if (wiktkuLabelSets || wiktkuLabelsPending) return;
wiktkuLabelsPending = wiktkuEdit.labels.load().then(function (sets) {
wiktkuLabelSets = sets;
wiktkuLabelsPending = null;
$.each(wiktkuLabelListeners, function (_, fn) { fn(); });
}, function () {
wiktkuLabelsPending = null;
});
}
// Which list each picker shows. Dever/Şîwe is the regions of the language being edited; Etîket adds
// the qualifiers and subject labels on top; Kategorî is the subject labels alone.
function wiktkuLabelSuggestions(which) {
if (!wiktkuLabelSets) return [];
var lang = (getValues().lang || "ku");
if (which === "dever") return wiktkuEdit.labels.regionalFor(wiktkuLabelSets, lang);
if (which === "etiket") return wiktkuEdit.labels.etiketFor(wiktkuLabelSets, lang);
return wiktkuEdit.labels.kategoriNames(wiktkuLabelSets);
}
// Lower-cased alias -> canonical label, so typing a name the module lists as an alias still finds it.
// Built once per label load rather than per keystroke.
var wiktkuAliasCache = null;
function wiktkuLabelAliases() {
if (!wiktkuLabelSets) return {};
if (!wiktkuAliasCache) {
wiktkuAliasCache = wiktkuEdit.labels.aliasIndex(wiktkuLabelSets);
}
return wiktkuAliasCache;
}
// Appends a ⊕ button to a word input <li>. Clicking it shows a compact
// modifier picker; selecting one prompts for the value and appends
// <modifier:value> to the textarea's current content.
// Sub-groups (inside a sense/definition block) that get the same ⊕
// modifier button as makeWordListStep fields.
/**
* The word lists (Hevmane, Dijmane, Formên alternatîv, Termên koordîne, Jornav, Jêrnav), taken from
* data-main.json's senseGroups so the wizard, DefSideBoxes and the wikitext writer all name them once.
* That array's own order is the order the "#:" lines are written to the page in; "order" is the
* order they are shown in, which is not the same. cls is how each one's box is found in the DOM.
*/
function wordGroups() {
return SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return { cls: "wiktku-dfn-" + g.field, label: g.label, field: g.field, izah: g.hint || "" };
});
}
/** True for the six word lists, which get the ⊕-modifier row; Nimûne and Jêgirtin do not. */
function isWordGroupCls(cls) {
var found = false;
$.each(SENSE_GROUPS, function (_, g) {
if ("wiktku-dfn-" + g.field === cls) found = true;
});
return found;
}
/**
* Wraps a textarea in a <li> with a ⊕ modifier button and returns the <li>.
*
* `ta` stays hidden and holds the raw "word<key:value>…" text every reader and writer expects. What
* is typed into is a plain word box, with one pill per modifier above it; clicking a pill edits its
* value, ⊕ adds another.
*/
function makeModifierLi(ta) {
var narrow = (ta.attr("class") || "").split(/\s+/).indexOf("wiktku-start-narrow") !== -1;
// Not ta's own classes: code elsewhere counts elements by those, so a second matching textarea
// would skew the counts. wiktku-word-input carries the same look.
var wordInput = $("<textarea></textarea>")
.attr("rows", ta.attr("rows") || 1)
.addClass("wiktku-word-input" + (narrow ? " wiktku-start-narrow" : ""))
.attr("placeholder", ta.attr("placeholder") || "");
ta.hide();
// Placeholders on the (hidden) canonical field are set by shared code that has no reason to know
// wordInput exists -- mirror them instead of touching every call site.
if (window.MutationObserver) {
new MutationObserver(function () {
wordInput.attr("placeholder", ta.attr("placeholder") || "");
}).observe(ta[0], { attributes: true, attributeFilter: ["placeholder"] });
}
// ta stays the source of truth: the row writes back to it, and the caller's growth listener is
// bound to ta's "input" event.
var row = wiktkuUtil.makeModifierRow({
prefix: "wiktku-",
wordInput: wordInput,
modTitle: "Modifiyerê lê zêde bike",
removeTitle: wiktkuUtil.msg.removeWord,
removeModTitle: wiktkuUtil.msg.removeModifier,
onChange: function (raw) {
if (raw === null) return; // the row removed itself; ta goes with it
ta.val(raw);
ta.trigger("input");
},
// "bixe qutiyan" puts each comma-separated word in a row of its own. The list grows off the
// hidden field's own "input" event, so filling the trailing row is what makes the next one
// appear; the caret ends up in the empty row at the bottom. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way -- a
// comma left inside a parameter would not be.
onSplit: function (rest) {
// A tick later, not now: the row writes its own value to the hidden field after this
// call returns, and it is that field's "input" that grows the list -- so the empty row
// to put the next word in does not exist yet.
setTimeout(function () {
var $list = row.$li.closest("ol, ul");
rest.forEach(function (raw) {
if (!raw) return;
var setRaw = $list.children("li").last().data("setRawValue");
if (setRaw) setRaw(raw);
});
$list.children("li").last().find(".wiktku-word-input").focus();
}, 0);
}
});
row.setRaw(ta.val());
var li = row.$li;
// restoreSubList's "fill an existing row" path sets a raw "word<key:value>" value here.
li.data("setRawValue", function (raw) {
row.setRaw(raw);
ta.val(row.getRaw());
ta.trigger("input");
});
li.prepend(ta);
return li;
}
/**
* A "chip + toggle" field: values render as removable chips, and a "+ <label>" toggle opens a panel
* with quick-pick pills plus free text. panel is returned apart from wrap so callers can group all
* toggles in one row and all panels below, keeping toggles from shifting when one opens.
*
* Pills carry a × as well: hiding a built-in one, or typing a new value, is remembered per wrapCls
* in mw.storage.
*
* opts: { wrapCls, toggleLabel, hint, suggestions (array or function), placeholder, onOpen }
* Returns { wrap, panel, addChip, values, setOpenDefault, close }.
*/
var WIKTKU_SUGGESTIONS_KEY_PREFIX = "wiktku_suggestions_";
function loadSuggestionOverrides(wrapCls) {
if (!(window.mw && mw.storage)) return { removed: [], added: [] };
var raw;
try { raw = mw.storage.get(WIKTKU_SUGGESTIONS_KEY_PREFIX + wrapCls); } catch (e) { raw = null; }
var parsed = {};
if (raw) { try { parsed = JSON.parse(raw); } catch (e) { parsed = {}; } }
return { removed: parsed.removed || [], added: parsed.added || [] };
}
function saveSuggestionOverrides(wrapCls, overrides) {
if (!(window.mw && mw.storage)) return;
try { mw.storage.set(WIKTKU_SUGGESTIONS_KEY_PREFIX + wrapCls, JSON.stringify(overrides)); } catch (e) { /* storage unavailable — override just won't persist */ }
}
// One document listener for every sense's "Hevmane û Bêhtir" menu, not one per sense: a long entry can hold a
// dozen senses and each would otherwise bind its own.
var moreMenuCloserBound = false;
function bindMoreMenuCloser() {
if (moreMenuCloserBound) return;
moreMenuCloserBound = true;
$(document).on("click.wiktkuMore", function (e) {
if ($(e.target).closest(".wiktku-dfn-more-wrap").length) return;
$(".wiktku-dfn-more-menu").hide();
$(".wiktku-dfn-more-arrow").html("▸");
});
}
function buildChipField(opts) {
var wrap = $("<div></div>").addClass("wiktku-dfn-chipfield-wrap " + opts.wrapCls + "-wrap");
var chipsRow = $("<span class='wiktku-dfn-chipfield-chips'></span>");
var toggle = $("<button type='button' class='wiktku-dfn-chipfield-toggle'></button>").text(opts.toggleLabel);
var panel = $("<div class='wiktku-dfn-chipfield-panel'></div>").hide();
var suggestRow = $("<div class='wiktku-dfn-chipfield-suggest'></div>");
var input = $("<input type='text' class='wiktku-dfn-chipfield-input'>")
.attr("placeholder", opts.placeholder || ((opts.suggestions && opts.suggestions.length) ? "An ya nû binivîse…" : "Binivîse…"));
var addBtn = $("<button type='button' class='wiktku-dfn-chipfield-add'>Qeyd bike</button>");
// Once the person opens/closes the panel themselves, lang-driven
// syncing (see setOpenDefault) backs off rather than fighting them.
var userToggled = false;
// The pick list shown is opts.suggestions with the person's own
// edits layered on top — see loadSuggestionOverrides above.
var overrides = loadSuggestionOverrides(opts.wrapCls);
var removedSuggestions = overrides.removed;
var addedSuggestions = overrides.added;
function persistOverrides() {
saveSuggestionOverrides(opts.wrapCls, { removed: removedSuggestions, added: addedSuggestions });
}
// opts.suggestions is either a fixed array or a function returning one -- the label-driven
// pickers depend on the language being edited, and on data that arrives from Modul:labels after
// the field is built, so they cannot be a snapshot taken here.
function baseSuggestions() {
var list = typeof opts.suggestions === "function" ? opts.suggestions() : opts.suggestions;
return list || [];
}
function allSuggestions() {
return $.grep(baseSuggestions(), function (s) { return removedSuggestions.indexOf(s) === -1; })
.concat(addedSuggestions);
}
function learnSuggestion(val) {
if (!val || allSuggestions().indexOf(val) !== -1) return;
addedSuggestions.push(val);
persistOverrides();
}
function forgetSuggestion(name) {
var idx = addedSuggestions.indexOf(name);
if (idx !== -1) addedSuggestions.splice(idx, 1);
else if (removedSuggestions.indexOf(name) === -1) removedSuggestions.push(name);
persistOverrides();
renderSuggest();
// renderSuggest() just destroyed the pill's × button the click
// came from — which was focused — dropping focus outside the
// panel entirely. Left alone, that reads as "focus left the
// widget" to the focusout handler below and closes the whole
// panel. Move focus back inside before that check runs.
input.focus();
}
function values() {
var vals = [];
chipsRow.find(".wiktku-dfn-chipfield-chip").each(function () {
vals.push($(this).attr("data-value"));
});
return vals;
}
// Label-driven pickers are built before Modul:labels has been read, so re-render once it lands.
if (typeof opts.suggestions === "function") {
wiktkuLabelListeners.push(function () {
if (panel.is(":visible")) renderSuggest();
});
}
// At most this many pills at once. Modul:labels offers 87 regions, 362 etîket and 209 kategorî;
// drawing them all was a wall of pills nobody reads and the field it belonged to scrolled away.
var MAX_PILLS = 24;
// What shows with an empty box: the short hand-picked list if the caller gave one, otherwise the
// full set (capped) as before, for pickers whose lists are small enough to browse.
function restingSuggestions() {
var quick = typeof opts.quickPicks === "function" ? opts.quickPicks() : opts.quickPicks;
if (!quick || !quick.length) {
return allSuggestions();
}
// A value the person added themselves outranks the hand-picked list -- it is theirs.
return quick.concat($.grep(addedSuggestions, function (s) { return quick.indexOf(s) === -1; }));
}
// Typed text searches the WHOLE set, not just the pills on screen, and matches the module's own
// aliases too, so "Behdînanî" finds Badînî and "USA" finds DYA.
function matching(query) {
var q = $.trim(query).toLowerCase();
if (!q) {
return restingSuggestions();
}
var aliases = wiktkuLabelAliases();
var hits = $.grep(allSuggestions(), function (name) {
return name.toLowerCase().indexOf(q) !== -1;
});
$.each(aliases, function (alias, canonical) {
if (alias.indexOf(q) !== -1 && hits.indexOf(canonical) === -1 &&
allSuggestions().indexOf(canonical) !== -1) {
hits.push(canonical);
}
});
return hits;
}
function renderSuggest(query) {
var taken = values();
suggestRow.empty();
var shown = 0;
$.each(matching(query || ""), function (_, name) {
if (taken.indexOf(name) !== -1 || shown >= MAX_PILLS) return;
shown++;
var pill = $("<span role='button' class='wiktku-dfn-chipfield-pill'></span>").text(name);
pill.on("click", function () { addChip(name); input.val(""); renderSuggest(""); input.focus(); });
var pillRemove = $("<button type='button' class='wiktku-dfn-chipfield-pill-remove' title='Vê tewsiyeyê veşêre'>×</button>");
pillRemove.on("click", function (e) {
e.stopPropagation(); // don't also trigger the pill's own add-chip click
forgetSuggestion(name);
});
pill.append(pillRemove);
suggestRow.append(pill);
});
suggestRow.toggle(!!suggestRow.children().length);
}
function addChip(val) {
val = $.trim(val || "");
if (!val || values().indexOf(val) !== -1) return;
var chip = $("<span class='wiktku-dfn-chipfield-chip'></span>").attr("data-value", val).text(val);
var remove = $("<button type='button' class='wiktku-dfn-chipfield-remove' title='Jê bibe'>×</button>");
// Same focus-loss hazard as forgetSuggestion() below — refocus
// the input so removing a chip while the panel is open doesn't
// also close the panel.
remove.on("click", function () { chip.remove(); renderSuggest(); input.focus(); });
chip.append(remove);
chip.insertBefore(toggle); // chips in insertion order, toggle stays last
renderSuggest();
}
toggle.on("click", function () {
userToggled = true;
if (panel.is(":visible")) { panel.hide(); return; }
if (opts.onOpen) opts.onOpen();
renderSuggest();
panel.show();
input.focus();
});
// Anything typed and committed by hand joins the pick list too, not
// just the current sense's chips.
function commitInput() {
var val = $.trim(input.val() || "");
if (val) {
addChip(val);
learnSuggestion(val);
}
input.val("").focus();
}
addBtn.on("click", commitInput);
input.on("keydown", function (e) {
if (e.which === 13) { e.preventDefault(); commitInput(); }
});
// Typing searches the whole list, so a value that is not among the pills is still one keystroke
// from being found rather than something you have to know by heart and type out in full.
input.on("input", function () {
renderSuggest(input.val());
});
// Close the panel once focus leaves toggle+panel together — they're
// siblings in the DOM (not nested), so both must be checked.
function focusStillInside() {
return $.contains(wrap[0], document.activeElement) || $.contains(panel[0], document.activeElement);
}
wrap.add(panel).on("focusout", function () {
setTimeout(function () {
if (!focusStillInside()) panel.hide();
}, 150);
});
if (opts.hint) panel.append($("<div class='wiktku-dfn-chipfield-hint'></div>").text(opts.hint));
// Box first, pills under it. The other way round, every keystroke re-filtered the pills above the
// box and the box itself jumped up and down the page as the list grew and shrank.
panel.append(
$("<div class='wiktku-dfn-chipfield-input-row'></div>").append(input).append(addBtn)
).append(suggestRow);
chipsRow.append(toggle);
wrap.append(chipsRow);
return {
wrap: wrap,
panel: panel,
addChip: addChip,
values: values,
/** Sets the panel's default open/closed state — a no-op once
* the person has manually toggled it themselves this session. */
setOpenDefault: function (open) {
if (userToggled) return;
// Same as the toggle click handler: (re)build the suggestion
// pills whenever the panel opens, not just on a manual click —
// otherwise a panel opened by this default-state sync (e.g.
// Dever/Şîwe starting open for ku) shows an empty pill row
// until the person closes and reopens it themselves.
if (open) {
if (opts.onOpen) opts.onOpen();
renderSuggest();
}
panel.toggle(!!open);
},
/** Force-closes the panel as though the person had done it
* themselves, so a later setOpenDefault() doesn't reopen it out
* from under them — used by onOpen to keep only one of these
* fields' panels open at a time (see the call site). */
close: function () {
userToggled = true;
panel.hide();
},
/** Opens the panel from somewhere other than the toggle — the
* "Hevmane û Bêhtir" menu, which is where Etîket and Kategorî are asked
* for now. Counts as a manual toggle for the same reason close()
* does. */
open: function () {
userToggled = true;
if (opts.onOpen) opts.onOpen();
renderSuggest();
panel.show();
input.focus();
}
};
}
/**
* One page-level word list (Formên alternatîv, Hevmane, … Herwiha binêre): a "====" section holding
* the words for the whole entry rather than for one sense.
*
* Only the question is passed in. The heading and the step's own name come from data-sections.json's
* pageSubsections, filled in below once the data lands — the same table DefSideBoxes reads to find
* these sections, so the wizard cannot write a heading the panels then fail to recognise.
*/
function makeWordListStep(id, question_fmt) {
return {
id: id,
name: id,
question_fmt: question_fmt,
multiple: true,
narrow: true,
modifiers: true,
// Function instead of a plain string so it can name the current
// language — computed fresh at build time and re-applied by
// refresh() whenever the language changes.
placeholder: function () {
var lang = getValues().lang || "";
var langLabel = lang ? getLangLabel(lang) : "";
return "Kelîme" + (langLabel ? " bi " + langLabel : "") + "…";
},
visible: function (values) {
return values.lang && values.pos;
},
output: function (values) {
var words = $.grep(values[id] || [], function (w) { return !!w; });
if (!words.length) return "";
var header = steps[id].header || capitalize(steps[id].name);
var lang = values.lang;
var section = "\n==== " + header + " ====\n";
var params = "cure=" + header + "\n" +
$.map(words, function (w) {
return "|" + w;
}).join("\n");
return section +
"{{" + WORDLIST_TEMPLATE + "|" + lang + "|" + params + "\n}}\n";
} };
}
var steps = {
lang: {
id: "lang",
required: true,
name: "ziman",
question_fmt: '"' + pageName + '" bi çi zimanî ye?',
// Populated from data-main.json (langChoices).
choices: [],
output: function (values) {
return "== {{ziman|" + values.lang + "}} ==\n";
}
},
pron: {
id: "pron",
name: "bilêvkirin",
question_fmt: '"' + pageName + '" çawa tê telafûzkirin?',
// pronTpls populated from data-main.json
pronTpls: {},
visible: function (values) {
if (!values.lang) return false;
// addLangMode: skip pron only if this lang already has a section.
if (addLangMode) {
// Clearing the page makes existing content irrelevant.
var clearPageCheck = $("#wiktku-clear-page-check");
if (clearPageCheck.length && clearPageCheck.prop("checked")) {
return true;
}
var existing = $("#wpTextbox1").val() || "";
return !findLangHeader(existing, values.lang);
}
return true;
},
// Fills the insert-dropdown from pronTpls, filtered by language.
refreshTplSelect: function () {
var sel = $("#wiktku-start-pron .wiktku-tpl-insert-select");
if (!sel.length) return;
var lang = getValues().lang || "";
sel.find("option[value!='']").remove();
$.each(steps.pron.pronTpls, function (name, tpl) {
// Filter: if tpl.langs exists, only show if lang is in that list; otherwise show always
var allowed = !tpl.langs || tpl.langs.indexOf(lang) !== -1;
if (allowed) {
sel.append($("<option></option>")
.val(tpl.snippet)
.text(name + " — " + tpl.desc)
.attr("data-nolang", tpl.noLang ? "1" : ""));
}
});
markTemplateOptions(sel, function (opt) { return tplNameFromSnippet(opt.val()); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-pron-wrap").length) return null;
var wrap = $("<div class='wiktku-pron-wrap'></div>");
// Reassures the user this field is optional — the dropdown
// below writes the template for them, no syntax needed up front.
wrap.append(
$("<p class='wiktku-step-hint'></p>").text(
"Ev qism ne mecbûrî ye. Heke tu nizanî kîjan şablonê hilbijêrî, vala bihêle."
)
);
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-pron-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-pron-text")
.attr("rows", 4)
.attr("placeholder", "Nimûne: {{IPA|ku|/pɛɪ̯n/}} yan {{kîte|ku|he|ce|ki|rin}}");
// Template insert dropdown — filtered by language.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
var lang = getValues().lang || "";
$.each(steps.pron.pronTpls, function (name, tpl) {
// Filter: if tpl.langs exists, only show if lang is in that list; otherwise show always
var allowed = !tpl.langs || tpl.langs.indexOf(lang) !== -1;
if (allowed) {
sel.append($("<option></option>")
.val(tpl.snippet)
.text(name + " — " + tpl.desc)
.attr("data-nolang", tpl.noLang ? "1" : ""));
}
});
sel.on("change", function () {
var snippet = $(this).val();
if (!snippet) return;
var lang = getValues().lang || "";
// Check if the selected option has noLang flag
var selectedOption = sel.find("option:selected");
var hasNoLang = selectedOption.attr("data-nolang") === "1";
if (!hasNoLang) {
snippet = snippet.replace(/CODE/g, lang).replace(/SOURCELANG/g, "?");
insertLineSnippet(ta[0], snippet);
$(this).val("");
// Clear the picker's search box
sel.trigger("wiktku:refresh");
ta.focus();
return;
}
// The noLang templates (ku-IPA, ku-kîte, zza-IPA, zza-kîte)
// auto-generate content from a Lua module reading the page
// title, so show a live preview and require confirmation
// before inserting.
var tplLabel = selectedOption.text();
$(this).val("");
sel.trigger("wiktku:refresh");
steps.pron.showTplPreview(stepDiv, ta, snippet, tplLabel);
});
wrap.append(sel).append(ta);
stepDiv.append(wrap);
// The palette lives in wikt.edit, shared with DefSideBoxes' own Bilêvkirin box.
wrap.append(wiktkuUtil.buildIpaPalette({
insertText: function (text, back) {
insertAtCursor(ta[0], text);
if (back) {
var pos = ta[0].selectionStart - back;
ta[0].selectionStart = ta[0].selectionEnd = pos;
}
ta.focus();
}
}));
initSelects(wrap);
return ta;
},
// ── IPA character palette ji [[MediaWiki:Gadget-charinsert-core.js]]────────────
// Modelled on fr.wiktionary's: every character is a button that
// drops itself in at the cursor. Two sub-tabs, because the full
// IPA chart is ~180 symbols and roughly twenty of them ever
// appear in Kurmancî — "IPA (kurdî)" opens by default and the
// full chart is one click away for loanwords and other
// languages. Grouped with a label down the left so it reads as
// a chart rather than one long line of glyphs.
/** Inline preview + confirm panel for a "noLang" (auto-content)
* pronunciation template — fetches what the snippet expands to
* for this page and only inserts it if the user confirms. */
showTplPreview: function (stepDiv, ta, snippet, tplLabel) {
// Only one preview panel open at a time.
stepDiv.find(".wiktku-pron-preview").remove();
var panel = $("<div class='wiktku-pron-preview'></div>");
panel.append($("<div class='wiktku-pron-preview-label'></div>")
.text("Pêşdîtina " + tplLabel + ":"));
var body = $("<div class='wiktku-pron-preview-body'></div>").text(wiktkuUtil.msg.loading);
panel.append(body);
var actions = $("<div class='wiktku-pron-preview-actions'></div>");
var confirmBtn = $("<button type='button' class='wiktku-pron-preview-confirm'>✓ Tevlî bike</button>")
.prop("disabled", true);
var cancelBtn = $("<button type='button' class='wiktku-pron-preview-cancel'></button>")
.text("✕ " + wiktkuUtil.msg.cancel);
actions.append(confirmBtn).append(cancelBtn);
panel.append(actions);
ta.before(panel);
confirmBtn.on("click", function () {
insertLineSnippet(ta[0], snippet);
panel.remove();
ta.focus();
});
cancelBtn.on("click", function () {
panel.remove();
});
// action=parse, not expandtemplates: the preview needs rendered HTML, not expanded
// wikitext. The leading "* " is dropped so it renders as a line, not a bulleted list.
new mw.Api().post({
action: "parse",
title: pageName,
text: snippet.replace(/^\*\s*/, ""),
prop: "text",
wrapoutputclass: "",
disablelimitreport: 1,
disableeditsection: 1,
contentmodel: "wikitext",
formatversion: 2
}).done(function (data) {
var html = (data && data.parse && data.parse.text) || "";
if ($.trim($("<div>").html(html).text())) {
body.html(html);
// Category links render as nothing but still exist;
// and any real link inside a preview should not
// navigate away from a half-filled form.
body.find("a").attr("target", "_blank").attr("rel", "noopener");
} else {
body.text("(vala — ji bo vê rûpelê tiştek nehate çêkirin)");
}
confirmBtn.prop("disabled", false);
}).fail(function () {
body.text("Pêşdîtin nehate barkirin — tu dikarî hîn jî wê tevlî bikî.");
confirmBtn.prop("disabled", false);
});
},
output: function () {
var text = $.trim($("#wiktku-start-pron .wiktku-pron-text").val());
if (!text) return "";
var lines = text.split("\n");
var out = $.map(lines, function (line) {
line = $.trim(line);
if (!line) return undefined;
return line;
}).join("\n");
// "===" heading under Ziman and above the POS sections; the
// field is shared across sections, so it's emitted once by
// getWikitext() rather than per-POS-section (would otherwise
// print the pronunciation twice for a two-POS entry).
return "\n=== Bilêvkirin ===\n" + out + "\n";
} },
pos: {
id: "pos",
required: true,
name: "Cureyê rêzimanî",
question_fmt: 'Cureyê rêzimanî yê "' + pageName + '" çi ye?',
visible: function (values) {
return values.lang;
},
// Populated from posChoices (data-sections.json) and posExamples (data-main.json).
choices: [],
// Definition of whichever cureyê rêzimanî is currently chosen,
// shown as plain text right under the dropdown (see the
// .wiktku-start-pos-example build/refresh() above). Kept clear of
// the Mane textarea's placeholder, which answers "what to type
// here" rather than "what this grammatical category means".
posExamples: {},
example: function (values) {
var pos = values.pos || "";
return steps.pos.posExamples[pos] ||
"Cureyê rêzimanî yê vê kelîmeyê hilbijêre — navdêr, lêker, rengdêr hwd.";
},
output: function (values) {
var pos = values.pos || "";
var capitalizedPos = capitalize(pos);
// "form" opt-param (checked in posparams) marks this as a
// form-of/inflected entry — the section title becomes
// "Formeke <pos>ê" instead of the plain pos name. The
// template call itself still uses the unchanged pos name.
var isForm = $("#wiktku-start-posparams [data-pp-opt='form']").is(":checked");
var header = isForm
? "\n=== Formeke " + pos + "ê ===\n"
: "\n=== " + capitalizedPos + " ===\n";
// Read the live wikitext from the posparams textarea.
// Fall back to a plain {{pos|lang}} if the step hasn't been built yet.
var tplLine = $.trim($("#wiktku-start-posparams .wiktku-posparams-text").val());
if (!tplLine) {
tplLine = "{{" + pos + "|" + (values.lang || "") + "}}";
}
// {{ku-tewîn-lk}} line for xwerû Kurmancî verbs, parsed straight out of tplLine (not the posparams input fields, which a cache/section restore doesn't repopulate) once niha/borî/transitivity are all present.
var tewinLkLine = "";
if (pos === "lêker" && values.lang === "ku") {
var niha = wiktkuWikitext.callParamValue(tplLine, "niha");
var borî = wiktkuWikitext.callParamValue(tplLine, "borî");
var lekturCode = wiktkuWikitext.callParamValue(tplLine, "c");
var cParts = lekturCode ? /^(?:([xphb])-)?(n?gh)$/.exec(lekturCode) : null;
var lekturForm = cParts ? (cParts[1] || "") : "";
var lktransitivity = cParts ? cParts[2] : "";
var lkForm = lktransitivity === "gh" ? "gerguhêz" : (lktransitivity === "ngh" ? "negerguhêz" : "");
if (niha && borî && lekturForm === "x" && lkForm) {
tewinLkLine = "{{ku-tewîn-lk|" + pageName + "|form=" + lkForm + "|niha=" + niha + "|borî=" + borî + "}}\n";
}
}
// Image line (between the conjugation line, if any, and the template call)
var imageLine = steps.resim.output(values);
return header + tewinLkLine + imageLine + tplLine + "\n";
}
},
// cinsiyet (gender) — shown for navdêr/serenav only, with choices built
// from metadata[lang].g since not every language has grammatical gender.
cinsiyet: {
id: "cinsiyet",
name: "cinsiyet",
question_fmt: 'Cinsiyeta "' + pageName + '" çi ye?',
visible: function () { return false; }, // handled inside posparams
// Single source of truth for the selected gender code. The visible
// dropdown lives inside the posparams step (see steps.posparams),
// but it must always read/write THIS hidden field — not a field of
// its own — since getValuesForStep("cinsiyet") reads from here.
build: function (stepDiv) {
var field = stepDiv.find(".wiktku-start-step-field");
if (field.length) return field;
field = $("<input type='hidden'>")
.addClass("wiktku-start-step-field")
.attr("id", "wiktku-start-cinsiyet-field")
.val("");
stepDiv.append(field);
return field;
},
output: function () { return ""; } // consumed by posparams/pos.output
},
// jimar (grammatical number) — same hidden-field pattern as cinsiyet
// above, but writes into its own j= template parameter instead of
// sharing c=. Shown for navdêr/serenav only, with the same choices
// for every language — data-main.json's numberLabels (see syncHelpers).
jimar: {
id: "jimar",
name: "jimar",
question_fmt: 'Jimara "' + pageName + '" çi ye?',
visible: function () { return false; }, // handled inside posparams
build: function (stepDiv) {
var field = stepDiv.find(".wiktku-start-step-field");
if (field.length) return field;
field = $("<input type='hidden'>")
.addClass("wiktku-start-step-field")
.attr("id", "wiktku-start-jimar-field")
.val("");
stepDiv.append(field);
return field;
},
output: function () { return ""; } // consumed by posparams/pos.output
},
// Lêker transitivity — hidden step, handled inside posparams
lektur: {
id: "lektur",
name: "Cureyê lêkerê (fiîlê)",
question_fmt: "Cureyê lêkerê (fiîlê) çi ye?",
visible: function () { return false; },
output: function () { return ""; }
},
// tr — hidden step, handled inside posparams
tr: {
id: "tr",
name: "transliterasyon",
question_fmt: "Transliterasyona vê kelîmeyê",
multiple: false,
narrow: true,
visible: function () { return false; }
},
// The card itself lives in wikt.edit/posparams.js, shared with DefSideBoxes -- see there for the
// fields and the per-language gating. This is only the wizard's side: which language and part of
// speech it is for, and mirroring gender/number into the two hidden steps the wizard keeps.
posparams: {
id: "posparams",
// Fused onto the "pos" card in styles.css so the two read as one question. The "xisûsiyet"
// name is a no-CSS fallback: its h3 is hidden, and every field below is self-labelled.
name: "xisûsiyet",
question_fmt: "",
card: null,
visible: function (values) {
return !!(values.lang && values.pos);
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-posparams-step").length) return null;
var values = getValues();
steps.posparams.card = wiktkuPosparams.build({
lang: values.lang,
pos: values.pos,
onChange: refresh,
// steps.cinsiyet/steps.jimar own these hidden fields; the card reports rather than
// writes, so it needs no knowledge of them.
onSync: function (cinsiyet, jimar) {
$("#wiktku-start-cinsiyet-field").val(cinsiyet || "");
$("#wiktku-start-jimar-field").val(jimar || "");
}
});
stepDiv.append(steps.posparams.card.$element);
return null;
},
rebuildTa: function () {
if (steps.posparams.card) steps.posparams.card.rebuild();
},
/** Called from refresh() — re-gates every row for the current language/pos. */
syncHelpers: function () {
if (!steps.posparams.card) return;
var values = getValues();
steps.posparams.card.setContext(values.lang, values.pos);
},
output: function () { return ""; } // pos.output reads the textarea directly
},
dfn: {
id: "dfn",
name: "Mane",
question_fmt: function (values) {
var base = "Maneya kelîmeyê binivîse. Mane otomatîk tê lînkkirin!";
if (values.lang === "ku") {
return base + " Ji bo tercimeyan beşa 'Werger' bi kar bîne!";
}
return base;
},
multiple: true,
ordered: true,
rows: 3,
visible: function (values) {
return values.lang && values.pos;
},
// Builds one sense block (<li>): mane textarea, nimûne list, and
// sub-lists for alternatîv/hevmane/dijmane/etc.
buildSenseBlock: function () {
var block = $("<li class='wiktku-dfn-sense'></li>");
// — Mane textarea (declared first so the template handler can reference it) —
var dfnArea = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-mane-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", "wiktku-start-step-field wiktku-dfn-mane")
.attr("rows", 6)
.attr("placeholder", "Maneya vê kelîmeyê binivîse…");
// Nothing on keystrokes: only the Werger gloss list needs a live Mane edit, and rebuilding
// it re-reads every sense. Deferred to blur; the wikitext is read fresh on output anyway.
dfnArea.on("blur", function () { refresh(); });
// — Îzaha maneyî (optional second half of the mane) —
// A non-Kurdish entry's gloss is a translation followed by {{îzah|…}} explaining it. The
// mane box is always the mane box and the explanation opens under it when asked for, so
// the pair reads as one gloss rather than as a different kind of field. getSenses() joins
// the two back together, so every consumer downstream sees one gloss string.
var izahArea = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-izah-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", "wiktku-dfn-subfield wiktku-dfn-izah")
.attr("rows", 2)
.attr("placeholder", "Îzaha maneyî… Ji bo school-a îngilîzî li vir 'cihê tehsîlgirtinê' binivîse");
var izahWrap = $("<div class='wiktku-dfn-izah-wrap'></div>")
.append($("<span class='wiktku-dfn-sublabel'></span>")
.append($("<span class='wiktku-dfn-sublabel-term'></span>").text("Îzaha maneyî")).append(": "))
.append(izahArea)
.hide();
// Opening it from a restore, where the text decides rather than a click.
var izahOpen = false;
function setIzahOpen(open) {
izahOpen = !!open;
updateManeMode();
}
block.data("setIzahOpen", setIzahOpen);
izahArea.on("input", function () {
// dfnArea drives the live preview and the auto-linker; the îzah is part of the gloss,
// so a change to it has to reach them too.
dfnArea.trigger("input");
});
izahArea.on("blur", function () { refresh(); });
// The mane box gives way to the template title box when a template is chosen; the îzah
// goes with it, since a template gloss has no separate explanation.
function updateManeMode() {
var isTpl = !!tplSelect.val();
if (isTpl) {
dfnArea.hide();
izahWrap.hide();
return;
}
dfnArea.show();
dfnArea.attr("placeholder", manePlaceholder(getValues().lang || ""));
// {{îzah}} explains a translation, so it belongs to entries in another language; a
// Kurdish sense is its own definition and is not offered one. A box that already holds
// text stays on screen whatever the language -- getSenses() joins it into the gloss
// either way, and hiding it would make that happen unseen.
var lang = getValues().lang || "";
var offer = !!lang && lang !== "ku";
// A non-Kurdish gloss is a translation and its explanation, so the box opens with the
// mane box rather than waiting to be asked for.
var open = offer || izahOpen || $.trim(izahArea.val()) !== "";
izahWrap.toggle(open);
}
// Store on the block so refresh() can re-sync every block when
// lang changes, and restoreDfn() can re-sync after restoring.
block.data("updateManeMode", updateManeMode);
// — Template selector (optional — replaces mane when chosen) —
// Template definitions loaded from data-main.json.
var tplDefs = steps.dfn.tplDefs || {};
// Cache of all non-placeholder option elements, so updateTplOptions
// can detach/re-attach them without losing the full list.
var _allTplOptions = [];
var tplSelect = $(mw.html.element("select", { "class": "wiktku-dfn-tpl-select" }));
tplSelect.attr("data-placeholder", wiktkuUtil.msg.searchTemplate);
tplSelect.append($("<option value=''>— yan jî şablonek hilbijêre —</option>"));
$.each(tplDefs, function (name, tpl) {
// Shows "name — tpl.desc" when a tplDefs entry has a desc
// in data-main.json, same pattern as steps.pron's template picker.
var opt = $("<option></option>")
.val(name)
.text(tpl.desc ? name + " — " + tpl.desc : name)
.attr("data-langs", tpl.langs ? tpl.langs.join(",") : "")
.attr("data-nolang", tpl.noLang ? "1" : "");
tplSelect.append(opt);
_allTplOptions.push(opt[0]);
});
// Same status-dot / 🔗 / ⓘ row treatment as every other
// template picker. This one carries the bare name in value=,
// so the name mapper is the identity rather than a snippet
// parse; descriptions come from each template's TemplateData.
markTemplateOptions(tplSelect, function (opt) { return opt.val(); });
function updateTplOptions() {
var lang = getValues().lang || "";
var currentVal = tplSelect.val();
var currentStillValid = false;
// Detach all non-placeholder options, then re-attach only the visible
// ones — native <select> won't reliably hide options any other way.
tplSelect.find("option[value!='']").detach();
$.each(_allTplOptions, function (_, opt) {
var allowed = $(opt).attr("data-langs");
var visible = !allowed || allowed.split(",").indexOf(lang) !== -1;
if (visible) {
tplSelect.append(opt);
if ($(opt).val() === currentVal) currentStillValid = true;
}
});
// Options that were detached when the descriptions arrived
// missed that pass, so describe whatever is attached now.
// The cache is warm by this point, so this is synchronous.
applyTplDescriptions(tplSelect);
tplSelect.trigger("wiktku:refresh");
if (currentVal && !currentStillValid) {
tplSelect.val("");
tplTitleInput.hide().val("");
updateManeMode();
}
}
// Store updater on the element so refresh() can call it for all blocks.
tplSelect.data("updateOptions", updateTplOptions);
var tplTitleInput = $("<input type='text'>")
.addClass("wiktku-dfn-tpl-title wiktku-start-narrow")
.attr("placeholder", "Navê rûpela hedefê…")
.hide();
// Same reasoning as dfnArea above: refresh() (which rebuilds the
// Werger/Bi zaravayên din gloss list) is deferred to blur
// rather than run on every keystroke.
tplTitleInput.on("blur", function () { refresh(); });
// Şablon senses have no plain gloss, so the word-list groups (hevmane, dijmane, formên
// alternatîv…) make no sense for them — the class hides both those groups and their
// entries in the "Hevmane û Bêhtir" menu, so they cannot be asked for either. Etîket, Kategorî,
// Nimûne and Jêgirtin stay available.
function updateFieldVisibility() {
block.toggleClass("wiktku-sense-is-tpl", !!tplSelect.val());
}
// Store on the block so restoreDfn() can re-sync visibility
// after setting tplSelect's value.
block.data("updateFieldVisibility", updateFieldVisibility);
tplSelect.on("change", function () {
var chosen = $(this).val();
if (chosen) {
dfnArea.hide();
izahWrap.hide();
tplTitleInput.show().focus();
} else {
tplTitleInput.hide().val("");
updateManeMode();
}
updateFieldVisibility();
// Re-run refresh so the Werger step visibility updates.
refresh();
});
// The template picker is an alternative to writing a
// definition, so this quiet link sits above the Mane box
// rather than getting buried under Dever/Etîket/Kategorî.
// It only expands for the minority of senses that are a
// pointer at another word (guharto, şaşnivîs, kevnbûyî)
// rather than a meaning.
var tplRow = $("<div></div>").addClass("wiktku-dfn-tpl-row")
.append(tplSelect).append(" ").append(tplTitleInput)
.hide();
var tplLink = $("<a href='#' class='wiktku-dfn-tpl-link'></a>")
.text("Maneyê bi şablonan çêke (rastnivîs, şaşnivîs, awayekî din)");
tplLink.on("click", function (e) {
e.preventDefault();
tplRow.show();
$(this).hide();
tplRow.find("input.wiktku-ac-input").first().focus();
});
/** Opens the picker without a click — used on restore, when a
* sense already has a template and hiding it would make the
* chosen value invisible. */
function revealTplRow() { tplRow.show(); tplLink.hide(); }
block.data("revealTplRow", revealTplRow);
// Dever/Şîwe, Etîket and Kategorî as chip+toggle fields, so a sense that leaves them empty
// stays short. getOutput() folds Dever/Şîwe into {{dever|}} for ku, {{e|}} otherwise.
// Each closes the other two on open; onOpen runs from a click, so forward references hold.
var deverField = buildChipField({
wrapCls: "wiktku-dfn-dever",
toggleLabel: "+ Dever/Şîwe",
hint: "Bi vê maneyê li kû derê an di kîjan devokê de tê bikaranîn?",
// Kurmancî's pills are the handful in data-main.json; another language gets the regions
// Modul:labels tags for it. The rest are found by typing.
quickPicks: function () {
var lang = getValues().lang || "ku";
if (lang === "ku") return DEVER_QUICK_PICKS;
return wiktkuLabelSets ? wiktkuEdit.labels.taggedRegionsFor(wiktkuLabelSets, lang) : [];
},
suggestions: function () { return wiktkuLabelSuggestions("dever"); },
onOpen: function () { etiketField.close(); kategoriField.close(); wiktkuEnsureLabels(); }
});
block.data("addDeverChip", deverField.addChip);
var etiketField = buildChipField({
wrapCls: "wiktku-dfn-etiket",
toggleLabel: "+ Etîket",
hint: "xisûsen, Mêrdîn, anatomî, Badînî",
suggestions: function () { return wiktkuLabelSuggestions("etiket"); },
onOpen: function () { deverField.close(); kategoriField.close(); wiktkuEnsureLabels(); }
});
block.data("addEtiketChip", etiketField.addChip);
var kategoriField = buildChipField({
wrapCls: "wiktku-dfn-kategori",
toggleLabel: "+ Kategorî",
hint: "wekî etîket lê xuya nabe tenê kategorîze dike",
suggestions: function () { return wiktkuLabelSuggestions("kategori"); },
onOpen: function () { deverField.close(); etiketField.close(); wiktkuEnsureLabels(); }
});
block.data("addKategoriChip", kategoriField.addChip);
// Maneid -- the anchor another entry links a sense by, written "{{maneid|lang|x}}" at the
// head of the line. One value rather than a list, so it is a plain box and not a chip
// field; it still behaves like one to the "Hevmane û Bêhtir" menu through the small api below.
var maneidInput = $("<input type='text'>")
.addClass("wiktku-dfn-maneid-input wiktku-start-narrow")
.attr("placeholder", "id");
var maneidWrap = $("<div class='wiktku-dfn-chipfield-wrap wiktku-dfn-maneid-wrap'></div>")
.append($("<label class='wiktku-dfn-maneid-label'></label>")
.text("Maneid ")
.append(maneidInput))
.hide();
var maneidField = {
wrap: maneidWrap,
open: function () { maneidWrap.show(); maneidInput.focus(); },
values: function () {
var v = $.trim(maneidInput.val() || "");
return v ? [v] : [];
}
};
// Dever/Şîwe starts open for ku (where it's the common case)
// and closed otherwise; backs off once the person toggles it
// themselves (see buildChipField's setOpenDefault). Re-synced
// from refresh() whenever lang changes — see block.data below.
//
// Shown only for a language with regions: every Kurdish variety, and any other language
// Modul:labels tags one for (read once the labels arrive). One that already has a value stays.
function updateDeverOpenState() {
var lang = getValues().lang || "";
var hasRegions = !lang || wiktkuEdit.labels.isKurdish(lang) ||
(wiktkuLabelSets && wiktkuEdit.labels.regionalFor(wiktkuLabelSets, lang).length > 0);
if (!hasRegions && !wiktkuLabelSets) wiktkuEnsureLabels();
var show = hasRegions || deverField.values().length > 0;
deverField.wrap.toggle(show);
if (!show) deverField.panel.hide();
deverField.setOpenDefault(lang === "ku");
}
block.data("updateDeverOpenState", updateDeverOpenState);
wiktkuLabelListeners.push(updateDeverOpenState);
updateDeverOpenState();
// Toggles always sit together in one row; panels render in their
// own area below so opening one never displaces the others.
// The widget is still built on a reconstruction page so every reference to it stays valid;
// it just never reaches the DOM, so it shows nothing and contributes nothing to getOutput().
//
// The row is the same shape DefSideBoxes puts under a sense -- a few chips for the common
// things, everything else behind "Hevmane û Bêhtir" -- so the two gadgets are learned once. Etîket
// and Kategorî start in the menu and their toggle only joins the row once picked.
var chipFieldRow = $("<div class='wiktku-dfn-chipfield-row'></div>");
var chipFieldPanels = $("<div class='wiktku-dfn-chipfield-panels'></div>");
if (!isReconstruction) {
chipFieldRow.append(deverField.wrap);
chipFieldPanels.append(deverField.panel);
}
etiketField.wrap.hide();
kategoriField.wrap.hide();
chipFieldRow.append(etiketField.wrap).append(kategoriField.wrap).append(maneidWrap);
chipFieldPanels.append(etiketField.panel).append(kategoriField.panel);
block.append(tplLink);
block.append(tplRow);
block.append(dfnArea);
block.append(izahWrap);
block.append(chipFieldRow);
block.append(chipFieldPanels);
// Apply lang filter on initial build.
updateTplOptions();
updateManeMode();
// Sub-field groups. Every one of them is built hidden and asked for by name -- from a chip
// in the row above (Nimûne, Jêgirtin) or from the "Hevmane û Bêhtir" menu -- so a sense that needs
// none of them is four boxes tall instead of ten. Jêgirtin is built after the loop as a
// compound widget and inserted after Nimûne, so the visible order matches this list.
var allGroups = [
{ cls: "wiktku-dfn-nimune", label: "Nimûne", rows: 2, narrow: false, attested: true }
].concat($.map(wordGroups(), function (g) {
return { cls: g.cls, label: g.label, rows: 1, narrow: true, izah: g.izah };
}));
if (isReconstruction) {
allGroups = $.grep(allGroups, function (g) { return !g.attested; });
}
$.each(allGroups, function (_, g) {
// wiktku-dfn-wordgroup marks the six word lists, which a şablon sense hides -- both the
// group and its entry in the menu (see the .wiktku-sense-is-tpl rules).
var groupDiv = $("<div></div>").addClass("wiktku-dfn-subgroup wiktku-dfn-subgroup-hidden " + g.cls + "-group");
if (g.cls !== "wiktku-dfn-nimune") groupDiv.addClass("wiktku-dfn-wordgroup");
// Declared once here rather than with `var` in each branch
// below — both branches assign the same pair, just with a
// different lbl and list, so one declaration avoids the
// (harmless but lint-flagged) re-declaration.
var lbl, list;
// The language the example is to be written in, named in the box itself. Read fresh
// rather than fixed at build time: the entry's language is often chosen later.
function nimunePlaceholder() {
var code = getValues().lang || "";
return wiktkuUtil.examplePrompt(code && getLangLabel(code), false);
}
function buildNimuneLi() {
var lang = getValues().lang || "";
var li = $("<li></li>").addClass("wiktku-dfn-nimune-li");
var mainTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-text-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", g.rows)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-text")
.attr("placeholder", nimunePlaceholder());
mainTa.on("input", function () {
highlightLinebreak(this);
var val = $.trim($(this).val());
var isLast = li.is(list.children("li:last-child"));
if (val && isLast) {
list.append(buildNimuneLi());
}
});
li.append(mainTa);
var subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
if (needsTransliteration(lang)) {
subWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-tr")
.attr("placeholder", "Transliterasyona bi alfabeya latînî")
);
}
if (lang) {
subWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-t")
.attr("placeholder", nimuneTranslationPrompt(lang))
);
}
if (subWrap.children().length) li.append(subWrap);
return li;
}
// The placeholder names the current language, but that's
// often not chosen yet when this block is first built —
// so keep a function around that refresh() (and onSubInput,
// for newly-grown fields) can call again once lang is set
// or changes.
function subPlaceholder() {
if (g.noLangSuffix) return g.label + "…";
var lang = getValues().lang || "";
var langLabel = lang ? getLangLabel(lang) : "";
return g.label + (langLabel ? " bi " + langLabel : "") + "…";
}
function updateSubPlaceholders() {
list.find("> li > textarea.wiktku-dfn-subfield").attr("placeholder", subPlaceholder());
}
if (g.cls === "wiktku-dfn-nimune") {
// Nimûne — compound widget with tr/t sub-fields.
lbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text(g.label))
.append(": ");
list = $("<ol></ol>").addClass("wiktku-dfn-sublist " + g.cls + "-list");
groupDiv.data("buildNimuneLi", buildNimuneLi);
// Refreshed with the word groups' own boxes when the entry's language changes.
list.data("updatePlaceholders", function () {
list.find("> li > textarea.wiktku-dfn-nimune-text")
.attr("placeholder", nimunePlaceholder());
});
list.append(buildNimuneLi());
groupDiv.append(lbl).append(list);
block.append(groupDiv);
} else {
// Regular always-visible group — label + growing textarea list.
lbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text(g.label))
.append(": ")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-hint").text(g.izah));
var useModifiers = isWordGroupCls(g.cls);
list = $("<ol></ol>").addClass("wiktku-dfn-sublist " + g.cls + "-list");
list.data("updatePlaceholders", updateSubPlaceholders);
var firstInput = $("<textarea></textarea>")
.attr("name", g.cls + "-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", g.rows)
.attr("class", "wiktku-dfn-subfield" + (g.narrow ? " wiktku-start-narrow" : ""))
.attr("placeholder", subPlaceholder());
firstInput.on("input", function () {
steps.dfn.onSubInput(list, this, g.rows, g.narrow, useModifiers);
});
list.append(useModifiers ? makeModifierLi(firstInput) : $("<li></li>").append(firstInput));
groupDiv.append(lbl).append(list);
block.append(groupDiv);
}
});
// — Jêder group — growing list, same structure as Nimûne. —
if (!isReconstruction) (function () {
var jederGroupDiv = $("<div></div>").addClass("wiktku-dfn-subgroup wiktku-dfn-subgroup-hidden wiktku-dfn-jeder-group");
var jederLbl = $("<span></span>").addClass("wiktku-dfn-sublabel")
.append($("<span></span>").addClass("wiktku-dfn-sublabel-term").text("Jêgirtin"))
.append(": ");
var jederList = $("<ol></ol>").addClass("wiktku-dfn-sublist wiktku-dfn-jeder-list");
// Refreshed with every other sub-box when the entry's language changes.
jederList.data("updatePlaceholders", function () {
jederList.find("> li > textarea.wiktku-dfn-nimune-quote")
.attr("placeholder", jederPlaceholder());
});
// Same shape as the nimûne box: the language belongs in the prompt, and is read fresh
// because the entry's language is often chosen after the block was built.
function jederPlaceholder() {
var code = getValues().lang || "";
return wiktkuUtil.examplePrompt(code && getLangLabel(code), true);
}
function buildJederLi() {
var lang = getValues().lang || "";
var li = $("<li></li>").addClass("wiktku-dfn-jeder-li");
// Quote text textarea
var quoteTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote")
.attr("placeholder", jederPlaceholder());
quoteTa.on("input", function () {
highlightLinebreak(this);
var val = $.trim($(this).val());
var isLast = li.is(jederList.children("li:last-child"));
if (val && isLast) {
jederList.append(buildJederLi());
}
});
li.append(quoteTa);
// tr/t sub-fields for the quote — shown as needed for the language.
var quoteSubWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
if (needsTransliteration(lang)) {
quoteSubWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-tr")
.attr("placeholder", "Transliterasyona jêgirtinê…")
);
}
if (lang && lang !== "ku") {
quoteSubWrap.append(
$("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-t")
.attr("placeholder", "Tercimeya jêgirtinê bi kurmancî")
);
}
if (quoteSubWrap.children().length) li.append(quoteSubWrap);
// The source, in the add-examples form's order and wording: the template, then the link
// with its Citoid button, then the template's own fields.
var jederDefs = steps.dfn.jederDefs || {};
var srcSel = $("<select></select>").addClass("wiktku-dfn-quote-src-sel wiktku-plain-select");
// "" is auto-detect: a Citoid fetch picks the template.
srcSel.append($("<option value=''></option>").text(wiktkuUtil.msg.citoidAuto));
srcSel.append($("<option></option>").val("__manual__").text(wiktkuUtil.msg.sourceByHand));
$.each(jederDefs, function (tplName, def) {
srcSel.append($("<option></option>").val(tplName).text(def.label));
});
srcSel.append($("<option></option>").val("__category__").text(wiktkuUtil.msg.sourceFromCategory));
// Paste a citation built on another page into the quote box and have it saved exactly
// as typed -- the quickest way to reuse one jêgirtin across many entries.
srcSel.append($("<option></option>").val("__raw__").text(wiktkuUtil.msg.sourceRaw));
var srcSelWrap = $("<div></div>").addClass("wiktku-dfn-quote-field")
.append($("<label></label>").text("Şablona çavkaniyê"), srcSel);
// Saved as url= in every mode but Kategorî and raw, which have no box for it.
var urlLbl = $("<label></label>");
var urlInput = $("<input type='text'>").addClass("wiktku-dfn-quote-url");
var citoidBtn = new OO.ui.ButtonWidget({
label: wiktkuUtil.msg.citoidFill,
classes: [ "wiktku-dfn-quote-citoid-btn", "wiktku-add-sense-btn" ]
});
var citoidStatus = $("<span></span>").addClass("wiktku-dfn-quote-citoid-status");
var urlHint = $("<p></p>").addClass("wiktku-step-hint wiktku-dfn-quote-url-hint");
var citoidWrap = $("<div></div>").addClass("wiktku-dfn-quote-field wiktku-dfn-quote-citoid")
.append(urlLbl, urlInput, citoidBtn.$element, citoidStatus, urlHint);
// The add-examples form's labels: plain "attach a link" wording for a hand-written
// source, Citoid wording otherwise.
function setUrlWording(chosen) {
if (chosen === "__manual__") {
urlLbl.text("Adresa webê ya nimûneyê");
urlInput.attr("placeholder", "URLyeke wekî https://…");
urlHint.text("Tenê binivîse eger lînk jixwe di referansa çavkaniyê de tinebe.");
} else {
urlLbl.text("Lînk, DOI an ISBNya çavkaniyê");
urlInput.attr("placeholder", "URLyek wekî https://… an jî DOI/ISBN");
urlHint.text(chosen
? "Ev dibe lînka çavkaniyê (url=). Ji bo şablonên jêder-*, dikare bi Citoid an bi destan jî bên dagirtin."
: "Li qutiyê URL, DOI an ISBN binivîse û pê lê bike butona '" + wiktkuUtil.msg.citoidFill + "' da ku otomatîk çavkanî were dagirtin.");
}
}
setUrlWording("");
// Fills only empty boxes, so a repeat fetch never overwrites what was typed by hand.
function fillEmptyFields(fields) {
$.each(fields, function (key, val) {
var input = srcFields.find("[data-field-key='" + key + "']");
if (input.length && !$.trim(input.val())) {
input.val(val);
}
});
}
// Prefills Dem from the link's own "t=" once jêder-vîdeo is active.
function autofillVideoTimestamp() {
var citoid = window.wikt && window.wikt.citoid;
var dem = srcFields.find("[data-field-key='dem']");
if (srcSel.val() !== "jêder-vîdeo" || !dem.length || $.trim(dem.val()) ||
!(citoid && citoid.extractVideoTimestamp)) {
return;
}
var t = citoid.extractVideoTimestamp($.trim(urlInput.val()));
if (t) dem.val(t);
}
function runCitoidFetch() {
var query = $.trim(urlInput.val());
if (!query) {
mw.notify(wiktkuUtil.msg.citoidNeedsUrl);
return;
}
citoidBtn.setDisabled(true);
citoidStatus.text("Tê barkirin…");
// Through wikt.cite rather than straight to the citoid gadget: the wrapper adds what
// Citoid returns but citoid-helper's mapping drops -- the access date, the ISSN and
// the DOI.
wiktkuCite.fetchFromCitoid(query).then(function (result) {
// Citoid picks the template only while none is chosen; a hand-picked one is kept.
var current = srcSel.val();
if (!current && jederDefs[result.jederKey]) {
srcSel.val(result.jederKey).trigger("change");
current = result.jederKey;
}
if (jederDefs[current]) {
// Citoid shaped its fields for the type it detected; re-derived for another.
fillEmptyFields(current === result.jederKey ? result.fields
: wiktkuCite.citoidFieldsFor(current, result.raw, query));
autofillVideoTimestamp();
} else {
// No template for this kind of source: a hand-written source line from what
// Citoid returned, only while that box is still empty.
if (current !== "__manual__") {
srcSel.val("__manual__").trigger("change");
}
var manualField = srcFields.find(".wiktku-dfn-quote-src-manual");
if (!$.trim(manualField.val())) {
var parts = [];
$.each(["nivîskar", "sernav", "websîte", "kovar", "weşanxane", "sal"], function (_, k) {
if (result.fields[k]) parts.push(result.fields[k]);
});
manualField.val(parts.join(", "));
}
}
citoidStatus.text(wiktkuUtil.msg.citoidFilled);
}, function (errorMsg) {
// Citoid cannot read some sites (x.com): Websîte with a guessed name instead.
var citoid = window.wikt && window.wikt.citoid;
var site = citoid && citoid.websiteNameForUrl && citoid.websiteNameForUrl(query);
if (site && jederDefs["jêder-websîte"]) {
if (srcSel.val() !== "jêder-websîte") {
srcSel.val("jêder-websîte").trigger("change");
}
fillEmptyFields({ "websîte": site });
citoidStatus.text("Citoid nikare vê malperê bixwîne; şablona Websîte hate hilbijartin, ji kerema xwe mayî bi destan tijî bike.");
return;
}
citoidStatus.text(typeof errorMsg === "string" ? errorMsg : "Nehat dîtin. Kontrol bike ka URL/DOI/ISBN rast e.");
}).always(function () {
citoidBtn.setDisabled(false);
});
}
citoidBtn.on("click", runCitoidFetch);
urlInput.on("input", autofillVideoTimestamp);
urlInput.on("keydown", function (e) {
if (e.which === 13) { // Enter
e.preventDefault();
runCitoidFetch();
}
});
// Dynamic source fields container.
var srcFields = $("<div></div>").addClass("wiktku-dfn-quote-src-fields");
// "Malûmatên çavkaniyê" over a jêder-* template's boxes, with the button that empties
// them and the link.
var clearBtn = new OO.ui.ButtonWidget({
label: "Vala bike",
framed: false,
title: "Lînkê û qadên jêder-* yên vê beşê vala bike",
classes: [ "wiktku-dfn-quote-src-clear" ]
});
clearBtn.on("click", function () {
urlInput.val("");
citoidStatus.text("");
srcFields.find(".wiktku-dfn-quote-src-input").val("");
});
var srcHead = $("<div></div>").addClass("wiktku-dfn-quote-src-head")
.append($("<span></span>").addClass("wiktku-dfn-quote-src-head-label").text("Malûmatên çavkaniyê"),
clearBtn.$element)
.hide();
// One labelled box for a citation parameter. Wide/narrow/medium layout: sernav-type
// fields get a full-width multiline box, short ones like sal/rûpel/isbn stay narrow.
// Sizes come from wikt.cite, so a Kategorî:Çavkanî template that happens to use the
// same parameter names as jêder-* is laid out the same way rather than generically.
function buildSrcFieldRow(field) {
var size = wiktkuCite.fieldSize(field.key);
var width = field.width || size.width || "medium";
var multiline = field.multiline !== undefined ? field.multiline : size.multiline;
var row = $("<div></div>")
.addClass("wiktku-dfn-quote-src-row wiktku-dfn-quote-src-row--" + width);
var fieldLbl = $("<label></label>").text(field.label + ": ");
var inp = $(multiline ? "<textarea></textarea>" : "<input type='text'>")
.addClass("wiktku-dfn-quote-src-input")
.attr("data-field-key", field.key)
.attr("placeholder", field.label + "…");
if (multiline) {
inp.attr("rows", 2).addClass("wiktku-dfn-quote-src-input--title");
}
// sal and tarîx say the same thing twice and Modul:quote refuses both; filling
// one clears the other rather than letting the citation break on save.
var partner = wiktkuCite.exclusivePartner(field.key);
if (partner) {
inp.on("input", function () {
if (!$.trim($(this).val())) return;
$(this).closest(".wiktku-dfn-quote-src-fields")
.find(".wiktku-dfn-quote-src-input[data-field-key='" + partner + "']")
.val("");
});
}
return row.append(fieldLbl).append(inp);
}
srcSel.on("change", function () {
srcFields.empty();
var chosen = $(this).val();
// As in the add-examples form: no link for Kategorî or raw, no Citoid for a
// hand-written source, and the fields' header only over a jêder-* template.
citoidWrap.toggle(chosen !== "__category__" && chosen !== "__raw__");
citoidBtn.toggle(chosen !== "__manual__");
citoidStatus.text("");
setUrlWording(chosen);
srcHead.toggle(!!jederDefs[chosen]);
if (!chosen) {
return;
}
if (chosen === "__raw__") {
// Nothing to fill in: the quote box above already holds the finished
// template, and it is written out exactly as typed. Only the entry name
// inside its jêgirtin= is emboldened (see wikt.cite's boldQuoteParam).
srcFields.append(
$("<p></p>")
.addClass("wiktku-step-hint wiktku-dfn-quote-raw-hint")
.text("Şablona temam li qutiya jêgirtinê ya jorê binivîse — wek xwe tê qeydkirin.")
);
return;
}
if (chosen === "__manual__") {
// Freeform source, written by the user — becomes the 2nd
// positional param of {{jêder|lang|…}} instead of çavkanî?.
srcFields.append(
$("<textarea></textarea>")
.attr("rows", 4)
.addClass("wiktku-dfn-subfield wiktku-dfn-quote-src-manual")
.attr("placeholder", "Çavkaniyê bi destê xwe binivîse…")
);
return;
}
if (chosen === "__category__") {
// Same [[Kategorî:Çavkanî]] search widget DefSideBoxes' own Çavkanî subsection box
// uses -- picking a result switches the row into the hidden manual mode with
// {{Şablon}} pre-filled. The picked şablon becomes the citation template itself:
// its own TemplateData parameters are drawn as fields below the picker, in the
// same markup the jêder-* branch uses, so readJederList() reads both with one loop.
var catPicker = wiktkuCite.buildCategoryTemplateSearch({
placeholder: wiktkuUtil.msg.searchTemplate,
describe: false,
onPick: function (entry) {
li.data("catTemplate", entry.target);
li.data("catLangParam", null);
catPicker.$status.text("Qutiyên şablonê tên barkirin…");
srcFields.find(".wiktku-dfn-quote-src-row").remove();
wiktkuCite.fieldSpecs(entry.target).then(function (spec) {
catPicker.$status.text("");
li.data("catLangParam", spec.langParamKey || null);
$.each(spec.fields, function (_, field) {
srcFields.append(buildSrcFieldRow(field));
});
}, function () {
catPicker.$status.text("Qutiyên şablonê nehatin anîn.");
});
}
});
srcFields.append(catPicker.$element);
return;
}
var def = jederDefs[chosen];
if (!def) return;
$.each(def.fields, function (_, field) {
srcFields.append(buildSrcFieldRow(field));
});
autofillVideoTimestamp();
});
// Exposed so draft restore can rebuild a saved category template's fields.
// A whole citation line pasted into the quote box ("#* {{jêder|…}}") means it was
// copied off another entry; switch to raw mode rather than wrapping it in a second
// {{nimûne}}. Only while no source type has been chosen by hand.
li.find(".wiktku-dfn-nimune-quote").on("input", function () {
if (srcSel.val()) return;
if (wiktkuCite.looksLikeCitationLine($(this).val())) {
srcSel.val("__raw__").trigger("change");
}
});
li.data("buildSrcFieldRow", buildSrcFieldRow);
li.append(srcSelWrap, citoidWrap, srcHead, srcFields);
return li;
}
jederGroupDiv.data("buildJederLi", buildJederLi);
jederList.append(buildJederLi());
jederGroupDiv.append(jederLbl).append(jederList);
// Straight after Nimûne rather than last: Jêgirtin is the
// same kind of thing (an attested use of the word) and the
// two belong together — see the allGroups comment above.
var nimuneGroup = block.find(".wiktku-dfn-nimune-group");
if (nimuneGroup.length) jederGroupDiv.insertAfter(nimuneGroup);
else block.append(jederGroupDiv);
}());
// Apply the initial hide/show state for the sub-groups (matches
// tplSelect's starting value, which is normally empty).
updateFieldVisibility();
// ── Which sub-field goes where: chips in the row above for the two everyday ones, the rest
// behind "Hevmane û Bêhtir". Same arrangement as DefSideBoxes' own sense menu, deliberately -- a
// person who has used one gadget should not have to learn the other.
(function () {
var PICK_DEFS = [
{ cls: "wiktku-dfn-nimune", label: "Nimûne", chip: true, attested: true },
{ cls: "wiktku-dfn-jeder", label: "Jêgirtin", chip: true, attested: true }
].concat($.map(wordGroups(), function (g) {
return { cls: g.cls, label: g.label, word: true };
}));
// Etîket and Kategorî are chip fields, not sub-groups, so they are revealed by showing
// their toggle rather than a whole box.
var CHIP_FIELD_DEFS = [
{ cls: "wiktku-dfn-etiket", label: "Etîket", field: etiketField },
{ cls: "wiktku-dfn-kategori", label: "Kategorî", field: kategoriField },
{ cls: "wiktku-dfn-maneid", label: "Maneid", field: maneidField }
];
function groupHasContent(groupDiv) {
return groupDiv.find("textarea, input").filter(function () {
return $.trim($(this).val() || "") !== "";
}).length > 0;
}
// Shows a group and takes its chip / menu entry out of the picker -- both carry the same
// data-pick, so one selector covers whichever of the two this group had.
// Which group carries no divider above it: the first one actually on screen, not the
// first in the DOM -- see the .wiktku-dfn-subgroup-first rule.
function updateFirstVisible() {
var $all = block.find(".wiktku-dfn-subgroup").removeClass("wiktku-dfn-subgroup-first");
$all.not(".wiktku-dfn-subgroup-hidden").first().addClass("wiktku-dfn-subgroup-first");
}
function revealGroup(cls, focus) {
var groupDiv = block.find("." + cls + "-group");
if (!groupDiv.length) return;
groupDiv.removeClass("wiktku-dfn-subgroup-hidden");
block.find("[data-pick='" + cls + "']").hide();
updateFirstVisible();
if (focus) groupDiv.find("textarea, input").first().focus();
}
block.data("revealSenseGroup", revealGroup);
// Hides it again and gives the picker its entry back. Anything typed is thrown away, so a
// group holding something asks first.
function hideGroup(cls) {
var groupDiv = block.find("." + cls + "-group");
if (!groupDiv.length) return;
if (groupHasContent(groupDiv) &&
!window.confirm("Tiştên di vê qutiyê de winda dibin. Dewam bike?")) {
return;
}
groupDiv.children("ol").children("li").not(":first").remove();
groupDiv.find("textarea, input").val("");
// The jêgirtin row's source <select> owns the fields under it; putting it back to ""
// is what clears them.
groupDiv.find("select.wiktku-dfn-quote-src-sel").val("").trigger("change");
groupDiv.addClass("wiktku-dfn-subgroup-hidden");
block.find("[data-pick='" + cls + "']").show();
updateFirstVisible();
}
// Drafts, restoreDfn() and the ⊕-grown lists all put content in without going through
// revealGroup(), so anything that ended up filled is shown on the next refresh pass.
function syncPicks() {
$.each(PICK_DEFS, function (_, def) {
var groupDiv = block.find("." + def.cls + "-group");
if (groupDiv.length && groupDiv.hasClass("wiktku-dfn-subgroup-hidden") &&
groupHasContent(groupDiv)) {
revealGroup(def.cls, false);
}
});
$.each(CHIP_FIELD_DEFS, function (_, def) {
if (def.field.values().length) {
def.field.wrap.show();
block.find("[data-pick='" + def.cls + "']").hide();
}
});
}
block.data("syncSensePicks", syncPicks);
// Every group gets an × in its label, so a box opened by mistake can go away again --
// the counterpart of DefSideBoxes' own box-remove link.
$.each(PICK_DEFS, function (_, def) {
var groupDiv = block.find("." + def.cls + "-group");
if (!groupDiv.length) return;
var $x = $("<button type='button' class='wiktku-dfn-subgroup-remove'>×</button>")
.attr("title", def.label + " veşêre");
$x.on("click", function (e) {
e.preventDefault();
hideGroup(def.cls);
});
groupDiv.children(".wiktku-dfn-sublabel").first().append($x);
});
// — the chips, beside Dever/Şîwe —
$.each(PICK_DEFS, function (_, def) {
if (!def.chip || (isReconstruction && def.attested)) return;
var $chip = $("<button type='button' class='wiktku-dfn-chipfield-toggle'></button>")
.attr("data-pick", def.cls)
.text("+ " + def.label);
$chip.on("click", function (e) {
e.preventDefault();
revealGroup(def.cls, true);
});
chipFieldRow.append($chip);
});
// — "Hevmane û Bêhtir" —
var moreArrow = $("<span class='wiktku-dfn-more-arrow'>▸</span>");
var moreToggle = $("<button type='button' class='wiktku-dfn-chipfield-toggle wiktku-dfn-more-toggle'></button>")
.append(document.createTextNode(wiktkuUtil.msg.moreMenu + " ")).append(moreArrow);
var moreMenu = $("<div class='wiktku-dfn-more-menu'></div>").hide();
function closeMenu() {
moreMenu.hide();
moreArrow.html("▸");
}
moreToggle.on("click", function (e) {
e.preventDefault();
$(".wiktku-dfn-more-menu").not(moreMenu).hide();
moreMenu.toggle();
moreArrow.html(moreMenu.is(":visible") ? "▾" : "▸");
});
function menuEntry(label, cls, extraCls) {
var $link = $("<a href='#' class='wiktku-dfn-more-item'></a>").text(label);
if (cls) $link.attr("data-pick", cls);
if (extraCls) $link.addClass(extraCls);
moreMenu.append($link);
return $link;
}
// Group 1: the two label fields. Their toggle only joins the chip row once asked for.
$.each(CHIP_FIELD_DEFS, function (_, def) {
var $link = menuEntry("+ " + def.label, def.cls);
$link.on("click", function (e) {
e.preventDefault();
closeMenu();
def.field.wrap.show();
def.field.open();
$link.hide();
});
});
// Group 2: the word lists. wiktku-dfn-pick-word so a şablon sense hides these entries
// along with the groups themselves.
var firstWord = true;
$.each(PICK_DEFS, function (_, def) {
if (!def.word) return;
var $link = menuEntry("+ " + def.label, def.cls,
"wiktku-dfn-pick-word" + (firstWord ? " wiktku-dfn-more-group-start" : ""));
firstWord = false;
$link.on("click", function (e) {
e.preventDefault();
closeMenu();
revealGroup(def.cls, true);
});
});
// Group 3: no "Maneê rake" counterpart -- the tab row already carries one bin for the
// open sense, and a second delete in here would be a second way to do the same thing.
var $newSense = menuEntry(wiktkuUtil.msg.addAnotherSense, null, "wiktku-dfn-more-group-start");
$newSense.on("click", function (e) {
e.preventDefault();
closeMenu();
steps.dfn.addSense($("#wiktku-start-dfn"), block);
});
chipFieldRow.append($("<span class='wiktku-dfn-more-wrap'></span>").append(moreToggle, moreMenu));
bindMoreMenuCloser();
syncPicks();
updateFirstVisible();
}());
return block;
},
/** Shows only `block` among the sense list; hides every other one. */
setOpenSense: function (stepDiv, block) {
stepDiv.find("li.wiktku-dfn-sense").each(function () {
$(this).toggleClass("wiktku-sense-collapsed", !$(this).is(block));
});
steps.dfn.renderSenseTabs(stepDiv);
},
/** Rebuilds the "Mane 1 / Mane 2 / [trash] / + Maneyeke din" tab row from the current sense list. */
renderSenseTabs: function (stepDiv) {
var tabsDiv = stepDiv.find(".wiktku-dfn-sense-tabs");
if (!tabsDiv.length) return;
tabsDiv.empty();
var blocks = stepDiv.find("li.wiktku-dfn-sense");
var openLi = null;
blocks.each(function (i) {
var li = $(this);
var isOpen = !li.hasClass("wiktku-sense-collapsed");
if (isOpen) openLi = li;
var tab = $("<span class='wiktku-dfn-sense-tab'></span>")
.text("Mane " + (i + 1))
.toggleClass("wiktku-tab-active", isOpen);
tab.on("click", function () { steps.dfn.setOpenSense(stepDiv, li); });
tabsDiv.append(tab);
});
// One delete button for the whole bar — not one per tab — so
// there's only ever a single, deliberate hit-target for a
// destructive action, positioned away from the tabs people
// click to switch senses. Always keep at least one Mane.
if (blocks.length > 1 && openLi) {
var openIdx = blocks.index(openLi);
var trash = makeTrashButton("Vê maneyê jê bibe (Mane " + (openIdx + 1) + ")");
trash.on("click", function () {
if (!window.confirm("Tu dixwazî Mane " + (openIdx + 1) + " jê bibî? Ev tişt nayê vegerandin.")) return;
steps.dfn.removeSense(stepDiv, openLi);
});
tabsDiv.append(trash.$element);
}
var addBtn = $("<button type='button' class='wiktku-add-sense'></button>")
.append($("<span class='wiktku-add-sense-plus'>+</span>"))
.append(document.createTextNode(wiktkuUtil.msg.anotherSense));
addBtn.on("click", function (e) {
e.preventDefault();
steps.dfn.addSense(stepDiv);
});
tabsDiv.append(addBtn);
},
/** Adds a sense, opens it, and hides the others. With afterBlock, the new one lands directly
* under that sense rather than at the end — "Maneyeke din zêde bike" in a sense's own
* "Hevmane û Bêhtir" menu is asking for one next to *this* meaning. */
addSense: function (stepDiv, afterBlock) {
var list = stepDiv.find("ol.wiktku-dfn-list");
var newBlock = steps.dfn.buildSenseBlock(stepDiv);
if (afterBlock && afterBlock.length) newBlock.insertAfter(afterBlock);
else list.append(newBlock);
initSelects(newBlock);
steps.dfn.setOpenSense(stepDiv, newBlock);
newBlock.find("textarea.wiktku-dfn-mane").first().focus();
refresh();
return newBlock;
},
/** Removes one sense block. Assumes the caller already confirmed
* and already checked more than one sense remains. If the
* removed sense was the open one, opens whichever sense ends up
* taking its place (or the last one, if it was the last). */
removeSense: function (stepDiv, li) {
var list = stepDiv.find("ol.wiktku-dfn-list");
var blocks = list.find("li.wiktku-dfn-sense");
var removedIdx = blocks.index(li);
var wasOpen = !li.hasClass("wiktku-sense-collapsed");
li.remove();
if (wasOpen) {
var remaining = list.find("li.wiktku-dfn-sense");
var next = remaining.eq(removedIdx);
if (!next.length) next = remaining.last();
if (next.length) steps.dfn.setOpenSense(stepDiv, next);
}
steps.dfn.renderSenseTabs(stepDiv);
refresh();
},
/** Grows a sub-list (nimûne/formên alternatîv/hevmane/dijmane/termên koordîne) on input. */
onSubInput: function (list, input, rows, narrow, modifiers) {
var val = $.trim($(input).val());
var item = $(input).parent();
var isLast = item.is(list.children("li:last-child"));
if (val && isLast) {
var next = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-sub-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", rows)
.attr("class", "wiktku-dfn-subfield" + (narrow ? " wiktku-start-narrow" : ""));
next.on("input", function () {
steps.dfn.onSubInput(list, this, rows, narrow, modifiers);
});
list.append(modifiers ? makeModifierLi(next) : $("<li></li>").append(next));
// New field starts with no placeholder — give it (and any
// others in the list) the current one.
var updatePlaceholders = list.data("updatePlaceholders");
if (typeof updatePlaceholders === "function") updatePlaceholders();
}
},
// Populated from data-main.json (tplDefs, jederDefs).
tplDefs: {},
jederDefs: {},
build: function (stepDiv) {
if (stepDiv.find("ol.wiktku-dfn-list").length) return null;
var tabsDiv = $("<div class='wiktku-dfn-sense-tabs'></div>");
stepDiv.append(tabsDiv);
var list = $("<ol class='wiktku-dfn-list'></ol>");
list.append(steps.dfn.buildSenseBlock(stepDiv));
stepDiv.append(list);
steps.dfn.renderSenseTabs(stepDiv);
return null;
},
/** Reads all sense blocks from the DOM and returns an array of sense objects. */
getSenses: function (stepDiv) {
var senses = [];
stepDiv.find("li.wiktku-dfn-sense").each(function () {
var block = $(this);
var tplSel = block.find("select.wiktku-dfn-tpl-select");
var tpl = tplSel.val() || "";
// noLang: template already encodes the language — don't add |lang| param.
var tplNoLang = tpl
? tplSel.find("option[value='" + tpl + "']").attr("data-nolang") === "1"
: false;
var tplTitle = $.trim(block.find("input.wiktku-dfn-tpl-title").val());
// The mane box and the îzah box are one gloss on the page: "tercime {{îzah|…}}". Joined
// here so everything downstream (getOutput, the linkify pass, the cache) keeps seeing a
// single string, exactly as it did when the pair was two boxes.
var mane = $.trim(block.find("textarea.wiktku-dfn-mane").val());
var izah = $.trim(block.find("textarea.wiktku-dfn-izah").val());
if (izah) {
mane = $.trim(mane + " {{îzah|" + izah + "}}");
}
function readList(cls) {
var vals = [];
// Matches only the canonical field, not the visible word-input mirror makeModifierLi() adds alongside it.
block.find("." + cls + "-list textarea.wiktku-dfn-subfield").each(function () {
var v = $.trim($(this).val());
if (v) vals.push(v);
});
return vals;
}
// Nimûne items are compound: { text, tr?, t? }
function readNimuneList() {
var items = [];
block.find(".wiktku-dfn-nimune-list li.wiktku-dfn-nimune-li").each(function () {
var text = $.trim($(this).find(".wiktku-dfn-nimune-text").val());
if (!text) return;
var obj = { text: text };
var tr = $.trim($(this).find(".wiktku-dfn-nimune-tr").val());
var t = $.trim($(this).find(".wiktku-dfn-nimune-t").val());
if (tr) obj.tr = tr;
if (t) obj.t = t;
items.push(obj);
});
return items;
}
// Jêder is a growing list of citations, one per li — mirrors readNimuneList().
function readJederList() {
var items = [];
block.find(".wiktku-dfn-jeder-list li.wiktku-dfn-jeder-li").each(function () {
var li = $(this);
var quote = $.trim(li.find(".wiktku-dfn-nimune-quote").val());
if (!quote) return;
var obj = { quote: quote };
var qtr = $.trim(li.find(".wiktku-dfn-nimune-quote-tr").val());
var qt = $.trim(li.find(".wiktku-dfn-nimune-quote-t").val());
if (qtr) obj.quoteTr = qtr;
if (qt) obj.quoteT = qt;
var tplName = li.find(".wiktku-dfn-quote-src-sel").val();
var url = tplName === "__raw__" || tplName === "__category__" ? ""
: $.trim(li.find(".wiktku-dfn-quote-url").val());
if (url) obj.url = url;
if (tplName === "__manual__") {
var manual = $.trim(li.find(".wiktku-dfn-quote-src-manual").val());
if (manual) obj.jederManual = manual;
} else if (tplName === "__raw__") {
// The quote box already holds a finished citation template; it is written
// out untouched, so there is nothing else on this row to read.
obj.raw = true;
} else if (tplName === "__category__") {
// A picked Kategorî:Çavkanî template IS the citation template, with its own
// TemplateData parameters -- rendered into the same markup the jêder-* branch
// below uses, so one loop reads both.
var catName = li.data("catTemplate");
if (catName) {
obj.jederCatTpl = catName;
var catFields = {};
li.find(".wiktku-dfn-quote-src-input").each(function () {
var ck = $(this).attr("data-field-key");
var cv = $.trim($(this).val());
if (ck && cv) catFields[ck] = cv;
});
// A restore in progress has no boxes yet -- its TemplateData is still on
// the way -- so the values it is restoring stand in until they exist.
// See restoreDfn, which parks them here and clears them once they land.
if (!Object.keys(catFields).length) {
catFields = li.data("catFields") || {};
}
obj.jederCatFields = catFields;
var catLang = li.data("catLangParam");
if (catLang) obj.jederCatLangParam = catLang;
}
} else if (tplName) {
obj.jederTpl = tplName;
var fields = {};
li.find(".wiktku-dfn-quote-src-input").each(function () {
var k = $(this).attr("data-field-key");
var v = $.trim($(this).val());
if (k && v) fields[k] = v;
});
obj.jêderFields = wiktkuCite.withLinkAsUrl(tplName, fields, url);
}
items.push(obj);
});
return items;
}
// Dever/Şîwe, Etîket and Kategorî are chip fields (buildChipField),
// not textareas, so readList() doesn't reach them — each chip's
// value lives in data-value, scoped by the field's wrap class.
function readChipList(wrapCls) {
var vals = [];
block.find("." + wrapCls + "-wrap .wiktku-dfn-chipfield-chip").each(function () {
var v = $.trim($(this).attr("data-value") || "");
if (v) vals.push(v);
});
return vals;
}
var sense = {
tpl: tpl,
tplNoLang: tplNoLang,
tplTitle: tplTitle,
mane: mane,
maneid: $.trim(block.find("input.wiktku-dfn-maneid-input").val() || ""),
etiket: readChipList("wiktku-dfn-etiket"),
kategori: readChipList("wiktku-dfn-kategori"),
nimune: readNimuneList(),
jeder: readJederList(),
dever: readChipList("wiktku-dfn-dever")
};
// alternativ, hevmane, dijmane, koordine, jornav, jernav — one property each, named by
// senseGroups' "field" so getOutput() and restoreDfn() find them under the same names.
$.each(wordGroups(), function (_, g) {
sense[g.field] = readList(g.cls);
});
senses.push(sense);
});
return senses;
},
getOutput: function (values, callback) {
var stepDiv = $("#wiktku-start-dfn");
var senses = steps.dfn.getSenses(stepDiv);
var lang = values.lang;
// A sense counts as "filled" if it has a mane, a chosen template + title,
// or any other content (etîket/hevmane/nimûne/jêder…) worth keeping.
var hasMane = senses.some(function (s) {
return s.mane !== "" || (s.tpl && s.tplTitle) || senseHasExtraContent(s);
});
var defLines = []; // one entry per kept sense (goes to linkify)
var citeLines = []; // parallel — nimûne+jêder for that same sense
var izahLines = []; // parallel — raw îzah gloss for that same sense
// True only once some sense's *actual* dfn text (post tercime/îzah
// split) is non-empty.
var hasLinkableMane = false;
$.each(senses, function (_, sense) {
// Declared once here rather than with `var` in each branch
// below — the template-sense branch returns early, so only
// one of the two assignments ever actually runs, but both
// are lexically in this same function scope.
var idx, cite;
// — Template sense (e.g. {{guharto|ku|somepage}}) —
if (sense.tpl) {
if (!sense.tplTitle) return; // skip empty template senses
var tplManeidTpl = buildManeidTpl(sense, lang);
var tplKatTpl = buildSenseTpl(sense, "kategori", lang, "K");
var tplDeverTpl = buildDeverTpl(sense, lang);
var tplLabelTpl = buildEtiketTpl(sense, lang);
var tplCall = sense.tplNoLang
? "{{" + sense.tpl + "|" + sense.tplTitle + "}}"
: "{{" + sense.tpl + "|" + lang + "|" + sense.tplTitle + "}}";
idx = defLines.length;
cite = buildCite(sense, lang);
defLines.push("# " + (tplManeidTpl ? tplManeidTpl + " " : "") + (tplKatTpl ? tplKatTpl + " " : "") + (tplDeverTpl ? tplDeverTpl + " " : "") +
(tplLabelTpl ? tplLabelTpl + " " : "") + tplCall +
(cite ? "{{wiktku-cite-" + idx + "}}" : ""));
citeLines.push(cite);
izahLines.push(""); // template senses have no îzah
return;
}
// — Normal mane sense —
var dfn = sense.mane;
if (!dfn && !senseHasExtraContent(sense)) return; // truly empty
// Pull the îzah gloss (if any) out of the combined tercime+îzah
// string — it's spliced back in separately below, after its
// own linkify pass (see izahLines / the second linkifyText call).
var izahRaw = null;
var tercimeIzah = splitTercimeIzah(dfn);
if (tercimeIzah) {
dfn = tercimeIzah.tercime;
izahRaw = stripWikilinks(tercimeIzah.izah);
}
if (dfn) {
dfn = stripWikilinks(dfn);
// Sentence-case + trailing period are for ku definitions,
// which are written as full glosses.
if (lang === "ku") {
if (dfn[0] !== dfn[0].toUpperCase()) dfn = capitalize(dfn);
if (!dfn.match(/[.…?!)\]]$/)) dfn += ".";
}
// Real prose — this is what makes the sense worth running
// through the main linkify pass.
hasLinkableMane = true;
} else if (izahRaw === null) {
// No mane at all — this is just the "{{mane?|lang}}"
// placeholder, a bare template call with no plain words
// in it. Do NOT count it as linkable: sending nothing but
// bracketed templates through the main linkify pass hangs
// the linkify gadget (see the hasLinkableMane/hasIzah
// check below).
//
// Every language, not just ku: a sense kept for its jêgirtin or word lists
// alone still needs the marker, or the line goes out as a bare "# ".
dfn = "{{mane?|" + lang + "}}";
}
var maneidTpl = buildManeidTpl(sense, lang);
var katTpl = buildSenseTpl(sense, "kategori", lang, "K");
var deverTpl = buildDeverTpl(sense, lang);
var labelTpl = buildEtiketTpl(sense, lang);
var out = "# " + (maneidTpl ? maneidTpl + " " : "") + (katTpl ? katTpl + " " : "") + (deverTpl ? deverTpl + " " : "") + (labelTpl ? labelTpl + " " : "") + dfn;
idx = defLines.length;
var hasThisIzah = izahRaw !== null && $.trim(izahRaw) !== "";
// Bare {{...}} token — like {{wiktku-cite-N}}, this survives
// linkify's first pass untouched (it treats template calls as
// opaque), then gets swapped for {{îzah|…}} once the actual
// gloss text has been linkified in its own separate pass.
if (hasThisIzah) {
out += (dfn ? " " : "") + "{{wiktku-izah-" + idx + "}}";
}
function wordLine(tpl, words) {
if (!words.length) return "";
return "\n#: {{" + tpl + "|" + lang + "|" + words.join("|") + "}}";
}
// Template name and sense property both come from senseGroups, so this and
// DefSideBoxes write the same "#:" lines.
$.each(SENSE_GROUPS, function (_, g) {
out += wordLine(g.key, sense[g.field] || []);
});
cite = buildCite(sense, lang);
defLines.push(out + (cite ? "{{wiktku-cite-" + idx + "}}" : ""));
citeLines.push(cite);
izahLines.push(hasThisIzah ? izahRaw : "");
});
// Splice citeLines back into a linkified string, replacing each
// {{wiktku-cite-N}} token with its stored nimûne/jêder block.
function restoreCites(t) {
return t.replace(/\{\{wiktku-cite-(\d+)\}\}/g, function (_, n) {
return citeLines[parseInt(n, 10)] || "";
});
}
// Runs text through the linkify gadget if it's loaded, otherwise
// just passes it through unchanged. Shared by both linkify passes
// below (main mane text, and — separately — îzah glosses).
var linkifyText = wiktkuUtil.linkifyText;
var hasIzah = izahLines.some(function (t) { return t && $.trim(t) !== ""; });
var text = defLines.join("\n") + "\n";
if (!hasMane) {
// Nothing written yet: the sense is still marked as needing one, but no example
// placeholder goes with it.
text = "# {{mane?|" + lang + "}}\n";
callback(text);
return;
}
// Runs the îzah-only linkify pass against `base` (either the raw
// text, when there's no linkable mane to run the main pass on
// first, or the already-linkified main text). Shared by both
// branches below so the token-splice logic only lives once.
function runIzahPass(base) {
var izahJoined = $.map(izahLines, function (raw, idx) {
if (!raw) return undefined;
return "{{wiktku-izah-open-" + idx + "}}" + raw + "{{wiktku-izah-close-" + idx + "}}";
}).join("\n");
linkifyText(izahJoined, function (izahLinked) {
var izahByIdx = {};
var re = /\{\{wiktku-izah-open-(\d+)\}\}([\s\S]*?)\{\{wiktku-izah-close-\1\}\}/g;
var m;
while ((m = re.exec(izahLinked)) !== null) {
izahByIdx[parseInt(m[1], 10)] = m[2];
}
var withIzah = base.replace(/\{\{wiktku-izah-(\d+)\}\}/g, function (_, n) {
var linked = izahByIdx[parseInt(n, 10)];
return linked !== undefined ? "{{îzah|" + linked + "}}" : "";
});
callback(restoreCites(withIzah));
});
}
if (!hasLinkableMane) {
if (!hasIzah) {
// All filled senses are template senses (or otherwise have
// no plain text at all) — nothing here needs linkify.
callback(restoreCites(text));
return;
}
// There's no plain mane text anywhere (e.g. tercime left blank
// with only îzah filled in), so every defLine is just a bare
// template call with nothing to word-link. Sending a line
// like "# {{wiktku-izah-0}}" through the main linkify pass
// gives it no plain words to work with, makes the linkify
// gadget hang — so skip that pass entirely and run only
// the îzah pass, splicing straight into the unlinkified text.
var loadIzahOnly = function () { runIzahPass(text); };
mw.loader.using("ext.gadget.linkify", loadIzahOnly, loadIzahOnly);
return;
}
var didTryToLoad = function () {
// First pass: the main definition text (mane), same as before. On failure the text
// comes back unchanged, so the save carries on with what the user wrote -- only the
// automatic linking is lost, and saying so is better than losing it silently.
linkifyText(text, function (mainLinked, error) {
if (error) {
mw.notify(error, { type: "warn" });
}
if (!hasIzah) {
callback(restoreCites(mainLinked));
return;
}
// Second pass: îzah glosses, linkified via their own
// separate call. Each gloss is wrapped in its own
// {{wiktku-izah-open/close-N}} pair; like the existing
// {{wiktku-cite-N}} tokens, these sit inside double
// braces so linkify leaves the tokens themselves alone —
// only the raw gloss text between each pair gets linked.
runIzahPass(mainLinked);
});
};
mw.loader.using("ext.gadget.linkify", didTryToLoad, didTryToLoad);
}
},
tewandin: {
id: "tewandin",
name: "tewandin",
question_fmt: 'Tewandina "' + pageName + '" binivîse',
visible: function (values) {
return values.lang && values.pos;
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-tewandin-wrap").length) return null;
var wrap = $("<div class='wiktku-tewandin-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-tewandin-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-tewandin-text")
.attr("rows", 4);
wrap.append(ta);
stepDiv.append(wrap);
function autoPopulate() {
if (!ta.length) return;
var vals = getValues();
var c = vals.cinsiyet || "";
var p = vals.pos || "";
if (!pageName.includes(" ") && c && GENDER_BASE_LABELS[c] &&
(p === "navdêr" || p === "serenav") && vals.lang === "ku" &&
c !== "mndudil") {
var label = (c === "mnmane") ? "mê û nêr" : GENDER_BASE_LABELS[c];
var out = "{{ku-tewîn-nav|" + label;
if (p === "serenav") out += "|y";
out += "}}";
if (!$.trim(ta.val())) ta.val(out);
}
}
// The example placeholder only fits Kurmancî nouns — showing it
// for a verb or another language would suggest the wrong template.
function updatePlaceholder() {
if (!ta.length) return;
var vals = getValues();
ta.attr("placeholder", (vals.lang === "ku" && vals.pos === "navdêr") ? "{{ku-tewîn-nav|nêr}}" : "");
}
setTimeout(function () {
autoPopulate();
updatePlaceholder();
$("#wiktku-start-form").on("change.tewandin",
"#wiktku-start-lang-field, #wiktku-start-cinsiyet-field, #wiktku-start-pos-field",
function () { autoPopulate(); updatePlaceholder(); });
}, 0);
return ta;
},
output: function () {
var manualText = $.trim($("#wiktku-start-tewandin .wiktku-tewandin-text").val());
if (manualText) {
return "\n==== Tewandin ====\n" + manualText + "\n";
}
return "";
}
},
// resim — one or more images
resim: {
id: "resim",
name: "resim",
question_fmt: "Resmekê lê zêde bike",
visible: function (values) { return !!(values.lang && values.pos); },
// The rows are wikt.edit's, shared with DefSideBoxes, which offers the same three boxes.
field: null,
build: function (stepDiv) {
if (stepDiv.find(".wiktku-resim-wrap").length) return null;
steps.resim.field = wiktkuUtil.buildImageListField({
prefix: "wiktku-",
rowClass: "wiktku-posparams-row",
inputClass: "wiktku-start-step-field wiktku-start-narrow",
pageName: pageName
});
stepDiv.append($("<div class='wiktku-resim-wrap'></div>").append(steps.resim.field.$element));
return null;
},
/** {file, caption, alt} per filled row, with the caption wrapped for a non-Kurdish entry. */
getImages: function (values) {
var lang = (values && values.lang) || getValues().lang || "";
if (!steps.resim.field) return [];
return $.map(steps.resim.field.values(), function (image) {
return {
file: image.file,
caption: wiktkuWikitext.captionForLang(image.caption, lang),
alt: image.alt,
size: image.size,
extra: image.extra
};
});
},
output: function (values) {
var images = steps.resim.getImages(values);
if (!images.length) return "";
return $.grep($.map(images, wiktkuWikitext.buildImageLine), Boolean).join("\n") + "\n";
}
},
// h-bnr — {{h-bnr|Page1|Page2|…}} prepended before the lang section (shared, not per-section)
bnrh: {
id: "bnrh",
name: "herwiha binêre (h-bnr)",
question_fmt: "Şablona h-bnr li qismê herî jor a rûpelê",
//visible: function (values) { return !!values.lang; },
multiple: true,
narrow: true,
placeholder: "Ji bo 'test' bnr 'Test', 'teşt'",
output: function (values) {
var call = wiktkuWikitext.buildHbnrCall(values.bnrh);
return call ? call + "\n" : "";
}
},
bikaranin: {
id: "bikaranin",
name: "Notên bikaranînê",
question_fmt: "Notên bikaranînê yên li ser vê kelîmeyê",
visible: function (values) {
return values.lang && values.pos;
},
multiple: false,
narrow: false,
output: function (values) {
var text = $.trim(values.bikaranin || "");
if (!text) return "";
return "\n==== Notên bikaranînê ====\n" + text + "\n";
},
},
xwendin: {
id: "xwendin",
name: "Bêhtir bixwîne",
question_fmt: "Lînkên derveyî eleqedarî vê madeyê",
visible: function (values) {
return values.lang && values.pos;
},
multiple: false,
narrow: false,
placeholder: function (values) {
if (!values || !values.lang) {
return `* {{wpedî}} ji bo ku meqaleya Wîkîpediyayê lînk bidî`;
}
const ziman = values.lang === "ku" ? "" : `|ziman=${values.lang}`;
return `* {{wpedî${ziman}}} ji bo ku meqaleya Wîkîpediyayê lînk bidî`;
},
output: function (values) {
var text = $.trim(values.xwendin || "");
if (!text) return "";
return "\n==== Bêhtir bixwîne ====\n" + text + "\n";
},
},
alfabeyendin: {
id: "alfabeyendin",
name: "Bi alfabeyên din",
question_fmt: function (values) {
var base = "Nivîsîna vê kelîmeyê bi alfabeyên din"
var langLabel = values.lang ? getLangLabel(values.lang) : "";
if (langLabel) {
return base + " ên " + langLabel;
}
return base;
},
// Latin-script Kurdish itself, and languages with only one script
// on record, don't need this — but any language metadata.json
// lists more than one script for (data-main.json's "sc") clearly does.
visible: function (values) {
if (!(values.lang && values.pos)) return false;
var lang = values.lang;
return lang === "ku" || ZARAVA_LANG_CODES.indexOf(lang) !== -1 || langHasMultipleScripts(lang);
},
multiple: false,
narrow: false,
placeholder: "Ji bo kurmancî: * {{ku-ar|şiklê alfaba erebî}}",
output: function (values) {
var text = $.trim(values.alfabeyendin || "");
if (!text) return "";
return "\n==== Bi alfabeyên din ====\n" + text + "\n";
},
},
formalt: makeWordListStep("formalt", "Formên alternatîv ên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
hevmane: makeWordListStep("hevmane", "Hevmaneyên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
dijmane: makeWordListStep("dijmane", "Dijmaneyên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
jornav: makeWordListStep("jornav", "Jornavên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
jernav: makeWordListStep("jernav", "Jêrnavên vê kelîmeyê ji bo hemû maneyan eger te li binê maneyê nenivîsandibe"),
je: makeWordListStep("je", "Kelîmeyên jê çêbûyî binivîse"),
tekildar: makeWordListStep("tekildar", "Kelîmeyên ji aliyê etîmolojiyê ve têkildarî vê kelîmeyê binivîse"),
binereherwiha: makeWordListStep("binereherwiha", "Kelîmeyên din ên eleqedar binivîse"),
etym: {
id: "etym",
name: "etîmolojî",
question_fmt: "Etîmolojiya kelîmeyê binivîse",
// etymTpls populated from data-main.json
etymTpls: {},
visible: function (values) {
return values.lang && values.pos;
},
// Fills the insert-dropdown from the current etymTpls object.
refreshTplSelect: function () {
var sel = $("#wiktku-start-etym .wiktku-tpl-insert-select");
if (!sel.length) return;
sel.find("option[value!='']").remove();
$.each(steps.etym.etymTpls, function (name, tpl) {
sel.append($("<option></option>").val(tpl.snippet).text(name + " — " + tpl.desc));
});
markTemplateOptions(sel, function (opt) { return tplNameFromSnippet(opt.val()); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-etym-wrap").length) return null;
var wrap = $("<div class='wiktku-etym-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-etym-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-etym-text")
.attr("rows", 4)
.attr("placeholder", "Ji {{ji|en|ku|apple}}. yan rasterast binivîse…");
// Template insert dropdown — options also refreshed after data-main.json loads.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
$.each(steps.etym.etymTpls, function (name, tpl) {
sel.append($("<option></option>").val(tpl.snippet).text(name + " — " + tpl.desc));
});
sel.on("change", function () {
var snippet = $(this).val();
if (!snippet) return;
var lang = getValues().lang || "";
snippet = snippet.replace(/CODE/g, lang).replace(/SOURCELANG/g, "?");
$(this).val("");
sel.trigger("wiktku:refresh");
// Same param dialog DefSideBoxes' own Etîmolojî editor opens on a fresh insert: filled
// from the template's own TemplateData, with CODE/SOURCELANG already substituted above
// as the fields' starting values, so the user only has to replace the "?" placeholders
// rather than hunt for them in raw wikitext afterward.
wiktkuCite.openTemplateDialog(snippet).then(function (edited) {
if (!edited) return; // cancelled, or "Jê bibe" on a call that was never inserted
insertAtCursor(ta[0], edited);
ta.focus();
});
});
wrap.append(sel);
wrap.append(ta);
stepDiv.append(wrap);
initSelects(wrap);
return ta;
},
output: function (values) {
var text = $.trim($("#wiktku-start-etym .wiktku-etym-text").val());
if (text) {
return "\n==== Etîmolojî ====\n" + text + "\n";
}
// {{ji?}} (a redirect to {{bêetîmolojî}}) is only ever added automatically to Kurdish
// entries.
if (values.lang !== "ku") return "";
// Multi-word page names skip the {{ji?|lang}} placeholder entirely -- "source unknown?" doesn't apply to a phrase.
if (pageName.includes(" ")) return "";
// No etymology entered — emit {{ji?|lang}} as a placeholder.
// Skip it only when every filled sense is a şablon sense
// a completely empty entry with no şablon chosen still gets the placeholder
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasFilledTplSense = dfnSenses.some(function (s) {
return !!s.tpl && !!s.tplTitle;
});
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
if (hasFilledTplSense && !hasRegularMane) return "";
return "\n==== Etîmolojî ====\n{{ji?|" + values.lang + "}}\n";
} },
dunde: {
id: "dunde",
name: "dûnde",
question_fmt: "Eger ev kelîme ji vî zimanî ketibe zimanekî din, vî zimanî û kelîmeya wî binivîse",
visible: function (values) {
return !!values.pos;
},
disabledKeys: function (values) {
return {"navz": true, [values.lang]: true}
},
// Language dropdown + growing word-input list; a new row appends
// when the dropdown gets a value. Mirrors steps.trans, but flat.
buildRow: function (stepDiv) {
var row = $("<li class='wiktku-dunde-row wiktku-lang-row'></li>");
// Language selector — the searchable picker is applied after insertion into DOM.
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zimanekî bigere");
populateMenus(langSel, steps.dunde.keys || [], steps.dunde.disabledKeys);
// When a language is chosen, ensure a next empty row exists.
langSel.on("change", function () {
steps.dunde.onLangChange(stepDiv, row);
});
// Word list for this language.
var wordList = $("<ul class='wiktku-dunde-words wiktku-lang-row-words'></ul>");
var firstWord = steps.dunde.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
// The searchable picker is attached by initSelects() once the row
// is in the DOM (see onLangChange/build below).
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-dunde-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Kelîme…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-dunde-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.dunde.makeWordInput();
list.append($("<li></li>").append(next));
}
});
return input;
},
onLangChange: function (stepDiv, row) {
var list = stepDiv.find("ul.wiktku-dunde-list");
var isLast = row.is(list.children("li.wiktku-dunde-row:last-child"));
if (isLast) {
var newRow = steps.dunde.buildRow(stepDiv);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
// Keep disabled keys in sync across all lang selectors.
steps.dunde.syncDisabledKeys(stepDiv);
},
/** Disables already-selected languages in all other dropdowns. */
syncDisabledKeys: function (stepDiv) {
var taken = $.extend({}, steps.dunde.disabledKeys);
stepDiv.find("li.wiktku-dunde-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
stepDiv.find("li.wiktku-dunde-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
build: function (stepDiv) {
if (stepDiv.find("ul.wiktku-dunde-list").length) return null;
var list = $("<ul class='wiktku-dunde-list wiktku-lang-row-list'></ul>");
list.append(steps.dunde.buildRow(stepDiv));
stepDiv.append(list);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return null;
},
/** Reads all [langcode, words[]] rows from the DOM. */
getRows: function (stepDiv) {
var result = [];
stepDiv.find("li.wiktku-dunde-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-dunde-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
output: function () {
var stepDiv = $("#wiktku-start-dunde");
var rows = steps.dunde.getRows(stepDiv);
if (!rows.length) return "";
var lines = $.grep($.map(rows, function (row) {
return wiktkuWikitext.buildDundeLine(row[0], row[1]);
}), Boolean);
return "\n==== Dûnde ====\n" + lines.join("\n") + "\n";
}
},
zarava: {
id: "zarava",
name: "bi zaravayên din",
question_fmt: "'"+ pageName + "' di zaravayên din de çawa tê gotin?",
visible: function (values) {
return steps.trans.visible(values);
},
disabledKeys: {},
buildRow: function (group) {
var row = $("<li class='wiktku-zarava-row wiktku-lang-row'></li>");
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zaravayekî bigere");
populateMenus(langSel, steps.zarava.keys || [], steps.zarava.disabledKeys);
langSel.on("change", function () {
steps.zarava.onLangChange(group, row);
refresh();
});
var wordList = $("<ul class='wiktku-zarava-words wiktku-lang-row-words'></ul>");
var firstWord = steps.zarava.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-zarava-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Tercime…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-zarava-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.zarava.makeWordInput();
list.append($("<li></li>").append(next));
}
});
// Blur, not input -- refresh() re-syncs the whole form, same
// reasoning as the Mane textareas' blur-only refresh below.
input.on("blur", function () { refresh(); });
return input;
},
onLangChange: function (group, row) {
var list = group.find("ul.wiktku-zarava-list");
var isLast = row.is(list.children("li.wiktku-zarava-row:last-child"));
if (isLast) {
var newRow = steps.zarava.buildRow(group);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
steps.zarava.syncDisabledKeys(group);
},
/** Disables already-selected dialects in all other dropdowns within one group. */
syncDisabledKeys: function (group) {
var taken = $.extend({}, steps.zarava.disabledKeys);
group.find("li.wiktku-zarava-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
group.find("li.wiktku-zarava-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
/** Builds (or fetches) the .wiktku-zarava-group for one gloss. */
buildGroup: function (stepDiv, gi) {
var group = $("<div class='wiktku-zarava-group'></div>").attr("data-gloss-pos", gi);
var list = $("<ul class='wiktku-zarava-list wiktku-lang-row-list'></ul>");
list.append(steps.zarava.buildRow(group));
group.append(list);
stepDiv.append(group);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return group;
},
/** Syncs one .wiktku-zarava-group per current gloss (same logic as steps.trans.build). */
build: function (stepDiv) {
var glosses = steps.trans.glossList();
if (!glosses.length) {
stepDiv.find(".wiktku-zarava-group").remove();
return null;
}
var existingGroups = stepDiv.find(".wiktku-zarava-group");
if (existingGroups.length > glosses.length) {
existingGroups.slice(glosses.length).remove();
}
$.each(glosses, function (gi, gloss) {
var group = stepDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
if (!group.length) group = steps.zarava.buildGroup(stepDiv, gi);
var label = group.find(".wiktku-zarava-group-label");
if (glosses.length > 1) {
if (!label.length) {
label = $("<div class='wiktku-zarava-group-label'></div>");
group.prepend(label);
}
label.text(gloss.index + " " + gloss.text);
} else if (label.length) {
label.remove();
}
});
return null;
},
/** Reads the dialect rows for a single gloss group. */
getGroupTranslations: function (group) {
var result = [];
group.find("li.wiktku-zarava-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-zarava-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
/** Reads all rows from the DOM, grouped by gloss position. */
getTranslations: function (stepDiv) {
var result = [];
stepDiv.find(".wiktku-zarava-group").each(function () {
result.push(steps.zarava.getGroupTranslations($(this)));
});
return result;
},
output: function (values) {
if (values.lang !== "ku") return "";
var stepDiv = $("#wiktku-start-zarava");
var glosses = steps.trans.glossList();
if (!glosses.length) return "";
// A completely empty entry still gets a placeholder section
// to fill in later; an individual placeholder gloss is only
// skipped once some other sense already has real content.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
var blocks = $.map(glosses, function (gloss, gi) {
var group = stepDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
var rows = group.length ? steps.zarava.getGroupTranslations(group) : [];
if (!gloss.text && !rows.length && hasRegularMane) return undefined;
if (rows.length) {
rows.sort(function (a, b) {
var aLang = langsByCode[a[0]] || {};
var bLang = langsByCode[b[0]] || {};
return (aLang.sortKey || "").localeCompare(bLang.sortKey || "", "ku");
});
}
var wordLines = $.map(rows, function (row) {
var lang = row[0];
var words = row[1];
var wikiWords = words.map(function (w) {
return "{{W|" + lang + "|" + w + "}}";
}).join(", ");
return "* {{Z|" + lang + "}}: " + wikiWords;
}).join("\n");
var block = "{{werger-ser|" + (gloss.text || "?") + "|" + gloss.index + "}}";
if (wordLines) block += "\n" + wordLines;
block += "\n{{werger-bin}}";
return block;
});
if (!blocks.length) return "";
return "\n==== Bi zaravayên din ====\n" + blocks.join("\n") + "\n";
}
},
trans: {
id: "trans",
name: "werger",
question_fmt: "Tercimeyên vê maneyê bi zimanên din binivîse",
visible: function (values) {
if (!(values.lang === "ku" && values.pos)) return false;
// Hide only when every sense is a filled template sense (no plain
// gloss to attach translations to). A mix, or nothing filled in
// yet, still shows Werger.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRealSense = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
var hasFilledTplSense = dfnSenses.some(function (s) {
return s.tpl && s.tplTitle;
});
return hasRealSense || !hasFilledTplSense;
},
disabledKeys: {
"navz": true,
"ku": true,
"ckb": true,
"sdh": true,
"lki": true,
"hac": true,
"zza": true },
// Language dropdown + growing word-input list, scoped to one gloss's
// group (each gloss has its own group and row list).
buildRow: function (group) {
var row = $("<li class='wiktku-trans-row wiktku-lang-row'></li>");
// Language selector — the searchable picker is applied after insertion into DOM.
var langSel = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
langSel.attr("data-placeholder", "Li zimanekî bigere");
populateMenus(langSel, steps.trans.keys || [], steps.trans.disabledKeys);
// When a language is chosen, ensure a next empty row exists.
langSel.on("change", function () {
steps.trans.onLangChange(group, row);
refresh();
});
// Word list for this language.
var wordList = $("<ul class='wiktku-trans-words wiktku-lang-row-words'></ul>");
var firstWord = steps.trans.makeWordInput();
wordList.append($("<li></li>").append(firstWord));
// The searchable picker is attached by initSelects() once the row
// is in the DOM (see onLangChange/buildGroup below).
row.append(langSel).append(wordList);
return row;
},
makeWordInput: function () {
var input = $("<textarea></textarea>")
.attr("name", "wiktku-trans-word-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.attr("placeholder", "Tercime…");
input.on("input", function () {
var list = $(this).closest("ul.wiktku-trans-words");
var item = $(this).parent();
var val = $.trim($(this).val());
if (val && item.is(list.children("li:last-child"))) {
var next = steps.trans.makeWordInput();
list.append($("<li></li>").append(next));
}
});
// Blur, not input -- refresh() re-syncs the whole form, same
// reasoning as the Mane textareas' blur-only refresh below.
input.on("blur", function () { refresh(); });
return input;
},
onLangChange: function (group, row) {
var list = group.find("ul.wiktku-trans-list");
var isLast = row.is(list.children("li.wiktku-trans-row:last-child"));
if (isLast) {
var newRow = steps.trans.buildRow(group);
list.append(newRow);
// Attach the searchable picker now the row is in the DOM.
initSelects(newRow);
}
// Keep disabled keys in sync across this gloss's lang selectors
// only — the same language can be used for different senses.
steps.trans.syncDisabledKeys(group);
},
/** Disables already-selected languages in all other dropdowns within one group. */
syncDisabledKeys: function (group) {
var taken = $.extend({}, steps.trans.disabledKeys);
group.find("li.wiktku-trans-row select").each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
group.find("li.wiktku-trans-row select").each(function () {
var self = $(this);
var own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (!code) return;
$(this).prop("disabled", taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
},
// Builds (or fetches) the .wiktku-trans-group for one gloss.
buildGroup: function (stepDiv, gi) {
var group = $("<div class='wiktku-trans-group'></div>").attr("data-gloss-pos", gi);
var list = $("<ul class='wiktku-trans-list wiktku-lang-row-list'></ul>");
list.append(steps.trans.buildRow(group));
group.append(list);
stepDiv.append(group);
// Attach the searchable picker now the select is in the DOM.
initSelects(list);
return group;
},
// Syncs one .wiktku-trans-group per current gloss, keyed by
// data-gloss-pos so groups stay matched across rebuilds.
build: function (stepDiv) {
var glosses = steps.trans.glossList();
if (!glosses.length) {
stepDiv.find(".wiktku-trans-group").remove();
return null;
}
// Drop groups for glosses that no longer exist (always from the
// tail — groups are only ever appended in gloss order below).
var existingGroups = stepDiv.find(".wiktku-trans-group");
if (existingGroups.length > glosses.length) {
existingGroups.slice(glosses.length).remove();
}
$.each(glosses, function (gi, gloss) {
var group = stepDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
if (!group.length) group = steps.trans.buildGroup(stepDiv, gi);
// Only label groups when there's more than one gloss to
// distinguish between — a single gloss needs no caption.
var label = group.find(".wiktku-trans-group-label");
if (glosses.length > 1) {
if (!label.length) {
label = $("<div class='wiktku-trans-group-label'></div>");
group.prepend(label);
}
label.text(gloss.index + " " + gloss.text);
} else if (label.length) {
label.remove();
}
});
return null;
},
/** Reads the translation rows for a single gloss group. */
getGroupTranslations: function (group) {
var result = [];
group.find("li.wiktku-trans-row").each(function () {
var lang = $(this).find("select").val();
if (!lang) return;
var words = [];
$(this).find("ul.wiktku-trans-words textarea").each(function () {
var v = $.trim($(this).val());
if (v) words.push(v);
});
if (words.length) result.push([lang, words]);
});
return result;
},
// Reads all translation rows, grouped by gloss position.
getTranslations: function (stepDiv) {
var result = [];
stepDiv.find(".wiktku-trans-group").each(function () {
result.push(steps.trans.getGroupTranslations($(this)));
});
return result;
},
// Builds the numbered "{ index, gloss }" list, one per dfn sense
// with a real mane (skips template senses). index is 1-based and
// matches the sense's position in the "#" definition list.
// When nothing's filled in, returns one placeholder entry so
// Werger still has a group to attach to; when every sense is a
// filled template sense, stays empty (matches trans.visible()).
glossList: function () {
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var glosses = [];
var hasFilledTplSense = false;
$.each(dfnSenses, function (i, s) {
if (s.tpl) {
if (s.tplTitle) hasFilledTplSense = true;
return; // template sense — no gloss to caption with
}
var dfn = s.mane;
if (!dfn) {
// No mane, but other fields are filled — still gets a
// gloss group since dfn.getOutput() emits {{mane?}} for it.
if (senseHasExtraContent(s)) glosses.push({ index: i + 1, text: "" });
return;
}
if (dfn[0] !== dfn[0].toLowerCase()) dfn = decapitalize(dfn);
dfn = stripWikilinks(dfn).replace(/[.…?!)\]،؟]+$/, "");
glosses.push({ index: i + 1, text: dfn });
});
if (!glosses.length && !hasFilledTplSense) {
glosses.push({ index: 1, text: "" });
}
return glosses;
},
output: function (values) {
if (values.lang !== "ku") return "";
var stepDiv = $("#wiktku-start-trans");
var glosses = steps.trans.glossList();
// Every sense is a template sense — there's no real gloss
// anywhere and no group in the DOM either (glossList() only
// returns empty in that case), so skip the section entirely.
if (!glosses.length) return "";
// A completely empty entry (nothing filled in dfn at all, no
// şablon chosen either) still gets a placeholder Werger section
// to fill in later. Only
// skip an individual placeholder gloss below when some other
// sense actually has real content, making that particular
// placeholder redundant.
var dfnSenses = steps.dfn.getSenses($("#wiktku-start-dfn"));
var hasRegularMane = dfnSenses.some(function (s) {
return !s.tpl && (s.mane !== "" || senseHasExtraContent(s));
});
// Each gloss now has its own group of translation rows —
// attach every gloss's own words under its own werger-ser/-bin
// pair instead of dumping everything under the first one.
var blocks = $.map(glosses, function (gloss, gi) {
var group = stepDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
var rows = group.length ? steps.trans.getGroupTranslations(group) : [];
// Placeholder gloss (no real mane entered yet) with nothing
// translated into it. If some other sense already has real
// content, this placeholder is redundant — skip it. If the
// whole entry is empty, keep it so Werger still gets a
// section to fill in.
if (!gloss.text && !rows.length && hasRegularMane) return undefined;
if (rows.length) {
// Sort by Kurdish sort key.
rows.sort(function (a, b) {
var aLang = langsByCode[a[0]] || {};
var bLang = langsByCode[b[0]] || {};
return (aLang.sortKey || "").localeCompare(bLang.sortKey || "", "ku");
});
}
var wordLines = $.map(rows, function (row) {
var lang = row[0];
var words = row[1];
var wikiWords = words.map(function (w) {
return "{{W|" + lang + "|" + w + "}}";
}).join(", ");
return "* {{Z|" + lang + "}}: " + wikiWords;
}).join("\n");
var block = "{{werger-ser|" + (gloss.text || "?") + "|" + gloss.index + "}}";
if (wordLines) block += "\n" + wordLines;
block += "\n{{werger-bin}}";
return block;
});
if (!blocks.length) return "";
return "\n==== Werger ====\n" + blocks.join("\n") + "\n";
}
},
cavkani: {
id: "cavkani",
name: "çavkanî",
question_fmt: "Çavkaniyên din (ne jêgirtin) lê zêde bike",
visible: function (values) {
return !!values.pos;
},
// Fills the insert-dropdown from the members of [[Kategorî:Çavkanî]].
refreshTplSelect: function () {
var sel = $("#wiktku-start-cavkani .wiktku-tpl-insert-select");
if (!sel.length) return;
sel.find("option[value!='']").remove();
$.each(catSrcTemplates || [], function (_, entry) {
sel.append($("<option></option>").val(entry.target).text(entry.label));
});
markTemplateOptions(sel, function (opt) { return opt.val(); });
// Tell the picker the option list changed.
sel.trigger("wiktku:refresh");
},
build: function (stepDiv) {
if (stepDiv.find(".wiktku-cavkani-wrap").length) return null;
var wrap = $("<div class='wiktku-cavkani-wrap'></div>");
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-cavkani-" + $.now())
.attr("class", "wiktku-start-step-field wiktku-cavkani-text")
.attr("rows", 4)
.attr("placeholder", wiktkuUtil.msg.pickOrTypeTemplate);
// Template insert dropdown, populated from [[Kategorî:Çavkanî]] —
// options refreshed once the category members are fetched.
var sel = $("<select class='wiktku-tpl-insert-select'></select>");
sel.attr("data-placeholder", wiktkuUtil.msg.addTemplate);
sel.append($("<option value=''>— şablonek lê zêde bike —</option>"));
var status = $("<span class='wiktku-cavkani-status'></span>");
// Same param dialog DefSideBoxes' own Çavkanî picker opens: the chosen template's
// TemplateData becomes a form, so "rûpel=", "sernav=" and the rest are filled in before
// anything lands in the box, instead of a bare {{Ç:…}} to go back and complete by hand.
sel.on("change", function () {
var name = $(this).val();
$(this).val("");
sel.trigger("wiktku:refresh");
if (!name) return;
wiktkuCite.openTemplateDialog("{{" + name + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or removed a call that was never inserted
}
insertAtCursor(ta[0], "* " + edited + "\n");
ta.trigger("focus");
});
});
wrap.append(sel);
wrap.append(status).append(ta);
stepDiv.append(wrap);
// build() runs again for every new beş (see the "+ Beşeke din"
// handler), after the initial page-wide pass — so this select needs
// its own init or it stays a plain, unsearchable native <select>.
initSelects(wrap);
if (catSrcTemplates) {
steps.cavkani.refreshTplSelect();
} else {
status.text(wiktkuUtil.msg.templatesLoading);
fetchCatSrcTemplates().then(function () {
steps.cavkani.refreshTplSelect();
status.text("");
}, function () {
status.text(wiktkuUtil.msg.templatesFailed);
});
}
return ta;
},
output: function () {
var text = $.trim($("#wiktku-start-cavkani .wiktku-cavkani-text").val());
if (!text) return "";
return "\n==== Çavkanî ====\n" + text + "\n";
}
},
};
steps.trans.keys = steps.lang.choices;
steps.dunde.keys = steps.lang.choices;
steps.zarava.keys = getZaravaKeys(steps.lang.choices);
// This array only controls DOM build order — nothing else keys off it
// (see the comment above stepIdToGroup). Mane tab order: Mane (senses,
// with malûmatên pêşketî nested inside each, see steps.dfn.build) →
// Wêne → Bi zaravayên din → Werger, so dfn/resim/zarava/trans sit
// adjacent to each other in that order.
var ordered_steps = [
steps.lang, steps.pos, steps.posparams,
steps.cinsiyet, steps.jimar, steps.lektur, steps.tr,
// Reading order: the senses, then what belongs to the entry as a whole. Etîmolojî follows
// "Bi alfabeyên din" and keeps a permanent slot; dunde and the three "eleqedar" lists are
// reached through the Beşên din picker like every other optional section.
steps.dfn, steps.tewandin, steps.alfabeyendin, steps.etym, steps.zarava, steps.trans,
steps.dunde, steps.je, steps.tekildar, steps.binereherwiha,
// Bilêvkirin isn't part of any STEP_GROUPS tab, but still needs a
// slot here because this array is what builds the DOM.
steps.pron,
// Beşên din fields — behind the picker.
steps.resim,
steps.bnrh, steps.bikaranin, steps.formalt, steps.hevmane, steps.dijmane, steps.jornav, steps.jernav,
steps.cavkani, steps.xwendin];
// ── Sections ─────────────────────────────────────────────────────────────
// One group, no tab bar. What is always on screen: the senses (whose own blocks carry Nimûne,
// Jêgirtin, Hevmane and Dijmane — see steps.dfn.allGroups), Tewandin, alfabeyendin, zarava, trans
// and Etîmolojî. Everything else is added from the "Beşên din" picker, which sits at the top of the
// form; a newly-picked section is inserted directly under it (see renderBesenDinPicker()), so the
// most recently added one is always the one nearest the picker.
//
// Tewandin keeps its own free-text {{ku-tewîn-nav}} field and does NOT absorb the Cins/zayend form
// parameters (m=/n=/p=), which stay on the headword template.
// Behind the "Beşên din" picker: shown only after an explicit open, the way DefSideBoxes' own
// "+ Beşek lê zêde bike" works -- nothing is on screen until it is asked for.
var BESENDIN_STEP_IDS = ["resim", "formalt", "hevmane", "dijmane", "jornav", "jernav", "bikaranin",
"cavkani", "xwendin", "bnrh", "dunde", "je", "tekildar", "binereherwiha"];
// One group, so no tab bar: the senses, the fields that go with them, and Etîmolojî, which is common
// enough to keep a permanent slot. Everything else is reached through the picker above.
var STEP_GROUPS = [
{ id: "mane", label: "Mane", steps: ["dfn", "tewandin", "alfabeyendin", "zarava", "trans", "etym"] }
];
// Step id -> index into STEP_GROUPS; undefined for the always-visible ungrouped steps.
var stepIdToGroup = {};
$.each(STEP_GROUPS, function (gi, g) {
$.each(g.steps, function (_, sid) { stepIdToGroup[sid] = gi; });
});
// Step ids that belong to each section (not shared across sections).
var PER_SECTION_STEP_IDS = [
"pos", "posparams", "cinsiyet", "jimar", "lektur", "tr",
"resim", "dfn", "tewandin", "formalt", "bikaranin", "alfabeyendin", "hevmane", "dijmane", "jornav", "jernav", "je", "tekildar", "etym", "dunde", "binereherwiha", "zarava", "trans", "cavkani", "xwendin"
];
// ── Multi-section state ──────────────────────────────────────────────────
// Each entry is a plain snapshot: { pos, cinsiyet, lektur, tr, dfn (senses[]),
// je, tekildar, binereherwiha, trans (rows[]) }.
var sections = [{}]; // section 0 starts empty (DOM is the truth)
var activeSection = 0;
// ── Bilêvkirin as a standalone section ───────────────────────────────
// Pronunciation belongs to the word, not to one part of speech, so it
// sits beside Navdêr/Lêker in the tab bar rather than inside any of them.
// Modelled as a flag rather than an entry in `sections` since
// activeSection is used as an array index in many places — viewing
// Bilêvkirin leaves activeSection pointing at the POS section you were on.
var viewingPron = false; // is it the tab currently on screen
// Which STEP_GROUPS tab is currently shown. Lives out here (like
// activeSection above) so refresh() — which is not nested inside build()
// — can read it and so renderGroupTabs()/updateGroupTabIndicators() below
// can be called from refresh() too. Both operate on the tab bar via its
// #wiktku-group-tabs id rather than a closured element reference, for the
// same reason.
var activeGroup = 0;
// Unsaved-changes / autosave state. Lives out here (same reason as
// activeGroup above) so destroy() — which runs on final submit and on
// switching to the source editor, and is not nested inside build() —
// can clear the timers and the beforeunload warning. build() assigns
// into these; it doesn't re-declare them.
var formDirty = false;
var autosaveDebounceTimer = null;
var autosaveInterval = null;
/** Re-renders the step-group tab bar to reflect the current activeGroup. */
// Scrolls the given tab bar to the top of the viewport, below the sticky offset, so a switched-to tab
// does not open off-screen. Deferred one frame: switching can shorten the page, and the browser may
// clamp the scroll position before the measurement runs.
function scrollTabBarIntoView(barId) {
window.requestAnimationFrame(function () {
var bar = document.getElementById(barId);
if (!bar) return;
var form = document.getElementById("wiktku-start-form");
var stabsH = form ? (parseFloat(form.style.getPropertyValue("--wiktku-stabs-h")) || 0) : 0;
var siteHeaderH = form ? (parseFloat(form.style.getPropertyValue("--wiktku-siteheader-h")) || 0) : 0;
var top = bar.getBoundingClientRect().top + window.pageYOffset - stabsH - siteHeaderH;
$("html, body").animate({ scrollTop: Math.max(0, top) }, 200);
});
}
function renderGroupTabs() {
var groupTabsDiv = $("#wiktku-group-tabs");
if (!groupTabsDiv.length) return;
// One group is nothing to switch between. Done here rather than only where the bar is built,
// because the form is rebuilt on language/part-of-speech changes and a bar left over from an
// earlier build would otherwise sit there until refresh() next got round to hiding it.
if (STEP_GROUPS.length < 2) {
groupTabsDiv.remove();
return;
}
groupTabsDiv.empty();
$.each(STEP_GROUPS, function (i, g) {
var tab = $("<span></span>")
.addClass("wiktku-group-tab")
.attr("data-group-idx", i)
.text(g.label)
.toggleClass("wiktku-tab-active", i === activeGroup);
(function (idx) {
tab.on("click", function () {
if (idx === activeGroup) return;
activeGroup = idx;
renderGroupTabs();
renderBesenDinPicker();
refresh();
scrollTabBarIntoView("wiktku-group-tabs");
});
}(i));
groupTabsDiv.append(tab);
});
}
/**
* Toggles a "needs attention" dot on any group tab that still has a
* required, unfilled field hidden inside it — so switching tabs doesn't
* leave the person wondering why "Bişîne" stays disabled.
*/
function updateGroupTabIndicators(values) {
var tabs = $("#wiktku-group-tabs .wiktku-group-tab");
if (!tabs.length) return;
$.each(STEP_GROUPS, function (i, g) {
var missing = false;
$.each(g.steps, function (_, sid) {
var step = steps[sid];
if (!step || !step.required) return;
var val = values[sid];
if (val === "" || (step.multiple && $.isArray(val) && val.join("") === "")) {
missing = true;
}
});
tabs.eq(i).toggleClass("wiktku-tab-missing", missing && i !== activeGroup);
});
}
// "Beşên din" reveals one field at a time via a picker rather than
// stacking them all. Per-section state, snapshotted in
// saveCurrentSection() and restored in restoreSection() below, and
// auto-seeded from any field that already has content (so a returning
// draft doesn't hide something already filled in).
var openedOtherSections = {};
var BESENDIN_SET = {};
$.each(BESENDIN_STEP_IDS, function (_, sid) { BESENDIN_SET[sid] = true; });
// The picker sits at the top of the form, above the senses, and is on screen whenever the form is.
/** Re-renders the "Beşên din" picker to list only the not-yet-opened sections. */
function renderBesenDinPicker() {
var wrap = $("#wiktku-besendin-picker");
if (!wrap.length) return;
wrap.empty();
var pickerValues = getValues();
// Every section it offers belongs to a part-of-speech section, so there is nothing to add until
// one is chosen. Checked here rather than only in refresh(): this function is also called on form
// build and on section changes, and each of those would otherwise show the picker on its own.
if (!pickerValues.pos) {
wrap.hide();
return;
}
var unopened = $.grep(BESENDIN_STEP_IDS, function (sid) {
if (openedOtherSections[sid]) return false;
// Don't offer a section that would open onto nothing: several of
// these are gated (Bi zaravayên din / Werger need lang=ku, most
// need a pos). refresh() would hide it again straight away, so
// picking it would read as a broken dropdown.
var step = steps[sid];
if (step && typeof step.visible === "function") {
try {
if (!step.visible(pickerValues)) return false;
} catch (e) { /* a step whose visible() needs DOM we lack yet — offer it */ }
}
return true;
});
if (!unopened.length) {
wrap.hide(); // everything's open — nothing left to pick
return;
}
// Same voice and styling as a step's own question line, not a section heading -- this picker sits
// between cards rather than being one.
wrap.append($("<label class='wiktku-start-step-question'></label>").text("Binebeşek lê zêde bike"));
var sel = $("<select class='wiktku-besendin-select'></select>");
sel.attr("data-placeholder", "— binebeşên din hilbijêre —");
sel.append($("<option value=''>— binebeşên din hilbijêre —</option>"));
$.each(unopened, function (_, sid) {
var step = steps[sid];
if (!step) return;
sel.append($("<option></option>").val(sid).text(capitalize(step.name)));
});
sel.on("change", function () {
var sid = $(this).val();
if (!sid) return;
openedOtherSections[sid] = true;
// Newly opened section moves right under the picker, ahead of
// whatever's already open, instead of wherever it happened to
// sit in the fixed step order.
$("#wiktku-start-" + sid).insertAfter(wrap);
renderBesenDinPicker();
refresh();
});
wrap.append(sel);
initSelects(wrap);
wrap.show();
}
/** Marks any BESENDIN_STEP_IDS field that already has real content as
* "opened", so restoring a draft doesn't bury filled-in sections behind
* the picker. Reuses each step's own output() as the emptiness check —
* it already knows what "empty" means per field, and it reads the DOM
* the restore just populated. Call this AFTER the DOM has the restored
* values in it. */
function seedOpenedFromContent() {
var values = getValues();
$.each(BESENDIN_STEP_IDS, function (_, sid) {
if (openedOtherSections[sid]) return;
var step = steps[sid];
if (!step || typeof step.output !== "function") return;
try {
if (step.output(values)) openedOtherSections[sid] = true;
} catch (e) { /* a step whose output() needs more state than we have yet — skip it */ }
});
}
function snapshotDfn() {
return steps.dfn.getSenses($("#wiktku-start-dfn"));
}
// Rebuilds the dfn DOM from a previously snapshotted senses array.
function restoreDfn(senses) {
var stepDiv = $("#wiktku-start-dfn");
// Tear down the existing tab bar + list so build() recreates them cleanly.
stepDiv.find(".wiktku-dfn-sense-tabs").remove();
stepDiv.find("ol.wiktku-dfn-list").remove();
steps.dfn.build(stepDiv);
var list = stepDiv.find("ol.wiktku-dfn-list");
$.each(senses, function (i, sense) {
// Ensure enough blocks exist (build() seeds one).
var blocks = list.find("li.wiktku-dfn-sense");
var block = blocks.eq(i);
if (!block.length) {
var nb = steps.dfn.buildSenseBlock(stepDiv);
list.append(nb);
block = list.find("li.wiktku-dfn-sense").last();
}
// Template selector.
block.find("select.wiktku-dfn-tpl-select").val(sense.tpl || "");
if (sense.tpl) {
block.find("textarea.wiktku-dfn-mane").hide();
block.find("input.wiktku-dfn-tpl-title").val(sense.tplTitle || "").show();
// The picker is collapsed behind a link by default; a restored
// sense that already uses a template has to show it, or the
// chosen template is invisible and looks lost.
var reveal = block.data("revealTplRow");
if (typeof reveal === "function") reveal();
} else {
block.find("textarea.wiktku-dfn-mane").val(sense.mane || "").show();
block.find("input.wiktku-dfn-tpl-title").hide().val("");
}
// dfnArea is the only thing actually saved, so split it back into
// tercime/îzah for editing (mirrors syncNonKuToMane's join format).
var maneParts = splitTercimeIzah(sense.mane);
block.find("textarea.wiktku-dfn-mane").val(maneParts ? $.trim(maneParts.tercime) : (sense.mane || ""));
block.find(".wiktku-dfn-izah").val(maneParts ? $.trim(maneParts.izah) : "");
var setIzahOpen = block.data("setIzahOpen");
if (setIzahOpen) setIzahOpen(!!(maneParts && $.trim(maneParts.izah)));
// Re-sync the formên alternatîv/hevmane/… hide state now that
// tplSelect's value was set (its own "change"
// handler doesn't fire from .val(), so this doesn't happen otherwise).
var updateVis = block.data("updateFieldVisibility");
if (updateVis) updateVis();
// Same for the mane-box mode (plain vs tercime+îzah), since this
// also depends on tplSelect's (and lang's) value.
var updateMane = block.data("updateManeMode");
if (updateMane) updateMane();
// Restore a sub-list (etiket, formên alternatîv, hevmane …) from a values array.
function restoreSubList(cls, values) {
if (!values || !values.length) return;
var groupDiv = block.find("." + cls + "-group");
var subList = groupDiv.find("ol, ul").first();
if (!subList.length) return; // group not found
var useModifiers = isWordGroupCls(cls);
// Fill existing items, append new ones as needed.
$.each(values, function (j, val) {
var items = subList.find("li");
if (j < items.length) {
// useModifiers rows keep the word and its modifier tags in
// separate elements (see makeModifierLi) — setRawValue()
// re-splits val() the same way parsing it fresh would,
// instead of dumping the raw "word<key:value>" string into
// the visible word box.
var setRaw = items.eq(j).data("setRawValue");
if (useModifiers && typeof setRaw === "function") {
setRaw(val);
} else {
items.eq(j).find("textarea").val(val);
}
} else {
var ta = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-restore-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow")
.val(val);
ta.on("input", function () {
steps.dfn.onSubInput(subList, this, 1, true, useModifiers);
});
subList.append(useModifiers ? makeModifierLi(ta) : $("<li></li>").append(ta));
}
});
if (!useModifiers) return;
// A fresh empty row goes on the end so there's always somewhere
// to add the next word without first having to click into a
// filled one.
var filled = subList.find("li.wiktku-word-li");
if (filled.last().is(":last-child")) {
var nextTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-restore-next-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 1)
.attr("class", "wiktku-dfn-subfield wiktku-start-narrow");
nextTa.on("input", function () {
steps.dfn.onSubInput(subList, this, 1, true, useModifiers);
});
subList.append(makeModifierLi(nextTa));
}
}
// Populate the word lists that the draft has something for. senseGroups' "field" is both the
// property getSenses() read them into and the tail of the box's own class.
$.each(wordGroups(), function (_, g) {
var vals = sense[g.field];
if (vals && vals.length) restoreSubList(g.cls, vals);
});
// Dever/Şîwe, Etîket, Kategorî — chip fields, not textarea
// sub-lists, restored via the adders buildSenseBlock stashed on
// the block.
block.find("input.wiktku-dfn-maneid-input").val(sense.maneid || "");
var chipRestoreMap = {
addDeverChip: sense.dever,
addEtiketChip: sense.etiket,
addKategoriChip: sense.kategori
};
$.each(chipRestoreMap, function (dataKey, vals) {
var addChip = block.data(dataKey);
if (addChip && vals && vals.length) {
$.each(vals, function (_, v) { addChip(v); });
}
});
// Nimûne items are objects { text, tr?, t? } — restore using the builder.
if (sense.nimune && sense.nimune.length) {
var nimuneGroupDiv = block.find(".wiktku-dfn-nimune-group");
var nimuneList = nimuneGroupDiv.find("ol.wiktku-dfn-nimune-list");
var buildFn = nimuneGroupDiv.data("buildNimuneLi");
nimuneList.empty();
$.each(sense.nimune, function (_, n) {
var text = typeof n === "object" ? (n.text || "") : n;
if (!text) return;
var li = buildFn ? buildFn() : $("<li class='wiktku-dfn-nimune-li'></li>");
li.find(".wiktku-dfn-nimune-text").val(text).each(function () { highlightLinebreak(this); });
if (n.tr) li.find(".wiktku-dfn-nimune-tr").val(n.tr);
if (n.t) li.find(".wiktku-dfn-nimune-t").val(n.t);
nimuneList.append(li);
});
// Always leave a blank trailing item.
if (buildFn) nimuneList.append(buildFn());
}
// Restore jêder entries — accepts current array shape or a legacy
// single object (older saved sections) for backwards compat.
var jederArr = $.isArray(sense.jeder) ? sense.jeder : (sense.jeder ? [sense.jeder] : []);
if (jederArr.length) {
var jederGroupDiv = block.find(".wiktku-dfn-jeder-group");
var jederList = jederGroupDiv.find("ol.wiktku-dfn-jeder-list");
var buildJederFn = jederGroupDiv.data("buildJederLi");
jederList.empty();
$.each(jederArr, function (_, j) {
if (!j || !j.quote) return;
var li = buildJederFn ? buildJederFn() : $("<li class='wiktku-dfn-jeder-li'></li>");
li.find(".wiktku-dfn-nimune-quote").val(j.quote).each(function () { highlightLinebreak(this); });
if (j.quoteTr) li.find(".wiktku-dfn-nimune-quote-tr").val(j.quoteTr);
if (j.quoteT) li.find(".wiktku-dfn-nimune-quote-t").val(j.quoteT);
if (j.url) li.find(".wiktku-dfn-quote-url").val(j.url);
if (j.jederManual) {
var manualSel = li.find(".wiktku-dfn-quote-src-sel");
manualSel.val("__manual__").trigger("change");
li.find(".wiktku-dfn-quote-src-manual").val(j.jederManual);
} else if (j.raw) {
li.find(".wiktku-dfn-quote-src-sel").val("__raw__").trigger("change");
} else if (j.jederCatTpl) {
// Reopen the category picker on the saved template and put its fields back. The
// fields arrive asynchronously (TemplateData), so the saved values are applied
// once they exist rather than straight away.
var catSelEl = li.find(".wiktku-dfn-quote-src-sel");
catSelEl.val("__category__").trigger("change");
li.data("catTemplate", j.jederCatTpl);
if (j.jederCatLangParam) li.data("catLangParam", j.jederCatLangParam);
li.find(".wiktku-dfn-quote-src-cat-input").val(j.jederCatTpl);
// Kept on the row from the moment the restore starts. Submitting walks every section
// -- restore, then read -- and the read is synchronous while the boxes below only
// exist once TemplateData comes back, so without this the values are read from boxes
// that are not there yet and every parameter but the quote is lost. readJederList()
// falls back to these until the boxes catch up.
li.data("catFields", j.jederCatFields || {});
wiktkuCite.fieldSpecs(j.jederCatTpl).then(function (spec) {
var srcFields = li.find(".wiktku-dfn-quote-src-fields");
li.data("catLangParam", j.jederCatLangParam || spec.langParamKey || null);
$.each(spec.fields, function (_, field) {
srcFields.append(li.data("buildSrcFieldRow")(field));
});
$.each(j.jederCatFields || {}, function (key, val) {
li.find(".wiktku-dfn-quote-src-input[data-field-key='" + key + "']").val(val);
});
// The boxes hold them now, so they are the truth again -- clearing this keeps a
// field the user empties from being refilled from the stale copy.
li.removeData("catFields");
});
} else if (j.jederTpl) {
var srcSel = li.find(".wiktku-dfn-quote-src-sel");
srcSel.val(j.jederTpl).trigger("change");
if (j.jêderFields) {
$.each(j.jêderFields, function (key, val) {
li.find(".wiktku-dfn-quote-src-input[data-field-key='" + key + "']").val(val);
});
}
}
jederList.append(li);
});
// Always leave a blank trailing item, same as Nimûne.
if (buildJederFn) jederList.append(buildJederFn());
}
});
// Unlike addSense(), build()/buildSenseBlock() above don't wrap their own <select>s -- do it once for the whole restored list (idempotent, so already-wrapped selects are skipped).
initSelects(list);
// Everything above filled boxes that are still behind the chip row, so each block is asked to
// show whichever of its sub-fields the draft actually had something in.
list.find("li.wiktku-dfn-sense").each(function () {
var syncPicks = $(this).data("syncSensePicks");
if (syncPicks) syncPicks();
});
// Restoring a draft opens the first sense's tab by default.
steps.dfn.setOpenSense(stepDiv, list.find("li.wiktku-dfn-sense").first());
}
// Snapshots simple (non-dfn) per-section steps from the DOM into obj.
function saveCurrentSection() {
var snap = {};
$.each(PER_SECTION_STEP_IDS, function (_, id) {
if (id === "dfn") {
snap.dfn = snapshotDfn();
return;
}
if (id === "etym") {
snap.etym = $.trim($("#wiktku-start-etym .wiktku-etym-text").val());
return;
}
if (id === "cavkani") {
snap.cavkani = $.trim($("#wiktku-start-cavkani .wiktku-cavkani-text").val());
return;
}
if (id === "posparams") {
snap.posparams = $.trim($("#wiktku-start-posparams .wiktku-posparams-text").val());
return;
}
if (id === "resim") {
// One entry per row, including blank ones, so a mid-edit draft
// (e.g. a filename typed but no caption yet) survives a cache
// restore instead of being silently dropped.
snap.resim = $("#wiktku-start-resim li.wiktku-resim-item").map(function () {
return {
file: $.trim($(this).find(".wiktku-resim-file").val()),
caption: $.trim($(this).find(".wiktku-resim-caption").val()),
alt: $.trim($(this).find(".wiktku-resim-alt").val())
};
}).get();
return;
}
var step = steps[id];
if (!step) return;
var stepDiv = $("#wiktku-start-" + id);
if (id === "trans") {
snap.trans = steps.trans.getTranslations(stepDiv);
return;
}
if (id === "zarava") {
snap.zarava = steps.zarava.getTranslations(stepDiv);
return;
}
if (id === "dunde") {
snap.dunde = steps.dunde.getRows(stepDiv);
return;
}
// Generic: read all step fields.
var fields = stepDiv.find(".wiktku-start-step-field");
if (step.multiple) {
snap[id] = fields.map(function () { return $(this).val(); }).get();
} else {
snap[id] = fields.val();
}
});
// Which "Beşên din" sub-sections this section has already opened
// via the picker — per-section, like everything else here.
snap.openedOtherSections = $.extend({}, openedOtherSections);
sections[activeSection] = snap;
}
// Restores a previously saved snapshot into the form DOM.
function restoreSection(snap) {
$.each(PER_SECTION_STEP_IDS, function (_, id) {
if (id === "dfn") {
restoreDfn(snap.dfn || []);
return;
}
if (id === "etym") {
$("#wiktku-start-etym .wiktku-etym-wrap").remove();
steps.etym.build($("#wiktku-start-etym"));
$("#wiktku-start-etym .wiktku-etym-text").val(snap.etym || "");
return;
}
if (id === "cavkani") {
$("#wiktku-start-cavkani .wiktku-cavkani-wrap").remove();
steps.cavkani.build($("#wiktku-start-cavkani"));
$("#wiktku-start-cavkani .wiktku-cavkani-text").val(snap.cavkani || "");
return;
}
if (id === "posparams") {
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-step").remove();
steps.posparams.build(ppDiv);
var savedTa = snap.posparams || "";
ppDiv.find(".wiktku-posparams-text").val(savedTa);
// Re-parse saved textarea to restore helper widget states.
if (savedTa) {
// Read with the shared parser, so a nested "c=" is never taken for the call's own, and a
// saved "z=" restores the gender the same as "c=".
var savedC = wiktkuWikitext.callParamValue(savedTa, "c");
var savedJ = wiktkuWikitext.callParamValue(savedTa, "j");
var savedTr = wiktkuWikitext.callParamValue(savedTa, "tr");
// Write into the shared hidden field (not the visible
// select, which has no <option>s yet) — syncHelpers()
// below reads it back to restore the dropdown once the
// gender/number options for this language are populated.
if (savedC) $("#wiktku-start-cinsiyet-field").val(savedC);
if (savedJ) $("#wiktku-start-jimar-field").val(savedJ);
if (savedTr) ppDiv.find(".wiktku-posparams-field[data-pp-key='tr']").val(savedTr);
// For lêker, c= holds the verb-type code instead of a
// gender — split it back across the two dropdowns it is
// now assembled from ("x-gh" → form "x" + base "gh").
if (savedC && /^lêker/.test(snap.pos || "")) {
var lekturParts = /^(?:([xphb])-)?(n?gh)$/.exec(savedC);
if (lekturParts) {
ppDiv.find(".wiktku-posparams-lektur-sel").val(lekturParts[2]);
ppDiv.find(".wiktku-posparams-lekturform-sel").val(lekturParts[1] || "");
}
}
// Restore checked optional params via the same param/value overrides rebuildTa uses.
ppDiv.find(".wiktku-posparams-opt-panel input[type='checkbox']").each(function () {
var key = $(this).val();
var override = wiktkuPosparams.OPT_PARAM_OVERRIDES[key];
var needle = override ? "|" + override.param + "=" + override.value : "|" + key + "=1";
$(this).prop("checked", savedTa.indexOf(needle) !== -1);
});
}
steps.posparams.syncHelpers(ppDiv);
return;
}
if (id === "resim") {
var resimDiv = $("#wiktku-start-resim");
resimDiv.find(".wiktku-resim-wrap").remove();
steps.resim.build(resimDiv);
// Accept both the new array-of-rows format and the older
// single {file, caption} object saved before multi-image
// support existed.
var resimSnap = snap.resim;
var rows = $.isArray(resimSnap) ? resimSnap : (resimSnap ? [resimSnap] : []);
// setValues() keeps a blank trailing row of its own, same as Nimûne/Dûnde.
if (rows.length && steps.resim.field) steps.resim.field.setValues(rows);
return;
}
if (id === "trans") {
// Rebuild trans groups (dfn must already be restored by this
// point in the $.each loop, so glossList() reflects it).
var transDiv = $("#wiktku-start-trans");
transDiv.find(".wiktku-trans-group").remove();
steps.trans.build(transDiv);
var groupsSnap = snap.trans || [];
$.each(groupsSnap, function (gi, rows) {
var group = transDiv.find(".wiktku-trans-group[data-gloss-pos='" + gi + "']");
if (!group.length) return; // gloss no longer exists — drop its rows
var list = group.find("ul.wiktku-trans-list");
$.each(rows, function (j, row) {
var lang = row[0], words = row[1];
var lastRow = list.find("li.wiktku-trans-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-trans-words");
$.each(words, function (k, word) {
var wordItems = wordList.find("li");
if (k < wordItems.length) {
wordItems.eq(k).find("textarea").val(word);
} else {
// .val() below doesn't fire "input", so the normal
// auto-grow never runs — append a matching item
// ourselves instead of silently dropping the word.
var extraInput = steps.trans.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
// Leave one trailing empty field so the row can still grow
// when the user types, matching a freshly-built row.
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.trans.makeWordInput()));
}
});
});
return;
}
if (id === "zarava") {
// Rebuild zarava groups (dfn must already be restored by this
// point in the $.each loop, so glossList() reflects it).
var zaravaDiv = $("#wiktku-start-zarava");
zaravaDiv.find(".wiktku-zarava-group").remove();
steps.zarava.build(zaravaDiv);
var zaravaGroupsSnap = snap.zarava || [];
$.each(zaravaGroupsSnap, function (gi, rows) {
var group = zaravaDiv.find(".wiktku-zarava-group[data-gloss-pos='" + gi + "']");
if (!group.length) return; // gloss no longer exists — drop its rows
var list = group.find("ul.wiktku-zarava-list");
$.each(rows, function (j, row) {
var lang = row[0], words = row[1];
var lastRow = list.find("li.wiktku-zarava-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-zarava-words");
$.each(words, function (k, word) {
var wordItems = wordList.find("li");
if (k < wordItems.length) {
wordItems.eq(k).find("textarea").val(word);
} else {
var extraInput = steps.zarava.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.zarava.makeWordInput()));
}
});
});
return;
}
if (id === "dunde") {
// Rebuild dunde rows.
var dundeDiv = $("#wiktku-start-dunde");
dundeDiv.find("ul.wiktku-dunde-list").remove();
steps.dunde.build(dundeDiv);
var dundeRows = snap.dunde || [];
$.each(dundeRows, function (i, row) {
var lang = row[0], words = row[1];
var list = dundeDiv.find("ul.wiktku-dunde-list");
var lastRow = list.find("li.wiktku-dunde-row").last();
lastRow.find("select").val(lang).trigger("change");
var wordList = lastRow.find("ul.wiktku-dunde-words");
$.each(words, function (j, word) {
var wordItems = wordList.find("li");
if (j < wordItems.length) {
wordItems.eq(j).find("textarea").val(word);
} else {
// .val() below doesn't fire "input", so the normal
// auto-grow never runs — append a matching item
// ourselves instead of silently dropping the word.
var extraInput = steps.dunde.makeWordInput();
wordList.append($("<li></li>").append(extraInput));
extraInput.val(word);
}
});
// Leave one trailing empty field so the row can still grow
// when the user types, matching a freshly-built row.
if ($.trim(wordList.children("li").last().find("textarea").val())) {
wordList.append($("<li></li>").append(steps.dunde.makeWordInput()));
}
});
return;
}
var step = steps[id];
if (!step) return;
var stepDiv = $("#wiktku-start-" + id);
var fields = stepDiv.find(".wiktku-start-step-field");
var val = snap[id];
if (val === undefined || val === null) return;
if (step.multiple) {
var arr = val;
fields.each(function (i) {
if (i < arr.length) $(this).val(arr[i]);
});
} else {
fields.val(val);
}
// Tell the pickers about the programmatic value change.
fields.filter("select").trigger("wiktku:refresh");
});
// Restore which "Beşên din" sub-sections were already opened for
// this section, then seed in anything the DOM restore above just
// filled in but that wasn't marked opened yet.
openedOtherSections = $.extend({}, snap.openedOtherSections || {});
seedOpenedFromContent();
renderBesenDinPicker();
}
function getWikitextForCurrentSection(callback) {
var values = getValues();
var entry = steps.pos.output(values);
// Bilêvkirin is NOT emitted here — it is one shared field for the
// whole word, so emitting it per section duplicated it. getWikitext()
// prints it once, above the POS sections.
steps.dfn.getOutput(values, function (output) {
entry += output;
entry += steps.tewandin.output(values);
entry += steps.bikaranin.output(values);
entry += steps.alfabeyendin.output(values);
entry += steps.formalt.output(values);
entry += steps.hevmane.output(values);
entry += steps.dijmane.output(values);
entry += steps.jornav.output(values);
entry += steps.jernav.output(values);
entry += steps.je.output(values);
entry += steps.tekildar.output(values);
entry += steps.etym.output(values);
entry += steps.dunde.output(values);
entry += steps.binereherwiha.output(values);
entry += steps.zarava.output(values);
entry += steps.trans.output(values);
entry += steps.cavkani.output(values);
entry += steps.xwendin.output(values);
callback(entry, values);
});
}
// Two sections of the same part of speech would otherwise both be "=== Rengdêr ===", which makes them
// indistinguishable on the page, in the table of contents and in every link that targets one. Run over
// the finished sections rather than guessed from the pos alone, so "Rengdêr" and "Formeke rengdêrê"
// -- different headings from the same pos -- are correctly left alone.
function numberRepeatedHeadings(results) {
var headingRE = /^(===\s*)([^=\n]+?)(\s*===)$/m;
var counts = {};
$.each(results, function (_, text) {
var m = headingRE.exec(text || "");
if (m) counts[m[2]] = (counts[m[2]] || 0) + 1;
});
var seen = {};
$.each(results, function (i, text) {
var m = headingRE.exec(text || "");
if (!m || counts[m[2]] < 2) {
return;
}
seen[m[2]] = (seen[m[2]] || 0) + 1;
results[i] = text.replace(headingRE, m[1] + m[2] + " " + seen[m[2]] + m[3]);
});
}
function getWikitextAllSections(callback) {
saveCurrentSection();
var results = new Array(sections.length);
var sharedValues = getValues(); // lang/pron are shared
function processSection(i) {
// Each hop through here can be reached asynchronously: the dfn
// step waits on mw.loader.using("ext.gadget.linkify") before
// calling back, so from that point on the submit handler's own
// try/catch is no longer on the stack. Guard every hop instead,
// or a throw after linkify loads vanishes into an unhandled
// rejection and the form just sits there looking busy.
try {
if (i >= sections.length) {
// All done — restore original active section.
restoreSection(sections[activeSection]);
refresh();
numberRepeatedHeadings(results);
callback(results, sharedValues);
return;
}
restoreSection(sections[i]);
refresh();
getWikitextForCurrentSection(function (sectionText) {
results[i] = sectionText;
processSection(i + 1);
});
} catch (e) {
if (typeof onSubmitError === "function") onSubmitError(e);
else throw e;
}
}
processSection(0);
}
// Assembles wikitext for all sections plus the shared lang header.
function getWikitext(callback) {
getWikitextAllSections(function (sectionTexts, sharedValues) {
var bnrh = steps.bnrh.output(sharedValues);
var entry = steps.lang.output(sharedValues);
// One pronunciation per word, printed once between the language
// header and the POS sections — see steps.pron.output.
entry += steps.pron.output(sharedValues);
entry += sectionTexts.join("\n");
// Reconstruction-namespace pages (ns 108) mark the language
// section with {{jinûvesazkirî|langcode}} directly above it —
// only prepended when generating fresh wikitext, same as h-bnr
// below (addLangMode handles an existing page separately).
if (!addLangMode && ns === 108) entry = "{{jinûvesazkirî|" + sharedValues.lang + "}}\n" + entry;
// h-bnr goes before the language section — only prepended when
// generating fresh wikitext (not addLangMode, which handles it separately).
if (!addLangMode && bnrh) entry = bnrh + entry;
// Edit summary uses section 0's values. Read from the saved snapshot
// (not the live DOM) so it doesn't mix section 0's pos with whichever
// section happens to be active/restored at this point.
var dfnSenses = (sections[0] && sections[0].dfn) || [];
var dfnSummary = dfnSenses.map(function (s) {
if (s.mane) return s.mane;
// Template sense (e.g. {{guharto|ku|somepage}}) — same
// construction as the tplCall built in getOutput, so the
// summary shows what actually gets written to the entry
// instead of going blank.
if (s.tpl && s.tplTitle) {
return s.tplNoLang
? "{{" + s.tpl + "|" + s.tplTitle + "}}"
: "{{" + s.tpl + "|" + sharedValues.lang + "|" + s.tplTitle + "}}";
}
return "";
}).filter(Boolean).join("; ");
var summary = "([[Mediawiki:Gadget-start.js|WF:NÛ]] v.2.0) +" +
sharedValues.lang + ":" + sections[0].pos + ":" + dfnSummary;
var summaryField = document.getElementById("wpSummary");
if (addLangMode && summaryField.value) {
summary = summaryField.value + " " + summary;
}
summaryField.value = summary;
callback(entry);
});
}
var continueBtn;
// Set by build() to its submitFailed(). Lives out here because the
// wikitext-assembly functions below are outside build()'s closure but
// still need a way to report a failure to the person rather than dying
// silently in the console.
var onSubmitError = null;
// Whether Mane/Bi zaravayên din/Werger has anything in it — kept
// outside refresh() so the click handler can read the latest value.
function getValuesForStep(stepId) {
var fields = $("#wiktku-start-" + stepId + " .wiktku-start-step-field");
var step = steps[stepId];
if (step.keys) {
var values = {};
fields.each(function (i, field) {
var key = $(field).prevAll(".wiktku-start-step-keyfield").val();
if (key) values[key] = $(field).val();
});
return values;
}
else if (step.multiple) {
return fields.map(function () {
return $(this).val();
}).get();
}
return fields.val();
}
function getValues() {
var values = {};
$.each(steps, function (stepId) {
values[stepId] = getValuesForStep(stepId);
});
return values;
}
function populateMenus(inputs, choices, disabledValues, placeholderText) {
if (!disabledValues) disabledValues = {};
var vals = inputs.map(function (i, input) {
return $(input).val();
});
inputs.empty();
inputs.append($("<option></option>").text(placeholderText || ""));
$.each(choices, function (j, choice) {
// Append the language code in parentheses so that typing a code
// (e.g. "ku") in the search box still finds the language name.
var displayText = capitalize(choice.label);
if (choice.sortKey !== undefined) {
// This is a language choice {code, label, sortKey} — show code.
displayText = capitalize(choice.label) + " (" + choice.code + ")";
}
var option = $(mw.html.element("option", {
value: choice.code }, displayText));
if (disabledValues[choice.code]) option.prop("disabled", true);
inputs.append(option);
});
inputs.val(function (i) {
return vals[i];
});
}
// Keys to disable for a key menu: already-taken or explicitly disabled.
function getDisabledKeys(step) {
var occupiedKeys = {};
$("#wiktku-start-" + step.id + " .wiktku-start-step-keyfield").each(function () {
occupiedKeys[$(this).val()] = true;
});
if (step.disabledKeys) $.extend(occupiedKeys, step.disabledKeys);
return occupiedKeys;
}
function buildKeyInput(step) {
if (!step.keys) return undefined;
var input = $(mw.html.element("select", {
"class": "wiktku-start-step-keyfield wiktku-start-narrow" }));
input.attr("data-placeholder", "Hilbijêre " + step.keyName);
populateMenus(input, step.keys, getDisabledKeys(step));
// Keep the key menus mutually exclusive: whatever one row selects is
// disabled in every other row. Recomputed from the DOM on each change.
input.on("change", function () {
var all = $("#wiktku-start-" + step.id + " .wiktku-start-step-keyfield");
var taken = {};
all.each(function () {
var v = $(this).val();
if (v) taken[v] = true;
});
all.not(this).each(function () {
var self = $(this), own = self.val();
self.find("option").each(function () {
var code = $(this).val();
if (code) $(this).prop("disabled", !!taken[code] && code !== own);
});
self.trigger("wiktku:refresh");
});
});
// Return the wrapper; the select lives inside it and still fires change.
return wrapNativeSelect(input);
}
// Adds another text field to the list if the last one now has text.
function refreshForTextField(currentInput, step) {
currentInput = $(currentInput);
var value = currentInput.val();
var item = currentInput.parent();
if (value && !item.next("li").length) {
var nextInput = $("<textarea></textarea>")
.attr("name", "wiktku-field-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("class", currentInput.attr("class"))
.attr("rows", currentInput.attr("rows") || 1);
if (step.placeholder) {
nextInput.attr("placeholder", typeof step.placeholder === "function" ? step.placeholder() : step.placeholder);
}
nextInput.on("input", function (evt) {
refreshForTextField(evt.target, step);
});
var nextKeyInput = buildKeyInput(step);
var nextItem = step.modifiers
? makeModifierLi(nextInput).prepend(nextKeyInput || "")
: $("<li></li>").append(nextKeyInput).append(nextInput);
item.parent().append(nextItem);
if (nextKeyInput) initSelects(nextItem);
}
var canContinue = !continueBtn.isDisabled();
if ((value && !canContinue) || (!value && canContinue)) {
refresh(step);
}
}
function prepareInput(input, step) {
if (!input) return;
if (step.required) input.prop('required', true);
input.change(step, refresh);
}
// Shows/hides steps based on whether their prerequisites are met, and
// updates placeholders where applicable.
function refresh(evt) {
var values = getValues();
var changedStep = evt && evt.data;
var canContinue = true;
// Re-filter template options in every sense block whenever lang changes.
$("#wiktku-start-dfn .wiktku-dfn-tpl-select").each(function () {
var fn = $(this).data("updateOptions");
if (typeof fn === "function") fn();
});
// Re-filter the pronunciation template dropdown when language changes -- etym/cavkani's lists don't depend on lang, so they're only built once at data load instead of on every refresh().
steps.pron.refreshTplSelect();
// Re-render any step's question label that depends on current values
// (e.g. dfn's Werger-section reminder, which only applies to ku).
$.each(steps, function (stepId, step) {
if (typeof step.question_fmt === "function") {
$("#wiktku-start-" + stepId + " .wiktku-start-step-question")
.text(step.question_fmt(values));
}
});
// Pos's definition/example text, right under its dropdown — see
// steps.pos.example and the .wiktku-start-pos-example build above.
$("#wiktku-start-pos .wiktku-start-pos-example").text(steps.pos.example(values));
// Sync every sense block's mane-box mode (plain mane vs tercime+îzah)
// to the current language, in case lang changes after blocks exist.
// Same pass also re-syncs Dever/Şîwe's default open/closed state
// (open for ku, closed otherwise — see updateDeverOpenState) and
// reveals any sub-field group that has ended up with content in it
// while still behind the chip row (a draft, a restore).
$("#wiktku-start-dfn li.wiktku-dfn-sense").each(function () {
var updateMane = $(this).data("updateManeMode");
if (updateMane) updateMane();
var updateDeverOpen = $(this).data("updateDeverOpenState");
if (updateDeverOpen) updateDeverOpen();
var syncPicks = $(this).data("syncSensePicks");
if (syncPicks) syncPicks();
});
// Sync tr/t sub-fields in every nimûne item to the current language.
// tr shown when language needs transliteration; t once a language is chosen.
(function () {
var lang = values.lang || "";
var showTr = needsTransliteration(lang);
var showT = !!lang;
$("#wiktku-start-dfn .wiktku-dfn-nimune-list li.wiktku-dfn-nimune-li").each(function () {
var li = $(this);
// Ensure the sub-wrapper exists when we need it.
var subWrap = li.find(".wiktku-dfn-nimune-sub");
// tr field
var trTa = li.find(".wiktku-dfn-nimune-tr");
if (showTr && !trTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
li.append(subWrap);
}
trTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-tr")
.attr("placeholder", "Transliterasyon…");
subWrap.prepend(trTa);
} else if (!showTr && trTa.length) {
trTa.remove();
}
// t field
var tTa = li.find(".wiktku-dfn-nimune-t");
if (showT && !tTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-nimune-sub");
li.append(subWrap);
}
tTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 5)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-t");
subWrap.append(tTa);
} else if (!showT && tTa.length) {
tTa.remove();
}
tTa.attr("placeholder", nimuneTranslationPrompt(lang));
// Remove the wrapper if it ended up empty.
subWrap = li.find(".wiktku-dfn-nimune-sub");
if (subWrap.length && !subWrap.children().length) subWrap.remove();
});
}());
// Re-sync the "<label> bi <ziman>…" placeholder on every always-visible
// sub-field list (etîket, kategorî, hevmane, …), since the language may
// not have been chosen yet when these lists were first built, or may
// have changed since.
$("#wiktku-start-dfn .wiktku-dfn-sublist").each(function () {
var upd = $(this).data("updatePlaceholders");
if (typeof upd === "function") upd();
});
// Same idea for word-list steps (jê, têkildar, binêre herwiha, …)
// whose placeholder is a function of the current language.
$.each(steps, function (stepId, step) {
if (typeof step.placeholder === "function") {
$("#wiktku-start-" + stepId + " textarea.wiktku-start-step-field")
.attr("placeholder", step.placeholder());
}
});
// Sync tr/t sub-fields in every jêder citation to the current language.
// tr shown when language needs transliteration; t shown when lang !== "ku".
(function () {
var lang = values.lang || "";
var showTr = needsTransliteration(lang);
var showT = lang && lang !== "ku";
$("#wiktku-start-dfn .wiktku-dfn-jeder-list li.wiktku-dfn-jeder-li").each(function () {
var li = $(this);
// Ensure the sub-wrapper exists when we need it. It must sit right
// after the quote textarea and before the source selector.
var subWrap = li.find(".wiktku-dfn-quote-sub");
// tr field
var trTa = li.find(".wiktku-dfn-nimune-quote-tr");
if (showTr && !trTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
// Insert after the quote textarea's resize-wrap (if the manual
// resize handle has already been attached to it), not after the
// bare textarea -- otherwise this lands *inside* that wrap,
// between the textarea and its own grip, breaking jêgirtin's
// handle and nesting the tr/t boxes inside it.
var quoteTa = li.find(".wiktku-dfn-nimune-quote");
var quoteResizeWrap = quoteTa.closest(".wiktku-resize-wrap");
(quoteResizeWrap.length ? quoteResizeWrap : quoteTa).after(subWrap);
}
trTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-tr-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-tr")
.attr("placeholder", "Transliterasyona jêgirtinê…");
subWrap.prepend(trTa);
} else if (!showTr && trTa.length) {
trTa.remove();
}
// t field
var tTa = li.find(".wiktku-dfn-nimune-quote-t");
if (showT && !tTa.length) {
if (!subWrap.length) {
subWrap = $("<div></div>").addClass("wiktku-dfn-quote-sub");
// See the tr-field branch above for why this targets the
// resize-wrap instead of the bare textarea.
var quoteTa2 = li.find(".wiktku-dfn-nimune-quote");
var quoteResizeWrap2 = quoteTa2.closest(".wiktku-resize-wrap");
(quoteResizeWrap2.length ? quoteResizeWrap2 : quoteTa2).after(subWrap);
}
tTa = $("<textarea></textarea>")
.attr("name", "wiktku-dfn-nimune-quote-t-" + $.now() + "-" + Math.random().toString(36).substr(2, 9))
.attr("rows", 2)
.attr("class", "wiktku-dfn-subfield wiktku-dfn-nimune-quote-t")
.attr("placeholder", "Tercimeya jêgirtinê bi kurmancî…");
subWrap.append(tTa);
} else if (!showT && tTa.length) {
tTa.remove();
}
// Remove the wrapper if it ended up empty.
subWrap = li.find(".wiktku-dfn-quote-sub");
if (subWrap.length && !subWrap.children().length) subWrap.remove();
});
}());
$.each(steps, function (stepId, step) {
var groupIdx = stepIdToGroup[stepId];
var hiddenByTab = (groupIdx !== undefined && groupIdx !== activeGroup) ||
// Beşên din picker: stays hidden even while its tab is
// active until explicitly opened from the dropdown.
(BESENDIN_SET[stepId] && !openedOtherSections[stepId]);
// Bilêvkirin is its own section now, so the two views are
// mutually exclusive: on it, only pron shows (plus Ziman, which
// is shared and stays above every section); off it, pron is
// hidden no matter which tab is active. pron is deliberately
// ungrouped — stepIdToGroup has no entry for it — so without
// this branch it would show on all four tabs at once.
if (stepId === "pron") {
hiddenByTab = hiddenByTab || !viewingPron;
} else if (viewingPron && stepId !== "lang") {
hiddenByTab = true;
}
if (hiddenByTab || (typeof(step.visible) === "function" && !step.visible(values))) {
$("#wiktku-start-" + stepId).hide();
}
else {
var stepDiv = $("#wiktku-start-" + stepId);
if (changedStep !== step && typeof(step.build) === "function") {
prepareInput(step.build(stepDiv, values), step);
}
// After build, sync the posparams helper widgets whenever pos or lang changes.
if (step.id === "posparams" && typeof step.syncHelpers === "function") {
step.syncHelpers(stepDiv);
}
var input = stepDiv.find(".wiktku-start-step-field");
if (typeof(step.example) === "function") {
// .find() above already reaches nested fields (e.g. the dfn
// step's mane textareas inside each sense block), so one
// assignment covers every step; dfn keeps its own fallback.
var exampleText = step.example(values) || (step.id === "dfn" ? "Mane…" : "");
input.attr("placeholder", exampleText);
}
stepDiv.slideDown();
}
if (step.required &&
(values[stepId] === "" ||
(step.multiple && values[stepId].join("") === ""))) {
canContinue = false;
}
});
// One group means no bar to show; it only appears again if STEP_GROUPS ever grows back.
$("#wiktku-group-tabs").toggle(STEP_GROUPS.length > 1 && !!values.pos && !viewingPron);
// Bilêvkirin hides the picker outright; otherwise renderBesenDinPicker() decides, showing itself
// only once a part of speech is chosen and there are sections left to offer. The decision lives
// there rather than here because that function has other callers, which would each need the same
// test.
var besenDinActive = !viewingPron;
if (!besenDinActive) {
$("#wiktku-besendin-picker").hide();
}
// The option list is filtered by each step's visible(), which depends
// on lang/pos — so it has to be rebuilt when those change, not just
// when a section is opened. Safe to call from here: rendering the
// picker doesn't itself call refresh(), only picking an option does.
if (besenDinActive) renderBesenDinPicker();
updateGroupTabIndicators(values);
// Ziman and Cureyê rêzimanî are all it takes: an entry with a heading and a template but no
// definition yet is a skeleton somebody means to come back to, not a mistake to be blocked.
continueBtn.setDisabled(!canContinue);
}
function destroy() {
if (autosaveDebounceTimer) clearTimeout(autosaveDebounceTimer);
if (autosaveInterval) clearInterval(autosaveInterval);
formDirty = false;
$(window).off("beforeunload.wiktkuUnsaved");
$("#wiktku-start-form, .noafterwizard").slideUp(function () {
$(this).remove();
});
$(".noarticletext .nowizard").slideDown();
// #editform was kept laid-out but off-screen (see build()) instead of
// display:none, precisely so the editor never had to size itself
// against a zero-width/height box. Just bring it back on-screen —
// no slideDown needed since it was never actually collapsed.
$("#editform").removeClass("wiktku-offscreen");
// Belt-and-braces: if WikiEditor/CodeMirror/mobile's auto-grow still
// cached a stale size from some earlier point, nudge them to
// recalculate now that the box is genuinely visible.
$(window).trigger("resize");
if (window.CodeMirror) {
var cm = $("#wpTextbox1").data("CodeMirror") || $("#wpTextbox1").data("cmInstance");
if (cm && typeof cm.refresh === "function") cm.refresh();
}
$("#ca-edit").removeClass("selected");
$("#ca-wiktku-nowizard").addClass("selected");
}
// Priority order for Kurdish dialects and navz.
// Codes listed here come before all other languages; unlisted codes
// are sorted alphabetically by their display name via localeCompare.
var LANG_PRIORITY = ["navz", "ku", "hac", "sdh", "lki", "ckb", "zza"];
// Inserts a new "== {{ziman|code}} ==" section at the correct sorted
// position: navz, then ku/hac/sdh/lki/ckb/zza, then alphabetical by name.
function insertNewLangSection(existing, wikitext) {
var newCodeMatch = wikitext.match(new RegExp("^\n?" + langHeaderSource("[^}]+")));
var newCode = newCodeMatch ? newCodeMatch[1] : "";
var sections = [];
var preamble = "";
var anyHeaderSrc = "\n?" + langHeaderSource("[^}]+");
var firstMatch = new RegExp("^" + anyHeaderSrc, "m").exec(existing);
if (!firstMatch) {
// No existing lang sections — just write the new section.
// Add \n\n only when there is real non-whitespace content above.
var trimmed = existing.replace(/\s+$/, "");
return trimmed ? trimmed + "\n\n" + wikitext : wikitext;
}
// Everything before the first == {{ziman|...}} == is the preamble.
var firstSectionStart = firstMatch.index;
// Walk back to the start of that line.
var lineStart = existing.lastIndexOf("\n", firstSectionStart - 1);
var preambleEnd = lineStart !== -1 ? lineStart + 1 : 0;
preamble = existing.slice(0, preambleEnd);
// Parse each language section: header line + everything until the next header.
var body = existing.slice(preambleEnd);
// (?![\s\S]) matches only true end-of-string, since a bare "$" with
// /m would stop at the section's first line break instead.
var langSectionRe = new RegExp(
"^(" + anyHeaderSrc + "\\n?)([\\s\\S]*?)(?=\\n" + anyHeaderSrc + "|(?![\\s\\S]))",
"gm"
);
var m;
while ((m = langSectionRe.exec(body)) !== null) {
sections.push({
code: m[2],
content: m[0] // full text of this section including header
});
}
// Determine sort rank: priority list first (by index), then alphabetical.
function getLangName(code) {
var entry = langsByCode[code];
return entry ? entry.label : code;
}
function sortRank(code) {
var idx = LANG_PRIORITY.indexOf(code);
return idx !== -1 ? idx : LANG_PRIORITY.length;
}
function compareLangs(a, b) {
var ra = sortRank(a), rb = sortRank(b);
if (ra !== rb) return ra - rb;
// Both are outside priority list — sort by display name.
return getLangName(a).localeCompare(getLangName(b), userLang);
}
// Insert the new section into the sorted list.
var newSection = { code: newCode, content: wikitext };
var inserted = false;
for (var i = 0; i < sections.length; i++) {
if (compareLangs(newCode, sections[i].code) < 0) {
sections.splice(i, 0, newSection);
inserted = true;
break;
}
}
if (!inserted) sections.push(newSection);
// Re-join: preamble + sections, each separated by exactly \n\n.
// Trim trailing whitespace off each section's content first so we
// control the spacing precisely.
var joined = sections.map(function (s) {
return s.content.replace(/\s+$/, "");
}).join("\n\n");
// Only add a separator after the preamble when it has real content.
var preambleTrimmed = preamble.replace(/\s+$/, "");
var prefix = preambleTrimmed ? preambleTrimmed + "\n\n" : "";
return prefix + joined + "\n";
}
function build() {
$(".nowizard, .nowizard .noafterwizard").hide();
var callToAction = $("#wiktku-calltoaction");
var origCallToActionText = callToAction.text();
$(mw.util.addPortletLink("p-views", "#", "Edîtora çavkaniyê veke",
"ca-wiktku-nowizard", "Vê rûpelê bi edîtora çavkaniyê çêke",
undefined, "#ca-watch")).click(function (evt) {
evt.preventDefault();
callToAction.text(origCallToActionText);
destroy();
});
if (callToAction.length) {
callToAction.text(origCallToActionText +
" Forma li jêr ji bo çêkirina madeyeke nû bi kar bîne.");
}
var startForm = $("<form id='wiktku-start-form'></form>");
var clearPageCheck = null;
if (addLangMode) {
startForm.append($("<p class='wiktku-addlang-notice'></p>").html(
"Bi vê formê tu yê zimanekî nû li rûpelê îlawe bikî. Eger ev ziman jixwe hebe, beşa nû li dawiya vî zimanî dê were îlawekirin. " +
"Ji kerema xwe netîceya dawî kontrol bike, eger problemek derkeve, li <a href='" +
mw.util.getUrl('Mediawiki talk:Gadget-start.js') +
"'>nîqaşa vê amûrê</a> problemê ragihîne."
));
clearPageCheck = $("<input type='checkbox' id='wiktku-clear-page-check'>");
var clearPageLabel = $("<label for='wiktku-clear-page-check'></label>")
.text(" Muhtewaya rûpelê paqij bike (her tiştî jê bibe û rûpelê ji nû ve binivîse)");
startForm.append(
$("<p class='wiktku-addlang-notice'></p>")
.append(clearPageCheck)
.append(clearPageLabel)
);
// Re-evaluate step visibility (e.g. the pron step) whenever this
// is toggled, since it changes what counts as "existing content".
clearPageCheck.on("change", function () {
refresh();
});
}
var spinner;
// Tab bar for switching between POS sections.
var sectionTabsDiv = $("<div id='wiktku-section-tabs'></div>");
// Tab bar for switching between step-groups (topics) within a section.
// renderGroupTabs()/updateGroupTabIndicators() (which populate and
// maintain it) live outside build() — see near "var activeSection".
var groupTabsDiv = $("<div id='wiktku-group-tabs'></div>");
// Both tab bars are sticky. group-tabs sits below section-tabs' rendered height, which varies
// when the bar wraps, so it is measured and published as a CSS custom property.
// Vector 2022's own sticky header would cover section-tabs, so its occupied height goes into
// --wiktku-siteheader-h. It slides with scroll, hence the re-measure on scroll as well as resize.
function siteHeaderOffset() {
var el = document.querySelector(".vector-sticky-header");
if (!el) return 0;
var rect = el.getBoundingClientRect();
// Present in the DOM at all times on Vector 2022, but only
// actually occupying screen space once scrolled into place —
// before that it sits translated above the viewport (negative/
// zero rect), which must not add a phantom gap under our bars.
if (rect.bottom <= 0 || rect.top > 4) return 0;
return Math.max(0, rect.bottom);
}
var stickyOffsetQueued = false;
function syncStickyTabsOffset() {
var h = sectionTabsDiv.is(":visible") ? (sectionTabsDiv.outerHeight() || 0) : 0;
startForm.css("--wiktku-stabs-h", h + "px");
startForm.css("--wiktku-siteheader-h", siteHeaderOffset() + "px");
}
function queueStickyTabsOffsetSync() {
if (stickyOffsetQueued) return;
stickyOffsetQueued = true;
window.requestAnimationFrame(function () {
stickyOffsetQueued = false;
syncStickyTabsOffset();
});
}
$(window).on("resize.wiktkuStickyTabs scroll.wiktkuStickyTabs", queueStickyTabsOffsetSync);
// "Beşên din" picker — the search box for adding a section, at the top of the form;
// renderBesenDinPicker() fills it.
var besenDinPickerDiv = $("<div id='wiktku-besendin-picker' class='wiktku-besendin-picker'></div>");
/** Puts the form back in a usable state after a failed submit and
* says what happened. Without this the button stays disabled and
* the spinner spins forever on any exception — the person is left
* staring at a form that looks busy but is dead, with the reason
* visible only in the browser console. */
function submitFailed(err) {
spinner.hide();
continueBtn.setDisabled(false);
continueBtn.$element.removeClass("wiktku-btn-sending");
var detail = (err && (err.message || err.toString())) || "sedem nediyar";
mw.notify(
"Şandin bi ser neket: " + detail +
" — tiştê ku te nivîsî winda nebûye. Heke ev dubare bibe, " +
"bi 'Edîtora çavkaniyê veke' berdewam bike.",
{ type: "error", tag: "wiktku-submit", autoHide: false }
);
// Deliberately console.error as well: the notification is for the
// person, this is for whoever they report the bug to.
if (window.console && console.error) console.error("[wiktku] submit failed", err);
}
onSubmitError = submitFailed;
startForm.submit(function (evt) {
evt.preventDefault();
continueBtn.setDisabled(true);
continueBtn.$element.addClass("wiktku-btn-sending");
spinner.show();
try {
getWikitext(function (wikitext) {
try {
if (addLangMode && clearPageCheck && clearPageCheck.prop("checked")) {
// User asked to wipe the page — write fresh content.
$("#wpTextbox1").val(wikitext);
} else if (addLangMode) {
var textbox = $("#wpTextbox1");
var existing = textbox.val() || "";
// ── h-bnr merge ──────────────────────────────────────────
// If the user filled bnr-h inputs, either merge into the
// existing {{h-bnr|…}} call or prepend a new one.
var bnrhOut = steps.bnrh.output(getValues());
if (bnrhOut) {
var newPages = bnrhOut.replace(/^\{\{h-bnr\|/, "").replace(/\}\}\n?$/, "").split("|");
var bnrhRe = /\{\{h-bnr\|([^}]*)\}\}/i;
if (bnrhRe.test(existing)) {
existing = existing.replace(bnrhRe, function (_, inner) {
var existingPages = inner.split("|").map(function (p) { return p.trim(); });
$.each(newPages, function (_, p) {
p = p.trim();
if (p && existingPages.indexOf(p) === -1) existingPages.push(p);
});
return "{{h-bnr|" + existingPages.join("|") + "}}";
});
} else {
existing = bnrhOut + existing;
}
}
// If this language's section already exists (whitespace
// differences aside), merge into it; otherwise insert new.
var langHeaderMatch = wikitext.match(new RegExp("^(\n?" + langHeaderSource("[^}]+") + "\\n)"));
if (langHeaderMatch) {
var langHeader = langHeaderMatch[1]; // e.g. "== {{ziman|ku}} ==\n"
var newCode = langHeaderMatch[2];
var existingHeader = findLangHeader(existing, newCode);
if (existingHeader) {
// Strip the header from the new output.
var newContent = wikitext.slice(langHeader.length);
// Find the insertion point: end of the existing lang section.
// A new L2 heading marks the start of the next language block.
var searchFrom = existingHeader.index + existingHeader.match.length;
// Find the next == … == heading after this section's start.
var nextL2 = existing.indexOf("\n== ", searchFrom);
var insertAt = nextL2 !== -1 ? nextL2 : existing.length;
// Ensure there's a blank line before the inserted block.
var before = existing.slice(0, insertAt);
var after = existing.slice(insertAt);
var sep = /\n\s*$/.test(before) ? "" : "\n";
var merged = before + sep + newContent + after;
// The section just added may repeat a part of speech the page already had. Both
// are renumbered across the whole language section, so a lone "Rengdêr" that was
// there before becomes "Rengdêr 1" and the new one "Rengdêr 2" -- leaving the old
// one unnumbered would make the pair unreadable on the page.
var secStart = existingHeader.index;
var nextLang = merged.indexOf("\n== ", secStart + existingHeader.match.length);
var secEnd = nextLang === -1 ? merged.length : nextLang;
textbox.val(
merged.slice(0, secStart) +
wiktkuEdit.wt.numberRepeatedPosHeadings(merged.slice(secStart, secEnd)) +
merged.slice(secEnd)
);
} else {
// New language — insert at the correct sorted position.
textbox.val(insertNewLangSection(existing, wikitext));
}
} else {
// Unexpected format — insert at sorted position anyway.
textbox.val(insertNewLangSection(existing, wikitext));
}
} else {
$("#wpTextbox1").val(wikitext);
}
$("html, body").animate({scrollTop: 0});
callToAction.text("Hindik ma!");
// The draft is now live in the editor, submitted or not — keeping
// it cached would just offer to restore already-used content
// the next time the wizard opens on this page.
try { formCache.Set({}); } catch (e) { /* storage unavailable — harmless, the content is already in the editor */ }
destroy();
} catch (e) {
// Thrown while writing the result into #wpTextbox1.
submitFailed(e);
}
});
} catch (e) {
// Catches anything thrown synchronously inside getWikitext.
submitFailed(e);
}
});
// The step the "Beşên din" picker is inserted directly above.
var PICKER_BEFORE_STEP = "tewandin";
// Tracks whether groupTabsDiv has been inserted yet.
var groupTabsInserted = false;
$.each(ordered_steps, function (i, step) {
var stepDiv = $(mw.html.element("div", {
"class": "wiktku-start-step",
id: "wiktku-start-" + step.id }));
var name = capitalize(step.name);
var nameH3 = $("<h3></h3>").addClass("wiktku-start-step-name");
var nameLabel = $("<label></label>")
.attr("for", "wiktku-start-" + step.id + "-field")
.text(name);
nameH3.append(nameLabel);
stepDiv.append(nameH3);
if (step.question_fmt) {
// question_fmt can be a plain string, or a function of the
// current values (used by steps whose question text depends
// on e.g. the chosen language) — refresh() keeps it in sync
// afterwards via the .wiktku-start-step-question class.
var questionText = typeof step.question_fmt === "function"
? step.question_fmt(getValues())
: step.question_fmt;
var questionLabel = $(mw.html.element("label", {
"class": "wiktku-start-step-question",
"for": "wiktku-start-" + step.id + "-field" }, questionText));
stepDiv.append(questionLabel);
}
var input;
if (step.choices) {
input = $(mw.html.element("select", {
"class": "wiktku-start-step-field",
id: "wiktku-start-" + step.id + "-field" }));
input.attr("data-placeholder",
capitalize(step.name) + " hilbijêre ");
populateMenus(input, step.choices);
// The language step ships ~300 languages; if a search comes up empty, fetch the other ~3400
// and refill this select before telling the user there is no such language.
if (step.id === "lang") {
input.data("wiktkuOnNoMatch", function (query, refresh) {
if (!wiktkuMergeLanguages || wiktkuLangs.isFull()) {
return;
}
wiktkuLangs.ensureFull().then(function (all) {
populateMenus(input, wiktkuMergeLanguages(all));
refresh();
});
});
}
if (step.multiple) input.attr("multiple", true);
// initSelects() runs over the whole form once it is in the DOM.
stepDiv.append(input);
// Pos-specific: the definition/example for whichever cureyê
// rêzimanî is currently chosen, as plain text right under the
// dropdown — kept in sync by refresh() via .wiktku-start-pos-example.
if (step.id === "pos") {
stepDiv.append(
$("<p class='wiktku-step-hint wiktku-start-pos-example'></p>").text(step.example(getValues()))
);
}
} else if (typeof(step.build) === "function") {
input = step.build(stepDiv);
} else {
input = $("<textarea></textarea>")
.attr("class", "wiktku-start-step-field")
.attr("id", "wiktku-start-" + step.id + "-field")
.attr("rows", step.rows || 2);
if (step.narrow) input.addClass("wiktku-start-narrow");
if (step.placeholder) {
input.attr("placeholder", typeof step.placeholder === "function" ? step.placeholder() : step.placeholder);
}
if (step.multiple) {
input.on("input", function (evt) {
refreshForTextField(evt.target, step);
});
var keyInput = buildKeyInput(step);
// Same one-box-per-word layout as the per-sense ⊕-modifier
// groups (see makeModifierLi) when this step has modifiers.
var list = $(step.ordered ? "<ol></ol>" : "<ul></ul>")
.append(step.modifiers
? makeModifierLi(input)
: $("<li></li>").append(keyInput).append(input));
stepDiv.append(list);
}
else stepDiv.append(input);
}
prepareInput(input, step);
stepDiv.hide();
// Section tabs (switch between POS sections) sit above the
// per-section fields, starting at "pos".
if (step.id === "pos") {
startForm.append(sectionTabsDiv);
}
// Step-group tabs sit just before the first grouped step in
// ordered_steps. Derived from stepIdToGroup so this keeps
// working no matter how STEP_GROUPS/ordered_steps get reordered.
if (!groupTabsInserted && stepIdToGroup[step.id] !== undefined) {
// With a single group there is nothing to switch between, so the bar is never built --
// rather than built and then hidden, which left it on screen whenever the form was
// rebuilt before refresh() next ran.
if (STEP_GROUPS.length > 1) {
startForm.append(groupTabsDiv);
}
groupTabsInserted = true;
}
// The picker sits between the senses and Tewandin: sections are added underneath it, so a
// newly picked one lands right here and pushes the rest of the form down.
if (step.id === PICKER_BEFORE_STEP) {
startForm.append(besenDinPickerDiv);
}
startForm.append(stepDiv);
});
// ── Section tab helpers ──────────────────────────────────────────────
// ── Add-section button — lives in the tab bar, disabled until pos chosen ──
var addSectionBtn = new OO.ui.ButtonWidget({
label: "+ Beşeke din",
disabled: true,
classes: [ "wiktku-add-sense-btn" ]
});
/** Adds a new, empty POS section and switches to it. */
function addPosSection() {
saveCurrentSection();
viewingPron = false;
sections.push({});
activeSection = sections.length - 1;
$.each(PER_SECTION_STEP_IDS, function (_, id) {
// Declared once here rather than with `var` in the "dfn"
// branch and again in the fallback branch below — each
// branch returns before the other runs, but both are
// lexically in this same function scope.
var stepDiv;
if (id === "dfn") {
stepDiv = $("#wiktku-start-dfn");
stepDiv.find(".wiktku-dfn-sense-tabs").remove();
stepDiv.find("ol.wiktku-dfn-list").remove();
steps.dfn.build(stepDiv);
return;
}
if (id === "etym") {
$("#wiktku-start-etym .wiktku-etym-wrap").remove();
steps.etym.build($("#wiktku-start-etym"));
return;
}
if (id === "cavkani") {
$("#wiktku-start-cavkani .wiktku-cavkani-wrap").remove();
steps.cavkani.build($("#wiktku-start-cavkani"));
return;
}
if (id === "posparams") {
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-step").remove();
return;
}
if (id === "resim") {
$("#wiktku-start-resim .wiktku-resim-wrap").remove();
return;
}
if (id === "trans") {
var transDiv = $("#wiktku-start-trans");
transDiv.find(".wiktku-trans-group").remove();
return;
}
if (id === "zarava") {
var zaravaDiv = $("#wiktku-start-zarava");
zaravaDiv.find(".wiktku-zarava-group").remove();
return;
}
if (id === "dunde") {
var dundeDiv = $("#wiktku-start-dunde");
dundeDiv.find("ul.wiktku-dunde-list").remove();
return;
}
stepDiv = $("#wiktku-start-" + id);
stepDiv.find(".wiktku-start-step-field").val("");
stepDiv.find("select.wiktku-start-step-field").trigger("wiktku:refresh");
});
// Fresh section — no "Beşên din" sub-section has been opened yet.
openedOtherSections = {};
renderBesenDinPicker();
renderSectionTabs();
refresh();
}
addSectionBtn.on("click", addPosSection);
/** Removes one POS section. Assumes the caller already confirmed and
* already checked more than one section remains. If the removed
* section was the one on screen, its in-progress DOM edits are
* discarded along with it (never saved back into sections[]) and a
* neighboring section is restored in its place. */
function removePosSection(idx) {
sections.splice(idx, 1);
if (idx === activeSection) {
viewingPron = false;
activeSection = Math.min(idx, sections.length - 1);
restoreSection(sections[activeSection]);
refresh();
} else if (idx < activeSection) {
activeSection -= 1;
}
renderSectionTabs();
}
/** Re-render the tab bar to reflect current sections[] and activeSection. */
function renderSectionTabs() {
// Detach (not remove) the button first: .empty() strips bound
// event data from any children left inside it, which would
// silently kill addSectionBtn's click handler on every re-render.
addSectionBtn.$element.detach();
sectionTabsDiv.empty();
var lang = getValues().lang || "";
if (!lang) {
sectionTabsDiv.hide();
syncStickyTabsOffset();
return;
}
sectionTabsDiv.show();
// Bilêvkirin leads the bar, ahead of the POS sections.
var pronTab = $("<span></span>")
.addClass("wiktku-section-tab wiktku-section-tab-pron")
.text("Bilêvkirin")
.toggleClass("wiktku-tab-active", viewingPron);
pronTab.on("click", function () {
if (viewingPron) return;
saveCurrentSection();
viewingPron = true;
refresh();
renderSectionTabs();
scrollTabBarIntoView("wiktku-section-tabs");
});
sectionTabsDiv.append(pronTab);
$.each(sections, function (i) {
var posVal = sections[i].pos || "";
// Numbered while there is more than one section of this part of speech, so two tabs are
// never both "Rengdêr" -- the same numbering the headings get on save.
var samePos = $.grep(sections, function (s) { return (s.pos || "") === posVal; });
var nth = 0;
$.each(sections, function (j, s) {
if ((s.pos || "") === posVal && j <= i) nth++;
});
var tabLabel = posVal
? capitalize(posVal) + (samePos.length > 1 ? " " + nth : "")
: ("Beş " + (i + 1));
var tab = $("<span></span>")
.addClass("wiktku-section-tab")
.text(tabLabel)
.toggleClass("wiktku-tab-active", i === activeSection && !viewingPron);
(function (idx) {
tab.on("click", function () {
if (idx === activeSection && !viewingPron) return;
// Leaving Bilêvkirin doesn't need a save — its field
// is shared and never part of a section snapshot.
if (!viewingPron) saveCurrentSection();
viewingPron = false;
activeSection = idx;
restoreSection(sections[activeSection]);
refresh();
renderSectionTabs();
scrollTabBarIntoView("wiktku-section-tabs");
});
}(i));
sectionTabsDiv.append(tab);
});
// One delete button for the whole bar — not one per tab — same
// reasoning as the Mane tabs' trash button: deletes whichever
// Beş is currently open, so there's only one thing a click on it
// can possibly remove. Not shown on Bilêvkirin (nothing to
// delete there) or when it's the only Beş left.
if (sections.length > 1 && !viewingPron) {
var activeTabLabel = sections[activeSection] && sections[activeSection].pos
? capitalize(sections[activeSection].pos)
: ("Beş " + (activeSection + 1));
var sectionTrash = makeTrashButton("Vê beşê jê bibe (" + activeTabLabel + ")");
sectionTrash.on("click", function () {
if (!window.confirm("Tu dixwazî \"" + activeTabLabel + "\" jê bibî? Ev tişt nayê vegerandin.")) return;
removePosSection(activeSection);
});
sectionTabsDiv.append(sectionTrash.$element);
}
// Enable "+ Beşeke din" only when the active section has a pos chosen.
var activePos = sections[activeSection] && sections[activeSection].pos
? sections[activeSection].pos
: getValues().pos;
addSectionBtn.setDisabled(!activePos);
sectionTabsDiv.append(addSectionBtn.$element);
syncStickyTabsOffset();
}
renderSectionTabs();
/**
* Re-populates a growing "multiple" field list (bnrh, je, tekildar, …) from a saved array.
* Values are set with .val() plus an "input" event so refreshForTextField() grows the rows,
* exactly as typing into the last row does.
*/
function restoreMultiField(id, values) {
var container = $("#wiktku-start-" + id);
$.each(values || [], function (i, v) {
var field = container.find(".wiktku-start-step-field").eq(i);
if (!field.length || !v) return;
field.val(v).trigger("input");
});
}
/**
* Restores a full cached draft: shared lang/pron/bnrh fields, then
* every section (via the same restoreSection()/refresh()/
* renderSectionTabs() sequence already used when switching tabs).
* @param cached {{lang: string, pron: string, bnrh: Array, sections: Array, activeSection: number}}
*/
function restoreFromCache(cached) {
if (cached.lang) {
$("#wiktku-start-lang-field").val(cached.lang);
$("#wiktku-start-lang-field").trigger("wiktku:refresh");
}
// Rebuild dependent steps (pos, pron wrap, etc.) now that lang is set.
refresh();
if (cached.pron !== undefined) {
$("#wiktku-start-pron .wiktku-pron-text").val(cached.pron);
}
if (cached.bnrh) {
restoreMultiField("bnrh", cached.bnrh);
}
sections = (cached.sections && cached.sections.length) ? cached.sections : [{}];
activeSection = (typeof cached.activeSection === "number" &&
cached.activeSection >= 0 && cached.activeSection < sections.length)
? cached.activeSection : 0;
restoreSection(sections[activeSection]);
refresh();
renderSectionTabs();
updateLastSavedIndicator(cached.savedAt || null);
formDirty = false;
wiktkuNotify("Danûstandina berê ya qeydkirî ji cache hate barkirin.",
{ type: "info", tag: "wiktku-cache-restore" });
}
// Update the active tab's label whenever pos changes.
startForm.on("change", "#wiktku-start-pos-field", function () {
// renderSectionTabs() labels each tab from sections[i].pos, which
// is only synced from the DOM by saveCurrentSection() (on tab
// switch/section add). Without this, the active tab's own label
// stays on the previous pos (or "Beş N") until the user switches
// away and back. Update it directly so the rename is immediate.
if (sections[activeSection]) sections[activeSection].pos = $(this).val();
renderSectionTabs();
// A new cureyê rêzimanî means any previously chosen cinsiyet/
// cureyê lêkerê no longer applies — clear both selections (and
// the shared hidden cinsiyet field, since rebuildTa()/syncHelpers()
// fall back to it) before wiping the textarea, so nothing stale
// gets carried over into the rebuilt template.
var ppDiv = $("#wiktku-start-posparams");
ppDiv.find(".wiktku-posparams-cinsiyet-sel").val("");
ppDiv.find(".wiktku-posparams-jimar-sel").val("");
ppDiv.find(".wiktku-posparams-lektur-sel").prop("selectedIndex", 0);
ppDiv.find(".wiktku-posparams-lekturform-sel").prop("selectedIndex", 0);
$("#wiktku-start-cinsiyet-field").val("");
$("#wiktku-start-jimar-field").val("");
ppDiv.find(".wiktku-posparams-text").val("");
// refresh() runs steps.posparams.syncHelpers(), which toggles the
// param rows for the new pos and — since the textarea is now
// empty — calls rebuildTa() to repopulate it.
refresh();
});
// Show the tab bar as soon as a language is selected.
startForm.on("change", "#wiktku-start-lang-field", function () {
renderSectionTabs();
});
var footer = $("<div id='wiktku-start-footer'></div>");
spinner = $(mw.html.element("img", {
id: "wiktku-start-spinner",
src: "//upload.wikimedia.org/wikipedia/commons/d/de/Ajax-loader.gif",
alt: "Tê barkirin…",
title: "Tê barkirin…" }));
footer.append(spinner.hide());
continueBtn = new OO.ui.ButtonWidget({
id: "wiktku-unhide-editform",
label: "Bişîne",
flags: [ "primary", "progressive" ]
});
continueBtn.on("click", function () {
// Immediate feedback that the click registered
continueBtn.$element.addClass("wiktku-btn-flash");
setTimeout(function () {
continueBtn.$element.removeClass("wiktku-btn-flash");
}, 500);
wiktkuNotify("Tê şandin…", { type: "info", tag: "wiktku-submit" });
startForm.trigger("submit");
});
continueBtn.$element.addClass("wiktku-footer-primary");
footer.append(continueBtn.$element);
// "Edîtora çavkaniyê veke" button — inline fallback for the portlet link
var sourceEditorBtn = new OO.ui.ButtonWidget({
label: "Edîtora çavkaniyê veke",
flags: [ "destructive" ],
});
sourceEditorBtn.on("click", function () {
destroy();
});
sourceEditorBtn.$element.addClass("wiktku-footer-primary");
footer.append(sourceEditorBtn.$element);
// ── Cache save/clear buttons ─────────────────────────────────────────
// Manual, page-scoped save of the entire wizard (all sections, not
// just the active one) via [[MediaWiki:Gadget-StorageUtils.js]]'s
// ObjectStorage. Restoring happens silently on load at the end of
// build(), once the full language list is available.
//
// Saving also happens automatically (debounced while typing, plus a
// periodic fallback) — see "Autosave" below — since relying on the
// person to remember to click "Li cache qeyd bike" before an
// accidental tab close/reload defeats the point of having a cache
// at all.
function buildCacheSnapshot() {
return {
lang: getValues().lang || "",
pron: $("#wiktku-start-pron .wiktku-pron-text").val() || "",
// bnrh is shared (page-level), same as lang/pron above — not
// per-section, so it doesn't live in sections[].
bnrh: getValuesForStep("bnrh"),
sections: sections,
activeSection: activeSection,
savedAt: Date.now()
};
}
// ── Last-saved indicator ─────────────────────────────────────────────
var lastSavedEl = $("<span id='wiktku-last-saved'></span>");
function formatSavedTime(ts) {
try {
return new Date(ts).toLocaleTimeString(mw.config.get("wgUserLanguage"),
{ hour: "2-digit", minute: "2-digit" });
} catch (e) {
return new Date(ts).toLocaleTimeString();
}
}
function updateLastSavedIndicator(ts) {
lastSavedEl.text(ts ? ("Cache: qeydkirî di saet " + formatSavedTime(ts) + " de") : "");
}
// ── Dirty-state tracking + autosave ──────────────────────────────────
// formDirty mirrors "does the live form differ from what's in cache".
// Anything the person types/selects sets it; a successful save (auto
// or manual) clears it; destroy() (final submit, or switching to the
// source editor) also clears it since the wizard's job is over at
// that point either way. formDirty/autosaveDebounceTimer/autosaveInterval
// are declared in the outer scope (near activeGroup) — assigned here,
// not re-declared — so destroy() can reach them too.
var AUTOSAVE_DEBOUNCE_MS = 8000; // save shortly after typing pauses
var AUTOSAVE_INTERVAL_MS = 60000; // …and at least this often regardless
function performAutosave() {
if (!formDirty) return;
saveCurrentSection();
try {
formCache.Set(buildCacheSnapshot());
formDirty = false;
updateLastSavedIndicator(Date.now());
} catch (e) {
// Silent on autosave failure (storage blocked/full, etc.) — the
// manual button still surfaces a proper error if the person
// tries it directly, no need to interrupt typing with a toast.
}
}
function scheduleAutosave() {
formDirty = true;
if (autosaveDebounceTimer) clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = setTimeout(performAutosave, AUTOSAVE_DEBOUNCE_MS);
}
startForm.on("input change", "input, textarea, select", scheduleAutosave);
autosaveInterval = setInterval(performAutosave, AUTOSAVE_INTERVAL_MS);
// Warn on accidental tab close/reload while there's anything unsaved.
$(window).on("beforeunload.wiktkuUnsaved", function (e) {
if (!formDirty) return undefined;
e.preventDefault();
e.returnValue = "";
return "";
});
var saveCacheBtn = new OO.ui.ButtonWidget({
label: "Li cache qeyd bike",
title: "Halê vê formê yê niha (ziman, bilêvkirin û hemû beş) li cache (localStorage) qeyd bike, da ku eger rûpel bêyî xwestin were girtin an ji nû ve were barkirin, tu karibî berdewam bikî."
});
saveCacheBtn.on("click", function () {
saveCurrentSection();
// ObjectStorage.Set() doesn't return a success/failure value, so
// the only way to detect a real failure (storage blocked, quota
// exceeded, JSON.stringify choking on something unexpected) is to
// catch it here rather than inspect a (nonexistent) return value.
try {
var snapshot = buildCacheSnapshot();
formCache.Set(snapshot);
formDirty = false;
updateLastSavedIndicator(snapshot.savedAt);
wiktkuNotify("Halê formê li cache hate qeydkirin.", { type: "success", tag: "wiktku-cache-save" });
} catch (e) {
mw.notify("Qeydkirina li cache bi ser neket.", { type: "error", tag: "wiktku-cache-save" });
}
});
var clearCacheBtn = new OO.ui.ButtonWidget({
label: "Cache paqij bike",
flags: [ "destructive" ],
title: "Datayên qeydkirî yên vê rûpelê ji cache jê bibe (forma ku niha xuya dike naguherîne)."
});
clearCacheBtn.on("click", function () {
formCache.Set({});
// Cancel the scheduled autosave, or it would rewrite the cache from the still-filled form.
// The form is left clean, so autosave resumes only once something is edited again.
if (autosaveDebounceTimer) {
clearTimeout(autosaveDebounceTimer);
autosaveDebounceTimer = null;
}
formDirty = false;
updateLastSavedIndicator(null);
wiktkuNotify("Cache hate paqijkirin.", { type: "success", tag: "wiktku-cache-clear" });
});
// Everything sits on the bar: no overflow menu, nothing a click away.
// Settings button — opens the Tercîh dialog. Plain OOUI icon names depend on which icon-pack
// module happens to be loaded, so this uses the actual Codex "settings" (color-subtle) SVG from
// Commons directly, the same way the spinner above hotlinks its gif.
var prefsBtn = $("<button></button>", {
type: "button",
"class": "wiktku-prefs-btn",
title: "Tercîh — sazkariyên vê amûrê biguherîne",
"aria-label": "Tercîh"
});
prefsBtn.on("click", function (e) {
e.preventDefault();
wiktkuOpenPrefsDialog();
});
clearCacheBtn.$element.addClass("wiktku-btn-danger");
saveCacheBtn.$element.addClass("wiktku-footer-cache");
clearCacheBtn.$element.addClass("wiktku-footer-cache");
footer.append(saveCacheBtn.$element)
.append(clearCacheBtn.$element)
.append(prefsBtn)
.append($("<span class='wiktku-cache-note'></span>").append(lastSavedEl));
startForm.append(footer);
// Moved off-screen rather than display:none. WikiEditor and the mobile editor measure
// #wpTextbox1 while the wizard is up, and a hidden ancestor reports zero, which they then keep.
// Off-screen keeps it laid out and measurable.
$("#editform").addClass("wiktku-offscreen").before(startForm);
// renderGroupTabs() looks up #wiktku-group-tabs via a live DOM
// selector, so it must run after startForm is actually in the document.
renderGroupTabs();
renderBesenDinPicker();
// One pass over the whole form: every select that hasn't already been
// initialised, and isn't opted out with .wiktku-plain-select, gets
// the searchable picker.
initSelects(startForm);
refresh();
// Give every draggable textarea a custom resize grip (works
// consistently on Safari/iOS, unlike the native corner grip — see
// the "Manual drag-to-resize handle" comment in styles.css), and
// keep applying it to textareas added later on (new sense blocks,
// quote-source fields, nimûne rows, …).
wiktkuInitManualResize(startForm);
new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
$(mutation.addedNodes).each(function () {
if (this.nodeType !== 1) return;
wiktkuInitManualResize(this);
});
});
}).observe(startForm[0], { childList: true, subtree: true });
// Now that the full language list is loaded, silently restore any
// draft the editor previously saved for this page.
var cachedDraft = formCache.Get();
if (cachedDraft && cachedDraft.sections && cachedDraft.sections.length) {
restoreFromCache(cachedDraft);
}
}
// Only oojs-ui-core blocks the initial render now — icons-moderation
// (the trash glyph) is fetched lazily by makeTrashButton() instead, so
// slow connections aren't held up loading it before anyone needs it.
mw.loader.using(["oojs-ui-core"], function () {
function wiktkuProceedWithData(wiktkuData) {
var d = wiktkuData || {};
// Populate metadata.
if (d.metadata) $.extend(metadata, d.metadata);
// Populate gender labels.
if (d.genderLabels) $.extend(GENDER_BASE_LABELS, d.genderLabels);
// Jimar (number) code definitions
if (d.numberLabels) $.extend(NUMBER_LABELS, d.numberLabels);
if (d.senseGroups) SENSE_GROUPS = d.senseGroups;
// Headings for the page-level word lists, from the same table DefSideBoxes reads.
$.each(d.pageSubsections || [], function (_, sub) {
if (sub.kind !== "wordlist" || !steps[sub.id]) return;
steps[sub.id].header = sub.header;
steps[sub.id].name = decapitalize(sub.header);
});
if (d.wordlistTemplates && d.wordlistTemplates.length) WORDLIST_TEMPLATE = d.wordlistTemplates[0];
if (d.deverQuickPicks) DEVER_QUICK_PICKS = d.deverQuickPicks;
if (d.lekturChoices) LEKTUR_CHOICES = d.lekturChoices;
if (d.lekturFormChoices) LEKTUR_FORM_CHOICES = d.lekturFormChoices;
// Populate lang choices (pinned list).
if (d.langChoices) steps.lang.choices = choicesFromObject(d.langChoices, true);
// Populate langsByCode and merge full language list from d.ziman.
// d.ziman is a {code: name} object with all known languages.
$.each(steps.lang.choices, function (i, choice) {
if (choice.sortKey === undefined) choice.sortKey = choice.label;
langsByCode[choice.code] = choice;
});
// Only the ~300 languages of ziman-core.json are merged in at load; the other ~3400 arrive
// through wiktkuMergeLanguages() the first time somebody searches for one of them. The pinned
// choices are kept aside so a second merge rebuilds the list instead of appending to it.
var wiktkuPinnedChoices = steps.lang.choices;
wiktkuMergeLanguages = function (zimanNames) {
var pinnedCodes = {};
$.each(wiktkuPinnedChoices, function (i, c) { pinnedCodes[c.code] = true; });
var extraChoices = [];
$.each(zimanNames || {}, function (code, name) {
// A proto-language is never the language of an ordinary entry, but it is the whole point
// of a reconstruction page, so ns 108 keeps them in the list.
if (!isReconstruction && /-pro$/i.test(code)) return;
if (/ziman[eê]\s+destan/i.test(name)) return;
if (pinnedCodes[code]) return;
var choice = { code: code, label: name, sortKey: name };
extraChoices.push(choice);
langsByCode[code] = choice;
});
// One Intl.Collator built once and reused for the whole sort, instead of localeCompare() rebuilding a collator on every pair -- same order, far faster over ~3700 entries.
var langCollator = new Intl.Collator(userLang);
extraChoices.sort(function (a, b) {
return langCollator.compare(a.sortKey, b.sortKey);
});
steps.lang.choices = wiktkuPinnedChoices.concat(extraChoices);
// The translation/dialect steps reference any language, so they follow the same list.
steps.trans.keys = steps.lang.choices;
steps.dunde.keys = steps.lang.choices;
steps.zarava.keys = getZaravaKeys(steps.lang.choices);
return steps.lang.choices;
};
wiktkuMergeLanguages(wiktkuLangs.names());
// Keep the full merged list around for trans/dunde/zarava — those
// should still be able to reference any language even when the
// Tercîh panel restricts the main "ziman" step below — then, if
// the user set a restriction there, narrow steps.lang.choices
// itself to just those codes.
var wiktkuFullLangChoices = steps.lang.choices;
if (wiktkuPrefs.langCodes && wiktkuPrefs.langCodes.length) {
var wiktkuAllowedLangSet = {};
$.each(wiktkuPrefs.langCodes, function (i, code) { wiktkuAllowedLangSet[code] = true; });
var wiktkuRestrictedChoices = $.grep(wiktkuFullLangChoices, function (c) {
return !!wiktkuAllowedLangSet[c.code];
});
// Fall back to the full list instead of showing an empty
// dropdown if none of the saved codes matched anything
// (typo'd code, data-main.json changed since, …).
steps.lang.choices = wiktkuRestrictedChoices.length ? wiktkuRestrictedChoices : wiktkuFullLangChoices;
}
// Populate POS choices — also a plain {code: label} object, from data-sections.json.
if (d.posChoices) steps.pos.choices = choicesFromObject(d.posChoices, false);
// Populate template definitions for the dfn sense-block builder.
if (d.tplDefs) steps.dfn.tplDefs = d.tplDefs;
// Populate pos placeholder examples.
if (d.posExamples) steps.pos.posExamples = d.posExamples;
// Populate citation template definitions for the quote widget.
if (d.jederDefs) steps.dfn.jederDefs = d.jederDefs;
// Populate pronunciation and etymology template insert lists.
if (d.pronTpls) {
steps.pron.pronTpls = d.pronTpls;
steps.pron.refreshTplSelect();
}
if (d.etymTpls) {
steps.etym.etymTpls = d.etymTpls;
steps.etym.refreshTplSelect();
}
// Sync trans/dunde/zarava keys with the full language list
// (not the possibly Tercîh-restricted steps.lang.choices).
steps.trans.keys = wiktkuFullLangChoices;
steps.dunde.keys = wiktkuFullLangChoices;
steps.zarava.keys = getZaravaKeys(wiktkuFullLangChoices);
build();
}
// The data ships inside wikt.edit, so it is already parsed and in memory.
wiktkuProceedWithData(wiktkuEdit.data);
});
});
//</nowiki>
gdsp3f3wjz6q3dyimv1psnpc4oa9xb4
MediaWiki:Gadget-wikt.start-wizard.css
8
1359176
6531673
6426129
2026-09-15T17:58:58Z
Wikihez
7344
Wizard jêgirtin follows the add-examples form: template first, link box kept and saved as url=; Kurmancî nimûne gets an îzah box, Kurmancî jêgirtin none; fill button in the wizard's pill style
6531673
css
text/css
/* <nowiki> */
/*
* Page: [[MediaWiki:Gadget-wikt.start-wizard.css]]
*/
/* ===================================================================
#editform while the wizard is active
=================================================================== */
/* Keeps #editform genuinely laid out instead of display:none*/
.wiktku-offscreen {
position: absolute !important;
left: -9999px !important;
top: 0 !important;
width: 100%;
max-width: 100%;
visibility: hidden;
}
/* ===================================================================
Base scale
=================================================================== */
#wiktku-start-form {
font-size: 0.92em;
line-height: 1.35;
}
#wiktku-start-form * {
box-sizing: border-box;
}
.wiktku-addlang-notice {
margin: 0 0 0.8em;
padding: 0.6em 0.8em;
background: var(--wikt-palette-paleblue);
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
}
/* ===================================================================
Step cards
a compact card with a left accent bar, so each step reads as
a distinct unit
=================================================================== */
.wiktku-start-step {
padding: 0.7em 0.9em;
margin-bottom: 0.6em;
border: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
border-left: 4px solid var(--wikt-palette-blue-9, #3366cc);
border-radius: 6px;
background: var(--wikt-palette-white);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: border-left-color 0.15s ease, box-shadow 0.15s ease;
}
.wiktku-start-step:hover {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.wiktku-start-step:focus-within {
border-left-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
/* Fuses the posparams card onto the bottom of the pos card (shared left
accent bar, no border/gap) since posparams always renders directly
after #wiktku-start-pos once a pos is chosen. */
#wiktku-start-pos {
border-radius: 6px 6px 0 0;
margin-bottom: 0;
border-bottom: none;
}
#wiktku-start-posparams {
border-radius: 0 0 6px 6px;
border-top: none;
padding-top: 0.3em;
}
#wiktku-start-posparams .wiktku-start-step-name {
display: none;
}
/* Scoped and !important: this is a real <h3> tag (see start.js), so the
skin's default heading styles would otherwise override a plain class
rule. Coloured to match the card's accent bar; .wiktku-start-step-question
below stays lighter/muted so the two don't compete. */
#wiktku-start-form .wiktku-start-step-name {
font-size: 1.1em !important;
font-weight: 700;
letter-spacing: 0.03em;
margin: 0 0 0.3em;
line-height: 1.2;
color: var(--wikt-palette-blue-9, #3366cc);
border: 0;
padding: 0;
}
/* Sized close to the heading so the card reads consistently instead of
the text below it looking like fine print. */
.wiktku-start-step-question {
font-size: 1em;
font-weight: 400;
color: var(--wikt-palette-grey-9, #555);
margin: 0 0 0.5em;
line-height: 1.45;
}
.wiktku-start-step label {
padding-right: 1.2em;
font-size: 1em;
}
/* Muted hint text next to a field, styled to read as a hint rather than
another question competing with .wiktku-start-step-question. */
.wiktku-step-hint {
font-size: 0.88em;
font-style: italic;
color: var(--wikt-palette-grey-8, #72777d);
margin: 0 0 0.5em;
line-height: 1.4;
}
/* Same specificity as the rule just above, so it must come after to
actually win: the name label isn't a field label needing right
padding or the form's base label size, it's a heading. */
#wiktku-start-form .wiktku-start-step-name label {
padding-right: 0;
font-size: inherit !important;
color: inherit;
}
input[type="text"].wiktku-start-step-field {
width: 100%;
padding: 5px 8px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 4px;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
input[type="text"].wiktku-start-step-field:focus {
border-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: inset 0 0 0 1px var(--wikt-palette-blue-9, #3366cc);
outline: 0;
}
input[type="text"].wiktku-start-step-field.wiktku-start-narrow {
width: 20em;
}
/* Only used by selects that opt out via .wiktku-plain-select; other
selects get their width from the searchable picker instead (see
input.wiktku-ac-input). */
select.wiktku-start-narrow,
input.wiktku-dfn-tpl-title {
width: 20em;
max-width: 100%;
box-sizing: border-box;
}
#wiktku-start-spinner {
float: right;
margin-left: 1em;
}
#wiktku-start-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5em;
padding: 0.6em 0.2em;
position: sticky;
bottom: 0;
background: var(--wikt-palette-white);
border-top: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
z-index: 5;
}
/* Quiet, non-competing text — sits next to the cache buttons so the
person can tell at a glance whether their latest edits are backed
up, without it fighting the buttons for attention. */
#wiktku-last-saved {
font-size: 0.85em;
color: var(--wikt-palette-grey-9, #767676);
white-space: nowrap;
}
/* ── Tercîh (settings) icon button — sits in the footer next to the
cache buttons. Uses the Codex "settings" (color-subtle) icon from
Commons directly rather than an OOUI icon-pack module, so it renders
the exact requested glyph without pulling in an extra dependency. */
/* Icon-only, sized to sit level with the OOUI buttons beside it in the footer: same pill, same height,
just square. The label lives in title/aria-label. */
.wiktku-prefs-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 32px;
height: 32px;
padding: 0;
border: 1px solid #a2a9b1;
border-radius: var(--wikt-r-pill, 999px);
background-color: #f8f9fa;
background-image: url(//upload.wikimedia.org/wikipedia/commons/e/e0/Codex_icon_settings_color-subtle.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 14px 14px;
cursor: pointer;
transition: background-color 0.12s ease, border-color 0.12s ease;
}
.wiktku-prefs-btn:hover {
background-color: var(--wikt-card, #fffdf8);
border-color: var(--wikt-accent, #2a5db0);
}
.wiktku-prefs-btn:active {
background-color: var(--wikt-accent-tint, #eef2fa);
}
.wiktku-prefs-btn:focus-visible {
outline: 0;
border-color: var(--wikt-accent, #2a5db0);
box-shadow: 0 0 0 3px var(--wikt-accent-tint, #eef2fa);
}
/* Tercîh dialog body — OOUI's bare ProcessDialog body has no inset of its own, so text and fields
otherwise sit flush against the window edges. */
.wiktku-prefs-dialog-body {
padding: 18px 20px 22px;
background: var(--wikt-card, #fffdf8);
}
/* ── Tercîh dialog ──
OOUI's ProcessDialog chrome is the plain Vector grey; these rules put it on the wizard's own
palette and give the head/body/foot bars a shared rhythm. Scoped to .wiktku-prefs-dialog (set in
initialize) so no other OOUI window on the page is touched. */
.wiktku-prefs-dialog .oo-ui-window-frame {
border-radius: var(--wikt-r-card, 14px);
overflow: hidden;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
}
.wiktku-prefs-dialog .oo-ui-processDialog-navigation {
background: var(--wikt-card-sunken, #fbf8f1);
border-bottom: 1px solid var(--wikt-rule, #ded6c8);
}
.wiktku-prefs-dialog .oo-ui-processDialog-title {
font-family: var(--wikt-serif, "Spectral", "Iowan Old Style", Georgia, "Times New Roman", serif);
font-size: 17px;
font-weight: 600;
color: var(--wikt-ink, #1c1a17);
}
/* Head and foot actions read as the same pills as the wizard's own buttons. OOUI gives the three
action groups no shared class, so each is named. */
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
border-radius: var(--wikt-r-pill, 999px) !important;
font-family: var(--wikt-sans, "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif);
font-size: 14px;
font-weight: 500;
padding: 7px 18px;
min-height: 34px;
box-shadow: none !important;
}
/* OOUI stretches the two head groups edge to edge and corner to corner, which pushed the pills flush
against the rounded frame. Inset them and let them size to the button. */
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary {
top: 50%;
bottom: auto;
height: auto;
transform: translateY(-50%);
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe {
left: 10px;
right: auto;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary {
right: 10px;
left: auto;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button {
background: transparent;
border-color: var(--wikt-field-edge, #cfc7b8);
color: var(--wikt-ink-2, #3d3830);
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button {
background: var(--wikt-accent, #2a5db0) !important;
border-color: var(--wikt-accent, #2a5db0) !important;
color: var(--wikt-on-accent, #fff) !important;
}
/* The destructive action is the only thing in the foot, so it sits at the trailing edge instead of
stretching along a bare full-width bar. */
.wiktku-prefs-dialog .oo-ui-window-foot {
background: var(--wikt-card-sunken, #fbf8f1);
border-top: 1px solid var(--wikt-rule, #ded6c8);
padding: 10px 14px;
text-align: right;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other {
display: inline-block;
float: none;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
background: var(--wikt-card, #fffdf8) !important;
border: 1px solid var(--wikt-danger, #b32424) !important;
color: var(--wikt-danger, #b32424) !important;
}
/* ── Groups ── */
.wiktku-prefs-dialog .oo-ui-fieldsetLayout + .oo-ui-fieldsetLayout {
margin-top: 20px;
padding-top: 18px;
border-top: 1px solid var(--wikt-rule-soft, #efe8db);
}
.wiktku-prefs-dialog .oo-ui-fieldsetLayout-header > .oo-ui-labelElement-label {
font-family: var(--wikt-serif, "Spectral", "Iowan Old Style", Georgia, "Times New Roman", serif);
font-size: 15px;
font-weight: 600;
color: var(--wikt-ink, #1c1a17);
margin-bottom: 10px;
padding: 0;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout {
margin-top: 14px;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout:first-child {
margin-top: 0;
}
.wiktku-prefs-dialog .oo-ui-fieldLayout > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
font-size: 14px;
font-weight: 500;
color: var(--wikt-ink-2, #3d3830);
padding-bottom: 6px;
}
.wiktku-prefs-dialog .oo-ui-inline-help {
font-size: 12.5px;
line-height: 1.5;
color: var(--wikt-muted, #8a8378);
margin-top: 5px;
}
/* ── Fields ── */
.wiktku-prefs-dialog .oo-ui-textInputWidget input,
.wiktku-prefs-dialog .oo-ui-dropdownWidget-handle {
border: 1px solid var(--wikt-field-edge, #cfc7b8);
border-radius: var(--wikt-r-field, 10px);
background: var(--wikt-field-bg, #fff);
font-family: var(--wikt-sans, "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif);
font-size: 14px;
color: var(--wikt-ink, #1c1a17);
min-height: 36px;
padding: 6px 10px;
box-shadow: none;
}
.wiktku-prefs-dialog .oo-ui-textInputWidget input:focus,
.wiktku-prefs-dialog .oo-ui-dropdownWidget.oo-ui-widget-enabled:hover .oo-ui-dropdownWidget-handle {
border-color: var(--wikt-accent, #2a5db0);
box-shadow: none;
outline: 0;
}
.wiktku-prefs-dialog .oo-ui-textInputWidget input::placeholder {
color: var(--wikt-placeholder, #b3ab9e);
}
/* Checkbox rows: the label carries the row, so the text beside the box is the target, and the whole
row lifts on hover to say so. */
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline {
padding: 8px 10px;
border-radius: var(--wikt-r-field, 10px);
}
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline:hover {
background: var(--wikt-card-sunken, #fbf8f1);
}
.wiktku-prefs-dialog .oo-ui-fieldLayout-align-inline > .oo-ui-fieldLayout-body > .oo-ui-fieldLayout-header {
padding-bottom: 0;
color: var(--wikt-ink, #1c1a17);
cursor: pointer;
}
/* Narrow screens: the dialog is full-bleed, so the corners it would round are off-screen anyway. */
@media (max-width: 720px) {
.wiktku-prefs-dialog-body {
padding: 14px 14px 18px;
}
.wiktku-prefs-dialog .oo-ui-processDialog-actions-safe .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-primary .oo-ui-buttonElement-button,
.wiktku-prefs-dialog .oo-ui-processDialog-actions-other .oo-ui-buttonElement-button {
font-size: 13px;
padding: 8px 14px;
}
}
/* ── "Bişîne" click feedback ── */
@keyframes wiktku-btn-flash-pulse {
0% { box-shadow: 0 0 0 0 rgba(51, 102, 204, 0.55); }
70% { box-shadow: 0 0 0 9px rgba(51, 102, 204, 0); }
100% { box-shadow: 0 0 0 0 rgba(51, 102, 204, 0); }
}
#wiktku-unhide-editform.wiktku-btn-flash {
animation: wiktku-btn-flash-pulse 0.5s ease-out;
}
/* Persistent state while the wikitext is being built/submitted, set
alongside OOUI's own setDisabled(true) so it reads clearly even if
the skin's default disabled style is subtle. */
#wiktku-unhide-editform.wiktku-btn-sending .oo-ui-buttonElement-button {
background-color: var(--wikt-palette-grey-9, #767676) !important;
border-color: var(--wikt-palette-grey-9, #767676) !important;
cursor: progress;
opacity: 1 !important;
}
/* ── "Beşên din" picker ── */
.wiktku-besendin-picker {
margin: 0.6em 0 1em;
}
.wiktku-besendin-picker .wiktku-besendin-select {
width: 100%;
max-width: 24em;
}
#wiktku-start-form textarea.wiktku-start-step-field,
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield,
#wiktku-start-form textarea.wiktku-word-input {
resize: vertical !important;
overflow: auto !important;
box-sizing: border-box !important;
vertical-align: top;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
transition: border-color 0.1s ease, box-shadow 0.1s ease;
}
/* Modern focus state to match the searchable-picker inputs. */
#wiktku-start-form textarea.wiktku-start-step-field:focus,
#wiktku-start-form textarea.wiktku-start-narrow:focus,
#wiktku-start-form textarea.wiktku-dfn-subfield:focus,
#wiktku-start-form textarea.wiktku-word-input:focus {
border-color: var(--wikt-palette-blue-9, #3366cc);
box-shadow: inset 0 0 0 1px var(--wikt-palette-blue-9, #3366cc);
outline: 0;
}
/* Standard: every textarea in the form is full width by default */
#wiktku-start-form textarea:not(.wiktku-start-narrow) {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield.wiktku-start-narrow {
width: 20em;
max-width: 100%;
min-width: 8em;
height: calc(1.7em + 10px);
min-height: calc(1.7em + 10px);
padding-top: 4px !important;
padding-bottom: 4px !important;
line-height: 1.35 !important;
resize: none !important;
}
/* Exception: "Sernav" / "Sernavê orjînal" citation-source fields (kitêb,
kovar, malper, …) — long enough to be worth 2 rows and manual resizing. */
#wiktku-start-form textarea.wiktku-dfn-quote-src-input.wiktku-dfn-quote-src-input--title {
height: calc(2 * 1.35em + 10px);
min-height: calc(2 * 1.35em + 10px);
}
/* Image rows (util.js's buildImageListField): each image is its own block
with its own name, so two sets of the same three boxes never run together. */
#wiktku-start-form .wiktku-resim-list {
margin: 0;
padding-left: 0;
}
#wiktku-start-form li.wiktku-resim-item {
list-style: none;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--wikt-rule-soft);
}
#wiktku-start-form li.wiktku-resim-item:first-child {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
#wiktku-start-form .wiktku-resim-index {
margin-bottom: 6px;
font-size: 12px;
font-weight: 600;
color: var(--wikt-ink-2);
}
/* ── Manual drag-to-resize handle ── */
#wiktku-start-form textarea:not(.wiktku-start-narrow) {
resize: none !important;
}
/* Inline modifier button (⊕) */
/* Each word is always a plain box (never collapses into a pill shape); the
box + its ⊕/× buttons sit on one line, and the attached-modifier chips,
if any, on their own line right under it. */
.wiktku-word-li {
display: block;
}
/* ── ⊕-modifier word lists (Hevmane/Dijmane/Formên alternatîv/Termên
koordîne/Jornav/Jêrnav — both the top-level steps and the per-sense
sub-groups): every word is its own always-visible, always-editable box,
one per line — no packing/wrapping of multiple words onto one row. ── */
#wiktku-start-form .wiktku-dfn-sublist > li.wiktku-word-li {
margin-bottom: 12px;
}
/* Modifier chip row: sits under the word's box, indented and closer to it
than the next word is -- that gap difference is what says which chips
belong to which box. */
#wiktku-start-form .wiktku-word-mods {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 3px;
margin-left: 10px;
}
#wiktku-start-form .wiktku-word-mod-chip {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--wikt-card);
color: var(--wikt-ink-2);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
font-size: 11.5px;
padding: 2px 4px 2px 8px;
cursor: pointer;
}
#wiktku-start-form .wiktku-word-mod-chip-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 12px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-word-mod-chip-remove:hover {
color: var(--wikt-danger);
}
/* Word box + its ⊕ (add modifier) / × (remove word) buttons, side by side. */
#wiktku-start-form .wiktku-word-box-row {
display: flex;
align-items: center;
gap: 4px;
}
#wiktku-start-form .wiktku-word-box-row .wiktku-word-input {
flex: 1 1 auto;
}
/* Offered only while the box holds a comma that separates two words. */
#wiktku-start-form .wiktku-word-split-row {
margin-top: 4px;
}
#wiktku-start-form .wiktku-word-split {
flex: 0 0 auto;
height: 1.6em;
padding: 0 8px;
line-height: 1;
font-size: 0.85em;
border: 1px solid var(--wikt-accent-edge);
border-radius: 6px;
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
cursor: pointer;
}
#wiktku-start-form .wiktku-word-split:hover {
border-color: var(--wikt-accent);
}
#wiktku-start-form .wiktku-word-remove {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 6px;
background: var(--wikt-palette-white);
color: var(--wikt-muted);
font-size: 1em;
cursor: pointer;
}
#wiktku-start-form .wiktku-word-remove:hover {
border-color: var(--wikt-danger);
color: var(--wikt-danger);
}
.wiktku-mod-btn {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 2px;
background: var(--wikt-palette-white);
color: var(--wikt-palette-blue-9, #3366cc);
font-size: 1em;
cursor: pointer;
}
.wiktku-mod-btn:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-paleblue, #eaf3ff);
}
.wiktku-mod-picker {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
width: 100%;
margin-top: 0.15em;
padding: 0.35em;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
background: var(--wikt-palette-white);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.wiktku-mod-pill {
padding: 2px 8px;
border: 1px solid var(--wikt-palette-grey-3, #e0e0e0);
border-radius: 12px;
background: var(--wikt-palette-paleblue, #eaf3ff);
font-size: 0.85em;
white-space: nowrap;
cursor: pointer;
}
.wiktku-mod-pill:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
}
.wiktku-mod-pill:disabled,
.wiktku-mod-pill.wiktku-mod-pill-used {
border-color: var(--wikt-palette-grey-3, #e0e0e0);
background: var(--wikt-palette-lightgrey);
color: var(--wikt-palette-grey-3, #999);
cursor: not-allowed;
opacity: 0.6;
}
.wiktku-mod-pill:disabled:hover,
.wiktku-mod-pill.wiktku-mod-pill-used:hover {
border-color: var(--wikt-palette-grey-3, #e0e0e0);
background: var(--wikt-palette-lightgrey);
color: var(--wikt-palette-grey-3, #999);
}
.wiktku-mod-val-wrap {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
width: 100%;
margin-top: 0.15em;
}
/* The key's full name -- the chip itself shows only "crz:", so this is where
it is spelled out. */
.wiktku-mod-val-label {
flex: 0 0 auto;
font-size: 11.5px;
color: var(--wikt-ink-2, #54595d);
white-space: nowrap;
}
.wiktku-mod-val-input {
flex: 0 1 14em;
max-width: 14em;
padding: 3px 6px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: var(--wikt-r-field, 10px);
}
.wiktku-mod-confirm,
.wiktku-mod-cancel {
flex: 0 0 auto;
width: 1.6em;
height: 1.6em;
padding: 0;
line-height: 1;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 6px;
background: var(--wikt-palette-white);
cursor: pointer;
}
.wiktku-mod-confirm {
color: #178a17;
border-color: #178a17;
}
.wiktku-mod-cancel {
color: var(--wikt-palette-red-9, #d33);
border-color: var(--wikt-palette-red-9, #d33);
}
/* .wiktku-resize-wrap, .wiktku-resize-grip and .wiktku-linebreak-wrap are util.js's own, so they live in
wikt-edit.css with the file that builds them -- DefSideBoxes uses the same grip and does not load this
stylesheet. */
/* Inline preview + confirm panel shown before inserting an auto-content
pronunciation template, part of the form layout rather than a browser
confirm()/prompt() dialog. */
.wiktku-pron-preview {
margin: 6px 0;
padding: 8px 10px;
border: 1px solid var(--wikt-palette-blue-9, #3366cc);
border-radius: 6px;
background: var(--wikt-palette-paleblue, #eaf3ff);
}
.wiktku-pron-preview-label {
font-size: 0.88em;
font-weight: 600;
color: var(--wikt-palette-grey-9, #555);
margin-bottom: 3px;
}
.wiktku-pron-preview-body {
font-family: monospace;
font-size: 1em;
white-space: pre-wrap;
word-break: break-word;
margin-bottom: 6px;
}
.wiktku-pron-preview-actions {
display: flex;
gap: 6px;
}
.wiktku-pron-preview-confirm,
.wiktku-pron-preview-cancel {
font-size: 0.88em;
padding: 3px 10px;
border-radius: 4px;
cursor: pointer;
border: 1px solid var(--wikt-palette-lightgrey, #a2a9b1);
background: var(--wikt-palette-white, #fff);
}
.wiktku-pron-preview-confirm {
border-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-blue-9, #3366cc);
font-weight: 600;
}
.wiktku-pron-preview-confirm:disabled {
opacity: 0.5;
cursor: wait;
}
.wiktku-pron-preview-cancel:hover {
background: var(--wikt-palette-grey-0, #f8f9fa);
}
/* Template pickers fill their container since the search box, not the
<select>, is what the user sees. */
.wiktku-pron-wrap .wiktku-native-select-wrap,
.wiktku-etym-wrap .wiktku-native-select-wrap,
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
width: 100%;
}
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 0.88em;
}
.wiktku-cavkani-wrap .wiktku-native-select-wrap,
.wiktku-cavkani-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
width: 100%;
}
/* Template selector inside a sense block. */
.wiktku-dfn-tpl-row .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 0.88em;
}
/* Language/dialect rows: picker must line up with the word field beside it. */
.wiktku-lang-row .wiktku-native-select-wrap {
flex: 0 0 auto;
}
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t,
#wiktku-start-form textarea.wiktku-dfn-quote-src-manual,
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text,
#wiktku-start-form .wiktku-tewandin-wrap textarea.wiktku-tewandin-text,
#wiktku-start-form textarea.wiktku-cavkani-text,
#wiktku-start-form textarea.wiktku-dfn-izah {
--wiktku-ta-rows: 2;
height: calc(var(--wiktku-ta-rows) * 1.35em + 6px);
min-height: calc(var(--wiktku-ta-rows) * 1.35em + 6px);
}
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text {
--wiktku-ta-rows: 3;
}
#wiktku-start-form textarea.wiktku-cavkani-text {
--wiktku-ta-rows: 4;
}
/* Mane, Nimûne + Jêgirtin (citation) boxes, and their tr/t sub-fields: at
least 4 rows so there's room to actually write a definition/example/quote.
Manual resize (see util.js) still lets the person grow or shrink any of
these afterward — this only sets where they start. */
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t,
#wiktku-start-form textarea.wiktku-dfn-quote-src-manual {
--wiktku-ta-rows: 4;
}
/* Nudges the border when the box contains a typed/pasted linebreak, so
the user notices before it's silently turned into <br> on output; the
break itself is marked via a mirrored backdrop layer (see below). */
#wiktku-start-form textarea.wiktku-has-linebreak {
border-color: var(--wikt-palette-yellow6, #ac6600) !important;
}
/* Sits behind the textarea showing the same text, with a highlighted
marker at each line followed by a break; font/padding/line-height are
copied from the textarea via computed style, not shared CSS classes. */
.wiktku-linebreak-backdrop {
position: absolute;
inset: 0;
z-index: 0;
margin: 0;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
color: transparent;
background: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
resize: none !important;
pointer-events: none;
}
/* Raise the real textarea above its backdrop so typing/selection/caret
behave completely normally; the backdrop's mark shows through only
where the real textarea's own background is transparent. */
#wiktku-start-form textarea.wiktku-has-linebreak-source {
position: relative;
z-index: 1;
background-color: transparent;
}
/* Zero width, so the backdrop's text wraps exactly where the textarea's does; the marker itself is
painted out of flow from here, into the empty space the line break leaves. */
.wiktku-linebreak-anchor {
position: relative;
display: inline-block;
width: 0;
}
.wiktku-linebreak-mark {
position: absolute;
top: 0;
left: 0;
background-color: var(--wikt-palette-yellow6, #ac6600);
color: var(--wikt-palette-white, #fff);
border-radius: 2px;
}
/* ── Îzaha maneyî ──
Opens under the mane box; the two are one gloss, so it sits tight against it rather than reading as
a separate field. */
.wiktku-dfn-izah-wrap {
display: flex;
flex-direction: column;
gap: 3px;
margin-top: 4px;
}
.wiktku-dfn-nimune-sub,
.wiktku-dfn-quote-sub {
margin-top: 3px;
margin-left: 1em;
padding-left: 0.6em;
border-left: 2px solid var(--wikt-palette-lightgrey);
display: flex;
flex-direction: column;
gap: 3px;
}
.wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
.wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t {
min-width: 8em;
padding-top: 2px !important;
padding-bottom: 2px !important;
line-height: 1.35 !important;
font-size: 0.88em;
}
/* Sense list (Mane step) — one sense visible at a time, switched via
.wiktku-dfn-sense-tabs. */
ol.wiktku-dfn-list {
list-style: none;
margin: 0;
padding: 0;
}
ol.wiktku-dfn-list > li.wiktku-dfn-sense {
margin-bottom: 0.9em;
}
/* Template selector row inside a sense block */
.wiktku-dfn-tpl-row {
margin-bottom: 3px;
}
/* ── Sub-field titles (Etîket, Kategorî, Hevmane, Dijmane, Nimûne,
Jêgirtin, …): each group reads as a small labelled field — the term
stands out, the hint stays quiet. ── */
.wiktku-dfn-subgroup {
margin-top: 0.5em;
padding-top: 0.4em;
border-top: 1px dashed var(--wikt-palette-grey-3, #e0e0e0);
}
.wiktku-dfn-subgroup:first-of-type {
margin-top: 0.3em;
padding-top: 0;
border-top: none;
}
.wiktku-dfn-sublabel {
display: block;
margin-bottom: 3px;
font-size: 0.88em;
}
.wiktku-dfn-sublabel-term {
font-weight: 600;
color: var(--wikt-palette-blue-9, #3366cc);
letter-spacing: 0.03em;
font-size: 0.92em;
}
.wiktku-dfn-sublabel-hint {
color: var(--wikt-palette-grey-9, #767676);
font-style: italic;
}
.wiktku-pron-wrap,
.wiktku-etym-wrap,
.wiktku-tewandin-wrap,
.wiktku-cavkani-wrap {
display: flex;
flex-direction: column;
gap: 3px;
}
/* ── POS section tabs — rounded pill style ── */
#wiktku-section-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.6em;
align-items: center;
}
#wiktku-section-tabs .wiktku-section-tab {
padding: 3px 12px;
border: 1px solid var(--wikt-palette-lightgrey);
border-radius: 999px;
background: var(--wikt-palette-dullcyan);
cursor: pointer;
font-size: 0.86em;
user-select: none;
transition: background 0.12s ease, border-color 0.12s ease;
}
#wiktku-section-tabs .wiktku-section-tab:hover {
border-color: var(--wikt-palette-blue-9, #3366cc);
}
#wiktku-section-tabs .wiktku-section-tab.wiktku-tab-active {
background: var(--wikt-palette-blue-9, #3366cc);
border-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
font-weight: 600;
}
/* ── Step-group tabs (Mane / Bilêvkirin / Beşên din — see STEP_GROUPS in
start.js) ── Sits directly under the pill-style section tabs above;
styled as an underlined secondary nav so the two bars stay visually
distinct. */
#wiktku-group-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.8em;
border-bottom: 2px solid var(--wikt-palette-lightgrey);
}
#wiktku-group-tabs .wiktku-group-tab {
position: relative;
padding: 7px 14px 8px;
margin-bottom: -2px;
border-bottom: 3px solid transparent;
border-radius: 3px 3px 0 0;
cursor: pointer;
font-size: 0.95em;
font-weight: 600;
color: var(--wikt-palette-grey-9, #333);
background: var(--wikt-palette-grey-0, #f8f9fa);
user-select: none;
transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
}
#wiktku-group-tabs .wiktku-group-tab:hover {
color: var(--wikt-palette-blue-9, #3366cc);
background: var(--wikt-palette-paleblue, #eaf3ff);
}
#wiktku-group-tabs .wiktku-group-tab.wiktku-tab-active {
border-bottom-color: var(--wikt-palette-blue-9, #3366cc);
color: var(--wikt-palette-white);
background: var(--wikt-palette-blue-9, #3366cc);
font-weight: 700;
}
/* Small dot marking a tab that still has an unfilled required field
hidden inside it (see updateGroupTabIndicators in start.js). Cleared
automatically once that tab becomes the active one. */
#wiktku-group-tabs .wiktku-group-tab.wiktku-tab-missing::after {
content: "";
position: absolute;
top: 4px;
right: -1px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--wikt-palette-red-9, #d33);
}
.wiktku-lang-row-list {
margin: 0;
padding: 0;
list-style: none;
}
.wiktku-lang-row {
display: flex;
align-items: flex-start;
gap: 6px;
margin-bottom: 3px;
flex-wrap: wrap;
}
.wiktku-lang-row-words {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 3px;
}
/* Dûnde/Werger/Zarava rows: align the picker's height with the adjacent
word textarea (see .wiktku-start-narrow above). */
.wiktku-lang-row .wiktku-native-select-wrap input.wiktku-ac-input {
height: calc(1.7em + 10px);
min-height: calc(1.7em + 10px);
padding-top: 4px;
padding-bottom: 4px;
}
/* ── Werger / Bi zaravayên din groups: one per gloss, shared spacing ── */
#wiktku-start-trans .wiktku-trans-group,
#wiktku-start-zarava .wiktku-zarava-group {
margin-bottom: 0.6em;
}
#wiktku-start-trans .wiktku-trans-group:last-child,
#wiktku-start-zarava .wiktku-zarava-group:last-child {
margin-bottom: 0;
}
#wiktku-start-trans .wiktku-trans-group-label,
#wiktku-start-zarava .wiktku-zarava-group-label {
font-weight: 600;
font-size: 0.88em;
margin-bottom: 3px;
}
.wiktku-dfn-jeder-group {
margin-top: 3px;
}
/* Numbers each citation ("Jêgirtin 1:", "Jêgirtin 2:"…) and separates entries with a rule so multiple ones don't blur together. */
.wiktku-dfn-jeder-list {
counter-reset: wiktku-jeder-num;
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li {
counter-increment: wiktku-jeder-num;
padding-top: 8px;
border-top: 1px solid var(--wikt-palette-lightgrey, #eaecf0);
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li:first-child {
padding-top: 0;
border-top: none;
}
.wiktku-dfn-jeder-list > li.wiktku-dfn-jeder-li::before {
content: "Jêgirtin " counter(wiktku-jeder-num) ":";
display: block;
margin-bottom: 3px;
font-weight: 600;
font-size: 0.82em;
color: var(--wikt-palette-blue-9, #3366cc);
letter-spacing: 0.03em;
}
.wiktku-dfn-quote-src-sel {
font-size: 0.88em;
margin-top: 3px;
display: block;
/* A native select with no width rule sizes to fit its longest <option>
(here, the longest çavkanî template name) rather than its container,
which overflows the card on a phone — cap it like every other field. */
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* Wide/narrow/medium layout, same idea as MediaWiki:Gadget-wikt.add-
examples.js: rows wrap freely, each sized by its own width tier
(.wiktku-dfn-quote-src-row--wide/narrow/medium) instead of always
stacking one field per line. */
.wiktku-dfn-quote-src-fields {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 6px 10px;
margin-top: 3px;
}
/* Fills the flex row so the manual-source textarea's resize wrapper doesn't shrink to the browser's default width. */
.wiktku-dfn-quote-src-fields > .wiktku-resize-wrap {
flex: 1 1 100%;
width: 100%;
}
/* The Kategorî:Çavkanî picker is not one of the parameter boxes -- it decides which template's boxes
these are -- so it takes a line of its own above them. Left as a plain flex item it shrank to its
content and the first narrow field wrapped up beside it, which read as a nameless box paired with
someone else's label. */
.wiktku-dfn-quote-src-fields > .wiktku-cat-tpl-picker {
flex: 1 1 100%;
width: 100%;
}
/* A labelled source box -- "Şablona çavkaniyê", the link -- in the add-examples form's stacked shape:
label, box, then the button and hints under it at their natural size. */
.wiktku-dfn-quote-field {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.4em;
margin-top: 8px;
}
.wiktku-dfn-quote-url {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.wiktku-dfn-quote-citoid-btn,
.wiktku-dfn-quote-citoid-status {
align-self: flex-start;
}
#wiktku-start-form .wiktku-dfn-quote-url-hint {
margin: 0;
}
/* "Malûmatên çavkaniyê" and its "Vala bike", over a jêder-* template's boxes. */
.wiktku-dfn-quote-src-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--wikt-palette-lightgrey, #eaecf0);
}
.wiktku-dfn-quote-src-head-label {
font-size: 11px;
letter-spacing: 0.12em;
color: var(--wikt-muted, #72777d);
}
.wiktku-dfn-quote-src-clear {
font-size: 12px;
}
.wiktku-dfn-quote-src-cat-status {
display: block;
font-size: 0.82em;
color: var(--wikt-palette-lightgrey, #767676);
margin-top: 2px;
}
.wiktku-dfn-quote-src-row {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1 1 12em;
}
.wiktku-dfn-quote-src-row--wide {
flex-basis: 100%;
}
.wiktku-dfn-quote-src-row--narrow {
flex: 0 1 7em;
}
.wiktku-dfn-quote-src-row--medium {
flex: 1 1 12em;
}
.wiktku-dfn-quote-src-row label {
font-size: 0.82em;
white-space: nowrap;
}
.wiktku-dfn-quote-src-input {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
@media (max-width: 720px) {
.wiktku-lang-row {
flex-direction: column;
}
/* Re-asserts against the skin's own mobile-breakpoint heading styles,
which load after ours in the cascade. */
#wiktku-start-form .wiktku-start-step-name {
font-size: 1.05em !important;
margin: 0 0 0.2em !important;
line-height: 1.2 !important;
border: 0 !important;
padding: 0 !important;
}
/* Single column: search box, list, and narrow textareas (Etîket,
Hevmane, Dûnde/Jê/Têkildar/Binêre herwiha word boxes, citation
source fields, …) all use the full width. */
#wiktku-start-form textarea.wiktku-start-narrow,
#wiktku-start-form textarea.wiktku-dfn-subfield.wiktku-start-narrow {
width: 100%;
max-width: 100%;
}
.wiktku-mod-val-input {
flex: 1 1 auto;
max-width: 100%;
}
#wiktku-start-form .wiktku-pron-wrap textarea.wiktku-pron-text,
#wiktku-start-form .wiktku-etym-wrap textarea.wiktku-etym-text {
--wiktku-ta-rows: 3;
font-size: 1em;
}
#wiktku-start-form .wiktku-tewandin-wrap textarea.wiktku-tewandin-text,
#wiktku-start-form textarea.wiktku-cavkani-text {
--wiktku-ta-rows: 4;
font-size: 1em;
}
/* Mane, Nimûne + Jêgirtin and their tr/t sub-fields: keep at least 4 rows
on mobile too, rather than shrinking further than the desktop size. */
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form textarea.wiktku-dfn-nimune-text,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-tr,
#wiktku-start-form .wiktku-dfn-nimune-sub textarea.wiktku-dfn-nimune-t,
#wiktku-start-form .wiktku-dfn-jeder-group textarea.wiktku-dfn-nimune-quote,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-tr,
#wiktku-start-form .wiktku-dfn-quote-sub textarea.wiktku-dfn-nimune-quote-t {
--wiktku-ta-rows: 4;
font-size: 1em;
}
.wiktku-dfn-nimune-sub,
.wiktku-dfn-quote-sub {
margin-left: 0.5em;
padding-left: 0.4em;
}
.wiktku-pron-wrap .wiktku-native-select-wrap input.wiktku-ac-input,
.wiktku-etym-wrap .wiktku-native-select-wrap input.wiktku-ac-input {
font-size: 1em;
min-height: 2.2em;
}
/* Every field stacks full-width on mobile, regardless of its wide/
narrow/medium tier — there's no room to wrap several per line. */
#wiktku-start-form .wiktku-dfn-quote-src-row {
flex-basis: 100%;
}
.wiktku-start-step {
padding: 0.5em 0.6em;
}
}
/* ===================================================================
Night mode
Palette.css already flips --wikt-palette-* vars for night mode; this
hook only covers real colours (panel border) that aren't a palette var,
since a drop shadow is invisible against an equally dark page.
=================================================================== */
@media screen {
html.skin-theme-clientpref-night .wiktku-start-step,
html.skin-theme-clientpref-night .wiktku-start-step:hover,
html.skin-theme-clientpref-night .wiktku-start-step:focus-within {
box-shadow: none;
}
}
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os .wiktku-start-step,
html.skin-theme-clientpref-os .wiktku-start-step:hover,
html.skin-theme-clientpref-os .wiktku-start-step:focus-within {
box-shadow: none;
}
}
/* ── posparams step (live wikitext + helper widgets) ── */
.wiktku-posparams-wrap {
display: flex;
flex-direction: column;
gap: 6px;
}
.wiktku-posparams-helpers {
display: flex;
flex-direction: column;
gap: 4px;
}
.wiktku-posparams-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.wiktku-posparams-row label {
font-size: 1em;
min-width: 10em;
padding-right: 0;
}
.wiktku-posparams-subheading {
font-weight: 600;
font-size: 0.88em;
margin-top: 6px;
}
.wiktku-posparams-pronoun-hint {
font-size: 0.9em;
color: var(--wikt-ink-2);
font-style: italic;
}
/* Keeps a pronoun hint and its box on one line, immediately next to each
other, even when the row around them stacks label-over-field (see the
desktop grid layout for .wiktku-posparams-helpers below). */
.wiktku-posparams-field-line {
display: flex;
align-items: center;
gap: 6px;
}
.wiktku-posparams-text {
width: 100%;
box-sizing: border-box;
font-family: monospace;
font-size: 0.9em;
}
.wiktku-posparams-opt-toggle {
font-size: 0.85em;
margin-top: 2px;
display: inline-block;
}
.wiktku-posparams-opt-panel {
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px 0 2px 1em;
}
.wiktku-posparams-opt-row {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.85em;
}
/* ── resim step (one or more images) ── */
.wiktku-resim-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.wiktku-resim-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.wiktku-resim-item .wiktku-posparams-row {
flex-direction: column;
align-items: flex-start;
gap: 2px;
}
.wiktku-resim-item .wiktku-posparams-row label {
min-width: 0;
font-size: 0.9em;
}
/* ── dfn: which sub-field is on screen ──
Every sub-group (Nimûne, Jêgirtin, Hevmane, …) is built hidden and shown
only once it is picked from the chip row or the "Hevmane û Bêhtir" menu — see the
PICK_DEFS block in start.js. */
#wiktku-start-form .wiktku-dfn-subgroup-hidden {
display: none;
}
/* A şablon sense ({{guharto}}, {{şaşnivîs}}…) has no gloss of its own, so
the word lists make no sense for it — both the boxes and the menu entries
that would ask for them go away. */
#wiktku-start-form li.wiktku-sense-is-tpl .wiktku-dfn-wordgroup,
#wiktku-start-form li.wiktku-sense-is-tpl .wiktku-dfn-pick-word {
display: none;
}
/* × on a revealed sub-group's label, to put it away again. Dim until the
group is hovered, so it is never the loudest thing in the row. */
#wiktku-start-form .wiktku-dfn-subgroup-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 14px;
line-height: 1;
padding: 0 4px;
margin-left: 4px;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-dfn-subgroup:hover .wiktku-dfn-subgroup-remove,
#wiktku-start-form .wiktku-dfn-subgroup-remove:focus {
opacity: 1;
}
#wiktku-start-form .wiktku-dfn-subgroup-remove:hover {
color: var(--wikt-danger);
opacity: 1;
}
/* ── "Hevmane û Bêhtir" menu ──
An overlay anchored to its own toggle, so opening it never pushes the
boxes below it down the page. */
#wiktku-start-form .wiktku-dfn-more-wrap {
position: relative;
display: inline-block;
}
#wiktku-start-form .wiktku-dfn-more-arrow {
font-size: 9px;
vertical-align: 1px;
}
/* Same menu DefSideBoxes draws (.dsb-more-menu): a plain vertical list of links, sized to its widest
entry, floating over the page rather than pushing it down. */
#wiktku-start-form .wiktku-dfn-more-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 30;
display: flex;
flex-direction: column;
align-items: flex-start;
width: max-content;
max-width: min(320px, calc(100vw - 40px));
gap: 6px;
margin: 2px 0 0 4px;
padding: 8px 12px;
background: var(--wikt-card);
border: 1px solid var(--wikt-rule);
border-radius: var(--wikt-r-field);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}
#wiktku-start-form .wiktku-dfn-more-item {
align-self: flex-start;
font-size: 12px;
color: var(--wikt-accent);
white-space: nowrap;
cursor: pointer;
text-decoration: none;
/* The form's own link style underlines every <a> with a border; on a block-level menu entry that
reads as a rule between the items. */
border-bottom: 0;
}
#wiktku-start-form .wiktku-dfn-more-item:hover,
#wiktku-start-form .wiktku-dfn-more-item:focus {
color: var(--wikt-accent-deep);
text-decoration: underline;
border-bottom: 0;
}
/* Rules the menu's three sections apart: label fields, word lists, and "Maneyeke din zêde bike". */
#wiktku-start-form .wiktku-dfn-more-group-start {
margin-top: 7px;
padding-top: 7px;
border-top: 1px solid var(--wikt-rule-soft);
}
/* ===================================================================
Desktop text bump
Modifier pills/buttons and posparams helper text read fine at their
small mobile sizes but look too small on desktop, so bump them up
past the 720px breakpoint only.
=================================================================== */
@media (min-width: 721px) {
.wiktku-mod-btn,
.wiktku-mod-confirm,
.wiktku-mod-cancel {
width: 1.8em;
height: 1.8em;
font-size: 1.1em;
}
.wiktku-mod-pill {
font-size: 0.95em;
padding: 3px 10px;
}
.wiktku-mod-val-input {
font-size: 0.95em;
}
.wiktku-posparams-row label {
font-size: 1em;
}
.wiktku-posparams-text {
font-size: 1em;
}
.wiktku-posparams-opt-toggle {
font-size: 0.95em;
}
.wiktku-posparams-opt-row {
font-size: 0.95em;
}
/* Multi-column card layout for posparams helper fields (transliteration,
gendered forms, comparative/superlative, hanja, verb-tense roots, …),
desktop-only. Rows are shown/hidden via jQuery inline display; the
grid only rearranges visible rows, not which ones are visible. */
.wiktku-posparams-helpers {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px 16px;
align-items: start;
}
.wiktku-posparams-helpers > .wiktku-posparams-row {
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
gap: 3px;
}
.wiktku-posparams-helpers > .wiktku-posparams-row label {
min-width: 0;
}
.wiktku-posparams-helpers > .wiktku-posparams-row .wiktku-posparams-field,
.wiktku-posparams-helpers > .wiktku-posparams-row select.wiktku-posparams-cinsiyet-sel,
.wiktku-posparams-helpers > .wiktku-posparams-row select.wiktku-posparams-lektur-sel {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* The "Jimar" row lives here too — just another .wiktku-posparams-row,
no extra CSS needed. */
/* These two aren't per-field rows — span the full card width so the
collapsed toggle and its checkbox list don't get squeezed into one
narrow column. */
.wiktku-posparams-helpers > .wiktku-posparams-opt-toggle,
.wiktku-posparams-helpers > .wiktku-posparams-opt-panel,
.wiktku-posparams-helpers > .wiktku-posparams-subheading {
grid-column: 1 / -1;
}
}
/* ===================================================================
SÊWIRANA NÛ — design system layer
===================================================================
Purely the visual skin (colour, type, weight, radius, spacing), layered
after the layout rules above so it wins the cascade.
No @import of Spectral / IBM Plex from Google Fonts: Wikimedia's CSP
blocks third-party font hosts, so the stacks below name the spec's
faces first (used if the wiki installs them locally) and fall back to
faces with the same feel.
Accent colour is reserved for actionable elements (focus, links, the
active tab, the primary button, chips); section headings are near-black.
=================================================================== */
#wiktku-start-form {
font-family: var(--wikt-sans);
font-size: 15px;
line-height: 1.55;
color: var(--wikt-ink);
background: var(--wikt-page);
padding: 24px;
border-radius: var(--wikt-r-card);
}
/* ── Cards ───────────────────────────────────────────────────────────
Flat cream panel held by a single hairline border, no shadow. */
#wiktku-start-form .wiktku-start-step {
background: var(--wikt-card);
border: 1px solid var(--wikt-card-edge);
border-left: 1px solid var(--wikt-card-edge);
border-radius: var(--wikt-r-card);
box-shadow: none;
padding: 24px 24px 20px;
margin-bottom: 16px;
transition: border-color 0.12s ease;
}
#wiktku-start-form .wiktku-start-step:hover,
#wiktku-start-form .wiktku-start-step:focus-within {
box-shadow: none;
border-color: var(--wikt-rule);
}
/* The fused pos + posparams pair keeps reading as one card. margin-bottom
has to be reset here too — .wiktku-start-step's own 16px otherwise wins
(higher specificity than the older #wiktku-start-pos-only rule this was
meant to override), leaving a visible gap between the two "halves". */
#wiktku-start-form #wiktku-start-pos {
border-radius: var(--wikt-r-card) var(--wikt-r-card) 0 0;
border-bottom: none;
padding-bottom: 8px;
margin-bottom: 0;
}
#wiktku-start-form #wiktku-start-posparams {
border-radius: 0 0 var(--wikt-r-card) var(--wikt-r-card);
border-top: none;
padding-top: 4px;
}
/* ── Type ────────────────────────────────────────────────────────────
Step headings: serif, regular weight, near-black. 20px matches what the
narrow breakpoint already resolves to, so the two read the same; at 24px
every step title was a third taller than the answer beneath it and a form
with a dozen sections spent most of a desktop screen on headings. */
#wiktku-start-form .wiktku-start-step-name {
font-family: var(--wikt-serif);
font-size: 20px !important;
font-weight: 400;
letter-spacing: -0.01em;
line-height: 1.15;
color: var(--wikt-ink);
margin: 0 0 8px;
}
/* display:block matters: this is a <label>, so it was sharing a line with
the control it introduces wherever start.js didn't emit a <br>. */
#wiktku-start-form .wiktku-start-step-question {
display: block;
font-size: 15px;
font-weight: 400;
color: var(--wikt-ink-3);
margin: 0 0 12px;
line-height: 1.6;
max-width: 62ch;
}
/* Collapses the label entirely when empty (posparams' "Cins/zayend" title
can be blank) instead of leaving its margin behind as a gap; a no-op
for every other step, whose question text is never empty. */
#wiktku-start-form .wiktku-start-step-question:empty {
display: none;
margin: 0;
}
/* Same reason — a field label belongs above its field, not beside it. */
#wiktku-start-form .wiktku-posparams-row label,
#wiktku-start-form .wiktku-dfn-sublabel {
display: block;
margin-bottom: 6px;
padding-right: 0;
}
/* Field labels: small, medium weight, full-strength ink — they label a
control, so they should not read as body copy. */
#wiktku-start-form .wiktku-start-step label,
#wiktku-start-form .wiktku-besendin-picker label,
#wiktku-start-form .wiktku-posparams-row label,
#wiktku-start-form .wiktku-dfn-sublabel {
font-size: 13px;
font-weight: 500;
color: var(--wikt-ink);
letter-spacing: 0.01em;
}
/* The parenthetical/example half of a label is supporting text. */
#wiktku-start-form .wiktku-dfn-sublabel-hint,
#wiktku-start-form .wiktku-step-hint {
font-weight: 400;
font-style: italic;
color: var(--wikt-muted);
font-size: 12.5px;
}
/* ── Fields ──────────────────────────────────────────────────────────
One shape for every control: 1px edge, 10px radius, 44px tall. */
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form input.wiktku-ac-input,
#wiktku-start-form textarea,
#wiktku-start-form select {
font-family: var(--wikt-sans);
font-size: 14px;
color: var(--wikt-ink);
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
padding: 6px 10px;
box-shadow: none;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* --wikt-tap is the 44px touch minimum, which a mouse does not need: on a pointer-precise screen it made
every dropdown a third taller than its own text. The touch size is put back under the coarse-pointer
and narrow rules below. */
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form select {
min-height: 34px;
}
#wiktku-start-form textarea {
line-height: 1.6;
}
/* Wikitext and anything template-shaped reads as code. The monospace face carries that on its own;
these are fields you type into, so they keep the same white fill and edge as every other field. */
#wiktku-start-form .wiktku-posparams-text,
#wiktku-start-form .wiktku-pron-text,
#wiktku-start-form .wiktku-etym-text,
#wiktku-start-form .wiktku-cavkani-text,
#wiktku-start-form .wiktku-tewandin-text {
font-family: var(--wikt-mono);
font-size: 13.5px;
background: var(--wikt-field-bg);
}
/* The pronunciation preview is generated output, not an input, so it stays tinted. */
#wiktku-start-form .wiktku-pron-preview-body {
font-family: var(--wikt-mono);
font-size: 13.5px;
background: var(--wikt-code-bg);
border-color: var(--wikt-card-edge);
}
#wiktku-start-form ::placeholder {
color: var(--wikt-placeholder);
opacity: 1;
}
/* Focus: accent edge + 3px tint halo. The only "elevation" in the design. */
#wiktku-start-form input[type="text"]:focus,
#wiktku-start-form input[type="search"]:focus,
#wiktku-start-form textarea:focus,
#wiktku-start-form select:focus,
#wiktku-start-form input.wiktku-ac-input:focus {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
outline: 0;
}
/* The searchable picker's visible input is the field, so it takes the
field shape too (it otherwise inherits an older, boxier rule). */
#wiktku-start-form input.wiktku-ac-input {
min-height: 34px;
border-radius: var(--wikt-r-field);
border-color: var(--wikt-field-edge);
font-size: 14px;
}
#wiktku-start-form .wiktku-ac-results {
border: 1px solid var(--wikt-rule);
border-radius: var(--wikt-r-field);
background: var(--wikt-card);
box-shadow: none;
}
#wiktku-start-form .wiktku-ac-results li.wiktku-ac-active,
#wiktku-start-form .wiktku-ac-results li:hover {
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
}
/* ── Tabs → the accent's main job ────────────────────────────────────
Active tab: tint fill + 3px accent underline, per the spec's
"xala hûrgulî" note. Inactive tabs are plain text. */
#wiktku-start-form #wiktku-group-tabs,
#wiktku-start-form #wiktku-section-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
border-bottom: 1px solid var(--wikt-rule);
margin-bottom: 16px;
padding-bottom: 0;
}
/* Both bars are direct children of #wiktku-start-form, so a sticky child
stays stuck for as long as any part of the form is in view. Background
is opaque so content scrolling underneath doesn't show through.
Section-tabs pins below --wiktku-siteheader-h — an offset for Vector
2022's own sticky header, which otherwise sits on top of it at top:0
(Minerva has no such header, so mobile needs no offset); group-tabs
pins below that plus --wiktku-stabs-h (section-tabs' own height). Both
custom properties are set by syncStickyTabsOffset() in start.js and
fall back to 0 on skins without a Vector sticky header. */
#wiktku-start-form #wiktku-section-tabs {
position: sticky;
top: var(--wiktku-siteheader-h, 0px);
z-index: 20;
background: var(--wikt-card);
}
#wiktku-start-form #wiktku-group-tabs {
position: sticky;
top: calc(var(--wiktku-siteheader-h, 0px) + var(--wiktku-stabs-h, 0px));
z-index: 19;
background: var(--wikt-card);
}
#wiktku-start-form .wiktku-group-tab,
#wiktku-start-form .wiktku-section-tab {
font-size: 13.5px;
font-weight: 500;
color: var(--wikt-ink-3);
background: transparent;
border: 0;
border-bottom: 3px solid transparent;
border-radius: var(--wikt-r-pill) var(--wikt-r-pill) 0 0;
padding: 9px 14px 11px;
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-group-tab:hover,
#wiktku-start-form .wiktku-section-tab:hover {
background: var(--wikt-card-sunken);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-group-tab.wiktku-tab-active,
#wiktku-start-form .wiktku-section-tab.wiktku-tab-active {
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom-color: var(--wikt-accent);
}
/* ── Progressive-disclosure toggles ──────────────────────────────────
Shared by "Tercîhên pêşketî" (posparams) and "Malûmatên pêşketî"
(Mane): a rotating chevron plus a count of what's hidden. */
#wiktku-start-form .wiktku-adv-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin: 4px 0 12px;
padding-top: 16px;
border-top: 1px solid var(--wikt-rule-soft);
}
#wiktku-start-form .wiktku-adv-toggle {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 500;
color: var(--wikt-accent);
text-decoration: none;
cursor: pointer;
}
#wiktku-start-form .wiktku-adv-toggle:hover .wiktku-adv-toggle-text {
text-decoration: underline;
}
#wiktku-start-form .wiktku-adv-toggle-chevron {
width: 22px;
height: 22px;
flex: 0 0 22px;
display: grid;
place-items: center;
font-size: 11px;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
border-radius: 6px;
transform: rotate(-90deg);
transition: transform 0.15s ease;
}
/* Open state: dfn flips the class on the step, posparams on the link. */
#wiktku-start-form .wiktku-adv-toggle.wiktku-adv-open .wiktku-adv-toggle-chevron,
#wiktku-start-dfn:not(.wiktku-dfn-subs-hidden) .wiktku-adv-toggle-chevron {
transform: rotate(0deg);
}
#wiktku-start-form .wiktku-posparams-adv-panel {
border: 1px solid var(--wikt-rule-soft);
border-radius: 12px;
background: var(--wikt-card-sunken);
padding: 20px 18px;
margin-bottom: 4px;
}
/* Vertical rhythm between stacked field rows (e.g. "Tewandina lêkerê"),
wider than a row's own label-to-field gap so rows read as distinct
groups instead of bleeding together. */
#wiktku-start-form .wiktku-posparams-helpers {
display: flex;
flex-direction: column;
gap: 14px;
}
/* ── posparams grid ──────────────────────────────────────────────────
"Tor, ne rêz" — two columns, nothing narrower than 220px, so the six
gender/number fields stop being a six-across scanline. */
@media (min-width: 721px) {
#wiktku-start-form .wiktku-posparams-helpers,
#wiktku-start-form .wiktku-posparams-adv-panel {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 18px 22px;
align-items: start;
}
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-row,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-row {
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
gap: 7px;
}
/* The row's own gap already separates the label from its field — the
label's margin-bottom (set for the stacked mobile layout) would
otherwise double that spacing here. */
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-row label,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-row label {
margin-bottom: 0;
}
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-adv-toggle-row,
#wiktku-start-form .wiktku-posparams-helpers > .wiktku-posparams-adv-panel,
#wiktku-start-form .wiktku-posparams-adv-panel > .wiktku-posparams-opt-panel {
grid-column: 1 / -1;
}
}
#wiktku-start-form .wiktku-posparams-opt-panel {
border-top: 1px solid var(--wikt-rule-soft);
padding-top: 14px;
margin-top: 4px;
}
#wiktku-start-form .wiktku-posparams-opt-row {
font-size: 13px;
color: var(--wikt-ink-3);
}
/* ── Sense blocks ────────────────────────────────────────────────────
Each sense is a sunken panel inside the Mane card, with its number as
an accent counter — so "how many meanings am I writing" is legible at
a glance. */
#wiktku-start-form .wiktku-dfn-subgroup {
margin: 14px 0 0;
padding-top: 14px;
border-top: 1px solid var(--wikt-rule-soft);
}
/* No divider above the first item — it would be a rule against the
sense's own text. The class is put on the first *shown* group by
updateFirstVisible() in start.js: :first-of-type would keep picking a
group that is still hidden behind the chip row, leaving the first
visible one with a stray rule above it. */
#wiktku-start-form .wiktku-dfn-subgroup-first {
margin-top: 8px;
padding-top: 0;
border-top: none;
}
#wiktku-start-form .wiktku-dfn-sublabel-term {
color: var(--wikt-ink);
}
/* Plain white field like any other, not a tinted fill — a tint would
read as "disabled" next to the white Mane box above it. */
#wiktku-start-form .wiktku-dfn-subfield,
#wiktku-start-form textarea.wiktku-dfn-mane,
#wiktku-start-form .wiktku-word-input {
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
}
/* No "1." "2." markers on the sub-lists: numbered rows would read as a
form to fill top-down, when each row is really "another one if you
have one" — and the marker would land on its own line above the
block-level textarea, looking like a stray character. */
#wiktku-start-form .wiktku-dfn-sublist {
margin: 0;
padding-left: 0;
list-style: none;
}
#wiktku-start-form .wiktku-dfn-sublist > li {
margin-bottom: 6px;
}
/* ── Chips: tags, modifiers, template pills ──────────────────────────── */
#wiktku-start-form .wiktku-mod-pill,
#wiktku-start-form .wiktku-besendin-select + .wiktku-chip {
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
font-size: 13px;
padding: 6px 12px;
}
#wiktku-start-form .wiktku-mod-btn,
#wiktku-start-form .wiktku-mod-confirm,
#wiktku-start-form .wiktku-mod-cancel {
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-radius: 8px;
}
/* ── Chip + toggle fields ("1c" design) — Dever/Şîwe, Etîket, Kategorî:
a small "+ …" toggle on the sense, not an always-visible field (see
buildChipField in start.js, used by all three). Chips sit inline before
the toggle; clicking it opens a compact panel with optional quick-pick
suggestions plus free text below. Each field's own wrap class (e.g.
.wiktku-dfn-dever-wrap) only scopes chip reads/writes in start.js — all
the visual styling here is shared via .wiktku-dfn-chipfield-*. The three
toggles live together in .wiktku-dfn-chipfield-row; their panels render
separately in .wiktku-dfn-chipfield-panels below it, so opening one
field's panel never pushes the other toggles around. ── */
#wiktku-start-form .wiktku-dfn-chipfield-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 8px 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-panels {
display: flex;
flex-direction: column;
gap: 8px;
}
#wiktku-start-form .wiktku-dfn-chipfield-wrap {
margin: 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-chips {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
#wiktku-start-form .wiktku-dfn-chipfield-chip {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
font-size: 12px;
padding: 3px 6px 3px 10px;
}
#wiktku-start-form .wiktku-dfn-chipfield-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-dfn-chipfield-remove:hover {
color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-dfn-chipfield-toggle {
font-size: 12px;
color: var(--wikt-ink-2);
background: var(--wikt-card);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
padding: 3px 10px;
cursor: pointer;
}
#wiktku-start-form .wiktku-dfn-chipfield-toggle:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* Maneid: one value, so a labelled box in the chip row rather than a picker panel. */
#wiktku-start-form .wiktku-dfn-maneid-label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--wikt-ink-2);
}
#wiktku-start-form input.wiktku-dfn-maneid-input {
width: 10em;
padding: 3px 8px;
font-size: 12px;
color: var(--wikt-ink);
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
}
#wiktku-start-form input.wiktku-dfn-maneid-input:focus {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
outline: 0;
}
#wiktku-start-form .wiktku-dfn-chipfield-panel {
margin-top: 8px;
border: 1px solid var(--wikt-field-edge);
background: var(--wikt-card);
border-radius: var(--wikt-r-card, 6px);
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
/* max-width bounds .wiktku-dfn-chipfield-suggest so its pills (e.g.
Dever/Şîwe's 11 suggestions) wrap onto multiple lines instead of
stretching the panel into one long unbroken row. */
min-width: 260px;
max-width: 340px;
}
#wiktku-start-form .wiktku-dfn-chipfield-hint {
font-size: 13px;
color: var(--wikt-ink-2);
}
#wiktku-start-form .wiktku-dfn-chipfield-suggest {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
cursor: pointer;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
padding: 3px 8px 3px 10px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
#wiktku-start-form .wiktku-dfn-chipfield-pill-remove:hover {
color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-dfn-chipfield-pill:hover {
background: var(--wikt-accent-tint);
border-color: var(--wikt-accent-edge);
color: var(--wikt-accent-deep);
}
#wiktku-start-form .wiktku-dfn-chipfield-input-row {
display: flex;
gap: 6px;
}
#wiktku-start-form .wiktku-dfn-chipfield-input {
flex: 1;
/* Without this, flexbox gives a text input an automatic minimum size
based on its default content width, which is wider than this panel
ever gets on a phone — the row can't shrink to fit and "Qeyd bike"
gets pushed out past the card's edge instead of wrapping/shrinking. */
min-width: 0;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field, 10px);
font-size: 13px;
padding: 6px 8px;
background: var(--wikt-card);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-dfn-chipfield-add {
flex-shrink: 0;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
background: var(--wikt-card);
color: var(--wikt-ink-2);
font-family: var(--wikt-sans);
font-size: 13px;
padding: 6px 14px;
cursor: pointer;
}
#wiktku-start-form .wiktku-dfn-chipfield-add:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* ── Footer + buttons ────────────────────────────────────────────────
One primary filled pill; destructive actions are outline-only in the
danger colour so they stay quiet next to it. */
#wiktku-start-form #wiktku-start-footer {
background: var(--wikt-card-sunken);
border-top: 1px solid var(--wikt-rule);
border-radius: 0 0 var(--wikt-r-card) var(--wikt-r-card);
padding: 12px 16px;
gap: 8px;
}
/* OOUI ships each button widget with its own margin-right, which lands on top of the flex gap and
costs an extra 8px per button when the row is deciding whether to wrap. The gap spaces them. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement {
margin-right: 0;
}
/* --wikt-tap is the 44px touch minimum, which a mouse does not need; at 14px/22px the bar read as
five oversized slabs. Touch sizing is restored under the narrow/coarse rules below. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
border-radius: var(--wikt-r-pill) !important;
font-family: var(--wikt-sans);
font-size: 13px;
font-weight: 500;
padding: 7px 16px;
box-shadow: none !important;
min-height: 32px;
}
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
background: var(--wikt-accent) !important;
border-color: var(--wikt-accent) !important;
color: var(--wikt-on-accent) !important;
}
/* Destructive actions: outline, not fill. */
#wiktku-start-form #wiktku-start-footer .oo-ui-flaggedElement-destructive .oo-ui-buttonElement-button {
background: var(--wikt-card) !important;
border-color: var(--wikt-danger) !important;
color: var(--wikt-danger) !important;
}
#wiktku-start-form #wiktku-last-saved {
font-family: var(--wikt-mono);
font-size: 12px;
color: var(--wikt-muted);
}
/* ── Beşên din picker ────────────────────────────────────────────────
This dropdown is now the entry point to eighteen sections, so it gets
room and a label rather than sitting flush under the tab bar. */
#wiktku-start-form .wiktku-besendin-picker {
margin: 0 0 20px;
padding: 16px 18px;
background: var(--wikt-card);
border: 1px solid var(--wikt-card-edge);
border-radius: 12px;
}
#wiktku-start-form .wiktku-besendin-picker .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-besendin-picker input.wiktku-ac-input {
width: 100%;
max-width: 28em;
}
/* ── Notices ─────────────────────────────────────────────────────────── */
#wiktku-start-form .wiktku-addlang-notice {
background: var(--wikt-accent-tint);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-field);
color: var(--wikt-ink-2);
padding: 12px 16px;
margin-bottom: 16px;
}
#wiktku-start-form .wiktku-pron-preview {
background: var(--wikt-accent-tint);
border-color: var(--wikt-accent-edge);
border-radius: 12px;
}
#wiktku-start-form .wiktku-pron-preview-confirm,
#wiktku-start-form .wiktku-pron-preview-cancel {
border-radius: var(--wikt-r-pill);
padding: 8px 16px;
min-height: 36px;
}
/* Links carry the accent and a soft underline, per the spec's `a` rule. */
#wiktku-start-form a {
color: var(--wikt-accent);
text-decoration: none;
border-bottom: 1px solid var(--wikt-link-under);
}
#wiktku-start-form a:hover {
color: var(--wikt-accent-deep);
}
/* A frameless OOUI button ("Vala bike") is an <a> inside, but not a link. */
#wiktku-start-form .oo-ui-buttonElement-frameless > a.oo-ui-buttonElement-button {
border-bottom: 0;
}
#wiktku-start-form .wiktku-adv-toggle,
#wiktku-start-form .wiktku-group-tab,
#wiktku-start-form .wiktku-section-tab {
border-bottom-style: solid;
}
#wiktku-start-form .wiktku-adv-toggle {
border-bottom: 0;
}
/* ===================================================================
Dark mode
===================================================================
Every colour above is a token on #wiktku-start-form, so this is a
swap of that one block.
Only applies given an explicit dark signal (skin-theme-clientpref-night /
skin-nightmode, or skin-theme-clientpref-os plus prefers-color-scheme:
dark) — no bare prefers-color-scheme fallback, since legacy Vector sets
no theme class either and would otherwise go dark with nothing else on
the page to match.
Palette is warm, not neutral grey, to keep the cream design's character.
Accent lightens to #7ba7e8 since #2a5db0 on a dark card is ~2.4:1
contrast and reads as near-black.
=================================================================== */
/* A handful of rules still name literal light colours instead of tokens;
neutralise the ones that actually show through in night mode. */
html.skin-theme-clientpref-night #wiktku-start-form .wiktku-ac-results,
html.skin-nightmode #wiktku-start-form .wiktku-ac-results {
background: var(--wikt-card);
border-color: var(--wikt-rule);
color: var(--wikt-ink);
}
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os #wiktku-start-form .wiktku-ac-results {
background: var(--wikt-card);
border-color: var(--wikt-rule);
color: var(--wikt-ink);
}
}
/* ===================================================================
Autocomplete list: stop it escaping its card
===================================================================
A fixed width would run past the card's border on full-width pickers,
since the list is absolutely positioned at the wrapper's left edge.
width:100% ties it to the (shrink-to-fit) wrapper's input width instead;
min/max-width keep short pickers readable and stop viewport overflow. */
#wiktku-start-form ul.wiktku-ac-results {
width: 100%;
min-width: min(18em, 100%);
max-width: min(92vw, 34em);
box-sizing: border-box;
border-radius: var(--wikt-r-field);
box-shadow: none;
}
/* And the wrapper itself must not exceed the card it sits in. */
#wiktku-start-form .wiktku-native-select-wrap {
max-width: 100%;
}
#wiktku-start-form .wiktku-cavkani-wrap .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-cavkani-wrap input.wiktku-ac-input,
#wiktku-start-form .wiktku-pron-wrap .wiktku-native-select-wrap,
#wiktku-start-form .wiktku-etym-wrap .wiktku-native-select-wrap {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* ===================================================================
Mobile footer
===================================================================
Below 720px the primary button keeps its full size and everything
else shrinks to a secondary scale, so the sticky footer doesn't
dominate a phone viewport.
=================================================================== */
@media (max-width: 720px) {
#wiktku-start-form {
padding: 12px;
font-size: 14px;
}
#wiktku-start-form .wiktku-start-step {
padding: 16px 14px 14px;
border-radius: 12px;
}
#wiktku-start-form .wiktku-start-step-name {
font-size: 20px !important;
}
#wiktku-start-form #wiktku-start-footer {
padding: 10px 10px;
gap: 6px;
row-gap: 8px;
}
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
font-size: 13px;
padding: 8px 14px;
min-height: 38px;
}
#wiktku-start-form .wiktku-prefs-btn {
width: 38px;
height: 38px;
background-size: 15px 15px;
}
/* Exactly two button rows, whatever the labels measure: send + editor, then the three small ones.
A flex container's ::after is itself a flex item, so this is the row break; `order` puts it
between the two groups. */
#wiktku-start-form #wiktku-start-footer::after {
content: "";
flex: 0 0 100%;
height: 0;
order: 2;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary {
order: 1;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-cache {
order: 3;
}
/* The gear rides with the primaries: those two leave room for it, the two cache labels do not.
Horizontal padding is trimmed just enough that all three still fit a 360px screen. */
#wiktku-start-form #wiktku-start-footer .wiktku-prefs-btn {
order: 1;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary .oo-ui-buttonElement-button {
padding-left: 12px;
padding-right: 12px;
}
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
padding-left: 14px;
padding-right: 14px;
}
}
/* Below the narrowest common phone the labels no longer fit two to a row at 13px, so the whole bar
steps down one size rather than spilling onto a third and fourth row. */
@media (max-width: 359px) {
#wiktku-start-form #wiktku-start-footer {
padding: 10px 8px;
column-gap: 4px;
}
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement-button {
font-size: 12px;
padding-left: 8px;
padding-right: 8px;
}
#wiktku-start-form #wiktku-start-footer .wiktku-footer-primary .oo-ui-buttonElement-button,
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
padding-left: 8px;
padding-right: 8px;
}
/* The one you actually press stays thumb-sized. */
#wiktku-start-form #wiktku-unhide-editform .oo-ui-buttonElement-button {
font-size: 14px;
padding: 10px 20px;
min-height: var(--wikt-tap);
}
/* Secondary actions share a row instead of each claiming a full one. */
#wiktku-start-form #wiktku-start-footer .oo-ui-buttonElement {
flex: 0 1 auto;
}
#wiktku-start-form #wiktku-last-saved {
flex: 1 0 100%;
order: 10;
font-size: 11px;
white-space: normal;
}
#wiktku-start-form .wiktku-adv-toggle-row {
gap: 8px;
}
}
/* ===================================================================
IPA character palette (v3 spec)
===================================================================
Click-to-insert chart under Bilêvkirin, after fr.wiktionary's. Two
sub-tabs — a short Kurmancî set that opens by default, and the full
chart — because ~20 of the ~180 IPA symbols ever occur in Kurmancî
and showing all of them first is the same "wall of fields" problem
this redesign exists to fix.
=================================================================== */
/* Templates are not characters, so they sit apart from the chart —
pinned to the right of the sub-tab row. */
/* Full IPA chart: a dropdown picks one group (Bêdeng 1, Bêdeng 2, …)
instead of showing all ~180 symbols' rows at once. */
/* Ligatures like d͡ʒ and long vowels need a little more room than one
glyph; let those keys grow instead of clipping. */
/* A combining mark is shown on a dotted circle so it has something to
attach to; only the mark itself is inserted. */
@media (max-width: 720px) {
}
/* ── "+ Beş zêde bike" menu ──────────────────────────────────────────
Adding a part of speech and adding Bilêvkirin are different actions,
so the button opens a small menu instead of doing one of them. */
#wiktku-start-form .wiktku-addsection-menu {
position: absolute;
z-index: 60;
margin-top: 6px;
min-width: 15em;
background: var(--wikt-card);
border: 1px solid var(--wikt-rule);
border-radius: 10px;
padding: 4px;
display: flex;
flex-direction: column;
}
#wiktku-start-form .wiktku-addsection-item {
display: flex;
flex-direction: column;
gap: 1px;
padding: 8px 10px;
border-radius: 7px;
border-bottom: 0;
text-decoration: none;
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-addsection-item:hover {
background: var(--wikt-accent-tint);
}
#wiktku-start-form .wiktku-addsection-item-label {
font-size: 13.5px;
font-weight: 500;
}
#wiktku-start-form .wiktku-addsection-item-hint {
font-size: 11.5px;
color: var(--wikt-muted);
}
#wiktku-start-form #wiktku-section-tabs .oo-ui-buttonElement {
position: relative;
}
/* Bilêvkirin is a peer of the POS sections but not a part of speech —
a little extra space before it marks the boundary, no divider line. */
#wiktku-start-form .wiktku-section-tab-pron {
margin-left: 6px;
}
/* ===================================================================
Per-sense tabs
===================================================================
"Mane 1 / Mane 2 / + Mane zêde bike" switches which sense is shown;
only the active one is in the flow, so a multi-sense entry doesn't
turn into a long scroll. */
#wiktku-start-form .wiktku-dfn-sense-tabs {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
border-bottom: 1px solid var(--wikt-rule);
margin-bottom: 14px;
}
#wiktku-start-form .wiktku-dfn-sense-tab {
font-size: 13px;
font-weight: 500;
color: var(--wikt-ink-3);
background: transparent;
border: 0;
border-bottom: 3px solid transparent;
border-radius: var(--wikt-r-pill) var(--wikt-r-pill) 0 0;
padding: 7px 12px 9px;
cursor: pointer;
user-select: none;
transition: background 0.12s ease, color 0.12s ease;
}
#wiktku-start-form .wiktku-dfn-sense-tab:hover {
background: var(--wikt-card-sunken);
color: var(--wikt-ink);
}
#wiktku-start-form .wiktku-dfn-sense-tab.wiktku-tab-active {
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom-color: var(--wikt-accent);
}
/* Delete button on the Mane / Beş tab bars — a real OOUI ButtonWidget
(icon: "trash", flags: ["destructive"], framed: false), so its glyph,
red destructive tint and hover/focus states all come from OOUI's own
WikimediaUI theme rather than being reimplemented here. This class
only places it: a little breathing room from the tab before it, and
vertically centered in the flex tab bar (OOUI's own icon-button box
is already close to the tabs' height, so no extra sizing is needed). */
#wiktku-start-form .wiktku-trash-btn {
margin-left: 4px;
}
#wiktku-start-form li.wiktku-dfn-sense {
border: 1px solid var(--wikt-card-edge);
border-radius: 12px;
background: var(--wikt-card);
margin-bottom: 10px;
padding: 14px;
list-style: none;
overflow: visible;
}
#wiktku-start-form li.wiktku-dfn-sense.wiktku-sense-collapsed {
display: none;
}
/* ===================================================================
Template rows in the pickers (v3 spec)
===================================================================
One row shape everywhere a template is chosen: the ⓘ explains terms
like "guharto/kevnbûyî/ku-Latn/şaşnivîs" without leaving the form,
and the 🔗 links out for when it doesn't.
=================================================================== */
#wiktku-start-form ul.wiktku-ac-results li.wiktku-ac-tplrow {
display: flex;
align-items: center;
gap: 8px;
min-height: 44px;
white-space: normal;
padding: 6px 10px;
}
#wiktku-start-form .wiktku-ac-tpltext {
flex: 1 1 auto;
min-width: 0;
font-family: var(--wikt-mono);
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Status, not category: green is the one to reach for, yellow means
"only if you specifically need it" (kevnbûyî, şaşnivîs, kêmpeyda). */
#wiktku-start-form .wiktku-tpl-dot {
flex: 0 0 8px;
width: 8px;
height: 8px;
border-radius: 999px;
}
#wiktku-start-form .wiktku-tpl-dot-ok { background: #3d8a55; }
#wiktku-start-form .wiktku-tpl-dot-special { background: #c9971f; }
#wiktku-start-form .wiktku-tpl-icons {
flex: 0 0 auto;
display: flex;
gap: 5px;
align-items: center;
}
#wiktku-start-form .wiktku-tpl-icon {
width: 26px;
height: 26px;
display: grid;
place-items: center;
font-size: 12px;
line-height: 1;
cursor: pointer;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
color: var(--wikt-accent);
border-bottom: 1px solid var(--wikt-accent-edge);
text-decoration: none;
padding: 0;
}
#wiktku-start-form .wiktku-tpl-icon-link { border-radius: 7px; }
#wiktku-start-form .wiktku-tpl-icon-info {
border-radius: 999px;
font-family: var(--wikt-serif);
font-style: italic;
font-weight: 600;
background: transparent;
border-color: var(--wikt-field-edge);
color: var(--wikt-muted);
}
#wiktku-start-form .wiktku-tpl-icon:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
/* ── The ⓘ panel ─────────────────────────────────────────────────────
Three parts, in the spec's order: what it does, its parameters, and a
way out to the template page. */
/* Floated over the page from the ⓘ that opened it, so no rule here hangs off the wizard's own id --
the panel is appended to <body> to stay clear of the form's scrolling and stacking. */
.wiktku-tpl-info {
position: fixed;
z-index: 1000;
max-height: 60vh;
overflow-y: auto;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
padding: 14px 16px;
border: 1px solid var(--wikt-accent-edge);
background: var(--wikt-accent-tint);
border-radius: 12px;
font-size: 13.5px;
line-height: 1.6;
color: var(--wikt-ink-2);
}
/* A tip above the dropdown it explains, pointing down at it -- the look of MediaWiki's own filter tips.
The skin's tokens give it the page's own light or dark colours. The arrow sits outside the box, so the
box no longer scrolls; a long parameter list scrolls on its own instead. */
.wiktku-tpl-info {
overflow: visible;
max-height: none;
background: var(--background-color-base, #fff);
color: var(--color-base, #202122);
border: 1px solid var(--border-color-base, #a2a9b1);
border-radius: 2px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
.wiktku-tpl-info::after {
content: "";
position: absolute;
left: var(--wiktku-arrow-x, 16px);
width: 12px;
height: 12px;
margin-left: -6px;
background: inherit;
border: inherit;
transform: rotate(45deg);
}
.wiktku-tpl-info-above::after {
bottom: -7px;
border-top: 0;
border-left: 0;
}
.wiktku-tpl-info-below::after {
top: -7px;
border-bottom: 0;
border-right: 0;
}
.wiktku-tpl-info-params ul {
max-height: 30vh;
overflow-y: auto;
}
.wiktku-tpl-info-name {
font-family: var(--wikt-mono);
font-size: 13px;
color: var(--wikt-accent-deep);
margin-bottom: 6px;
}
.wiktku-tpl-info-params {
margin-top: 8px;
}
.wiktku-tpl-info-params-title {
font-size: 12px;
color: var(--wikt-muted);
margin-bottom: 2px;
}
.wiktku-tpl-info-params ul {
margin: 0;
padding-left: 18px;
font-family: var(--wikt-mono);
font-size: 12.5px;
}
.wiktku-tpl-info-link {
display: inline-block;
margin-top: 10px;
font-size: 13px;
}
.wiktku-tpl-info-close {
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 999px;
border: 0;
background: transparent;
color: var(--wikt-muted);
cursor: pointer;
font-size: 13px;
line-height: 1;
}
.wiktku-tpl-info-close:hover {
background: var(--wikt-card);
color: var(--wikt-ink);
}
/* ===================================================================
Footer actions
===================================================================
Everything sits on the bar: Bişîne, "Edîtora çavkaniyê veke", both
cache buttons, Tercîh and the saved-at note. The bar wraps rather
than hiding anything behind a menu.
=================================================================== */
#wiktku-start-form .wiktku-btn-danger .oo-ui-buttonElement-button,
#wiktku-start-form .wiktku-btn-danger.oo-ui-buttonElement .oo-ui-buttonElement-button {
color: var(--wikt-danger);
border-color: var(--wikt-danger);
}
#wiktku-start-form .wiktku-cache-note {
flex: 1 1 100%;
order: 99;
font-size: 12px;
color: var(--wikt-muted);
}
#wiktku-start-form .wiktku-cache-note #wiktku-last-saved {
white-space: normal;
}
/* "+ Mane zêde bike" sits as the last item in .wiktku-dfn-sense-tabs. */
#wiktku-start-form .wiktku-add-sense {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 3px 0;
padding: 6px 14px;
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-pill);
background: var(--wikt-card);
color: var(--wikt-ink-2);
font-family: var(--wikt-sans);
font-size: 13px;
cursor: pointer;
min-height: 34px;
}
#wiktku-start-form .wiktku-add-sense:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent);
background: var(--wikt-accent-tint);
}
#wiktku-start-form .wiktku-add-sense-plus {
color: var(--wikt-accent);
font-size: 15px;
line-height: 1;
}
/* "+ Beşeke din zêde bike" and "Bi otomatîk tijî bike" are OO.ui.ButtonWidgets
(see start.js), which otherwise render with OOUI's own framed chrome —
override the inner .oo-ui-buttonElement-button to match .wiktku-add-sense's
flat-pill shape above instead, same !important pattern already used for
#wiktku-start-footer's buttons. */
#wiktku-start-form .wiktku-add-sense-btn .oo-ui-buttonElement-button {
border: 1px solid var(--wikt-field-edge) !important;
border-radius: var(--wikt-r-pill) !important;
background: var(--wikt-card) !important;
color: var(--wikt-ink-2) !important;
font-family: var(--wikt-sans);
font-size: 13px;
font-weight: normal;
padding: 6px 14px;
min-height: 34px;
box-shadow: none !important;
}
#wiktku-start-form .wiktku-add-sense-btn .oo-ui-buttonElement-button:hover {
border-color: var(--wikt-accent) !important;
color: var(--wikt-accent) !important;
background: var(--wikt-accent-tint) !important;
}
/* ── Şablon picker, demoted to a link ────────────────────────────────
Picking a template replaces the definition rather than adding to it,
so it sits above the Mane box as a quiet alternative route, hidden
behind one line of text until it is actually wanted. */
#wiktku-start-form .wiktku-dfn-tpl-link {
display: inline-block;
margin-bottom: 8px;
font-size: 12.5px;
color: var(--wikt-muted);
border-bottom: 1px solid transparent;
}
#wiktku-start-form .wiktku-dfn-tpl-link:hover {
color: var(--wikt-accent);
border-bottom-color: var(--wikt-link-under);
}
#wiktku-start-form .wiktku-dfn-tpl-row {
margin-bottom: 10px;
padding-bottom: 12px;
border-bottom: 1px solid var(--wikt-rule-soft);
}
/* ── Template-row icons on touch screens ─────────────────────────────
26px is fine for a mouse and too small for a thumb, and these two sit
right next to each other at the end of a row you can also tap to
select. Give them real targets and some separation on small screens. */
@media (max-width: 720px), (pointer: coarse) {
#wiktku-start-form .wiktku-tpl-icon {
width: 34px;
height: 34px;
font-size: 14px;
}
#wiktku-start-form .wiktku-tpl-icons {
gap: 8px;
padding-left: 4px;
}
#wiktku-start-form ul.wiktku-ac-results li.wiktku-ac-tplrow {
min-height: 50px;
}
}
/* The rendered pronunciation preview is real parser output, so it needs
to sit as content rather than in the monospace wikitext treatment the
panel inherits. */
#wiktku-start-form .wiktku-pron-preview-body {
font-family: var(--wikt-sans);
font-size: 15px;
background: var(--wikt-card);
border: 1px solid var(--wikt-accent-edge);
border-radius: 8px;
padding: 10px 12px;
white-space: normal;
}
#wiktku-start-form .wiktku-pron-preview-body .IPA,
#wiktku-start-form .wiktku-pron-preview-body code,
#wiktku-start-form .wiktku-pron-preview-body tt {
font-family: var(--wikt-mono);
}
/* Parser output brings its own margins; strip the outer ones so the
preview box doesn't gain a blank first line. */
#wiktku-start-form .wiktku-pron-preview-body > *:first-child { margin-top: 0; }
#wiktku-start-form .wiktku-pron-preview-body > *:last-child { margin-bottom: 0; }
/* ===================================================================
Touch: the fields go back to thumb size
===================================================================
The base sizes above are tuned for a mouse. Anything coarse-pointered
or narrow gets the 44px minimum back, and >=16px text so iOS Safari
does not zoom the page when a field takes focus.
=================================================================== */
@media (max-width: 720px), (pointer: coarse) {
#wiktku-start-form input[type="text"],
#wiktku-start-form input[type="search"],
#wiktku-start-form input.wiktku-ac-input,
#wiktku-start-form select {
min-height: var(--wikt-tap);
padding: 10px 14px;
font-size: 16px;
}
#wiktku-start-form textarea {
padding: 10px 14px;
font-size: 16px;
}
}
/* </nowiki> */
tk8sm3piv44nebzbxjhx91mkyg7m2dw
Modul:etymon/ceribandin
828
1359184
6531678
6433845
2026-09-15T18:08:58Z
Wikihez
7344
etymon: crz as the Kurdish name of pos, Kurdish pos values (reh, kelîme, pêşgir, paşgir, navgir), root/word categories in Kurdish (rehê ... yê, kelîmeya ... ya)
6531678
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
- [[Module:etymon/tracking]]: tracking
]=]
local export = {}
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,
linked_page_partial_etymology_sections = false,
partial_etymology_targets = {},
skip_partial_etymology_category = false,
max_depth_reached = 0,
total_nodes = 0,
language_count = {},
toplevel_keyword_stats = {},
id_stats = nil,
warnings = {},
}
local function reset_invocation_state()
__state.current_page_has_inline_etymology = false
__state.current_page_has_redundant_etymology = false
__state.used_idless_etymon = false
__state.toplevel_has_inline_etymology = false
__state.toplevel_redundant_etymology = false
__state.toplevel_idless_etymon = false
__state.has_mismatched_id = false
__state.linked_page_multiple_etymons_idless = false
__state.linked_page_partial_etymology_sections = false
__state.max_depth_reached = 0
__state.total_nodes = 0
__state.language_count = {}
__state.toplevel_keyword_stats = {}
__state.warnings = {}
end
local M = require("Module:module loader").init({
require = {
data = "Module:etymon/data/ceribandin",
tree = "Module:etymon/tree/ceribandin",
text = "Module:etymon/text/ceribandin",
categories = "Module:etymon/categories/ceribandin",
tracking = "Module:etymon/tracking",
descendants = "Module:etymon/descendants/ceribandin",
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",
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/ceribandin",
text_allowed = "Module:etymon/data/text_allowed",
},
})
-- Template names that carry etymon data, and the heading that opens an etymology section.
-- Pages are scraped for these, so both must match what ku.wiktionary actually uses:
-- the section is "Etîmolojî", and the new template is "etîmon".
-- Only the new template: pages still using Şablon:etymon carry the old ">"-separated syntax, which
-- this module would misparse into garbage node labels. Add "etymon" here only once
-- Şablon:etymon itself invokes this module.
local ETYMON_TEMPLATES = {
["etîmon"] = true,
["etymon/nû"] = true, -- redirect to Şablon:etîmon; still recognised if written out
-- Do NOT add "etymon" here while Şablon:etymon still uses the old ">"-separated syntax:
-- this module would read those calls and parse the bare keyword ("inh") as a term.
}
local ETYMOLOGY_HEADING = "^Etîmolojî"
-- ku's reconstruction namespace is "Jinûvesazî" (id 108), where en.wiktionary uses
-- "Reconstruction" (id 118). A page here is a reconstructed term, so its title needs the
-- leading "*" that Modul:links requires for reconstructed languages.
local RECONSTRUCTION_NS_NAME = "Jinûvesazî"
local RECONSTRUCTION_NS_ID = 108
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
-- Match a term language against a text=:lang stop target (supports etymology-only codes).
function Util.lang_matches_stop_code(term_lang, stop_code)
if not term_lang or not stop_code or stop_code == "" then
return false
end
local stop_lang = Util.get_lang(stop_code, true)
if not stop_lang then
return false
end
if term_lang:getCode() == stop_lang:getCode() then
return true
end
if stop_lang:getFullCode() == stop_lang:getCode() then
return term_lang:getFullCode() == stop_lang:getCode()
end
return false
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
function Util.resolve_context_lang(lang, node_args)
if type(node_args) ~= "table" then return lang end
if node_args.status == M.data.STATUS.INLINE then return lang end
if not (lang.hasType and lang:hasType("etymology-only")) then return lang end
local full = lang.getFull and lang:getFull()
if not full or full:getCode() == lang:getCode() then return lang end
if full.hasAncestor and full:hasAncestor(lang) then return lang end
return full
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
local REQUEST_TEMPLATE_PARAM_MODS = {
rfe = {
nocat = { type = "boolean" },
sort = {},
y = {},
m = {},
fragment = {},
section = {},
box = { type = "boolean" },
noes = { type = "boolean" },
},
etystub = {
nocat = { type = "boolean" },
sort = {},
nocap = { type = "boolean" },
nodot = { type = "boolean" },
},
}
function Util.expand_request_template(frame, template_name, param_value, lang_code)
local param_mods = REQUEST_TEMPLATE_PARAM_MODS[template_name]
local with_defaults = Util.add_boolean_defaults(param_value, param_mods)
local parsed = M.parse_utilities.parse_inline_modifiers(with_defaults, {
param_mods = param_mods,
generate_obj = function(text)
if M.yesno(text, false) then
return { is_boolean = true }
end
return { text = text }
end,
})
local template_args = { [1] = lang_code }
for name in pairs(param_mods) do
template_args[name] = parsed[name]
end
if not parsed.is_boolean then
template_args[2] = parsed.text
end
return " " .. frame:expandTemplate({
title = template_name,
args = template_args,
})
end
-- Kurdish values accepted for |pos= (|crz=) and <postype:>, mapped to the English values the rest of
-- the module compares against.
local POS_ALIASES = {
["reh"] = "root",
["kelîme"] = "word",
["pêşgir"] = "prefix",
["paşgir"] = "suffix",
["navgir"] = "interfix",
}
function Util.normalize_pos(value)
if type(value) ~= "string" then
return value
end
value = mw.text.trim(value)
return POS_ALIASES[value] or value
end
-- {{etîmon}} arguments read straight from another page skip Module:parameters, so `crz` (the Kurdish
-- name of `pos`) and the Kurdish values are resolved here.
function Util.normalize_raw_etymon_args(args)
if args.pos == nil and args.crz ~= nil then
args.pos = args.crz
end
args.pos = Util.normalize_pos(args.pos)
return args
end
-- Centralized term formatting: handles suppress_term (-), unknown_term (empty/+), 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
-- An explicit <t:...> wins; otherwise fall back to the etymon id, which on ku is usually a
-- Kurdish gloss in its own right ("berik", "hêştir"). An id that is a key rather than a
-- translation ("H2O", "paşgira meslekî") can be overridden with a <t:...>.
local t = (term.t and term.t ~= "") and term.t or nil
local id_gloss = (term.id and term.id ~= "") and term.id or nil
local gloss = t or id_gloss
-- gloss = "separate" leaves the gloss out of the link and returns it as a second value, so
-- the tree can put it on its own line.
if opts.gloss ~= "suppress" and opts.gloss ~= "separate" then
link_params.gloss = gloss
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
local link = M.links.full_link(link_params, "term", nil, show_qualifiers and true or nil)
if opts.gloss == "separate" then
return link, gloss
end
return link
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
local function is_affix_group_keyword(keyword)
local config = keyword and M.data.keywords[keyword]
return config and config.affix_categories or false
end
local function reject_removed_surf_keyword(param)
local base = get_keyword_base(param)
if base == "surf" then
error("The `:surf` keyword has been removed. Use `<surf>` on a formation keyword instead (e.g. `:af<surf>`, `:bor<surf>`).")
end
end
local function copy_keyword_info(source)
local copy = {}
for k, v in pairs(source) do
copy[k] = v
end
return copy
end
local function lowercase_glossary_display(text)
return text:gsub("(%[%[Appendix:Glossary#[^|]+|)([^%]])([^%]]*)%]%]", function(prefix, first, rest)
return prefix .. mw.ustring.lower(first) .. rest .. "]]"
end)
end
local function surf_should_keep_formation_phrase(base)
if not base.phrase then
return false
end
if base.glossary then
return true
end
return not (base.phrase == "from" and (base.text == "From" or base.text == "from"))
end
-- Runtime overrides when <surf> is present on a keyword.
local function get_effective_keyword_info(keyword, modifiers)
local base = M.data.keywords[keyword]
if not base or not modifiers or not modifiers.surf then
return base
end
local effective = copy_keyword_info(base)
local surf_text = "By [[Appendix:Glossary#surface_analysis|surface analysis]],"
local surf_phrase = "by surface analysis,"
effective.new_sentence = true
effective.invisible = "tree"
if surf_should_keep_formation_phrase(base) then
effective.phrase = surf_phrase .. " " .. base.phrase
if base.text then
effective.text = surf_text .. " " .. lowercase_glossary_display(base.text)
else
effective.text = surf_text .. " " .. base.phrase
end
else
effective.text = surf_text
effective.phrase = surf_phrase
end
return effective
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:find("-", 1, true) then
-- ku combines a gender with a number by hyphenating them ("m-p"), where en.wikt runs the
-- letters together ("mp"); build the label from the parts. Splitting two-letter codes the
-- en.wikt way would mislabel ku codes that are not combinations at all, such as "np"
-- (nepersonel).
local parts, all_known = {}, true
for part in code:gmatch("[^-]+") do
local part_adj = codes[part]
if part_adj then
table.insert(parts, part_adj)
else
all_known = false
end
end
if all_known and parts[1] then
adj = table.concat(parts, " ")
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 = { affix_group = true } },
conj = {}, -- conjunction for alternatives: "and", "or", "and/or", etc.
g = { restrict = { keywords = { "nominalization" } } },
surf = { type = "boolean" },
senseid = { restrict = { keywords = { "semantic loan" } } },
-- Kurdish names, resolved by resolve_modifier_aliases() below.
maneid = { alias_of = "senseid", restrict = { keywords = { "semantic loan" } } },
}
-- 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 = { affix_group = true } },
postype = {},
bor = { type = "boolean", restrict = { affix_group = true } },
slbor = { type = "boolean", restrict = { affix_group = true } },
lbor = { type = "boolean", restrict = { affix_group = true } },
-- Kurdish names for the same modifiers, resolved by resolve_modifier_aliases() below.
-- cuda/t/w/crz are the names ku's own templates already accept; the affix-group
-- borrowing flags reuse the Kurdish keyword names.
cuda = { alias_of = "alt" },
-- ASCII only: Modul:parse utilities matches a modifier prefix with "^([a-zA-Z0-9+_-]+):",
-- so a name containing "î" can never be recognised as a prefix.
etimon = { alias_of = "ety" },
w = { alias_of = "t" },
crz = { alias_of = "pos" },
nediyar = { type = "boolean", alias_of = "unc" },
deyn = { type = "boolean", alias_of = "bor", restrict = { affix_group = true } },
deynh = { type = "boolean", alias_of = "lbor", restrict = { affix_group = true } },
deynhq = { type = "boolean", alias_of = "slbor", restrict = { affix_group = true } },
}
--[==[
Move any Kurdish modifier onto the canonical name it aliases, so the rest of the module only ever
sees the canonical set. Called right after the inline modifiers are parsed.
]==]
function EtymonParser.resolve_modifier_aliases(obj, param_mods)
for name, def in pairs(param_mods) do
if def.alias_of and obj[name] ~= nil then
if obj[def.alias_of] ~= nil then
error("`<" .. name .. ":>` and `<" .. def.alias_of .. ":>` are the same modifier; use one.")
end
obj[def.alias_of] = obj[name]
obj[name] = nil
end
end
return obj
end
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" and key ~= "alias_of" 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 then
if mod_def.restrict.affix_group then
if not is_affix_group_keyword(current_keyword) then
local mod_display = mod_value == true and "<" .. mod_name .. ">" or "<" .. mod_name .. ":" .. tostring(mod_value) .. ">"
error("The modifier `" .. mod_display .. "` is only allowed for affix-group keywords (e.g. `:af`, `:blend`, `:univ`).")
end
elseif 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
end
local TERM_RULE_DISALLOW = {
suppress = { field = "suppress_term", label = "suppressed" },
unknown = { field = "unknown_term", label = "unknown" },
family = { field = "is_family", label = "family" },
}
function EtymonParser.check_etymon_limits(count, limits, label, opts)
if not limits then
return
end
opts = opts or {}
local min_etymons = limits.min_etymons
if min_etymons == nil and not opts.skip_default_min then
min_etymons = 1
end
if min_etymons and count < min_etymons then
if min_etymons > 1 then
error("Detected " .. label .. " group with fewer than " .. min_etymons .. " etymons.")
else
error("Detected " .. label .. " with no etymons.")
end
end
if limits.max_etymons and count > limits.max_etymons then
local unit = (limits.max_etymons == 1) and "etymon" or "etymons"
error("Detected " .. label .. " with more than " .. limits.max_etymons .. " " .. unit .. ".")
end
end
function EtymonParser.check_term_rules(etymon_data, entry_lang, rules, label)
label = label or "term"
if rules and rules.disallow then
local disallowed = {}
for _, typ in ipairs(rules.disallow) do
local spec = TERM_RULE_DISALLOW[typ]
if spec and etymon_data[spec.field] then
table.insert(disallowed, spec.label)
end
end
if #disallowed > 0 then
error(label .. " does not support " ..
mw.text.listToText(disallowed, "or") .. " etymons.")
end
end
if etymon_data.is_family then
if rules and rules.family == "disallowed" then
error(label .. " does not support family codes" .. (rules.family_suffix or "."))
elseif not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
end
if rules then
if rules.require_term and (not etymon_data.term or etymon_data.term == "") then
error(label .. " requires a term for each listed form.")
end
if rules.entry_lang then
if Util.get_norm_lang(etymon_data.lang):getFullCode() ~=
Util.get_norm_lang(entry_lang):getFullCode() then
error(label .. " terms must be in the entry language (" ..
entry_lang:getFullCode() .. "), got '" .. etymon_data.lang:getFullCode() .. "'.")
end
end
if rules.ancestor_check then
M.etymology.check_ancestor(entry_lang, etymon_data.lang)
end
elseif etymon_data.is_family and not etymon_data.suppress_term then
error("Family codes require suppressed term (use family:-).")
end
end
function EtymonParser.check_keyword_term(etymon_data, entry_lang, keyword)
local config = M.data.keywords[keyword]
EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "`:" .. keyword .. "`")
end
function EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type)
local config = M.data.supplements[supplement_type]
EtymonParser.check_term_rules(etymon_data, entry_lang, config and config.term_rules, "|" .. supplement_type .. "=")
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 })
EtymonParser.resolve_modifier_aliases(parsed, EtymonParser.keyword_param_mods)
local modifiers = {
unc = parsed.unc or false,
ref = parsed.ref,
text = parsed.text,
lit = parsed.lit,
conj = parsed.conj,
g = parsed.g,
surf = parsed.surf or false,
senseid = parsed.senseid,
}
-- Validate modifiers against restrictions
EtymonParser.check_modifier_restrictions(modifiers, canonical_keyword, EtymonParser.keyword_param_mods)
return canonical_keyword, modifiers
end
local function normalize_keyword_param(keyword_with_mods)
local trimmed = M.string_utilities.trim(keyword_with_mods)
reject_removed_surf_keyword(trimmed:match("^:") and trimmed or (":" .. trimmed))
local base = get_keyword_base(trimmed)
if not base or not M.data.keywords[base] then
error("Invalid keyword '" .. trimmed .. "' in inline etymology")
end
local canonical_base = get_canonical_keyword(base)
local without_colon = trimmed:gsub("^:", "")
local mods_part = without_colon:sub(#base + 1)
local kw_param = normalize_keyword(canonical_base .. mods_part)
EtymonParser.parse_keyword_modifiers(kw_param)
return kw_param
end
local function get_keyword_mod_names()
local names = {}
for mod_name in pairs(EtymonParser.keyword_param_mods) do
names[mod_name] = true
end
return names
end
local function parse_inline_ety_run(ety_string)
local body = ety_string or ""
if body == "" then
error("Empty inline etymology")
end
local keyword_mod_names = get_keyword_mod_names()
local pos = 1
local len = #body
local function parse_err(msg)
error(msg .. " in inline etymology: '" .. body .. "'")
end
local function peek_double()
return body:sub(pos, pos + 1) == "<<"
end
local function mod_name_from_unwrapped(unwrapped)
return unwrapped:match("^<([^:>]+)")
end
local function is_keyword_mod(unwrapped)
local name = mod_name_from_unwrapped(unwrapped)
return name and keyword_mod_names[name] or false
end
local function read_double_bracket()
if not peek_double() then
return nil
end
local start = pos
pos = pos + 2
while pos <= len - 1 do
if body:sub(pos, pos + 1) == ">>" then
local token = body:sub(start, pos + 1)
pos = pos + 2
return token, token:sub(2, -2)
end
pos = pos + 1
end
parse_err("Unmatched <<")
end
local function read_angle_cell()
if body:sub(pos, pos) ~= "<" or peek_double() then
return nil
end
local open = pos
pos = pos + 1
local depth = 1
local i = pos
while i <= len do
local ch = body:sub(i, i)
if ch == "<" then
depth = depth + 1
elseif ch == ">" then
depth = depth - 1
if depth == 0 then
local inner = body:sub(open + 1, i - 1)
pos = i + 1
return inner
end
end
i = i + 1
end
parse_err("Unmatched <")
end
local function read_bare_run()
local start = pos
while pos <= len and body:sub(pos, pos) ~= "<" do
pos = pos + 1
end
return body:sub(start, pos - 1)
end
local function absorb_double_keyword_mods(keyword_str)
while peek_double() do
local saved = pos
local _, unwrapped = read_double_bracket()
if is_keyword_mod(unwrapped) then
keyword_str = keyword_str .. unwrapped
else
pos = saved
break
end
end
return keyword_str
end
local kw_start = pos
while pos <= len and body:sub(pos, pos) ~= "<" do
pos = pos + 1
end
local keyword = body:sub(kw_start, pos - 1)
if keyword:match("^%s*$") then
parse_err("Missing keyword")
end
keyword = absorb_double_keyword_mods(keyword)
local cells = {}
while pos <= len do
if peek_double() then
local _, unwrapped = read_double_bracket()
if is_keyword_mod(unwrapped) then
parse_err("Unexpected keyword modifier " .. unwrapped .. " outside of a keyword")
end
table.insert(cells, "+" .. unwrapped)
elseif body:sub(pos, pos) == "<" then
local inner = read_angle_cell()
if inner ~= "" then
table.insert(cells, inner)
end
else
local bare = read_bare_run()
if bare ~= "" then
if bare:sub(1, 1) ~= ":" then
parse_err("Unexpected bare text '" .. bare .. "' (use :keyword for nested keywords in inline etymology)")
end
if not is_keyword(bare, true) then
parse_err("Invalid keyword '" .. bare .. "' in inline etymology")
end
table.insert(cells, absorb_double_keyword_mods(bare))
end
end
end
return {
keyword = keyword,
cells = cells,
}
end
function EtymonParser.inline_ety_to_pipe(ety_string)
local run = parse_inline_ety_run(ety_string)
if not run.keyword or run.keyword:match("^%s*$") then
return "|"
end
local pipe_parts = { normalize_keyword_param(M.string_utilities.trim(run.keyword)) }
for _, segment in ipairs(run.cells) do
if is_keyword(segment, true) then
table.insert(pipe_parts, normalize_keyword_param(segment))
else
table.insert(pipe_parts, segment)
end
end
return "|" .. table.concat(pipe_parts, "|") .. "|"
end
function EtymonParser.pipe_to_inline_ety(pipe_string)
local cells = {}
for cell in pipe_string:gmatch("([^|]+)") do
if cell ~= "" then
table.insert(cells, cell)
end
end
if #cells == 0 then
return ""
end
local inline_parts = {}
for index, cell in ipairs(cells) do
local base = get_keyword_base(cell)
if base and M.data.keywords[base] then
local without_colon = cell:gsub("^:", "")
local kw_base, mods = without_colon:match("^([^<]+)(.*)$")
local inline_kw = (kw_base or without_colon) .. (mods or ""):gsub("<([^>]+)>", "<<%1>>")
if index > 1 then
inline_kw = ":" .. inline_kw
end
table.insert(inline_parts, inline_kw)
elseif cell:sub(1, 1) == "+" then
local mod = cell:sub(2)
if mod:match("^<.->$") then
mod = mod:sub(2, -2)
end
table.insert(inline_parts, "<<" .. mod .. ">>")
else
table.insert(inline_parts, "<" .. cell .. ">")
end
end
return table.concat(inline_parts, "")
end
function EtymonParser.parse_inline_ety(ety_string, context_lang)
local run = parse_inline_ety_run(ety_string)
local keyword = M.string_utilities.trim(run.keyword)
reject_removed_surf_keyword(":" .. keyword)
if not is_keyword(keyword, true) then
error("Invalid keyword '" .. keyword .. "' in inline etymology <ety:" .. keyword .. "...>")
end
local args = { context_lang:getCode(), normalize_keyword_param(keyword) }
for _, segment in ipairs(run.cells) do
if is_keyword(segment, true) then
table.insert(args, normalize_keyword_param(segment))
else
table.insert(args, segment)
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
M.tracking.track_term(rest)
if rest == "" or 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 == "" or 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 })
EtymonParser.resolve_modifier_aliases(parsed_obj, EtymonParser.etymon_param_mods)
parsed_obj.postype = Util.normalize_pos(parsed_obj.postype)
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), ", ")
.. " (Kurdish: " .. table.concat(require("Module:table").keysToList(POS_ALIASES), ", ") .. ").")
end
local current_keyword = "from"
local current_keyword_explicit = false
local keyword_etymons = {}
local keywords = M.data.keywords
local function checkKeyword()
local config = keywords[current_keyword]
if current_keyword == "from" and not current_keyword_explicit and #keyword_etymons == 0 then
keyword_etymons = {}
return
end
EtymonParser.check_etymon_limits(#keyword_etymons, config, "`:" .. current_keyword .. "`")
keyword_etymons = {}
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
reject_removed_surf_keyword(param)
error("Invalid keyword '" .. param .. "'. Did you mean a valid keyword like ':bor', ':inh', etc.?")
elseif is_keyword(param) then
checkKeyword()
current_keyword = get_canonical_keyword(get_keyword(param))
current_keyword_explicit = true
else
local etymon_data = EtymonParser.parse_etymon(param, lang)
if etymon_data then
table.insert(keyword_etymons, param)
EtymonParser.check_keyword_term(etymon_data, lang, current_keyword)
-- 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\" (Kurdish \"reh\" or \"kelîme\").")
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(keyword_etymons, param)
end
end
end
checkKeyword()
end
local DataRetriever = {}
local function format_etymon_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 function etymon_target_page_link(page, norm_lang)
-- `page` is an already-resolved page name ("Jinûvesazî:proto-îranî/hižwáH"), so link to it
-- directly. Passing it back through full_link() with a reconstructed language raises an error,
-- because the resolved name no longer carries the leading "*" those languages require -- and
-- this link is built even when the warning it belongs to is later discarded outside preview.
return "[[" .. page .. "]]"
end
-- Summarize {{etymon}} id slots on a linked page for preview warnings.
local function summarize_available_etymon_ids(ids)
local id_list = {}
local all_idless = true
local target_has_idless = false
local any_pos = false
for i, id_data in ipairs(ids) do
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
all_idless = false
else
target_has_idless = true
end
if pos and pos ~= "" then
any_pos = true
end
table.insert(id_list, format_etymon_id_hint(id_data, i))
end
return {
id_list = id_list,
all_idless = all_idless,
target_has_idless = target_has_idless,
any_pos = any_pos,
count = #ids,
options_text = mw.text.listToText(id_list),
}
end
local function ambiguous_etymon_suggestion(page_link, summary)
if summary.all_idless then
if summary.any_pos then
return " None set `|id=` yet; add a unique `|id=` to each on " .. page_link
.. ", then `<id:identifier>` after the term here. Section order / hints: "
.. summary.options_text .. "."
end
return " 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
return " Specify which one with `<id:identifier>` after the term. Options: " .. summary.options_text .. "."
end
local function warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel)
local page_link = etymon_target_page_link(page, norm_lang)
local summary = summarize_available_etymon_ids(ids)
if is_toplevel and summary.target_has_idless then
__state.linked_page_multiple_etymons_idless = true
end
local lang_name = norm_lang:getCanonicalName()
local lead = "Etymology link to " .. page_link .. " is ambiguous (" .. summary.count
.. " {{etymon}} templates for " .. lang_name .. ")."
Util.add_warning(lead .. ambiguous_etymon_suggestion(page_link, summary), true)
end
local function is_mismatched_explicit_id(base_key, cached_args, parent_etymon)
return cached_args == M.data.STATUS.MISSING and not parent_etymon
and #(__state.available_etymon_ids[base_key] or {}) > 0
end
local function maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel)
if not is_toplevel or __state.skip_partial_etymology_category then
return
end
if not __state.partial_etymology_targets[base_key] then
return
end
if etymon_data.id and type(cached_args) == "table" then
return
end
__state.linked_page_partial_etymology_sections = true
end
local function is_nonlemma_etymon_template(template_args)
return template_args and M.yesno(template_args.nl, false)
end
local function warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_id)
local page_link = etymon_target_page_link(page, norm_lang)
local summary = summarize_available_etymon_ids(__state.available_etymon_ids[base_key] or {})
local lang_name = norm_lang:getCanonicalName()
local lead = "Etymology link to " .. page_link .. " uses `<id:" .. etymon_id
.. ">`, but no {{etymon}} on that page has `|id=" .. etymon_id .. "|` for " .. lang_name .. "."
Util.add_warning(lead .. " Valid IDs: " .. summary.options_text .. ".", true)
end
-- 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
-- ku.wiktionary writes L2 headings as "== {{ziman|CODE}} ==" rather than the language
-- name, and this scrape runs on raw wikitext, so the template form has to be tried too.
content = M.pages.get_section(content, etymon_lang:getFullName(), 2)
or M.pages.get_section(content, "{{ziman|" .. etymon_lang:getFullCode() .. "}}", 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
local etymology_section_count = 0
local etymology_sections_with_etymon = 0
local current_etymology_has_etymon = false
local current_etymology_has_nonlemma = false
local function finalize_current_etymology_section()
if etymology_section_count == 0 then
return
end
if current_etymology_has_etymon or current_etymology_has_nonlemma then
etymology_sections_with_etymon = etymology_sections_with_etymon + 1
end
current_etymology_has_etymon = false
current_etymology_has_nonlemma = false
end
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_HEADING) then
finalize_current_etymology_section()
etymology_section_count = etymology_section_count + 1
active_etymon_args = nil
end
elseif node_class == "template" then
local template_name = node:get_name()
if ETYMON_TEMPLATES[template_name] then
local template_args = Util.normalize_raw_etymon_args(node:get_arguments())
-- Check if this etymon is for our language
if template_args[1] == etymon_lang_code then
if is_nonlemma_etymon_template(template_args) then
if etymology_section_count > 0 then
current_etymology_has_nonlemma = true
end
else
if etymology_section_count > 0 then
current_etymology_has_etymon = true
end
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
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
finalize_current_etymology_section()
if lang_section_for_descendants
and etymology_section_count > 1
and etymology_sections_with_etymon > 0
and etymology_sections_with_etymon < etymology_section_count
then
__state.partial_etymology_targets[lang_page_key] = true
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
local function has_linkable_term(etymon_data)
if etymon_data.is_family or etymon_data.suppress_term or etymon_data.unknown_term then
return false
end
local term = etymon_data.term
if term == nil or term == "" then
return false
end
return M.string_utilities.trim(term) ~= ""
end
local function record_term_id_tracking(etymon_data)
if not has_linkable_term(etymon_data) then
return
end
local term_page = M.links.get_link_page(etymon_data.term, etymon_data.lang)
M.tracking.record_term_id_usage(__state.id_stats, etymon_data, term_page)
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)
if not has_linkable_term(etymon_data) then
return M.data.STATUS.MISSING, nil, nil, nil
end
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
local mismatched_id = is_mismatched_explicit_id(base_key, cached_args, parent_etymon)
if mismatched_id and is_toplevel then
__state.has_mismatched_id = true
M.tracking.record_mismatched_id_usage(__state.id_stats, norm_lang, page, etymon_data.id)
warn_mismatched_explicit_id(page, norm_lang, base_key, etymon_data.id)
end
maybe_flag_partial_etymology_reference(base_key, etymon_data, cached_args, is_toplevel)
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
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "postype")
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
local matched_args = __state.cached_etymon_args[matched_key]
maybe_flag_partial_etymology_reference(base_key, etymon_data, matched_args, is_toplevel)
return matched_args, __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 "*"
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "single")
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
local single_args = __state.single_etymons[base_key]
maybe_flag_partial_etymology_reference(base_key, etymon_data, single_args, is_toplevel)
return single_args, __state.cached_etymon_pages[base_key .. ":" .. only_id], nil, descendants_check
elseif count > 1 then
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "ambiguous")
warn_ambiguous_etymon_link(page, norm_lang, ids, is_toplevel)
maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.AMBIGUOUS, is_toplevel)
return M.data.STATUS.AMBIGUOUS, nil, nil, nil
else
M.tracking.record_idless_resolution(__state.id_stats, norm_lang, page, "missing")
maybe_flag_partial_etymology_reference(base_key, etymon_data, M.data.STATUS.MISSING, is_toplevel)
return M.data.STATUS.MISSING, nil, nil, nil
end
end
end
local function keyword_invisible_in_tree(keyword_info)
if not keyword_info then
return false
end
local inv = keyword_info.invisible
return inv == "all" or inv == true or inv == "tree"
end
-- True when the node has at least one top-level child container visible in the tree.
local function node_has_visible_tree_children(node)
for _, container in ipairs(node.children or {}) do
if not keyword_invisible_in_tree(container.keyword_info) then
return true
end
end
return false
end
-- Count visible term nodes in the tree.
local function get_visible_tree_depth(node, skip_child_rendering)
local max_depth = 1
if skip_child_rendering or not node then
return max_depth
end
for _, container in ipairs(node.children or {}) do
local keyword_info = container.keyword_info
if not keyword_invisible_in_tree(keyword_info) then
local skip_grandchildren = keyword_info and keyword_info.no_child_categories
for _, term in ipairs(container.terms or {}) do
if term.is_duplicate then
if term.original_has_children then
max_depth = math.max(max_depth, 2)
end
else
max_depth = math.max(max_depth, 1 + get_visible_tree_depth(term, skip_grandchildren))
end
end
end
end
return max_depth
end
local function as_param_list(val)
if val == nil then
return {}
end
if type(val) == "table" then
return val
end
if type(val) == "string" and val ~= "" then
return { val }
end
return {}
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
if node.supplements then
for _, supplement in ipairs(node.supplements) do
if supplement.terms then
for _, term in ipairs(supplement.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
-- Copy parsed etymon modifiers onto a tree/supplement term node.
function TreeBuilder.apply_etymon_fields(term, etymon_data)
-- A reference without <id:> still resolves when the target page has a single etymon, and the
-- node already carries that etymon's own id. Keep it instead of clearing it, so the tree shows
-- the target's id as the gloss without every reference having to repeat it. An explicit
-- <id:...> still wins.
term.id = etymon_data.id or term.id
term.t = etymon_data.t
term.tr = etymon_data.tr
term.ts = etymon_data.ts
term.alt = etymon_data.alt
term.g = etymon_data.g
term.pos = etymon_data.pos
term.ng = etymon_data.ng
term.ref = etymon_data.ref
term.is_uncertain = etymon_data.unc
term.lit = etymon_data.lit
term.q = etymon_data.q
term.qq = etymon_data.qq
term.l = etymon_data.l
term.ll = etymon_data.ll
term.suppress_term = etymon_data.suppress_term
term.unknown_term = etymon_data.unknown_term
term.is_family = etymon_data.is_family
term.override = etymon_data.override
term.aftype = etymon_data.aftype
term.postype = etymon_data.postype
term.bor = etymon_data.bor
term.lbor = etymon_data.lbor
term.slbor = etymon_data.slbor
end
function TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type)
EtymonParser.check_supplement_term(etymon_data, entry_lang, supplement_type)
local term = {
lang = etymon_data.lang,
title = etymon_data.term,
children = {},
status = M.data.STATUS.OK,
}
TreeBuilder.apply_etymon_fields(term, etymon_data)
return term
end
function TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value)
local terms = {}
for _, term_param in ipairs(as_param_list(param_value)) do
if type(term_param) == "string" and term_param ~= "" then
local etymon_data = EtymonParser.parse_etymon(term_param, entry_lang)
if etymon_data then
table.insert(terms, TreeBuilder.build_supplement_term(etymon_data, entry_lang, supplement_type))
end
end
end
return terms
end
-- Attach a |param= supplement defined in etymon_data.supplements (e.g. doublet=).
function TreeBuilder.append_term_supplement(data_tree, entry_lang, supplement_type, param_value)
local config = M.data.supplements[supplement_type]
if not config then
error("Unknown supplement '" .. tostring(supplement_type) .. "'.")
end
local terms = TreeBuilder.build_supplement_terms(entry_lang, supplement_type, param_value)
if #terms == 0 then
return
end
data_tree.supplements = data_tree.supplements or {}
table.insert(data_tree.supplements, {
type = supplement_type,
config = config,
terms = terms,
})
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, supplement_type, entry_lang, entry_lang, true)
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
local _, kw_modifiers = EtymonParser.parse_keyword_modifiers(param:sub(1, 1) == ":" and param or (":" .. param))
if not keyword_invisible_in_tree(get_effective_keyword_info(keyword_base, kw_modifiers)) 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
local function ensure_container()
if not current_container or current_container.keyword ~= current_keyword then
local keyword_info = get_effective_keyword_info(current_keyword, current_keyword_modifiers)
current_container = {
keyword = current_keyword,
keyword_info = keyword_info,
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 = copy_keyword_info(keyword_info)
current_container.keyword_info.text = labels.text
current_container.keyword_info.phrase = labels.phrase
end
end
return current_container
end
local parse_context_lang = Util.resolve_context_lang(lang, args)
for i = 2, #args do
local param = args[i]
if is_keyword(param) then
local keyword, modifiers = EtymonParser.parse_keyword_modifiers(param)
if not keyword then
error("Invalid keyword '" .. param .. "'.")
end
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
reject_removed_surf_keyword(param)
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, parse_context_lang)
if etymon_data then
-- Track keyword usage at top level
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, current_keyword, lang, etymon_data.lang, is_toplevel)
local term_node = {}
local container
-- Handle suppress_term (-) and unknown_term (empty or +) directly
if etymon_data.suppress_term or etymon_data.unknown_term then
container = 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
TreeBuilder.apply_etymon_fields(term_node, etymon_data)
else
-- Regular term: fetch arguments from page
record_term_id_tracking(etymon_data)
local etymon_args, page_of, resolved_etymon_id, descendants_check =
DataRetriever.get_etymon_args(etymon_data, is_toplevel)
-- 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
M.tracking.record_keyword_usage(__state.toplevel_keyword_stats, inline_keyword, etymon_data.lang, inline_etymon.lang, is_toplevel)
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
container = ensure_container()
-- Check if current keyword has no_child_categories - if so, stop recursion
local keyword_info = 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.etymon_id = resolved_etymon_id -- The actual etymon id when resolved via senseid
term_node.page_of = page_of
TreeBuilder.apply_etymon_fields(term_node, etymon_data)
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(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
-- 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()
__state.id_stats = M.tracking.new_id_stats()
__state.skip_partial_etymology_category = options.skip_partial_etymology_category == true
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 == RECONSTRUCTION_NS_ID 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)
__state.entry_title = title
__state.entry_lang_code = lang:getCode()
__state.id_stats = M.tracking.new_id_stats()
-- 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:makeEntryName(M.links.remove_links(tree_title)) ~= lang:makeEntryName(M.links.remove_links(title)) then
M.tracking.track_title_pagename_mismatch(lang)
end
reset_invocation_state()
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/ceribandin/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, {
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
gloss = "separate",
})
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)
args.pos = Util.normalize_pos(args.pos)
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 doublet = args.doublet
local rfe = args.rfe
local etystub = args.etystub
local is_nonlemma = M.yesno(args.nl, false)
local page_data = Util.get_page_data()
if not title then
title = page_data.pagename
if page_data.namespace == RECONSTRUCTION_NS_NAME then title = "*" .. title end
end
local entry_pagename = page_data.pagename
if page_data.namespace == RECONSTRUCTION_NS_NAME then
entry_pagename = "*" .. entry_pagename
end
if lang:makeEntryName(M.links.remove_links(title)) ~= lang:makeEntryName(M.links.remove_links(entry_pagename)) then
M.tracking.track_title_pagename_mismatch(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
reset_invocation_state()
local ety_data_tree = export.get_tree(lang, title, etymon_args, {
validate = true,
pos = args.pos,
id = id,
json = args.json,
skip_partial_etymology_category = is_nonlemma,
})
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.supplements = ety_data_tree.supplements or {}
table.insert(ety_data_tree.supplements, {
type = "etydate",
etydate_text = M.etydate.format_etydate(etydate_args, { omit_refs = true }),
etydate_refs = (parsed_etydate.ref and #parsed_etydate.ref > 0) and parsed_etydate.ref or nil,
})
end
TreeBuilder.append_term_supplement(ety_data_tree, lang, "doublet", doublet)
if ety_data_tree.supplements then
parse_tree_references(ety_data_tree)
end
local has_visible_children = node_has_visible_tree_children(ety_data_tree)
-- Suppress trees for multiword entries and one-step chains
local visible_tree_depth = get_visible_tree_depth(ety_data_tree)
local is_trivial_tree = visible_tree_depth <= 2
local is_multiword = title:find("%s") ~= nil or title:find("_") ~= nil
if tree and (is_multiword or is_trivial_tree) then
tree = false
end
if tree then
table.insert(output, M.template_styles("Module:etymon/ceribandin/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, {
pos = "suppress",
lit = "suppress",
tree_ql = "suppress",
gloss = "separate",
})
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)
local text_stop_lang_missing = nil
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
local text_output, text_render_meta = M.text.render({
data_tree = ety_data_tree,
format_term_func = Util.format_term,
lang_matches_stop_code = Util.lang_matches_stop_code,
max_depth = max_depth,
stop_at_blue_link = stop_at_blue_link,
curr_page = page_data.pagename,
nodot = args.nodot,
dot = args.dot,
stop_at_lang = stop_at_lang,
stop_at_lang_or_bluelink = stop_at_lang_or_bluelink,
})
table.insert(output, text_output)
if stop_at_lang and text_render_meta and not text_render_meta.stop_lang_reached then
M.tracking.track_text_stop_lang_missing(lang, stop_at_lang)
text_stop_lang_missing = stop_at_lang
end
end
if rfe then
table.insert(output, Util.expand_request_template(frame, "rfe", rfe, lang:getCode()))
end
if etystub then
table.insert(output, Util.expand_request_template(frame, "etystub", etystub, lang:getCode()))
end
if is_nonlemma then
table.insert(output, " " .. frame:expandTemplate({
title = "nonlemma",
args = {},
}))
end
local categories = {}
if Util.is_content_page() then
M.tracking.track_tree_metrics({
max_depth_reached = __state.max_depth_reached,
total_nodes = __state.total_nodes,
language_count = __state.language_count,
lang = lang,
})
categories = M.categories.build({
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,
suppress_categories = lang_exc and lang_exc.suppress_categories,
nocat = args.nocat,
tree = tree,
text = text,
exnihilo = args.exnihilo,
toplevel_has_inline_etymology = __state.toplevel_has_inline_etymology,
toplevel_redundant_etymology = __state.toplevel_redundant_etymology,
toplevel_idless_etymon = __state.toplevel_idless_etymon,
has_mismatched_id = __state.has_mismatched_id,
linked_page_multiple_etymons_idless = __state.linked_page_multiple_etymons_idless,
linked_page_partial_etymology_sections = __state.linked_page_partial_etymology_sections,
text_stop_lang_missing = text_stop_lang_missing,
})
M.tracking.track_keywords(__state.toplevel_keyword_stats, lang)
M.tracking.track_page_id(lang, id)
M.tracking.track_ids(__state.id_stats, lang)
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
em4ts0w76ajetkfjjikd77uxwp9gipj
Modul:etymon/categories/ceribandin
828
1359188
6531679
6433871
2026-09-15T18:11:18Z
Wikihez
7344
etymon: crz as the Kurdish name of pos, Kurdish pos values (reh, kelîme, pêşgir, paşgir, navgir), root/word categories in Kurdish (rehê ... yê, kelîmeya ... ya)
6531679
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/ceribandin",
},
})
-- 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,
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
local function lang_is_source(page_lang, source)
return page_lang:getCode() == source:getCode() or page_lang:hasParent(source)
end
local function is_borrowing_keyword_config(config)
return config and (config.borrowing_type or config.specialized_borrowing)
end
local function add_reborrow_category(categories, page_lang)
local lang_name = page_lang:getFullName()
add_category(categories, lang_name .. " terms borrowed back into " .. lang_name)
end
local function borrow_returns_to_page_lang(page_lang, source, in_foreign_branch)
if not in_foreign_branch then
return false
end
if source:getFullCode() == page_lang:getFullCode() then
return true
end
return page_lang:hasParent(source)
end
local function node_borrows_from_lang(node, page_lang, visited, in_foreign_branch)
visited = visited or {}
if not node or visited[node] then
return false
end
visited[node] = true
if node.is_duplicate then
if node.duplicate_of then
return node_borrows_from_lang(node.duplicate_of, page_lang, visited, in_foreign_branch)
end
return false
end
local node_is_foreign = in_foreign_branch
or (node.lang and node.lang:getFullCode() ~= page_lang:getFullCode())
for _, container in ipairs(node.children or {}) do
if is_borrowing_keyword_config(container.keyword_info) then
for _, child_term in ipairs(container.terms or {}) do
if borrow_returns_to_page_lang(page_lang, child_term.lang, node_is_foreign) then
return true
end
end
end
for _, child_term in ipairs(container.terms or {}) do
if child_term.status == M.data.STATUS.OK or child_term.status == M.data.STATUS.INLINE then
if node_borrows_from_lang(child_term, page_lang, visited, node_is_foreign) then
return true
end
end
end
end
return false
end
local function should_add_reborrow_category(page_lang, term)
if page_lang:getCode() == term.lang:getCode() then
return false
end
if term.status ~= M.data.STATUS.OK and term.status ~= M.data.STATUS.INLINE then
return false
end
return node_borrows_from_lang(term, page_lang, {}, false)
end
-- Add borrowing-related categories (top-level only)
local function collect_borrowing_categories(categories, page_lang, term, config, check_reborrow_path)
if check_reborrow_path and should_add_reborrow_category(page_lang, term) then
add_reborrow_category(categories, page_lang)
end
if config.borrowing_type == "borrowed" and not lang_is_source(page_lang, term.lang) 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 and not lang_is_source(page_lang, term.lang) 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,
-- insert_source_cat_get_display() composes "Peyvên <lang> <this> ji <source>", so this
-- has to be the Kurdish fragment. Matches what Modul:etîmolojî's format_inherited()
-- produces for {{mîrasmayî}}.
borrowing_type = "yên mîrasmayî",
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)
-- The word in the izafe form that joins it to the term ("rehê *leǵ-", "kelîmeya *X"), and the
-- connector after the term, which follows the word's gender: reh is nêr (yê), kelîme mê (ya).
local pos_types = {
root = { word = "rehê", connector = "yê" },
word = { word = "kelîmeya", connector = "ya" },
}
-- 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 = "Peyvên " .. lang_name .. " yên ji " .. pos_type.word .. " " .. entry_name .. " " .. pos_type.connector .. " " .. etymon_lang_name .. " dariştî"
else
cat_name = "Peyvên " .. lang_name .. " yên aîdî " .. pos_type.word .. " " .. 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, "Peyvên " .. lang_name .. " " .. cat.cat, cat.sort_key, cat.sort_base)
end
if node.supplements then
for _, supplement in ipairs(node.supplements) do
local config = supplement.config
if config and config.toplevel_category then
add_category(categories, config.toplevel_category .. " bi " .. lang_name)
end
end
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, "Madeyên bi referansa etîmoneke neheyî bi " .. lang_name)
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, config.toplevel_category .. " bi " .. lang_name)
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, true)
end
-- Borrowing categories from <bor>, <lbor>, or <slbor> modifiers on affix-group terms
local kw_config = M.data.keywords[keyword]
if kw_config and kw_config.affix_categories then
if term.bor then
local bor_config = { borrowing_type = "borrowed" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
elseif term.lbor then
local bor_config = { specialized_borrowing = "learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
elseif term.slbor then
local bor_config = { specialized_borrowing = "semi-learned" }
collect_borrowing_categories(categories, page_lang, term, bor_config, true)
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.build(opts)
opts = opts or {}
local categories = {}
if not opts.suppress_categories and not opts.nocat then
categories = export.render({
data_tree = opts.data_tree,
page_lang = opts.page_lang,
available_etymon_ids = opts.available_etymon_ids,
senseid_parent_etymon = opts.senseid_parent_etymon,
get_norm_lang_func = opts.get_norm_lang_func,
lang_exc = opts.lang_exc,
})
end
local page_lang = opts.page_lang
if not page_lang then
return categories
end
local lang_name = page_lang:getCanonicalName()
table.insert(categories, "Rûpelên bi etîmonê")
table.insert(categories, "Madeyên bi etîmonê bi " .. lang_name )
if opts.tree then
table.insert(categories, "Madeyên bi darên etîmolojiyê")
table.insert(categories, "Madeyên bi darên etîmolojiyê bi " .. lang_name)
end
if opts.text then
table.insert(categories, "Madeyên bi metna etîmolojiyê bi ".. lang_name)
end
if opts.exnihilo then
table.insert(categories, lang_name .. " terms coined ex nihilo")
end
if opts.toplevel_has_inline_etymology then
table.insert(categories, "Pages with inline etymon for redlinks")
end
if opts.toplevel_redundant_etymology then
table.insert(categories, "Pages with redundant inline etymon")
end
if opts.toplevel_idless_etymon then
table.insert(categories, "Rûpelên etîmoneke bê ID bi kar tînin")
end
if opts.has_mismatched_id then
table.insert(categories, lang_name .. " entries referencing etymons with mismatched IDs")
end
if opts.linked_page_multiple_etymons_idless then
table.insert(categories,
lang_name .. " entries referencing pages with multiple etymons missing IDs")
end
if opts.linked_page_partial_etymology_sections then
table.insert(categories,
lang_name .. " entries referencing pages with etymology sections missing etymons")
end
if opts.text_stop_lang_missing then
table.insert(categories, "Pages with etymology text stop language not in chain")
table.insert(categories, lang_name .. " entries with etymology text stop language not in chain")
end
return categories
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
lintw8xojwttuedygq859ihk5fo10zo
Şablon:etîmon/testcases
10
1359192
6531685
6434482
2026-09-15T18:12:02Z
Wikihez
7344
test cases: live vs ceribandin
6531685
wikitext
text/x-wiki
{| class="wikitable" style="width:100%; table-layout:fixed"
! Niha <code><nowiki>{{etîmon}}</nowiki></code> !! Ceribandin <code><nowiki>{{etîmon/ceribandin}}</nowiki></code>
|-
| colspan="2" | <b>1. zagon: drşnd, two uncertain terms, yan jî</b> <code><nowiki>{{etîmon|ku|:drşnd|ru:зако́н<t:qanûn><unc>|hy:զակոն<unc>|dar=1|metn=++|title=zagon}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="0-live"></span>{{etîmon|ku|:drşnd|ru:зако́н<t:qanûn><unc>|hy:զակոն<unc>|dar=1|metn=++|title=zagon}} || <span class="tc-mark" data-tc="0-ceri"></span>{{etîmon/ceribandin|ku|:drşnd|ru:зако́н<t:qanûn><unc>|hy:զակոն<unc>|dar=1|metn=++|title=zagon}}
|-
| colspan="2" | <b>2. ensîklopedî: deyn chain, id as gloss</b> <code><nowiki>{{etîmon|ku|:deyn|fr:encyclopédie<id:ensîklopedî>|dar=1|metn=++|id=kitêba referansê|title=ensîklopedî}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="1-live"></span>{{etîmon|ku|:deyn|fr:encyclopédie<id:ensîklopedî>|dar=1|metn=++|id=kitêba referansê|title=ensîklopedî}} || <span class="tc-mark" data-tc="1-ceri"></span>{{etîmon/ceribandin|ku|:deyn|fr:encyclopédie<id:ensîklopedî>|dar=1|metn=++|id=kitêba referansê|title=ensîklopedî}}
|-
| colspan="2" | <b>3. pickpocket: qf, en entry</b> <code><nowiki>{{etîmon|en|:qf|pick|pocket<id:berîk>|metn=+|dar=1|id=diz|title=pickpocket}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="2-live"></span>{{etîmon|en|:qf|pick|pocket<id:berîk>|metn=+|dar=1|id=diz|title=pickpocket}} || <span class="tc-mark" data-tc="2-ceri"></span>{{etîmon/ceribandin|en|:qf|pick|pocket<id:berîk>|metn=+|dar=1|id=diz|title=pickpocket}}
|-
| colspan="2" | <b>4. ziman: mîr from a reconstruction</b> <code><nowiki>{{etîmon|ku|:mîr|ira-pro:*hižwáH<id:ziman>|dar=1|metn=++|id=organê xeberdanê|title=ziman}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="3-live"></span>{{etîmon|ku|:mîr|ira-pro:*hižwáH<id:ziman>|dar=1|metn=++|id=organê xeberdanê|title=ziman}} || <span class="tc-mark" data-tc="3-ceri"></span>{{etîmon/ceribandin|ku|:mîr|ira-pro:*hižwáH<id:ziman>|dar=1|metn=++|id=organê xeberdanê|title=ziman}}
|-
| colspan="2" | <b>5. zimandirêj: qf, no explicit target ids</b> <code><nowiki>{{etîmon|ku|:qf|ziman|dirêj|dar=1|metn=++|id=kesê zêde xeber dide|title=zimandirêj}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="4-live"></span>{{etîmon|ku|:qf|ziman|dirêj|dar=1|metn=++|id=kesê zêde xeber dide|title=zimandirêj}} || <span class="tc-mark" data-tc="4-ceri"></span>{{etîmon/ceribandin|ku|:qf|ziman|dirêj|dar=1|metn=++|id=kesê zêde xeber dide|title=zimandirêj}}
|-
| colspan="2" | <b>6. alias :ji</b> <code><nowiki>{{etîmon|ku|:ji|testa|metn=1|id=tc-ji}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="5-live"></span>{{etîmon|ku|:ji|testa|metn=1|id=tc-ji}} || <span class="tc-mark" data-tc="5-ceri"></span>{{etîmon/ceribandin|ku|:ji|testa|metn=1|id=tc-ji}}
|-
| colspan="2" | <b>7. alias :tesîr</b> <code><nowiki>{{etîmon|ku|:mîr|ira-pro:*testa|:tesîr|ar:testb|metn=1|id=tc-tesir}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="6-live"></span>{{etîmon|ku|:mîr|ira-pro:*testa|:tesîr|ar:testb|metn=1|id=tc-tesir}} || <span class="tc-mark" data-tc="6-ceri"></span>{{etîmon/ceribandin|ku|:mîr|ira-pro:*testa|:tesîr|ar:testb|metn=1|id=tc-tesir}}
|-
| colspan="2" | <b>8. alias :deynh / :deynhq</b> <code><nowiki>{{etîmon|ku|:deynh|la:testa|:deynhq|la:testb|metn=1|id=tc-deynh}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="7-live"></span>{{etîmon|ku|:deynh|la:testa|:deynhq|la:testb|metn=1|id=tc-deynh}} || <span class="tc-mark" data-tc="7-ceri"></span>{{etîmon/ceribandin|ku|:deynh|la:testa|:deynhq|la:testb|metn=1|id=tc-deynh}}
|-
| colspan="2" | <b>9. alias :yekserq</b> <code><nowiki>{{etîmon|ku|:yekserq|en:testa|metn=1|id=tc-yekserq}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="8-live"></span>{{etîmon|ku|:yekserq|en:testa|metn=1|id=tc-yekserq}} || <span class="tc-mark" data-tc="8-ceri"></span>{{etîmon/ceribandin|ku|:yekserq|en:testa|metn=1|id=tc-yekserq}}
|-
| colspan="2" | <b>10. modifiers cuda/w/crz</b> <code><nowiki>{{etîmon|ku|:deyn|fr:testa<cuda:Testa><w:wergera><crz:navdêr>|metn=1|id=tc-mods}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="9-live"></span>{{etîmon|ku|:deyn|fr:testa<cuda:Testa><w:wergera><crz:navdêr>|metn=1|id=tc-mods}} || <span class="tc-mark" data-tc="9-ceri"></span>{{etîmon/ceribandin|ku|:deyn|fr:testa<cuda:Testa><w:wergera><crz:navdêr>|metn=1|id=tc-mods}}
|-
| colspan="2" | <b>11. modifier nediyar</b> <code><nowiki>{{etîmon|ku|:deyn|fr:testa<nediyar>|metn=1|id=tc-nediyar}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="10-live"></span>{{etîmon|ku|:deyn|fr:testa<nediyar>|metn=1|id=tc-nediyar}} || <span class="tc-mark" data-tc="10-ceri"></span>{{etîmon/ceribandin|ku|:deyn|fr:testa<nediyar>|metn=1|id=tc-nediyar}}
|-
| colspan="2" | <b>12. modifier deyn in qf</b> <code><nowiki>{{etîmon|ku|:qf|fr:testa<deyn>|testb|metn=1|id=tc-qfdeyn}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="11-live"></span>{{etîmon|ku|:qf|fr:testa<deyn>|testb|metn=1|id=tc-qfdeyn}} || <span class="tc-mark" data-tc="11-ceri"></span>{{etîmon/ceribandin|ku|:qf|fr:testa<deyn>|testb|metn=1|id=tc-qfdeyn}}
|-
| colspan="2" | <b>13. cotreh</b> <code><nowiki>{{etîmon|ku|:deyn|fr:testa|cotreh=testb|metn=1|id=tc-cotreh}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="12-live"></span>{{etîmon|ku|:deyn|fr:testa|cotreh=testb|metn=1|id=tc-cotreh}} || <span class="tc-mark" data-tc="12-ceri"></span>{{etîmon/ceribandin|ku|:deyn|fr:testa|cotreh=testb|metn=1|id=tc-cotreh}}
|-
| colspan="2" | <b>14. <t:> wins over the id</b> <code><nowiki>{{etîmon|ku|:deyn|fr:encyclopédie<id:ensîklopedî><t:kitêba referansê>|dar=1|metn=1|id=tc-t}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="13-live"></span>{{etîmon|ku|:deyn|fr:encyclopédie<id:ensîklopedî><t:kitêba referansê>|dar=1|metn=1|id=tc-t}} || <span class="tc-mark" data-tc="13-ceri"></span>{{etîmon/ceribandin|ku|:deyn|fr:encyclopédie<id:ensîklopedî><t:kitêba referansê>|dar=1|metn=1|id=tc-t}}
|-
| colspan="2" | <b>15. no gloss at all</b> <code><nowiki>{{etîmon|ku|:deyn|fr:testa|metn=1|id=tc-nogloss}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="14-live"></span>{{etîmon|ku|:deyn|fr:testa|metn=1|id=tc-nogloss}} || <span class="tc-mark" data-tc="14-ceri"></span>{{etîmon/ceribandin|ku|:deyn|fr:testa|metn=1|id=tc-nogloss}}
|-
| colspan="2" | <b>16. g on term</b> <code><nowiki>{{etîmon|ku|:nom|testa<g:n>|metn=1|id=tc-gn}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="15-live"></span>{{etîmon|ku|:nom|testa<g:n>|metn=1|id=tc-gn}} || <span class="tc-mark" data-tc="15-ceri"></span>{{etîmon/ceribandin|ku|:nom|testa<g:n>|metn=1|id=tc-gn}}
|-
| colspan="2" | <b>17. g on keyword</b> <code><nowiki>{{etîmon|ku|:nom<g:nt>|testa|metn=1|id=tc-gk}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="16-live"></span>{{etîmon|ku|:nom<g:nt>|testa|metn=1|id=tc-gk}} || <span class="tc-mark" data-tc="16-ceri"></span>{{etîmon/ceribandin|ku|:nom<g:nt>|testa|metn=1|id=tc-gk}}
|-
| colspan="2" | <b>18. no id</b> <code><nowiki>{{etîmon|ku|:qf|testa|testb|metn=1}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="17-live"></span>{{etîmon|ku|:qf|testa|testb|metn=1}} || <span class="tc-mark" data-tc="17-ceri"></span>{{etîmon/ceribandin|ku|:qf|testa|testb|metn=1}}
|-
| colspan="2" | <b>19. nocat</b> <code><nowiki>{{etîmon|ku|:qf|testa|testb|metn=1|id=tc-nocat|nocat=1}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="18-live"></span>{{etîmon|ku|:qf|testa|testb|metn=1|id=tc-nocat|nocat=1}} || <span class="tc-mark" data-tc="18-ceri"></span>{{etîmon/ceribandin|ku|:qf|testa|testb|metn=1|id=tc-nocat|nocat=1}}
|-
| colspan="2" | <b>20. text=+ / nodot</b> <code><nowiki>{{etîmon|ku|:deyn|fr:encyclopédie<id:ensîklopedî>|metn=+|nodot=1|id=tc-plus}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="19-live"></span>{{etîmon|ku|:deyn|fr:encyclopédie<id:ensîklopedî>|metn=+|nodot=1|id=tc-plus}} || <span class="tc-mark" data-tc="19-ceri"></span>{{etîmon/ceribandin|ku|:deyn|fr:encyclopédie<id:ensîklopedî>|metn=+|nodot=1|id=tc-plus}}
|-
| colspan="2" | <b>21. crz=reh</b> <code><nowiki>{{etîmon|ine-pro|id=tc-crz|crz=reh|title=*leǵ-}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="20-live"></span>{{etîmon|ine-pro|id=tc-crz|crz=reh|title=*leǵ-}} || <span class="tc-mark" data-tc="20-ceri"></span>{{etîmon/ceribandin|ine-pro|id=tc-crz|crz=reh|title=*leǵ-}}
|-
| colspan="2" | <b>22. pos=kelîme</b> <code><nowiki>{{etîmon|ine-pro|id=tc-kelime|pos=kelîme|title=*testa-}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="21-live"></span>{{etîmon|ine-pro|id=tc-kelime|pos=kelîme|title=*testa-}} || <span class="tc-mark" data-tc="21-ceri"></span>{{etîmon/ceribandin|ine-pro|id=tc-kelime|pos=kelîme|title=*testa-}}
|-
| colspan="2" | <b>23. <postype:reh> on a term</b> <code><nowiki>{{etîmon|ku|:mîr|ine-pro:*leǵ-<postype:reh>|metn=1|id=tc-postype}}</nowiki></code>
|-
| <span class="tc-mark" data-tc="22-live"></span>{{etîmon|ku|:mîr|ine-pro:*leǵ-<postype:reh>|metn=1|id=tc-postype}} || <span class="tc-mark" data-tc="22-ceri"></span>{{etîmon/ceribandin|ku|:mîr|ine-pro:*leǵ-<postype:reh>|metn=1|id=tc-postype}}
|}
a6t354f0j46zbybum4l06mbn5pw8uj3
Jinûvesazî:proto-hindûewropî/leǵ-
108
1359228
6531651
2026-09-15T17:16:19Z
Wikihez
7344
[[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:proto-hindûewropî/leǵ-|en.wikt]]
6531651
wikitext
text/x-wiki
{{bnr-h|Jinûvesazî:proto-hindûewropî/legʰ-}}
{{jinûvesazkirî}}
== {{ziman|ine-pro}} ==
{{etîmon|ine-pro|id=berhev kirin|crz=reh}}
===Reh===
{{reh|ine-pro|c=impf}}<ref name="de Vaan">{{R:itc:EDL|legō, -ere|332}}</ref><ref>{{R:ine:LIV|page=397}}</ref>
# to [[gather]], [[collect]], with derivatives meaning to [[speak]] by carefully selecting one's words.
==== Hevmane ====
* {{g|ine-pro|*les-}}
==== Jê ====
* {{etymon|ine-pro|id=reh present|:ji|*leǵ-<id:berhev kirin>}}{{g|ine-pro||*léǵ-e-ti|crz=reh present}}<ref name="de Vaan"/><ref name="Beekes">{{R:grc:EDG|alt=λέγω|pages=841–842}}</ref>
** {{dû|sqj-pro|*ledza}}<ref>{{R:sq:Orel|mbledh|251}}</ref>
** {{dû|grk-pro|*légō}}
** {{dû|itc-pro|*legō}}
** {{dû|ine-toc-pro|*läk-|w=lê nêrîn, dîtin|hrf=berhev kirin (bi çavan)}}<ref name="Adams">{{R:txb:Adams:2013|läk-|pages=596–597}}</ref>
*** {{dû|xto|läk-}}
*** {{dû|txb|läk-}}
* {{g|ine-pro||*lōǵ-éh₂ye-ti}}<ref>{{R:gem:HGE|*lōkōjanan|249}}</ref><ref name="Adams"/>
** {{dû|gem-pro|*lōkōną|unc=1}} {{mj|< {{qal|gem-pro||*lōkōjaną}}}}
*** {{dû|gmw-pro|*lōkōn|w=nêrîn}}
** {{dû|grk-pro|*lōgā́yō}}
*** {{dû|grc-dor|λωγάω}}
* {{g|ine-pro||*lḗǵ-s ~ léǵ-es|crz=îsmê rehî}}<ref>{{R:itc:EDL|lēx, lēgis|337}}</ref>
** {{dû|itc-pro|*lēks|unc=1}}
*** {{dû|la|lēx}}
*** {{dû|umc|lixs}}
** {{dû|iir-pro|-|unc=1}}
*** {{dû|sa|स्रज्}}
* {{g|ine-pro||*leǵ-no-m}}
** {{dû|itc-pro|*lignom}}
*** {{dû|la|lignum}}
* {{g|ine-pro||*leǵ-mn̥}}
** {{dû|itc-pro|*legūmen|unc=1}}
*** {{dû|la|legūmen}}
* {{g|ine-pro||*loǵ-o-s}}
** {{dû|grk-pro|*lógos}}
*** {{dû|grc|λόγος}}
; Formên nehatine rêzkirin:
* {{dû|pal|tr=lazūr|w=forest|unc=1}} {{mj|mihtemelen}}
===Çavkanî===
{{Çavkanî}}
==== Bêhtir bixwîne====
* {{R:ine:IEW|page=658}}
* [http://www.archive.org/details/acopiousandcrit00unkngoog A Copious and Critical Latin-English Lexicon]
* {{R:Sihler 1995}}
[[el:*leǵ-]]
o9h3o8r4g1vemctixndw9cu1hcoi5e5
6531652
6531651
2026-09-15T17:17:57Z
Wikihez
7344
/* Bêhtir bixwîne */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531652
wikitext
text/x-wiki
{{bnr-h|Jinûvesazî:proto-hindûewropî/legʰ-}}
{{jinûvesazkirî}}
== {{ziman|ine-pro}} ==
{{etîmon|ine-pro|id=berhev kirin|crz=reh}}
===Reh===
{{reh|ine-pro|c=impf}}<ref name="de Vaan">{{R:itc:EDL|legō, -ere|332}}</ref><ref>{{R:ine:LIV|page=397}}</ref>
# to [[gather]], [[collect]], with derivatives meaning to [[speak]] by carefully selecting one's words.
==== Hevmane ====
* {{g|ine-pro|*les-}}
==== Jê ====
* {{etymon|ine-pro|id=reh present|:ji|*leǵ-<id:berhev kirin>}}{{g|ine-pro||*léǵ-e-ti|crz=reh present}}<ref name="de Vaan"/><ref name="Beekes">{{R:grc:EDG|alt=λέγω|pages=841–842}}</ref>
** {{dû|sqj-pro|*ledza}}<ref>{{R:sq:Orel|mbledh|251}}</ref>
** {{dû|grk-pro|*légō}}
** {{dû|itc-pro|*legō}}
** {{dû|ine-toc-pro|*läk-|w=lê nêrîn, dîtin|hrf=berhev kirin (bi çavan)}}<ref name="Adams">{{R:txb:Adams:2013|läk-|pages=596–597}}</ref>
*** {{dû|xto|läk-}}
*** {{dû|txb|läk-}}
* {{g|ine-pro||*lōǵ-éh₂ye-ti}}<ref>{{R:gem:HGE|*lōkōjanan|249}}</ref><ref name="Adams"/>
** {{dû|gem-pro|*lōkōną|unc=1}} {{mj|< {{qal|gem-pro||*lōkōjaną}}}}
*** {{dû|gmw-pro|*lōkōn|w=nêrîn}}
** {{dû|grk-pro|*lōgā́yō}}
*** {{dû|grc-dor|λωγάω}}
* {{g|ine-pro||*lḗǵ-s ~ léǵ-es|crz=îsmê rehî}}<ref>{{R:itc:EDL|lēx, lēgis|337}}</ref>
** {{dû|itc-pro|*lēks|unc=1}}
*** {{dû|la|lēx}}
*** {{dû|umc|lixs}}
** {{dû|iir-pro|-|unc=1}}
*** {{dû|sa|स्रज्}}
* {{g|ine-pro||*leǵ-no-m}}
** {{dû|itc-pro|*lignom}}
*** {{dû|la|lignum}}
* {{g|ine-pro||*leǵ-mn̥}}
** {{dû|itc-pro|*legūmen|unc=1}}
*** {{dû|la|legūmen}}
* {{g|ine-pro||*loǵ-o-s}}
** {{dû|grk-pro|*lógos}}
*** {{dû|grc|λόγος}}
; Formên nehatine rêzkirin:
* {{dû|pal|tr=lazūr|w=forest|unc=1}} {{mj|mihtemelen}}
===Çavkanî===
{{Çavkanî}}
==== Bêhtir bixwîne====
* {{R:ine:IEW|page=658}}
* [http://www.archive.org/details/acopiousandcrit00unkngoog A Copious and Critical Latin-English Lexicon]
* {{R:Sihler 1995}}
ggts62892vq9mdlap8lw1ypaycsd5m1
6531659
6531652
2026-09-15T17:20:25Z
Wikihez
7344
6531659
wikitext
text/x-wiki
{{bnr-h|Jinûvesazî:proto-hindûewropî/legʰ-}}
{{jinûvesazkirî}}
== {{ziman|ine-pro}} ==
{{etîmon|ine-pro|id=berhev kirin|crz=reh}}
===Reh===
{{reh|ine-pro|c=impf}}<ref name="de Vaan">{{R:itc:EDL|legō, -ere|332}}</ref><ref>{{R:ine:LIV|page=397}}</ref>
# to [[gather]], [[collect]], with derivatives meaning to [[speak]] by carefully selecting one's words.
==== Hevmane ====
* {{g|ine-pro|*les-}}
==== Jê ====
* {{etymon|ine-pro|id=reh present|:ji|*leǵ-<id:berhev kirin>}}{{g|ine-pro||*léǵ-e-ti|crz=reh present}}<ref name="de Vaan"/><ref name="Beekes">{{R:grc:EDG|alt=λέγω|pages=841–842}}</ref>
** {{dû|sqj-pro|*ledza}}<ref>{{R:sq:Orel|mbledh|251}}</ref>
** {{dû|grk-pro|*légō}}
** {{dû|itc-pro|*legō}}
** {{dû|ine-toc-pro|*läk-|w=lê nêrîn, dîtin|hrf=berhev kirin (bi çavan)}}<ref name="Adams">{{R:txb:Adams:2013|läk-|pages=596–597}}</ref>
*** {{dû|xto|läk-}}
*** {{dû|txb|läk-}}
* {{g|ine-pro||*lōǵ-éh₂ye-ti}}<ref>{{R:gem:HGE|*lōkōjanan|249}}</ref><ref name="Adams"/>
** {{dû|gem-pro|*lōkōną|unc=1}} {{mj|< {{qal|gem-pro||*lōkōjaną}}}}
*** {{dû|gmw-pro|*lōkōn|w=nêrîn}}
** {{dû|grk-pro|*lōgā́yō}}
*** {{dû|grc-dor|λωγάω}}
* {{g|ine-pro||*lḗǵ-s ~ léǵ-es|crz=îsmê rehî}}<ref>{{R:itc:EDL|lēx, lēgis|337}}</ref>
** {{dû|itc-pro|*lēks|unc=1}}
*** {{dû|la|lēx}}
*** {{dû|umc|lixs}}
** {{dû|iir-pro|-|unc=1}}
*** {{dû|sa|स्रज्}}
* {{g|ine-pro||*leǵ-no-m}}
** {{dû|itc-pro|*lignom}}
*** {{dû|la|lignum}}
* {{g|ine-pro||*leǵ-mn̥}}
** {{dû|itc-pro|*legūmen|unc=1}}
*** {{dû|la|legūmen}}
* {{g|ine-pro||*loǵ-o-s}}
** {{dû|grk-pro|*lógos}}
*** {{dû|grc|λόγος}}
; Formên nehatine rêzkirin:
* {{dû|pal|tr=lazūr|w=daristan|unc=1}} {{mj|mihtemelen}}
===Çavkanî===
{{Çavkanî}}
==== Bêhtir bixwîne====
* {{R:ine:IEW|page=658}}
* [http://www.archive.org/details/acopiousandcrit00unkngoog A Copious and Critical Latin-English Lexicon]
* {{R:Sihler 1995}}
0v698mrtdljyduoacjm1u227o7eew9b
6531717
6531659
2026-09-15T20:03:42Z
Wikihez
7344
Proto-hindûewropî/Reh: mane hate guhartin (DefSideBoxes)
6531717
wikitext
text/x-wiki
{{bnr-h|Jinûvesazî:proto-hindûewropî/legʰ-}}
{{jinûvesazkirî}}
== {{ziman|ine-pro}} ==
{{etîmon|ine-pro|id=berhev kirin|crz=reh}}
===Reh===
{{reh|ine-pro|c=impf}}<ref name="de Vaan">{{R:itc:EDL|legō, -ere|332}}</ref><ref>{{R:ine:LIV|page=397}}</ref>
# [[berhev kirin]], [[kom kirin]], li gel kelîmeyên jêçêbûyî bi manaya [[xeber dan]]ê bi riya bi diqetî bijartina kelîmeyên xwe.
==== Hevmane ====
* {{g|ine-pro|*les-}}
==== Jê ====
* {{etymon|ine-pro|id=reh present|:ji|*leǵ-<id:berhev kirin>}}{{g|ine-pro||*léǵ-e-ti|crz=reh present}}<ref name="de Vaan"/><ref name="Beekes">{{R:grc:EDG|alt=λέγω|pages=841–842}}</ref>
** {{dû|sqj-pro|*ledza}}<ref>{{R:sq:Orel|mbledh|251}}</ref>
** {{dû|grk-pro|*légō}}
** {{dû|itc-pro|*legō}}
** {{dû|ine-toc-pro|*läk-|w=lê nêrîn, dîtin|hrf=berhev kirin (bi çavan)}}<ref name="Adams">{{R:txb:Adams:2013|läk-|pages=596–597}}</ref>
*** {{dû|xto|läk-}}
*** {{dû|txb|läk-}}
* {{g|ine-pro||*lōǵ-éh₂ye-ti}}<ref>{{R:gem:HGE|*lōkōjanan|249}}</ref><ref name="Adams"/>
** {{dû|gem-pro|*lōkōną|unc=1}} {{mj|< {{qal|gem-pro||*lōkōjaną}}}}
*** {{dû|gmw-pro|*lōkōn|w=nêrîn}}
** {{dû|grk-pro|*lōgā́yō}}
*** {{dû|grc-dor|λωγάω}}
* {{g|ine-pro||*lḗǵ-s ~ léǵ-es|crz=îsmê rehî}}<ref>{{R:itc:EDL|lēx, lēgis|337}}</ref>
** {{dû|itc-pro|*lēks|unc=1}}
*** {{dû|la|lēx}}
*** {{dû|umc|lixs}}
** {{dû|iir-pro|-|unc=1}}
*** {{dû|sa|स्रज्}}
* {{g|ine-pro||*leǵ-no-m}}
** {{dû|itc-pro|*lignom}}
*** {{dû|la|lignum}}
* {{g|ine-pro||*leǵ-mn̥}}
** {{dû|itc-pro|*legūmen|unc=1}}
*** {{dû|la|legūmen}}
* {{g|ine-pro||*loǵ-o-s}}
** {{dû|grk-pro|*lógos}}
*** {{dû|grc|λόγος}}
; Formên nehatine rêzkirin:
* {{dû|pal|tr=lazūr|w=daristan|unc=1}} {{mj|mihtemelen}}
===Çavkanî===
{{Çavkanî}}
==== Bêhtir bixwîne====
* {{R:ine:IEW|page=658}}
* [http://www.archive.org/details/acopiousandcrit00unkngoog A Copious and Critical Latin-English Lexicon]
* {{R:Sihler 1995}}
t7vmx892pwa9whv7fdmo3oea6l74dw1
Şablon:R:sq:Orel
10
1359229
6531653
2026-09-15T17:18:34Z
Wikihez
7344
Ji bo [[Şablon:R:sq:Orel:1998]] hat beralîkirin ([[Bikarhêner:Wikihez/skrîpt/js/kopi-enWikt.js|kopi-enWikt.js]])
6531653
wikitext
text/x-wiki
#BERALÎKIRIN [[Şablon:R:sq:Orel:1998]]
0pn0phc85c1tpvmkxf21147se6cbv4y
Şablon:Ç:sq:Orel:1998/belge
10
1359230
6531654
2026-09-15T17:18:35Z
Wikihez
7344
[[:en:Template:R:sq:Orel:1998/documentation]] hat kopîkirin ([[Bikarhêner:Wikihez/skrîpt/js/kopi-enWikt.js|kopi-enWikt.js]])
6531654
wikitext
text/x-wiki
{{documentation subpage}}
==Examples==
* <code><nowiki>{{R:sq:Orel:1998|gultoj|127}}</nowiki></code>
: {{R:sq:Orel:1998|gultoj|127}}
prp3ez88agzbh5cio39h1m01noqasdc
6531693
6531654
2026-09-15T18:15:16Z
Wikihez
7344
Wikihezî/ê navê [[Şablon:R:sq:Orel:1998/belge]] weke [[Şablon:Ç:sq:Orel:1998/belge]] guhart
6531654
wikitext
text/x-wiki
{{documentation subpage}}
==Examples==
* <code><nowiki>{{R:sq:Orel:1998|gultoj|127}}</nowiki></code>
: {{R:sq:Orel:1998|gultoj|127}}
prp3ez88agzbh5cio39h1m01noqasdc
Şablon:Ç:sq:Orel:1998
10
1359231
6531655
2026-09-15T17:18:34Z
Wikihez
7344
[[:en:Template:R:sq:Orel:1998]] hat kopîkirin ([[Bikarhêner:Wikihez/skrîpt/js/kopi-enWikt.js|kopi-enWikt.js]])
6531655
wikitext
text/x-wiki
{{#invoke:quote|call_template
|template=cite-book
|first=Vladimir
|last=Orel
|authorlink=Vladimir Oryol
|entry={{{head|{{{1|{{pagename}}}}}}}}
|entryurl=https://archive.org/details/albanian-etymological-dictionary/page/n{{#expr:{{#invoke:string/templates|match|{{{page|{{{pages|{{{p|{{{2|}}}}}}}}}}}}|[0-9]+|}}+35}}/mode/1up?view=theater
|title=Albanian Etymological Dictionary
|location=Leiden; Boston; Cologne
|publisher=Brill
|year=1998
|pageparam=p,2,pp
|isbn=978-90-04-11024-3
|textparam=ps,3
|allowparams=head,1
}}<!--
--><noinclude>{{refcat|sq}}{{documentation}}</noinclude>
5ess6efdmu2ut0kdrnyhw3gypqmum0l
6531690
6531655
2026-09-15T18:15:01Z
Wikihez
7344
6531690
wikitext
text/x-wiki
{{#invoke:quote|call_template
|template=cite-book|en
|first=Vladimir
|last=Orel
|authorlink=Vladimir Oryol
|entry={{{head|{{{1|{{pagename}}}}}}}}
|entryurl=https://archive.org/details/albanian-etymological-dictionary/page/n{{#expr:{{#invoke:string/templates|match|{{{page|{{{pages|{{{p|{{{2|}}}}}}}}}}}}|[0-9]+|}}+35}}/mode/1up?view=theater
|title=en:Albanian Etymological Dictionary<t:Ferhenga Etîmolojiyê ya Arnawidî>
|location=Leiden; Boston; Cologne
|publisher=Brill
|year=1998
|pageparam=p,2,pp,r,rûpel
|isbn=978-90-04-11024-3
|textparam=ps,3,jg,jêgirtin
|allowparams=head,1
}}<!--
--><noinclude>{{refcat|sq}}{{documentation}}</noinclude>
scy9nhr7el99lorchhy1828e2a4zybv
6531691
6531690
2026-09-15T18:15:15Z
Wikihez
7344
Wikihezî/ê navê [[Şablon:R:sq:Orel:1998]] weke [[Şablon:Ç:sq:Orel:1998]] guhart
6531690
wikitext
text/x-wiki
{{#invoke:quote|call_template
|template=cite-book|en
|first=Vladimir
|last=Orel
|authorlink=Vladimir Oryol
|entry={{{head|{{{1|{{pagename}}}}}}}}
|entryurl=https://archive.org/details/albanian-etymological-dictionary/page/n{{#expr:{{#invoke:string/templates|match|{{{page|{{{pages|{{{p|{{{2|}}}}}}}}}}}}|[0-9]+|}}+35}}/mode/1up?view=theater
|title=en:Albanian Etymological Dictionary<t:Ferhenga Etîmolojiyê ya Arnawidî>
|location=Leiden; Boston; Cologne
|publisher=Brill
|year=1998
|pageparam=p,2,pp,r,rûpel
|isbn=978-90-04-11024-3
|textparam=ps,3,jg,jêgirtin
|allowparams=head,1
}}<!--
--><noinclude>{{refcat|sq}}{{documentation}}</noinclude>
scy9nhr7el99lorchhy1828e2a4zybv
Şablon:R:txb:Adams:2013
10
1359232
6531656
2026-09-15T17:18:53Z
Wikihez
7344
Ji bo [[Şablon:R:txb:DTB]] hat beralîkirin ([[Bikarhêner:Wikihez/skrîpt/js/kopi-enWikt.js|kopi-enWikt.js]])
6531656
wikitext
text/x-wiki
#BERALÎKIRIN [[Şablon:R:txb:DTB]]
rcov2lwe20cp536nd9klx44ca243apx
Şablon:Ç:gem:HGE
10
1359233
6531657
2026-09-15T17:19:07Z
Wikihez
7344
[[:en: Template:R:gem:HGE]] hat kopîkirin ([[Bikarhêner:Wikihez/skrîpt/js/kopi-enWikt.js|kopi-enWikt.js]])
6531657
wikitext
text/x-wiki
<includeonly><onlyinclude>{{safesubst:<noinclude/>#invoke:R:gem:HGE|main}}</onlyinclude></includeonly><!--
-->{{documentation}}
jfufxacx7l8c6fhgza1xo1t29y9343z
6531680
6531657
2026-09-15T18:11:28Z
Wikihez
7344
Wikihezî/ê navê [[Şablon:R:gem:HGE]] weke [[Şablon:Ç:gem:HGE]] guhart
6531657
wikitext
text/x-wiki
<includeonly><onlyinclude>{{safesubst:<noinclude/>#invoke:R:gem:HGE|main}}</onlyinclude></includeonly><!--
-->{{documentation}}
jfufxacx7l8c6fhgza1xo1t29y9343z
Modul:R:gem:HGE
828
1359234
6531658
2026-09-15T17:19:55Z
Wikihez
7344
[[:en:Module:R:gem:HGE]] hat kopîkirin ([[Bikarhêner:Wikihez/skrîpt/js/kopi-enWikt.js|kopi-enWikt.js]])
6531658
Scribunto
text/plain
-- Prevent substitution.
if mw.isSubsting() then
return require("Module:unsubst")
end
local export = {}
local decode_entities = require("Module:string utilities").decode_entities
local function get_url_page(page)
if not page then
return
end
local pages = decode_entities(page)
-- If it's a range, allow the same dividers as [[Module:quote]].
return tonumber(pages) or tonumber(
pages:match("^(%d+)%s*[&%-]") or
pages:match("^(%d+)%s*\226\128[\147\148]") or
pages:match("^(%d+)%sand%s")
)
end
function export.main(frame)
local args = require("Module:parameters").process(frame:getParent().args, {
[1] = {alias_of = "head"},
[2] = {alias_of = "page"},
[3] = {alias_of = "passage"},
["head"] = true,
["page"] = true,
["pages"] = true,
["passage"] = true,
})
local expand_args = {
author = "w:Vladimir Orel",
title = "A Handbook of Germanic Etymology",
publisher = "w:[[Brill Publishers|Brill]]",
location = "Leiden, Boston",
entry = args.head,
year = "2003",
isbn = "9789004128750",
page = args.page,
pages = args.pages,
passage = args.passage,
url = "https://archive.org/details/Orel-AHandbookOfGermanicEtymology",
}
local url_page = get_url_page(args.page) or get_url_page(args.pages)
if url_page then
expand_args.pageurl = ("https://archive.org/details/Orel-AHandbookOfGermanicEtymology/page/n%d"):format(url_page + 38)
end
return frame:expandTemplate {title = "cite-book", args = expand_args}
end
return export
s72ai9w5oidwfbpeqhml1koi1rdn58g
Şablon:R:gem:HGE
10
1359235
6531681
2026-09-15T18:11:28Z
Wikihez
7344
Wikihezî/ê navê [[Şablon:R:gem:HGE]] weke [[Şablon:Ç:gem:HGE]] guhart
6531681
wikitext
text/x-wiki
#BERALÎKIRIN [[Şablon:Ç:gem:HGE]]
20p7ycl7r32z6iu0yyw9n9u047g80ni
Şablon:R:txb:DTB
10
1359236
6531684
2026-09-15T18:11:45Z
Wikihez
7344
Wikihezî/ê navê [[Şablon:R:txb:DTB]] weke [[Şablon:Ç:txb:DTB]] guhart
6531684
wikitext
text/x-wiki
#BERALÎKIRIN [[Şablon:Ç:txb:DTB]]
pu7vh5as9jqim6shmh2w6h4tea4ld12
Şablon:R:sq:Orel:1998
10
1359237
6531692
2026-09-15T18:15:16Z
Wikihez
7344
Wikihezî/ê navê [[Şablon:R:sq:Orel:1998]] weke [[Şablon:Ç:sq:Orel:1998]] guhart
6531692
wikitext
text/x-wiki
#BERALÎKIRIN [[Şablon:Ç:sq:Orel:1998]]
2igqgo5nh8e4ml3e8zdjaqml7wlb7t9
Şablon:R:sq:Orel:1998/belge
10
1359238
6531694
2026-09-15T18:15:16Z
Wikihez
7344
Wikihezî/ê navê [[Şablon:R:sq:Orel:1998/belge]] weke [[Şablon:Ç:sq:Orel:1998/belge]] guhart
6531694
wikitext
text/x-wiki
#BERALÎKIRIN [[Şablon:Ç:sq:Orel:1998/belge]]
38eeid48snaxgf4b56ic4l7jrm5iqjm
MediaWiki:Gadget-wikt.dsb
8
1359239
6531718
2026-09-15T20:09:27Z
Wikihez
7344
Rûpel bi "[[MediaWiki:Gadget-wikt.dsb.js|DefSideBoxes]] — Amûreke ji bo guhartina rûpelan bi tikandina qelemeke li ser beş, mane, resim, nimûne û jêgirtinan." hat çêkirin
6531718
wikitext
text/x-wiki
[[MediaWiki:Gadget-wikt.dsb.js|DefSideBoxes]] — Amûreke ji bo guhartina rûpelan bi tikandina qelemeke li ser beş, mane, resim, nimûne û jêgirtinan.
9f6hhidiksbji41q1fmhlypqk2rp5h4
MediaWiki:Gadget-TreeDescs.js
8
1359240
6531724
2026-09-15T20:14:03Z
Wikihez
7344
Import TreeDescs from en.wiktionary (MediaWiki:Gadget-TreeDescs.js, CC BY-SA 4.0): draws Dûnde lists as a tree
6531724
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-TreeDescs.js]]
*
* Draws the lists under a "Dûnde" heading as a tree. Imported from en.wiktionary's
* [[:en:MediaWiki:Gadget-TreeDescs.js]] (CC BY-SA 4.0); styles in TreeDescs-pagestyles.css.
*/
$(".mw-heading").filter(function () {
return $(this).children().first().text() === "Dûnde";
}).each(function () {
var $selected = $(this).nextUntil(".mw-heading");
// Two lists in a row are one list split by a blank line; joined, the tree runs on unbroken.
$selected.find("ul + ul").each(function () {
$(this).prev("ul").append($(this).children());
$(this).remove();
});
$selected.filter("ul").addClass("treeview");
$selected.filter(":not(ul)").find("ul").addClass("treeview");
});
// </nowiki>
odqcazk6zbf7xvdzj2ii33bd22xj4h8
MediaWiki:Gadget-TreeDescs-pagestyles.css
8
1359241
6531725
2026-09-15T20:14:13Z
Wikihez
7344
Import TreeDescs from en.wiktionary (MediaWiki:Gadget-TreeDescs.js, CC BY-SA 4.0): draws Dûnde lists as a tree
6531725
css
text/css
/*
* Page: [[MediaWiki:Gadget-TreeDescs-pagestyles.css]]
*
* Imported from en.wiktionary's [[:en:MediaWiki:Gadget-TreeDescs-pagestyles.css]] (CC BY-SA 4.0).
*/
/* code forked from [[w:Template:Tree list/styles.css]] */
.treeview ul {
padding: 0;
margin: 0;
}
.treeview li li {
margin: 0;
list-style-type: none;
list-style-image: none;
background: url("https://upload.wikimedia.org/wikipedia/commons/f/f2/Treeview-grey-line.png") no-repeat 0 -2979.5px;
/* @noflip */
padding: 0 0 0 21px;
text-indent: 0.3em;
}
.treeview li li:last-child {
background-position: 0 -5969.5px
}
/* first line here deals with new situation after RemexHTML switch,
* where emptyline cause the first child to become the 2nd child. Such a mess
*/
.treeview li.emptyline > ul > .mw-empty-elt:first-child + .emptyline,
.treeview li.emptyline > ul > li:first-child {
background-position: 0 9px
}
/* additional code for Wiktionary */
.treeview li dd {
margin: 0;
}
.treeview li dl {
margin: 0;
padding-left: 21px;
}
.treeview li dl:not(:last-child) {
background: url("https://upload.wikimedia.org/wikipedia/commons/f/f2/Treeview-grey-line.png") no-repeat 0 -2px;
/* @noflip */
padding-left: 21px;
text-indent: 0.3em;
}
.treeview .desc-arr {
color: var(--wikt-palette-grey-9, grey);
}
.treeview li li .desc-arr {
vertical-align: top;
}
ajgbxz069yuhrh6eumdpq6fxby7joda
MediaWiki:Gadget-TreeDescs
8
1359242
6531727
2026-09-15T20:15:35Z
Wikihez
7344
Preferences label for the TreeDescs gadget
6531727
wikitext
text/x-wiki
Beşên Dûnde wek darê nîşan bide
qbvo3uk69phyj753x6htv753i1hgzxh
6531732
6531727
2026-09-15T20:28:11Z
Wikihez
7344
6531732
wikitext
text/x-wiki
[[MediaWiki:Gadget-TreeDescs.js|TreeDescs]] — Beşên Dûnde wek darê nîşan bide
gnl9eb9ed0hwqvpuzgkfobdw2crb2a3
6531770
6531732
2026-09-16T05:07:37Z
Wikihez
7344
([[m:Special:MyLanguage/User:Jon Harald Søby/diffedit|diffedit]])
6531770
wikitext
text/x-wiki
[[MediaWiki:Gadget-TreeDescs.js|TreeDescs]] — Beşên Dûnde bi şiklê darê nîşan bide
0d5efc5b5by2mixykl9pcw6y7j9okys
hikûmdar
0
1359243
6531730
2026-09-15T20:23:42Z
Wikihez
7344
([[Mediawiki:Gadget-start.js|WF:NÛ]] v.2.0) +ku:navdêr:{{guharto|ku|hikûmdar}}
6531730
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Navdêr ===
{{navdêr|ku|c=mnmane}}
# {{guharto|ku|hukimdar}}
0e9g2i2fy0tgc17fzijyq3bcepjlfjb
-ία
0
1359244
6531751
2026-09-15T21:04:59Z
Wikihez
7344
([[Mediawiki:Gadget-start.js|WF:NÛ]] v.2.0) +grc:pêşgir:{{înm|Îsmên abstrakt mê çêdike}}
6531751
wikitext
text/x-wiki
== {{ziman|grc}} ==
=== Pêşgir ===
{{pêşgir|grc}}
# {{înm|Îsmên abstrakt mê çêdike}}
==== Etîmolojî ====
{{etîmon|grc|id=îsmê abstrakt|:mîr|grk-pro:*-íā<etimon:mîr<ine-pro:*-éh₂<alt:*-i-eh₂><id:i-extension>>>}}
pq8kmb5c2cp0zqxirgmd9uozdcuduj9
6531752
6531751
2026-09-15T21:05:25Z
Wikihez
7344
/* Etîmolojî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531752
wikitext
text/x-wiki
== {{ziman|grc}} ==
=== Pêşgir ===
{{pêşgir|grc}}
# {{înm|Îsmên abstrakt mê çêdike}}
==== Etîmolojî ====
{{etîmon|grc|id=îsmê abstrakt|:mîr|grk-pro:*-íā<etimon:mîr<ine-pro:*-éh₂<alt:*-i-eh₂><id:i-extension>>>|dar=1|metn=++}}
bjctrjytn9cecge441yvzmzcigvo4wt
6531753
6531752
2026-09-15T21:05:34Z
Wikihez
7344
/* Pêşgir */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531753
wikitext
text/x-wiki
== {{ziman|grc}} ==
=== Pêşgir ===
{{pêşgir|grc}}
# {{înm|Îsmên abstrakt ên mê çêdike}}
==== Etîmolojî ====
{{etîmon|grc|id=îsmê abstrakt|:mîr|grk-pro:*-íā<etimon:mîr<ine-pro:*-éh₂<alt:*-i-eh₂><id:i-extension>>>|dar=1|metn=++}}
tddcqlklgwp2spoa4g1yzz6fvek9zge
6531754
6531753
2026-09-15T21:05:39Z
Wikihez
7344
/* Pêşgir */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531754
wikitext
text/x-wiki
== {{ziman|grc}} ==
=== Pêşgir ===
{{pêşgir|grc}}
# {{înm|Îsmên abstrakt ên mê çêdike.}}
==== Etîmolojî ====
{{etîmon|grc|id=îsmê abstrakt|:mîr|grk-pro:*-íā<etimon:mîr<ine-pro:*-éh₂<alt:*-i-eh₂><id:i-extension>>>|dar=1|metn=++}}
nls3gbcapcf823fzvrl35n6ee5yqffw