Skip to content

Commit

Permalink
Bug 1818603 - Add "Detect language" to dropdown in about:translations…
Browse files Browse the repository at this point in the history
… UI r=gregtatum

Adds a "Detect language" option which detects the language to translate.
Language detection is enabled when the option is selected and is disabled
when a different language in the dropdown is selected explicitly.

Differential Revision: https://phabricator.services.mozilla.com/D170953
  • Loading branch information
nordzilla committed Mar 7, 2023
1 parent 74c7b3b commit 40849d3
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 57 deletions.
8 changes: 7 additions & 1 deletion browser/locales-preview/translations.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
about-translations-title = Translations
about-translations-header = { -translations-brand-name }
about-translations-results-placeholder = Translation
# Text displayed on language dropdowns when no language is selected yet
# Text displayed on from-language dropdown when no language is selected
about-translations-detect = Detect language
# Text displayed on from-language dropdown when a language is detected
# Variables:
# $language (string) - The localized display name of the detected language
about-translations-detect-lang = Detect language ({ $language })
# Text displayed on to-language dropdown when no language is selected
about-translations-select = Select language
about-translations-textarea =
.placeholder = Add text to translate
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ class LanguageIdWorker {
addEventListener("message", this.onMessage.bind(this));
}

/**
* Formats the language label returned by the language-identification model
* to conform to the correct two-character language tags.
*
* The current model returns labels of the format "__label_xx" where the last
* two characters are the two-character language tag.
*
* As such, this function strips of those final two characters.
* Updating the language-identification model may require updating this function.
*
* @param {string} label
* @returns {string}
*/
#formatLanguageLabel(label) {
return label.slice(-2);
}

/**
* Handle any message after the initialization message.
*
Expand Down Expand Up @@ -149,7 +166,7 @@ class LanguageIdWorker {

postMessage({
type: "language-id-response",
languageLabel,
languageLabel: this.#formatLanguageLabel(languageLabel),
confidence,
messageId,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ function handleMessages(engine) {
if (data.type === "initialize") {
throw new Error("The Translations engine must not be re-initialized.");
}
log("Received message", data);

switch (data.type) {
case "translation-request": {
Expand Down
2 changes: 1 addition & 1 deletion toolkit/components/translations/content/translations.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ <h1 data-l10n-id="about-translations-header"></h1>
class="about-translations-select"
id="language-from"
disabled>
<option data-l10n-id="about-translations-select" value=""></option>
<option data-l10n-id="about-translations-detect" value="detect"></option>
</select>
</div>
<div class="about-translations-header-end">
Expand Down
127 changes: 102 additions & 25 deletions toolkit/components/translations/content/translations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
// allow for the page to get access to additional privileged features.

/* global AT_getSupportedLanguages, AT_log, AT_getScriptDirection,
AT_getAppLocale, AT_logError, AT_destroyTranslationsEngine,
AT_createTranslationsEngine, AT_createLanguageIdEngine,
AT_translate, AT_identifyLanguage */
AT_logError, AT_destroyTranslationsEngine, AT_createTranslationsEngine,
AT_createLanguageIdEngine, AT_translate, AT_identifyLanguage */

// Allow tests to override this value so that they can run faster.
// This is the delay in milliseconds.
Expand Down Expand Up @@ -67,13 +66,17 @@ class TranslationsState {
translationsEngine = null;

constructor() {
AT_createLanguageIdEngine();
this.supportedLanguages = AT_getSupportedLanguages();
this.ui = new TranslationsUI(this);
this.ui.setup();
}

/**
* Identifies the human language in which the message is written and logs the result.
* Identifies the human language in which the message is written and returns
* the two-letter language label of the language it is determined to be.
*
* e.g. "en" for English.
*
* @param {string} message
*/
Expand All @@ -82,9 +85,10 @@ class TranslationsState {
const { languageLabel, confidence } = await AT_identifyLanguage(message);
const duration = performance.now() - start;
AT_log(
`[ ${languageLabel.slice(-2)}(${(confidence * 100).toFixed(2)}%) ]`,
`[ ${languageLabel}(${(confidence * 100).toFixed(2)}%) ]`,
`Source language identified in ${duration / 1000} seconds`
);
return languageLabel;
}

/**
Expand Down Expand Up @@ -173,9 +177,18 @@ class TranslationsState {
// If we may need to re-building the worker, the old translation is no longer valid.
this.ui.updateTranslation("");

if (!this.fromLanguage || !this.toLanguage) {
// A from or to language could have been removed. Don't do any more translations
// with it.
// These are cases in which it wouldn't make sense or be possible to load any translations models.
if (
// If fromLanguage or toLanguage are unpopulated we cannot load anything.
!this.fromLanguage ||
!this.toLanguage ||
// If fromLanguage's value is "detect", rather than a two-letter language tag, then no language
// has been detected yet.
this.fromLanguage === "detect" ||
// If fromLanguage and toLanguage are the same, this means that the detected language
// is the same as the toLanguage, and we do not want to translate from one language to itself.
this.fromLanguage === this.toLanguage
) {
if (this.translationsEngine) {
// The engine is no longer needed.
AT_destroyTranslationsEngine();
Expand Down Expand Up @@ -205,13 +218,47 @@ class TranslationsState {
}
}

/**
* Updates the fromLanguage to match the detected language only if the
* about-translations-detect option is selected in the language-from dropdown.
*
* If the new fromLanguage is different than the previous fromLanguage this
* may update the UI to display the new language and may rebuild the translations
* worker if there is a valid selected target language.
*/
async maybeUpdateDetectedLanguage() {
if (!this.ui.detectOptionIsSelected() || this.messageToTranslate === "") {
// If we are not detecting languages or if the message has been cleared
// we should ensure that the UI is not displaying a detected language
// and there is no need to run any language detection.
this.ui.setDetectOptionTextContent("");
return;
}

const [languageLabel, supportedLanguages] = await Promise.all([
this.identifyLanguage(this.messageToTranslate),
this.supportedLanguages,
]);

// Only update the language if the detected language matches
// one of our supported languages.
const entry = supportedLanguages.find(
({ langTag }) => langTag === languageLabel
);
if (entry) {
const { displayName } = entry;
await this.setFromLanguage(languageLabel);
this.ui.setDetectOptionTextContent(displayName);
}
}

/**
* @param {string} lang
*/
setFromLanguage(lang) {
async setFromLanguage(lang) {
if (lang !== this.fromLanguage) {
this.fromLanguage = lang;
this.maybeRebuildWorker();
await this.maybeRebuildWorker();
}
}

Expand All @@ -228,10 +275,10 @@ class TranslationsState {
/**
* @param {string} message
*/
setMessageToTranslate(message) {
async setMessageToTranslate(message) {
if (message !== this.messageToTranslate) {
this.identifyLanguage(message).catch(error => AT_logError(error));
this.messageToTranslate = message;
await this.maybeUpdateDetectedLanguage();
this.maybeRequestTranslation();
}
}
Expand All @@ -254,12 +301,21 @@ class TranslationsUI {
/** @type {TranslationsState} */
state;

/**
* The detect-language option element. We want to maintain a handle to this so that
* we can dynamically update its display text to include the detected language.
*
* @type {HTMLOptionElement}
*/
#detectOption;

/**
* @param {TranslationsState} state
*/
constructor(state) {
this.state = state;
this.translationTo.style.visibility = "visible";
this.#detectOption = document.querySelector('option[value="detect"]');
}

/**
Expand All @@ -268,10 +324,6 @@ class TranslationsUI {
setup() {
this.setupDropdowns();
this.setupTextarea();
const start = performance.now();
AT_createLanguageIdEngine();
const duration = performance.now() - start;
AT_log(`Created LanguageIdEngine in ${duration / 1000} seconds`);
}

/**
Expand All @@ -293,15 +345,6 @@ class TranslationsUI {
this.languageFrom.add(option);
}

// Set the translate "from" to the app locale, if it is in the list.
const appLocale = new Intl.Locale(AT_getAppLocale());
for (const option of this.languageFrom.options) {
if (option.value === appLocale.language) {
this.languageFrom.value = option.value;
break;
}
}

// Enable the controls.
this.languageFrom.disabled = false;
this.languageTo.disabled = false;
Expand Down Expand Up @@ -329,6 +372,39 @@ class TranslationsUI {
});
}

/**
* Returns true if about-translations-detect is the currently
* selected option in the language-from dropdown, otherwise false.
*
* @returns {boolean}
*/
detectOptionIsSelected() {
return this.languageFrom.value === "detect";
}

/**
* Sets the textContent of the about-translations-detect option in the
* language-from dropdown to include the detected language's display name.
*
* @param {string} displayName
*/
setDetectOptionTextContent(displayName) {
if (displayName) {
// Set the text to the fluent value that takes an arg to display the language name.
document.l10n.setAttributes(
this.#detectOption,
"about-translations-detect-lang",
{ language: displayName }
);
} else {
// Reset the text to the fluent value that does not display any language name.
document.l10n.setAttributes(
this.#detectOption,
"about-translations-detect"
);
}
}

/**
* React to language changes.
*/
Expand Down Expand Up @@ -364,6 +440,7 @@ class TranslationsUI {
option.hidden = true;
}
}
this.state.maybeUpdateDetectedLanguage();
}

/**
Expand Down
Loading

0 comments on commit 40849d3

Please sign in to comment.