TranslationsPanelShared.sys.mjs (8734B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * @typedef {typeof import("../../../../toolkit/components/translations/actors/TranslationsParent.sys.mjs").TranslationsParent} TranslationsParent 7 */ 8 9 /** @type {{ TranslationsParent: TranslationsParent }} */ 10 const lazy = {}; 11 12 ChromeUtils.defineESModuleGetters(lazy, { 13 TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs", 14 }); 15 16 /** 17 * A class containing static functionality that is shared by both 18 * the FullPageTranslationsPanel and SelectTranslationsPanel classes. 19 * 20 * It is recommended to read the documentation above the TranslationsParent class 21 * definition to understand the scope of the Translations architecture throughout 22 * Firefox. 23 * 24 * @see TranslationsParent 25 * 26 * The static instance of this class is a singleton in the parent process, and is 27 * available throughout all windows and tabs, just like the static instance of 28 * the TranslationsParent class. 29 * 30 * Unlike the TranslationsParent, this class is never instantiated as an actor 31 * outside of the static-context functionality defined below. 32 */ 33 export class TranslationsPanelShared { 34 /** 35 * A map from Translations Panel instances to their initialized states. 36 * There is one instance of each panel per top ChromeWindow in Firefox. 37 * 38 * See the documentation above the TranslationsParent class for a detailed 39 * explanation of the translations architecture throughout Firefox. 40 * 41 * @see TranslationsParent 42 * 43 * @type {Map<FullPageTranslationsPanel | SelectTranslationsPanel, string>} 44 */ 45 static #langListsInitState = new WeakMap(); 46 47 /** 48 * True if the next language-list initialization to fail for testing. 49 * 50 * @see TranslationsPanelShared.ensureLangListsBuilt 51 * 52 * @type {boolean} 53 */ 54 static #simulateLangListError = false; 55 56 /** 57 * Set to true once we've initialized the observers for this static global class, 58 * to ensure that we only ever create observers once. 59 * 60 * @type {boolean} 61 */ 62 static #observersInitialized = false; 63 64 /** 65 * Clears cached data regarding the initialization state of the 66 * FullPageTranslationsPanel and the SelectTranslationsPanel dropdown menu lists. 67 * 68 * This will cause all panels to rebuild their menulist items upon its next open event. 69 * There exists one SelectTranslationsPanel and one FullPageTranslationsPanel per open 70 * Firefox window. There are several situations in which this should be called: 71 * 72 * 1) In between test cases, which may explicitly test a different set of available languages. 73 * 2) Whenever the application locale changes, which requires new language display names. 74 * 3) Whenever a Remote Settings sync changes the list of available languages. 75 */ 76 static clearLanguageListsCache() { 77 TranslationsPanelShared.#langListsInitState = new WeakMap(); 78 } 79 80 /** 81 * Defines lazy getters for accessing elements in the document based on provided entries. 82 * 83 * @param {Document} document - The document object. 84 * @param {object} lazyElements - An object where lazy getters will be defined. 85 * @param {object} entries - An object of key/value pairs for which to define lazy getters. 86 */ 87 static defineLazyElements(document, lazyElements, entries) { 88 for (const [name, discriminator] of Object.entries(entries)) { 89 let element; 90 Object.defineProperty(lazyElements, name, { 91 get: () => { 92 if (!element) { 93 if (discriminator[0] === ".") { 94 // Lookup by class 95 element = document.querySelector(discriminator); 96 } else { 97 // Lookup by id 98 element = document.getElementById(discriminator); 99 } 100 } 101 if (!element) { 102 throw new Error(`Could not find "${name}" at "#${discriminator}".`); 103 } 104 return element; 105 }, 106 }); 107 } 108 } 109 110 /** 111 * Ensures that the next call to ensureLangListBuilt wil fail 112 * for the purpose of testing the error state. 113 * 114 * @see TranslationsPanelShared.ensureLangListsBuilt 115 * 116 * @type {boolean} 117 */ 118 static simulateLangListError() { 119 this.#simulateLangListError = true; 120 } 121 122 /** 123 * Retrieves the initialization state of language lists for the specified panel. 124 * 125 * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel 126 * - The panel for which to look up the state. 127 */ 128 static getLangListsInitState(panel) { 129 return TranslationsPanelShared.#langListsInitState.get(panel); 130 } 131 132 /** 133 * Builds the <menulist> of languages for both the "from" and "to". This can be 134 * called every time the popup is shown, as it will retry when there is an error 135 * (such as a network error) or be a noop if it's already initialized. 136 * 137 * @param {Document} document - The document object. 138 * @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel 139 * - The panel for which to ensure language lists are built. 140 */ 141 static async ensureLangListsBuilt(document, panel) { 142 if (!TranslationsPanelShared.#observersInitialized) { 143 TranslationsPanelShared.#observersInitialized = true; 144 145 // The language dropdowns must be rebuilt any time the application locale changes. 146 // Since the dropdowns are dynamically populated with localized language display names, 147 // we need to repopulate the display names for the new locale. 148 Services.obs.addObserver( 149 TranslationsPanelShared.clearLanguageListsCache, 150 "intl:app-locales-changed" 151 ); 152 153 // The language dropdowns must be rebuilt any time language pairs change. 154 // This is most often due to a Remote Settings sync, which could be triggered 155 // due to publishing a new language model, or by changing the Remote Settings channel. 156 Services.obs.addObserver( 157 TranslationsPanelShared.clearLanguageListsCache, 158 "translations:language-pairs-changed" 159 ); 160 } 161 162 const { panel: panelElement } = panel.elements; 163 switch (TranslationsPanelShared.#langListsInitState.get(panel)) { 164 case "initialized": 165 // This has already been initialized. 166 return; 167 case "error": 168 case undefined: 169 // Set the error state in case there is an early exit at any point. 170 // This will be set to "initialized" if everything succeeds. 171 TranslationsPanelShared.#langListsInitState.set(panel, "error"); 172 break; 173 default: 174 throw new Error( 175 `Unknown langList phase ${ 176 TranslationsPanelShared.#langListsInitState 177 }` 178 ); 179 } 180 /** @type {SupportedLanguages} */ 181 const { languagePairs, sourceLanguages, targetLanguages } = 182 await lazy.TranslationsParent.getSupportedLanguages(); 183 184 // Verify that we are in a proper state. 185 if (languagePairs.length === 0 || this.#simulateLangListError) { 186 this.#simulateLangListError = false; 187 throw new Error("No translation languages were retrieved."); 188 } 189 190 const fromPopups = panelElement.querySelectorAll( 191 ".translations-panel-language-menupopup-from" 192 ); 193 const toPopups = panelElement.querySelectorAll( 194 ".translations-panel-language-menupopup-to" 195 ); 196 197 for (const popup of fromPopups) { 198 // For the moment, the FullPageTranslationsPanel includes its own 199 // menu item for "Choose another language" as the first item in the list 200 // with an empty-string for its value. The SelectTranslationsPanel has 201 // only languages in its list with BCP-47 tags for values. As such, 202 // this loop works for both panels, to remove all of the languages 203 // from the list, but ensuring that any empty-string items are retained. 204 while (popup.lastChild?.value) { 205 popup.lastChild.remove(); 206 } 207 for (const { langTagKey, displayName } of sourceLanguages) { 208 const fromMenuItem = document.createXULElement("menuitem"); 209 fromMenuItem.setAttribute("value", langTagKey); 210 fromMenuItem.setAttribute("label", displayName); 211 popup.appendChild(fromMenuItem); 212 } 213 } 214 215 for (const popup of toPopups) { 216 while (popup.lastChild?.value) { 217 popup.lastChild.remove(); 218 } 219 for (const { langTagKey, displayName } of targetLanguages) { 220 const toMenuItem = document.createXULElement("menuitem"); 221 toMenuItem.setAttribute("value", langTagKey); 222 toMenuItem.setAttribute("label", displayName); 223 popup.appendChild(toMenuItem); 224 } 225 } 226 227 TranslationsPanelShared.#langListsInitState.set(panel, "initialized"); 228 } 229 }