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 &quot;yewnaniya kevn&quot; 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, "&mdash; " .. 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>&nbsp;", "") :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("&#9656;"); }); } 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'>&#215;</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'>&#9656;</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("&#9656;"); } moreToggle.on("click", function (e) { e.preventDefault(); $(".wiktku-dfn-more-menu").not(moreMenu).hide(); moreMenu.toggle(); moreArrow.html(moreMenu.is(":visible") ? "&#9662;" : "&#9656;"); }); 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("&#9656;"); }); } 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'>&#215;</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'>&#9656;</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("&#9656;"); } moreToggle.on("click", function (e) { e.preventDefault(); $(".wiktku-dfn-more-menu").not(moreMenu).hide(); moreMenu.toggle(); moreArrow.html(moreMenu.is(":visible") ? "&#9662;" : "&#9656;"); }); 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("&#9656;"); }); } 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'>&#215;</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'>&#9656;</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("&#9656;"); } moreToggle.on("click", function (e) { e.preventDefault(); $(".wiktku-dfn-more-menu").not(moreMenu).hide(); moreMenu.toggle(); moreArrow.html(moreMenu.is(":visible") ? "&#9662;" : "&#9656;"); }); 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