MediaWiki:Gadget-WishlistTranslation.js
Appearance
From Meta, a Wikimedia project coordination wiki
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// <nowiki> mw.loader.using(["vue","@wikimedia/codex","mediawiki.language.names","mediawiki.storage"]).then((require)=>{ /** * WishlistTranslation: A gadget for machine translation of pages within the Community Wishlist. * From [[Community Tech]] * Compiled from source at https://gitlab.wikimedia.org/repos/commtech/wishlist-intake * Please submit code changes as a merge request to the source repository. */ 'use strict'; function__$styleInject(css){ if(!css)return; if(typeofwindow=='undefined')return; varstyle=document.createElement('style'); style.setAttribute('media','screen'); style.innerHTML=css; document.head.appendChild(style); returncss; } varvue=require('vue'); varcodex=require('@wikimedia/codex'); varwishHomePage="Community Wishlist"; varwishIntakePage="Community Wishlist/Intake"; varwishEditParam="editwish"; varvoteRecordedStorageName="wishlist-intake-vote-added"; varwishIndexTemplate="Community Wishlist/Wishes"; varwishIndexTemplateAll="Community Wishlist/Wishes/All"; varwishIndexTemplateRecent="Community Wishlist/Wishes/Recent"; varwishIndexTemplateArchive="Community Wishlist/Wishes/Archive"; varwishCategory="Community Wishlist/Wishes"; varwishPagePrefix="Community Wishlist/Wishes/"; varwishTemplate="Community Wishlist/Wish"; varfocusAreaPagePrefix="Community Wishlist/Focus areas/"; varfocusAreaVoteCountSuffix="/Vote count"; varfocusAreaTemplate="Community Wishlist/Focus area"; varfocusAreasTemplate="Community Wishlist/Focus areas"; varfocusAreaTemplateAll="Community Wishlist/Focus areas/All"; varfocusAreaTemplateTop="Community Wishlist/Focus areas/Top"; varsupportTemplate="Community Wishlist/Support"; varmessagesPage="MediaWiki:Gadget-WishlistIntake/messages"; varinterfaceMessageGroupId="agg-Community_Wishlist_interface"; varwishesMessageGroupId="agg-Community_Wishlist_wishes"; varmaxRecentWishes=5; vargadgets={ WishlistIntake:{ description:"A gadget for the intake and editing of Community Wishlist proposals.", ResourceLoader:true, "default":true, hidden:true, "package":true, files:[ "WishlistIntake.js" ], rights:[ "editmyusercss" ], categories:[ "Community Wishlist/Intake" ], dependencies:[ "vue", "@wikimedia/codex", "mediawiki.util", "mediawiki.api", "user.options", "mediawiki.action.view.postEdit", "mediawiki.confirmCloseWindow", "mediawiki.jqueryMsg" ], peers:[ "WishlistIntake-pagestyles" ] }, "WishlistIntake-pagestyles":{ peer:true, hidden:true, files:[ "WishlistIntake-pagestyles.css" ], filesOnWiki:true }, WishlistManager:{ description:"A gadget helping with management tasks for the Community Wishlist.", ResourceLoader:true, "default":true, hidden:true, "package":true, files:[ "WishlistManager.js" ], rights:[ "editmyusercss" ], dependencies:[ "mediawiki.api", "mediawiki.util" ] }, WishlistTranslation:{ description:"A gadget for machine translation of pages within the Community Wishlist.", ResourceLoader:true, "package":true, files:[ "WishlistTranslation.js" ], categories:[ "Community Wishlist/Intake" ], dependencies:[ "vue", "@wikimedia/codex", "mediawiki.language.names", "mediawiki.storage" ], messages:[ "communityrequests-translation-translatable", "communityrequests-translation-switch", "communityrequests-translation-progress", "communityrequests-translation-errors" ] } }; varmessages={ "communitywishlist-wish-loading-error":"There was an error while parsing the wish source text. It may contain invalid wikitext. Please [1ドル refresh] and try again, use the [[2ドル|source editor]], or ask for help on the [[3ドル|talk page]].", "communitywishlist-edit-with-form":"Edit with form", "communitywishlist-form-subtitle":"Welcome to the new Community Wishlist. Please fill in the form below to submit your wish.", "communitywishlist-form-error":"Something went wrong. Please try saving again, or ask for help on the [[1ドル|talk page]].", "communitywishlist-description":"Describe your problem", "communitywishlist-description-description":"Explain in detail the wish or problem you are addressing.", "communitywishlist-title":"Wish title", "communitywishlist-title-error":"Please enter a value for this field (between 1ドル and 2ドル {{PLURAL:2ドル|character|characters}}).", "communitywishlist-title-description":"Make sure your title contains a brief description of the wish or problem.", "communitywishlist-description-error":"Please enter a value for this field (1ドル or more {{PLURAL:1ドル|character|characters}}).", "communitywishlist-wishtype-label":"Which type best describes your wish?", "communitywishlist-wishtype-description":"For submitting a policy change request, please consult the applicable project.", "communitywishlist-wishtype-feature-label":"Feature request", "communitywishlist-wishtype-feature-description":"You want new features and functions that do not exist yet.", "communitywishlist-wishtype-bug-label":"Bug report", "communitywishlist-wishtype-bug-description":"You want a problem or error fixed with existing features.", "communitywishlist-wishtype-change-label":"System change", "communitywishlist-wishtype-change-description":"You want a currently working feature or function to be changed.", "communitywishlist-wishtype-unknown-label":"I'm not sure or I don't know", "communitywishlist-wishtype-unknown-description":"After receiving your wish, we will assign a relevant type.", "communitywishlist-wishtype-error":"Please select a wish type.", "communitywishlist-project-intro":"Which projects is your wish related to?", "communitywishlist-project-help":"Select all projects your wish will have an impact on.", "communitywishlist-project-all-projects":"All projects", "communitywishlist-project-show-less":"Show less", "communitywishlist-project-show-all":"Show all", "communitywishlist-project-other-label":"It's something else", "communitywishlist-project-other-description":"e.g. gadgets, bots and external tools", "communitywishlist-project-other-error":"Please enter a value for this field (greater than 1ドル {{PLURAL:1ドル|character|characters}}), or select a project checkbox.", "communitywishlist-project-no-selection":"Please select at least 1ドル {{PLURAL:1ドル|project checkbox|project checkboxes}}, or enter a value for the \"2ドル\" field.", "communitywishlist-audience-label":"Primary affected users", "communitywishlist-audience-description":"Describe which user group and situation this will affect the most", "communitywishlist-audience-error":"Please enter a value for this field (between 1ドル and 2ドル {{PLURAL:2ドル|characters}}).", "communitywishlist-phabricator-label":"Phabricator tasks (optional)", "communitywishlist-phabricator-desc":"Enter Phabricator task IDs or URLs.", "communitywishlist-phabricator-chip-desc":"A list of Phabricator task IDs.", "communitywishlist-create-success":"Your wish has been submitted.", "communitywishlist-edit-success":"Your wish has been saved.", "communitywishlist-view-all-wishes":"View all wishes.", "communitywishlist-close":"Close", "communitywishlist-publish":"Publish wish", "communitywishlist-save":"Save changes", "communitywishlist-support-focus-area":"Support focus area", "communitywishlist-support-focus-area-dialog-title":"Support \"1ドル\"", "communitywishlist-optional-comment":"Optional comment", "communitywishlist-supported":"Already supported", "communitywishlist-support-focus-area-confirmed":"You have voted in support of this focus area.", "communitywishlist-unsupport-focus-area":"Remove your support vote" }; varimportedMessages=[ "communityrequests-status-draft", "communityrequests-status-submitted", "communityrequests-status-open", "communityrequests-status-in-progress", "communityrequests-status-delivered", "communityrequests-status-blocked", "communityrequests-status-archived", "project-localized-name-commonswiki", "project-localized-name-group-wikinews", "project-localized-name-group-wikipedia", "project-localized-name-group-wikiquote", "project-localized-name-group-wikisource", "project-localized-name-group-wikiversity", "project-localized-name-group-wikivoyage", "project-localized-name-group-wiktionary", "project-localized-name-mediawikiwiki", "project-localized-name-metawiki", "project-localized-name-specieswiki", "project-localized-name-wikidatawiki", "project-localized-name-wikifunctionswiki", "wikimedia-otherprojects-cloudservices", "cancel", "wikimedia-copyrightwarning" ]; varconfig={ wishHomePage:wishHomePage, wishIntakePage:wishIntakePage, wishEditParam:wishEditParam, voteRecordedStorageName:voteRecordedStorageName, wishIndexTemplate:wishIndexTemplate, wishIndexTemplateAll:wishIndexTemplateAll, wishIndexTemplateRecent:wishIndexTemplateRecent, wishIndexTemplateArchive:wishIndexTemplateArchive, wishCategory:wishCategory, wishPagePrefix:wishPagePrefix, wishTemplate:wishTemplate, focusAreaPagePrefix:focusAreaPagePrefix, focusAreaVoteCountSuffix:focusAreaVoteCountSuffix, focusAreaTemplate:focusAreaTemplate, focusAreasTemplate:focusAreasTemplate, focusAreaTemplateAll:focusAreaTemplateAll, focusAreaTemplateTop:focusAreaTemplateTop, supportTemplate:supportTemplate, messagesPage:messagesPage, interfaceMessageGroupId:interfaceMessageGroupId, wishesMessageGroupId:wishesMessageGroupId, maxRecentWishes:maxRecentWishes, gadgets:gadgets, messages:messages, importedMessages:importedMessages }; /** * Utility functions for the gadget */ classWebUtil{ /** * Get the full page name with underscores replaced by spaces. * We use this instead of wgTitle because it's possible to set up * the wishlist gadget for use outside the mainspace. * * @return {string} */ staticgetPageName(){ returnmw.config.get('wgPageName').replaceAll('_',' '); } /** * Is the current page a wish page? * * @return {boolean} */ staticisWishPage(){ returnthis.getPageName().startsWith(config.wishPagePrefix); } /** * Are we currently creating a new wish? * * @return {boolean} */ staticisNewWish(){ returnthis.getPageName().startsWith(config.wishIntakePage)&& mw.config.get('wgAction')==='view'&& !this.isWishEdit()&& // Don't load on diff pages !mw.config.get('wgDiffOldId'); } /** * Are we currently viewing (but not editing) a wish page? * * @return {boolean} */ staticisWishView(){ returnthis.isWishPage()&&!this.isWishEdit()&&mw.config.get('wgAction')==='view'; } /** * Are we currently editing a wish page? * * @return {boolean} */ staticisWishEdit(){ returnthis.isWishPage()&&!!mw.util.getParamValue(config.wishEditParam); } /** * Are we currently manually editing a wish page? * * @return {boolean} */ staticisManualWishEdit(){ returnthis.isWishPage()&& ( mw.config.get('wgAction')==='edit'|| document.documentElement.classList.contains('ve-active') ); } /** * Are we currently on a focus area page? * * @return {boolean} */ staticisFocusAreaPage(){ returnthis.getPageName().startsWith(config.focusAreaPagePrefix); } /** * Get the user's preferred language. * * @return {string} */ staticuserPreferredLang(){ if(mw.config.get('wgArticleId')===0){ // Use interface language for new pages. returnmw.config.get('wgUserLanguage'); } // Use content language for existing pages. returnmw.config.get('wgContentLanguage'); } /** * Is the user's preferred language right-to-left? * * @return {boolean} */ staticisRtl(){ return$('body').css('direction')==='rtl'; } /** * Are we on a page related to creating, editing, or viewing a wish? * This can include viewing the revision history, manual editing of wishes, etc. * * @return {boolean} */ staticisWishRelatedPage(){ returnthis.isNewWish()||this.isWishEdit()||this.isWishView()||this.isWishPage(); } /** * Should we show the intake form? * * @return {boolean} */ staticshouldShowForm(){ // Prevent form from loading on i.e. action=history returnmw.config.get('wgAction')==='view'&& (this.isNewWish()||this.isWishEdit()); } /** * Get the slug for the wish derived from the page title. * This is the subpage title and not necessarily the wish title, * which is stored in the proposal content. * * @return {string|null} null if not a wish-related page */ staticgetWishSlug(){ if(this.isNewWish()){ // New wishes have no slug yet. return''; }elseif(this.isWishPage()){ // Existing wishes have the page prefix stripped. constslugPortion=this.getPageName().slice(config.wishPagePrefix.length); // Strip off language subpage. Slashes are disallowed in wish slugs. returnslugPortion.split('/')[0]; } returnnull; } /** * Get the slug for the focus area derived from the page title. * * @return {string|null} null if not a focus area page */ staticgetFocusAreaSlug(){ if(this.isFocusAreaPage()){ constslugPortion=this.getPageName().slice(config.focusAreaPagePrefix.length); // Strip off language subpage. Slashes are disallowed in focus area slugs. returnslugPortion.split('/')[0]; } returnnull; } /** * Get the full page title of the wish from the slug. * * @param {string} slug * @return {string} */ staticgetWishPageTitleFromSlug(slug){ returnconfig.wishPagePrefix+slug; } /** * Is the user WMF staff? * * @todo WMF-specific * @return {boolean} */ staticisStaff(){ return/\s\(WMF\)$|-WMF$/.test(mw.config.get('wgUserName')); } /** * Log an error to the console. * * @param {string} text * @param {Error} error */ staticlogError(text,error){ mw.log.error(`[WishlistIntake] ${text}`,error); } /** * Get a CSS-only Codex Message component of the specified type. * This is for use outside the Vue application. * * @param {mw.Message} message * @param {string} type 'notice', 'warning', 'error' or 'success' * @return {HTMLDivElement} */ staticgetMessageBox(message,type){ constmessageBlock=document.createElement('div'); // The following messages may be used here: // * cdx-message--notice // * cdx-message--warning // * cdx-message--error // * cdx-message--success messageBlock.classList.add('cdx-message','cdx-message--block',`cdx-message--${type}`); if(type==='warning'){ messageBlock.role='alert'; }else{ messageBlock.ariaLive='polite'; } consticon=document.createElement('span'); icon.classList.add('cdx-message__icon'); constcontent=document.createElement('div'); content.classList.add('cdx-message__content'); content.innerHTML=message.parse(); messageBlock.appendChild(icon); messageBlock.appendChild(content); returnmessageBlock; } /** * Is the user viewing in mobile format? * * @return {boolean} */ staticisMobile(){ return!!mw.config.get('wgMFMode'); } /** * Fetch messages from the wiki and set them in mw.messages. * * @param {mw.Api} api * @return {jQuery.Promise} */ staticsetOnWikiMessages(api){ consttitles=[config.messagesPage+'/en'], langPageLocal=config.messagesPage+'/'+mw.config.get('wgUserLanguage'); if(mw.config.get('wgUserLanguage')!=='en'){ titles.push(langPageLocal); } returnapi.get({ action:'query', prop:'revisions', titles, rvprop:'content', rvslots:'main', format:'json', formatversion:2, // Cache for 30 minutes. maxage:1800, smaxage:1800 }).then((resp)=>{ letmessagesLocal={}, messagesEn={}; /** * The content model of the messages page is wikitext so that it can be used with * Extension:Translate. Consequently, it's possible to break things. This just does * a try/catch and returns the default English messages if it fails. * * @param {string} title * @param {string} content * @return {Object} */ constparseJSON=(title,content)=>{ try{ returnJSON.parse(content); }catch(e){ WebUtil.logError(`Failed to parse JSON for ${title}.`,e); return{messages:config.messages}; } }; resp.query.pages.forEach((page)=>{ if(!page.revisions){ // Missing return; } constpageObj=page.revisions[0].slots.main; constparsedContent=parseJSON(config.messagesPage,pageObj.content); if(page.title===langPageLocal&&mw.config.get('wgUserLanguage')!=='en'){ messagesLocal=parsedContent.messages; }else{ messagesEn=parsedContent.messages; } }); mw.messages.set(Object.assign(messagesEn,messagesLocal)); }); } } varscript=vue.defineComponent({ name:'WishlistTranslationBanner', components:{ CdxMessage:codex.CdxMessage, CdxProgressBar:codex.CdxProgressBar, CdxToggleSwitch:codex.CdxToggleSwitch }, props:{ translatableNodes:{type:Array,default:()=>[]}, targetLang:{type:String,default:''}, targetLangDir:{type:String,default:'ltr'} }, setup(){ // eslint-disable-next-line n/no-missing-require conststorage=require('mediawiki.storage').local; conststorageName='wishlist-intake-translation-enabled'; // @todo Load these from Codex. T311099. constcdxIconRobot='<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><!----><g><path d="M10.5 5h6.505C18.107 5 19 5.896 19 6.997V14h-7v2h5.005c1.102 0 1.995.888 1.995 2v2H1v-2c0-1.105.893-2 1.995-2H8v-2H1V6.997C1 5.894 1.893 5 2.995 5H9.5V2.915a1.5 1.5 0 111 0zm-4 6a1.5 1.5 0 100-3 1.5 1.5 0 000 3m7 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3"></path></g></svg>'; return{ storage, storageName, cdxIconRobot }; }, data(){ return{ enabled:this.storage.get(this.storageName)==='1', inprogress:0, translatedNodeCount:0, errors:[] }; }, computed:{ translatableNodeCount(){ returnthis.translatableNodes.length; }, userLanguageName(){ constlangNames=mw.language.getData(this.targetLang,'languageNames'); if(langNames&&langNames[this.targetLang]!==undefined){ returnlangNames[this.targetLang]; } returnthis.targetLang; } }, methods:{ onToggle(){ this.storage.set(this.storageName,this.enabled?'1':'0'); for(constnodeofthis.translatableNodes){ if(!node.isConnected){ // May have been removed since being queried in wishlistTranslation.init.js continue; } if(!this.enabled){ // Disable by returning to untranslated values. node.nodeValue=node.nodeValueUntranslated; node.parentElement.lang=node.langOriginal; node.parentElement.dir=node.dirOriginal; continue; }else{ if(node.nodeValueTranslated!==undefined){ // If this node has been translated already, switch to that value. node.nodeValue=node.nodeValueTranslated; node.parentElement.lang=this.targetLang; node.parentElement.dir=this.targetLangDir; }else{ // Otherwise, get the translation. node.nodeValueUntranslated=node.nodeValue; node.parentElement.style.opacity='0.6'; this.inprogress++; // Note that node.lang has been set in the init script. this.getTranslation(node.nodeValueUntranslated,node.lang) .then((translatedHtml)=>{ node.parentElement.style.opacity=''; this.inprogress--; node.langOriginal=node.lang; node.dirOriginal=node.dir; if(translatedHtml===''){ return; } node.parentElement.lang=this.targetLang; node.parentElement.dir=this.targetLangDir; this.translatedNodeCount++; node.nodeValueTranslated=translatedHtml; node.nodeValue=node.nodeValueTranslated; }); } } } }, /** * @param {string} html * @param {string} srcLang * @return {Promise<string>} */ getTranslation(html,srcLang){ consturl=`https://cxserver.wikimedia.org/v1/mt/${srcLang}/${this.targetLang}/MinT`; returnfetch(url,{ method:'POST', headers:{ Accept:'application/json', 'Content-Type':'application/json' }, body:JSON.stringify({html:html}) }).then((response)=>{ returnresponse.text().then((body)=>{ // It is not always JSON that is returned. T373418. // @todo i18n for error messages letresponseBody=''; try{ responseBody=JSON.parse(body); }catch(e){ this.errors.push('Unable to decode MinT response: '+body); return''; } if(!responseBody.contents){ this.errors.push('No MinT response contents. Response was: '+body); return''; } // Wrap output with spaces if the input was (MinT strips them). return(html.startsWith(' ')?' ':'')+ responseBody.contents+ (html.endsWith(' ')?' ':''); }); }); } }, mounted(){ if(this.enabled){ this.onToggle(); } } }); const_hoisted_1=["innerHTML"]; const_hoisted_2=["innerHTML"]; const_hoisted_3={key:0}; const_hoisted_4={ key:1, class:"wishlist-translation-errors" }; functionrender(_ctx,_cache,$props,$setup,$data,$options){ const_component_cdx_toggle_switch=vue.resolveComponent("cdx-toggle-switch"); const_component_cdx_progress_bar=vue.resolveComponent("cdx-progress-bar"); const_component_cdx_message=vue.resolveComponent("cdx-message"); return(vue.openBlock(),vue.createElementBlock(vue.Fragment,null,[ vue.createCommentVNode(" eslint-disable vue/no-v-html "), vue.createVNode(_component_cdx_message,{ "allow-user-dismiss":"", icon:_ctx.cdxIconRobot },{ default:vue.withCtx(()=>[ vue.createElementVNode("div",{ innerHTML:_ctx.$i18n( 'communityrequests-translation-translatable',_ctx.userLanguageName ).parse() },null,8/* PROPS */,_hoisted_1), vue.createElementVNode("div",null,[ vue.createVNode(_component_cdx_toggle_switch,{ modelValue:_ctx.enabled, "onUpdate:modelValue":[ _cache[0]||(_cache[0]=$event=>((_ctx.enabled)=$event)), _ctx.onToggle ] },{ default:vue.withCtx(()=>[ vue.createElementVNode("span",{ innerHTML:_ctx.$i18n('communityrequests-translation-switch').parse() },null,8/* PROPS */,_hoisted_2) ]), _:1/* STABLE */ },8/* PROPS */,["modelValue","onUpdate:modelValue"]) ]), (_ctx.enabled&&_ctx.inprogress) ?(vue.openBlock(),vue.createElementBlock("div",_hoisted_3,[ vue.createVNode(_component_cdx_progress_bar,{"aria-hidden":"true"}), vue.createTextVNode(" "+vue.toDisplayString(_ctx.$i18n('communityrequests-translation-progress') .params([_ctx.translatedNodeCount,_ctx.translatableNodeCount]) .text()),1/* TEXT */) ])) :vue.createCommentVNode("v-if",true), (_ctx.enabled&&_ctx.errors.length>0) ?(vue.openBlock(),vue.createElementBlock("div",_hoisted_4,[ vue.createElementVNode("strong",null,vue.toDisplayString(_ctx.$i18n('communityrequests-translation-errors').text()),1/* TEXT */), vue.createElementVNode("ul",null,[ (vue.openBlock(true),vue.createElementBlock(vue.Fragment,null,vue.renderList(_ctx.errors,(error)=>{ return(vue.openBlock(),vue.createElementBlock("li",{key:error},vue.toDisplayString(error),1/* TEXT */)) }),128/* KEYED_FRAGMENT */)) ]) ])) :vue.createCommentVNode("v-if",true) ]), _:1/* STABLE */ },8/* PROPS */,["icon"]) ],2112/* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)) } __$styleInject(".wishlist-translation-errors {\n color: #d73333;\n}\n"); script.render=render; /** * Entry point for the WishlistTranslation gadget. */ if(WebUtil.getPageName().startsWith(config.wishHomePage)){ mw.hook('wikipage.content').add(($content)=>{ consttargetLang=mw.config.get('wgUserLanguage'); getTranslatableNodes($content[0],targetLang).then((translatableNodes)=>{ // Do nothing if there's nothing to translate. if(translatableNodes.length===0){ return; } // Get the i18n messages, and mount the Vue app. constmessages=config.gadgets.WishlistTranslation.messages; (newmw.Api()).loadMessages(messages).then(()=>{ constappRoot=document.createElement('div'); $content[0].before(appRoot); constappData={ targetLang:targetLang, // @todo Get the lang dir in a better way. targetLangDir:document.querySelector('html').dir, translatableNodes:translatableNodes }; constVue=require('vue'); Vue.createMwApp(script,appData).mount(appRoot); }); }); }); } /** * Get all source languages supported by MinT for the given target language, * caching the result in localStorage for a day to avoid re-querying on every\ * page load. * * @param {string} targetLang The target language code. * @return {Array<string,Array<string>>} */ functiongetSupportedLangs(targetLang){ // eslint-disable-next-line n/no-missing-require conststorage=require('mediawiki.storage').local; constlocalStorageKey='wishlist-intake-langlist-'+targetLang; conststored=storage.get(localStorageKey); if(stored){ returnPromise.resolve(JSON.parse(stored)); } consturl='https://cxserver.wikimedia.org/v1/list/mt'; returnfetch(url).then((response)=>{ returnresponse.text().then((body)=>{ constsourceLangs=[]; try{ constmintLangs=JSON.parse(body).MinT; // The API maps each language to those that it can be translated to, // but we want a list of all possible source langs for our target. if(mintLangs[targetLang]){ for(constsourceLangofObject.keys(mintLangs)){ if(mintLangs[sourceLang].includes(targetLang)){ sourceLangs.push(sourceLang); } } } }catch(e){ // Unable to parse response. } // Store for 24 hours. storage.set(localStorageKey,JSON.stringify(sourceLangs),60*60*24); returnsourceLangs; }); }); } /** * Get all DOM nodes that need to be translated. * * @todo More needs to be done here to select nodes and/or elements that are * actually needing to be translated and that are of the most appropriate size * and scope. Probably we should be collecting elements and not leaf nodes, but * if we do that then in many cases we end up also having translations inside * them, so more work is needed there. * * @param {Element} content DOM containing at least one .mw-parser-output element. * @param {string} targetLang * @return {Promise<Array<Node>>} */ functiongetTranslatableNodes(content,targetLang){ constparserOutput=content.querySelector('.mw-parser-output'); if(parserOutput===null){ returnPromise.resolve([]); } returngetSupportedLangs(targetLang).then((supportedLangs)=>{ // Find all text nodes that are in a different language to the interface language. constwalker=document.createTreeWalker(parserOutput,NodeFilter.SHOW_TEXT,(node)=>{ // Skip empty nodes, and everything in the <languages /> bar. if(node.nodeValue.trim()===''|| node.parentElement.closest('.mw-pt-languages') ){ returnNodeFilter.FILTER_SKIP; } constlang=node.parentElement.closest('[lang]').lang; // Skip if they're the same language. if(lang===targetLang|| // Skip style elements. node.parentElementinstanceofHTMLStyleElement|| // Skip if any parent has `.translate-no`. T161486. // @todo Fix this to permit `.translate-yes` to be inside a `.translate-no`. node.parentElement.closest('.translate-no') ){ returnNodeFilter.FILTER_SKIP; } // Check if the source lang can be translated to the target lang. if(!supportedLangs.includes(lang)){ returnNodeFilter.FILTER_SKIP; } // Save the parent lang on the node for easier access when sending it for translation. node.lang=lang; returnNodeFilter.FILTER_ACCEPT; }); // Get all nodes. letn=walker.nextNode(); consttranslatableNodes=[]; while(n){ translatableNodes.push(n); n=walker.nextNode(); } returntranslatableNodes; }); } }); // </nowiki>