forked from mirrors/gecko-dev
Bug 1820259 - Implement the translation language download preferences; r=nordzilla,flod,mconley
Differential Revision: https://phabricator.services.mozilla.com/D176189
This commit is contained in:
parent
23de35b953
commit
cf91f08437
7 changed files with 497 additions and 8 deletions
|
|
@ -355,6 +355,7 @@
|
|||
data-l10n-args='{"localeName": "und"}'
|
||||
preference="intl.regional_prefs.use_os_locales"/>
|
||||
|
||||
<!-- TODO (Bug 1817084) This older implementation will be removed soon -->
|
||||
<hbox id="translationBox" hidden="true">
|
||||
<hbox align="center" flex="1">
|
||||
<checkbox id="translate" preference="browser.translation.detectLanguage"
|
||||
|
|
@ -384,6 +385,25 @@
|
|||
<checkbox id="checkSpelling"
|
||||
data-l10n-id="check-user-spelling"
|
||||
preference="layout.spellcheckDefault"/>
|
||||
|
||||
<!-- Translations -->
|
||||
<vbox id="translationsGroup" hidden="true" data-subcategory="translations">
|
||||
<label><html:h2 data-l10n-id="translations-manage-header"/></label>
|
||||
<description data-l10n-id="translations-manage-description"/>
|
||||
<vbox>
|
||||
<html:div id="translations-manage-install-list" hidden="true">
|
||||
<hbox class="translations-manage-language">
|
||||
<label data-l10n-id="translations-manage-all-language"></label>
|
||||
<button id="translations-manage-install-all"
|
||||
data-l10n-id="translations-manage-download-button"></button>
|
||||
<button id="translations-manage-delete-all"
|
||||
data-l10n-id="translations-manage-delete-button"></button>
|
||||
</hbox>
|
||||
<!-- The downloadable languages will be listed here. -->
|
||||
</html:div>
|
||||
<description id="translations-manage-error" hidden="true"></description>
|
||||
</vbox>
|
||||
</vbox>
|
||||
</groupbox>
|
||||
|
||||
<!-- Files and Applications -->
|
||||
|
|
|
|||
|
|
@ -327,6 +327,8 @@ var gMainPane = {
|
|||
// listener for future menu changes.
|
||||
gMainPane.initDefaultZoomValues();
|
||||
|
||||
gMainPane.initTranslations();
|
||||
|
||||
if (
|
||||
Services.prefs.getBoolPref(
|
||||
"media.videocontrols.picture-in-picture.enabled"
|
||||
|
|
@ -995,6 +997,424 @@ var gMainPane = {
|
|||
document.getElementById("zoomBox").hidden = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the translations view.
|
||||
*/
|
||||
async initTranslations() {
|
||||
if (!Services.prefs.getBoolPref("browser.translations.enable")) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which phase a language download is in.
|
||||
*
|
||||
* @typedef {"downloaded" | "loading" | "uninstalled"} DownloadPhase
|
||||
*/
|
||||
|
||||
// Immediately show the group so that the async load of the component does
|
||||
// not cause the layout to jump. The group will be empty initially.
|
||||
document.getElementById("translationsGroup").hidden = false;
|
||||
|
||||
class TranslationsState {
|
||||
/**
|
||||
* The fully initialized state.
|
||||
*
|
||||
* @param {TranslationsActor} translationsActor
|
||||
* @param {Object} supportedLanguages
|
||||
* @param {Array<{ langTag: string, displayName: string}} languageList
|
||||
* @param {Map<string, DownloadPhase>} downloadPhases
|
||||
*/
|
||||
constructor(
|
||||
translationsActor,
|
||||
supportedLanguages,
|
||||
languageList,
|
||||
downloadPhases
|
||||
) {
|
||||
this.translationsActor = translationsActor;
|
||||
this.supportedLanguages = supportedLanguages;
|
||||
this.languageList = languageList;
|
||||
this.downloadPhases = downloadPhases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles all of the async initialization logic.
|
||||
*/
|
||||
static async create() {
|
||||
const translationsActor = window.windowGlobalChild.getActor(
|
||||
"Translations"
|
||||
);
|
||||
const supportedLanguages = await translationsActor.getSupportedLanguages();
|
||||
const languageList = TranslationsState.getLanguageList(
|
||||
supportedLanguages
|
||||
);
|
||||
const downloadPhases = await TranslationsState.createDownloadPhases(
|
||||
translationsActor,
|
||||
languageList
|
||||
);
|
||||
|
||||
if (supportedLanguages.languagePairs.length === 0) {
|
||||
throw new Error(
|
||||
"The supported languages list was empty. RemoteSettings may not be available at the moment."
|
||||
);
|
||||
}
|
||||
|
||||
return new TranslationsState(
|
||||
translationsActor,
|
||||
supportedLanguages,
|
||||
languageList,
|
||||
downloadPhases
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique list of languages, sorted by the display name.
|
||||
*
|
||||
* @param {Object} supportedLanguages
|
||||
* @returns {Array<{ langTag: string, displayName: string}}
|
||||
*/
|
||||
static getLanguageList(supportedLanguages) {
|
||||
const displayNames = new Map();
|
||||
for (const languages of [
|
||||
supportedLanguages.fromLanguages,
|
||||
supportedLanguages.toLanguages,
|
||||
]) {
|
||||
for (const { langTag, displayName } of languages) {
|
||||
displayNames.set(langTag, displayName);
|
||||
}
|
||||
}
|
||||
|
||||
let appLangTag = new Intl.Locale(Services.locale.appLocaleAsBCP47)
|
||||
.language;
|
||||
|
||||
// Don't offer to download the app's language.
|
||||
displayNames.delete(appLangTag);
|
||||
|
||||
// Sort the list of languages by the display names.
|
||||
return [...displayNames.entries()]
|
||||
.map(([langTag, displayName]) => ({
|
||||
langTag,
|
||||
displayName,
|
||||
}))
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the download phase of each language file.
|
||||
*
|
||||
* @param {TranslationsChild} translationsActor
|
||||
* @param {Array<{ langTag: string, displayName: string}} languageList.
|
||||
* @returns {Map<string, DownloadPhase>} Map the language tag to whether it is downloaded.
|
||||
*/
|
||||
static async createDownloadPhases(translationsActor, languageList) {
|
||||
const downloadPhases = new Map();
|
||||
for (const { langTag } of languageList) {
|
||||
downloadPhases.set(
|
||||
langTag,
|
||||
(await translationsActor.hasAllFilesForLanguage(langTag))
|
||||
? "downloaded"
|
||||
: "uninstalled"
|
||||
);
|
||||
}
|
||||
return downloadPhases;
|
||||
}
|
||||
}
|
||||
|
||||
class TranslationsView {
|
||||
/** @type {Map<string, XULButton>} */
|
||||
deleteButtons = new Map();
|
||||
/** @type {Map<string, XULButton>} */
|
||||
downloadButtons = new Map();
|
||||
|
||||
/**
|
||||
* @param {TranslationsState} state
|
||||
*/
|
||||
constructor(state) {
|
||||
this.state = state;
|
||||
this.elements = {
|
||||
installList: document.getElementById(
|
||||
"translations-manage-install-list"
|
||||
),
|
||||
installAll: document.getElementById(
|
||||
"translations-manage-install-all"
|
||||
),
|
||||
deleteAll: document.getElementById("translations-manage-delete-all"),
|
||||
error: document.getElementById("translations-manage-error"),
|
||||
};
|
||||
this.setup();
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.buildLanguageList();
|
||||
|
||||
this.elements.installAll.addEventListener(
|
||||
"command",
|
||||
this.handleInstallAll
|
||||
);
|
||||
this.elements.deleteAll.addEventListener(
|
||||
"command",
|
||||
this.handleDeleteAll
|
||||
);
|
||||
}
|
||||
|
||||
handleInstallAll = async () => {
|
||||
this.hideError();
|
||||
this.disableButtons(true);
|
||||
try {
|
||||
await this.state.translationsActor.downloadAllFiles();
|
||||
this.markAllDownloadPhases("downloaded");
|
||||
} catch (error) {
|
||||
TranslationsView.showError(
|
||||
"translations-manage-error-download",
|
||||
error
|
||||
);
|
||||
await this.reloadDownloadPhases();
|
||||
this.updateAllButtons();
|
||||
}
|
||||
this.disableButtons(false);
|
||||
};
|
||||
|
||||
handleDeleteAll = async () => {
|
||||
this.hideError();
|
||||
this.disableButtons(true);
|
||||
try {
|
||||
await this.state.translationsActor.deleteAllLanguageFiles();
|
||||
this.markAllDownloadPhases("uninstalled");
|
||||
} catch (error) {
|
||||
TranslationsView.showError("translations-manage-error-delete", error);
|
||||
// The download phases are invalidated with the error and must be reloaded.
|
||||
await this.reloadDownloadPhases();
|
||||
console.error(error);
|
||||
}
|
||||
this.disableButtons(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} langTag
|
||||
* @returns {Function}
|
||||
*/
|
||||
getDownloadButtonHandler(langTag) {
|
||||
return async () => {
|
||||
this.hideError();
|
||||
this.updateDownloadPhase(langTag, "loading");
|
||||
try {
|
||||
await this.state.translationsActor.downloadLanguageFiles(langTag);
|
||||
this.updateDownloadPhase(langTag, "downloaded");
|
||||
} catch (error) {
|
||||
TranslationsView.showError(
|
||||
"translations-manage-error-download",
|
||||
error
|
||||
);
|
||||
this.updateDownloadPhase(langTag, "uninstalled");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} langTag
|
||||
* @returns {Function}
|
||||
*/
|
||||
getDeleteButtonHandler(langTag) {
|
||||
return async () => {
|
||||
this.hideError();
|
||||
this.updateDownloadPhase(langTag, "loading");
|
||||
try {
|
||||
await this.state.translationsActor.deleteLanguageFiles(langTag);
|
||||
this.updateDownloadPhase(langTag, "uninstalled");
|
||||
} catch (error) {
|
||||
TranslationsView.showError(
|
||||
"translations-manage-error-delete",
|
||||
error
|
||||
);
|
||||
// The download phases are invalidated with the error and must be reloaded.
|
||||
await this.reloadDownloadPhases();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
buildLanguageList() {
|
||||
const listFragment = document.createDocumentFragment();
|
||||
|
||||
for (const { langTag, displayName } of this.state.languageList) {
|
||||
const hboxRow = document.createXULElement("hbox");
|
||||
hboxRow.classList.add("translations-manage-language");
|
||||
|
||||
const languageLabel = document.createXULElement("label");
|
||||
languageLabel.textContent = displayName; // The display name is already localized.
|
||||
|
||||
const downloadButton = document.createXULElement("button");
|
||||
const deleteButton = document.createXULElement("button");
|
||||
|
||||
downloadButton.addEventListener(
|
||||
"command",
|
||||
this.getDownloadButtonHandler(langTag)
|
||||
);
|
||||
deleteButton.addEventListener(
|
||||
"command",
|
||||
this.getDeleteButtonHandler(langTag)
|
||||
);
|
||||
|
||||
document.l10n.setAttributes(
|
||||
downloadButton,
|
||||
"translations-manage-download-button"
|
||||
);
|
||||
document.l10n.setAttributes(
|
||||
deleteButton,
|
||||
"translations-manage-delete-button"
|
||||
);
|
||||
|
||||
downloadButton.hidden = true;
|
||||
deleteButton.hidden = true;
|
||||
|
||||
this.deleteButtons.set(langTag, deleteButton);
|
||||
this.downloadButtons.set(langTag, downloadButton);
|
||||
|
||||
hboxRow.appendChild(languageLabel);
|
||||
hboxRow.appendChild(downloadButton);
|
||||
hboxRow.appendChild(deleteButton);
|
||||
listFragment.appendChild(hboxRow);
|
||||
}
|
||||
this.updateAllButtons();
|
||||
this.elements.installList.appendChild(listFragment);
|
||||
this.elements.installList.hidden = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the DownloadPhase for a single langTag.
|
||||
* @param {string} langTag
|
||||
* @param {DownloadPhase} downloadPhase
|
||||
*/
|
||||
updateDownloadPhase(langTag, downloadPhase) {
|
||||
this.state.downloadPhases.set(langTag, downloadPhase);
|
||||
this.updateButton(langTag, downloadPhase);
|
||||
this.updateHeaderButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreates the download map when the state is invalidated.
|
||||
*/
|
||||
async reloadDownloadPhases() {
|
||||
this.state.downloadPhases = await TranslationsState.createDownloadPhases(
|
||||
this.state.translationsActor,
|
||||
this.state.languageList
|
||||
);
|
||||
this.updateAllButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all the downloads.
|
||||
* @param {DownloadPhase} downloadPhase
|
||||
*/
|
||||
markAllDownloadPhases(downloadPhase) {
|
||||
const { downloadPhases } = this.state;
|
||||
for (const key of downloadPhases.keys()) {
|
||||
downloadPhases.set(key, downloadPhase);
|
||||
}
|
||||
this.updateAllButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* If all languages are downloaded, or no languages are downloaded then
|
||||
* the visibility of the buttons need to change.
|
||||
*/
|
||||
updateHeaderButtons() {
|
||||
let allDownloaded = true;
|
||||
let allUninstalled = true;
|
||||
for (const downloadPhase of this.state.downloadPhases.values()) {
|
||||
if (downloadPhase === "loading") {
|
||||
// Don't count loading towards this calculation.
|
||||
continue;
|
||||
}
|
||||
allDownloaded &&= downloadPhase === "downloaded";
|
||||
allUninstalled &&= downloadPhase === "uninstalled";
|
||||
}
|
||||
|
||||
this.elements.installAll.hidden = allDownloaded;
|
||||
this.elements.deleteAll.hidden = allUninstalled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the buttons according to their download state.
|
||||
*/
|
||||
updateAllButtons() {
|
||||
this.updateHeaderButtons();
|
||||
for (const [langTag, downloadPhase] of this.state.downloadPhases) {
|
||||
this.updateButton(langTag, downloadPhase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} langTag
|
||||
* @param {DownloadPhase} downloadPhase
|
||||
*/
|
||||
updateButton(langTag, downloadPhase) {
|
||||
const downloadButton = this.downloadButtons.get(langTag);
|
||||
const deleteButton = this.deleteButtons.get(langTag);
|
||||
switch (downloadPhase) {
|
||||
case "downloaded":
|
||||
downloadButton.hidden = true;
|
||||
deleteButton.hidden = false;
|
||||
downloadButton.removeAttribute("disabled");
|
||||
break;
|
||||
case "uninstalled":
|
||||
downloadButton.hidden = false;
|
||||
deleteButton.hidden = true;
|
||||
downloadButton.removeAttribute("disabled");
|
||||
break;
|
||||
case "loading":
|
||||
downloadButton.hidden = false;
|
||||
deleteButton.hidden = true;
|
||||
downloadButton.setAttribute("disabled", true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} isDisabled
|
||||
*/
|
||||
disableButtons(isDisabled) {
|
||||
this.elements.installAll.disabled = isDisabled;
|
||||
this.elements.deleteAll.disabled = isDisabled;
|
||||
for (const button of this.downloadButtons.values()) {
|
||||
button.disabled = isDisabled;
|
||||
}
|
||||
for (const button of this.deleteButtons.values()) {
|
||||
button.disabled = isDisabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is static in case an error happens during the creation of the
|
||||
* TranslationsState.
|
||||
*
|
||||
* @param {string} l10nId
|
||||
* @param {Error} error
|
||||
*/
|
||||
static showError(l10nId, error) {
|
||||
console.error(error);
|
||||
const errorMessage = document.getElementById(
|
||||
"translations-manage-error"
|
||||
);
|
||||
errorMessage.hidden = false;
|
||||
document.l10n.setAttributes(errorMessage, l10nId);
|
||||
}
|
||||
|
||||
hideError() {
|
||||
this.elements.error.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
TranslationsState.create().then(
|
||||
state => {
|
||||
new TranslationsView(state);
|
||||
},
|
||||
error => {
|
||||
// This error can happen when a user is not connected to the internet, or
|
||||
// RemoteSettings is down for some reason.
|
||||
TranslationsView.showError("translations-manage-error-list", error);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
initPrimaryBrowserLanguageUI() {
|
||||
// Enable telemetry.
|
||||
Services.telemetry.setEventRecordingEnabled(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
<link rel="localization" href="browser/preferences/fonts.ftl"/>
|
||||
<link rel="localization" href="browser/preferences/moreFromMozilla.ftl"/>
|
||||
<link rel="localization" href="browser/preferences/preferences.ftl"/>
|
||||
<link rel="localization" href="locales-preview/translations.ftl"/>
|
||||
<link rel="localization" href="toolkit/branding/accounts.ftl"/>
|
||||
<link rel="localization" href="toolkit/branding/brandings.ftl"/>
|
||||
<link rel="localization" href="toolkit/featuregates/features.ftl"/>
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ add_task(async function search_for_password_show_passwordGroup() {
|
|||
"browsingCategory",
|
||||
"networkProxyCategory",
|
||||
"dataMigrationGroup",
|
||||
"translationsGroup",
|
||||
];
|
||||
// Only visible for non-MSIX builds
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -28,3 +28,14 @@ translations-panel-restore-header = Change the language?
|
|||
# $toLanguage (string) - The target language of the translation.
|
||||
translations-panel-restore-label = The page is being translated from { $fromLanguage } to { $toLanguage }.
|
||||
translations-panel-restore-button = Restore the page
|
||||
|
||||
## Firefox Translations language management in about:preferences.
|
||||
|
||||
translations-manage-header = Translations
|
||||
translations-manage-description = Download languages for offline translation.
|
||||
translations-manage-all-language = All languages
|
||||
translations-manage-download-button = Download
|
||||
translations-manage-delete-button = Delete
|
||||
translations-manage-error-download = There was a problem downloading the language files. Please try again.
|
||||
translations-manage-error-delete = There was an error deleting the language files. Please try again.
|
||||
translations-manage-error-list = Failed to get the list of available languages for translation. Refresh the page to try again.
|
||||
|
|
|
|||
|
|
@ -1455,6 +1455,37 @@ richlistitem .text-link:hover {
|
|||
margin-top: 0.4em;
|
||||
}
|
||||
|
||||
#translations-manage-install-list {
|
||||
height: 220px;
|
||||
overflow: scroll;
|
||||
background-color: var(--in-content-box-background);
|
||||
border: 1px solid var(--in-content-box-border-color);
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.translations-manage-language:first-child {
|
||||
border-bottom: 1px solid var(--in-content-box-border-color);
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.translations-manage-language {
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.translations-manage-language label {
|
||||
flex: 1;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
#translations-manage-error {
|
||||
color: var(--in-content-error-text-color);
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* Platform-specific tweaks & overrides */
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
|
|
|
|||
|
|
@ -862,12 +862,15 @@ export class TranslationsParent extends JSWindowActorParent {
|
|||
const client = this.#getTranslationModelsRemoteClient();
|
||||
const isForDeletion = true;
|
||||
return Promise.all(
|
||||
Array.from(await this.getMatchedRecords(language, isForDeletion)).map(
|
||||
record => {
|
||||
lazy.console.log("Deleting record", record);
|
||||
return client.attachments.deleteDownloaded(record);
|
||||
}
|
||||
)
|
||||
Array.from(
|
||||
await this.getRecordsForTranslatingToAndFromAppLanguage(
|
||||
language,
|
||||
isForDeletion
|
||||
)
|
||||
).map(record => {
|
||||
lazy.console.log("Deleting record", record);
|
||||
return client.attachments.deleteDownloaded(record);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -881,7 +884,9 @@ export class TranslationsParent extends JSWindowActorParent {
|
|||
|
||||
const queue = [];
|
||||
|
||||
for (const record of await this.getMatchedRecords(language)) {
|
||||
for (const record of await this.getRecordsForTranslatingToAndFromAppLanguage(
|
||||
language
|
||||
)) {
|
||||
const download = () => {
|
||||
lazy.console.log("Downloading record", record.name, record.id);
|
||||
return client.attachments.download(record);
|
||||
|
|
@ -941,7 +946,7 @@ export class TranslationsParent extends JSWindowActorParent {
|
|||
*/
|
||||
async hasAllFilesForLanguage(requestedLanguage) {
|
||||
const client = this.#getTranslationModelsRemoteClient();
|
||||
for (const record of await this.getMatchedRecords(
|
||||
for (const record of await this.getRecordsForTranslatingToAndFromAppLanguage(
|
||||
requestedLanguage,
|
||||
true
|
||||
)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue