зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1879933 - Add a button to change the translation direction in about:translations. r=translations-reviewers,fluent-reviewers,desktop-theme-reviewers,bolsson,nordzilla,dao
Add button. Disabled when the selected languages are equivalent. Differential Revision: https://phabricator.services.mozilla.com/D217045
This commit is contained in:
Родитель
8000b1bfad
Коммит
3de1b39518
|
@ -0,0 +1,8 @@
|
|||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity">
|
||||
<!-- This is a duplicate of browser/themes/shared/icons/import-export.svg for use in the about:translations page -->
|
||||
<path d="M4.197 3.623a.624.624 0 0 1 1.247 0l.005 6.709 2.051 0a.5.5 0 0 1 .353.852L5.037 14l-.423 0-2.816-2.816a.5.5 0 0 1 .353-.852l2.051 0-.005-6.709z"/>
|
||||
<path d="M11.812 12.377a.624.624 0 0 1-1.25 0l-.005-6.709-2.056 0a.5.5 0 0 1-.354-.852L10.97 2l.424 0 2.823 2.816a.499.499 0 0 1-.354.852l-2.056 0 .005 6.708z"/>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 809 B |
|
@ -34,21 +34,8 @@ body {
|
|||
}
|
||||
|
||||
.about-translations-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.about-translations-header > * {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.about-translations-header-start {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.about-translations-header-end {
|
||||
justify-content: end;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content 1fr;
|
||||
}
|
||||
|
||||
/* Increase the selector specificity to override the base `select` styles. */
|
||||
|
@ -57,10 +44,26 @@ select.about-translations-select {
|
|||
padding-inline: 10px 20px;
|
||||
padding-block: 0px;
|
||||
min-width: 50%;
|
||||
width: max-content;
|
||||
margin: 5px;
|
||||
background-position: right var(--AT-select-arrow-inset) center;
|
||||
}
|
||||
|
||||
select#language-to {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.language-swap-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url('chrome://global/content/translations/icons/swap-languages.svg');
|
||||
background-size: contain;
|
||||
margin: 0 auto;
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
transform: rotateY(180deg) rotateZ(90deg);
|
||||
}
|
||||
|
||||
select.about-translations-select:dir(rtl) {
|
||||
background-position-x: left var(--AT-select-arrow-inset);
|
||||
}
|
||||
|
|
|
@ -22,22 +22,21 @@
|
|||
<main class="about-translations-contents">
|
||||
|
||||
<header class="about-translations-header">
|
||||
<div class="about-translations-header-start">
|
||||
<select
|
||||
class="about-translations-select"
|
||||
id="language-from"
|
||||
disabled>
|
||||
<option data-l10n-id="about-translations-detect" value="detect"></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="about-translations-header-end">
|
||||
<select
|
||||
class="about-translations-select"
|
||||
id="language-to"
|
||||
disabled>
|
||||
<option data-l10n-id="about-translations-select" value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<select
|
||||
class="about-translations-select"
|
||||
id="language-from"
|
||||
disabled>
|
||||
<option data-l10n-id="about-translations-detect" value="detect"></option>
|
||||
</select>
|
||||
<button id="language-swap" data-l10n-id="about-translations-swap-languages" disabled>
|
||||
<div class="language-swap-icon"></div>
|
||||
</button>
|
||||
<select
|
||||
class="about-translations-select"
|
||||
id="language-to"
|
||||
disabled>
|
||||
<option data-l10n-id="about-translations-select" value=""></option>
|
||||
</select>
|
||||
</header>
|
||||
|
||||
<main class="about-translations-input">
|
||||
|
|
|
@ -351,6 +351,8 @@ class TranslationsUI {
|
|||
languageFrom = document.getElementById("language-from");
|
||||
/** @type {HTMLSelectElement} */
|
||||
languageTo = document.getElementById("language-to");
|
||||
/** @type {HTMLButtonElement} */
|
||||
languageSwap = document.getElementById("language-swap");
|
||||
/** @type {HTMLTextAreaElement} */
|
||||
translationFrom = document.getElementById("translation-from");
|
||||
/** @type {HTMLDivElement} */
|
||||
|
@ -396,6 +398,7 @@ class TranslationsUI {
|
|||
}
|
||||
this.setupDropdowns();
|
||||
this.setupTextarea();
|
||||
this.setupLanguageSwapButton();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -440,20 +443,75 @@ class TranslationsUI {
|
|||
|
||||
this.state.setFromLanguage(this.languageFrom.value);
|
||||
this.state.setToLanguage(this.languageTo.value);
|
||||
this.updateOnLanguageChange();
|
||||
|
||||
this.languageFrom.addEventListener("input", () => {
|
||||
await this.updateOnLanguageChange();
|
||||
|
||||
this.languageFrom.addEventListener("input", async () => {
|
||||
this.state.setFromLanguage(this.languageFrom.value);
|
||||
this.updateOnLanguageChange();
|
||||
await this.updateOnLanguageChange();
|
||||
});
|
||||
|
||||
this.languageTo.addEventListener("input", () => {
|
||||
this.languageTo.addEventListener("input", async () => {
|
||||
this.state.setToLanguage(this.languageTo.value);
|
||||
this.updateOnLanguageChange();
|
||||
await this.updateOnLanguageChange();
|
||||
this.translationTo.setAttribute("lang", this.languageTo.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the language swap button, so that when it's clicked, it:
|
||||
* - swaps the selected source adn target lanauges
|
||||
* - replaces the text to translate with the previous translation result
|
||||
*/
|
||||
setupLanguageSwapButton() {
|
||||
this.languageSwap.addEventListener("click", async () => {
|
||||
const translationToValue = this.translationTo.innerText;
|
||||
|
||||
const newFromLanguage = this.sanitizeTargetLangTagAsSourceLangTag(
|
||||
this.state.toLanguage
|
||||
);
|
||||
const newToLanguage = this.sanitizeSourceLangTagAsTargetLangTag(
|
||||
this.state.fromLanguage
|
||||
);
|
||||
this.state.setFromLanguage(newFromLanguage);
|
||||
this.state.setToLanguage(newToLanguage);
|
||||
|
||||
this.languageFrom.value = newFromLanguage;
|
||||
this.languageTo.value = newToLanguage;
|
||||
await this.updateOnLanguageChange();
|
||||
this.translationTo.setAttribute("lang", this.languageTo.value);
|
||||
|
||||
this.translationFrom.value = translationToValue;
|
||||
this.state.setMessageToTranslate(translationToValue);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target language dropdown option equivalent to the given source language dropdown option.
|
||||
* `detect` will be converted to `` as `detect` is not a valid option in the target language dropdown
|
||||
*
|
||||
* @param {string} sourceLangTag
|
||||
*/
|
||||
sanitizeSourceLangTagAsTargetLangTag(sourceLangTag) {
|
||||
if (sourceLangTag === "detect") {
|
||||
return "";
|
||||
}
|
||||
return sourceLangTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source language dropdown option equivalent to the given target language dropdown option.
|
||||
* `` will be converted to `detect` as `` is not a valid option in the source language dropdown
|
||||
*
|
||||
* @param {string} targetLangTag
|
||||
*/
|
||||
sanitizeTargetLangTagAsSourceLangTag(targetLangTag) {
|
||||
if (targetLangTag === "") {
|
||||
return "detect";
|
||||
}
|
||||
return targetLangTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an info message to the user.
|
||||
*
|
||||
|
@ -516,9 +574,10 @@ class TranslationsUI {
|
|||
/**
|
||||
* React to language changes.
|
||||
*/
|
||||
updateOnLanguageChange() {
|
||||
async updateOnLanguageChange() {
|
||||
this.#updateDropdownLanguages();
|
||||
this.#updateMessageDirections();
|
||||
await this.#updateLanguageSwapButton();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -592,10 +651,49 @@ class TranslationsUI {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the language swap button if fromLanguage is equivalent to toLanguage, or if the languages are not a valid option in the opposite direction
|
||||
*/
|
||||
async #updateLanguageSwapButton() {
|
||||
const sourceLanguage = this.state.fromLanguage;
|
||||
const targetLanguage = this.state.toLanguage;
|
||||
|
||||
if (
|
||||
sourceLanguage ===
|
||||
this.sanitizeTargetLangTagAsSourceLangTag(targetLanguage)
|
||||
) {
|
||||
this.languageSwap.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.translationFrom.value && !this.translationTo.innerText) {
|
||||
this.languageSwap.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const supportedLanguages = await this.state.supportedLanguages;
|
||||
|
||||
const isSourceLanguageValidAsTargetLanguage =
|
||||
sourceLanguage === "detect" ||
|
||||
supportedLanguages.languagePairs.some(
|
||||
({ toLang }) => toLang === sourceLanguage
|
||||
);
|
||||
const isTargetLanguageValidAsSourceLanguage =
|
||||
targetLanguage === "" ||
|
||||
supportedLanguages.languagePairs.some(
|
||||
({ fromLang }) => fromLang === targetLanguage
|
||||
);
|
||||
|
||||
this.languageSwap.disabled =
|
||||
!isSourceLanguageValidAsTargetLanguage ||
|
||||
!isTargetLanguageValidAsSourceLanguage;
|
||||
}
|
||||
|
||||
setupTextarea() {
|
||||
this.state.setMessageToTranslate(this.translationFrom.value);
|
||||
this.translationFrom.addEventListener("input", () => {
|
||||
this.state.setMessageToTranslate(this.translationFrom.value);
|
||||
this.translationFrom.addEventListener("input", async () => {
|
||||
await this.state.setMessageToTranslate(this.translationFrom.value);
|
||||
this.#updateLanguageSwapButton();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -603,6 +701,7 @@ class TranslationsUI {
|
|||
this.translationFrom.disabled = true;
|
||||
this.languageFrom.disabled = true;
|
||||
this.languageTo.disabled = true;
|
||||
this.languageSwap.disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -618,6 +717,7 @@ class TranslationsUI {
|
|||
this.translationTo.style.visibility = "hidden";
|
||||
this.translationToBlank.style.visibility = "visible";
|
||||
}
|
||||
this.#updateLanguageSwapButton();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ toolkit.jar:
|
|||
content/global/translations/translations.mjs (content/translations.mjs)
|
||||
content/global/translations/Translator.mjs (content/Translator.mjs)
|
||||
content/global/translations/TranslationsTelemetry.sys.mjs (TranslationsTelemetry.sys.mjs)
|
||||
content/global/translations/icons/swap-languages.svg (content/icons/swap-languages.svg)
|
||||
|
||||
# Uncomment this line to test a local build of Bergamot. It will automatically be loaded in.
|
||||
# content/global/translations/bergamot-translator-worker.wasm (bergamot-translator/thirdparty/build-wasm/bergamot-translator-worker.wasm)
|
||||
|
|
|
@ -26,6 +26,8 @@ skip-if = ["os == 'linux'"] # Bug 1821461
|
|||
|
||||
["browser_about_translations_enabling.js"]
|
||||
|
||||
["browser_about_translations_language_swap.js"]
|
||||
|
||||
["browser_about_translations_translations.js"]
|
||||
|
||||
["browser_translations_actor.js"]
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test the language swap behaviour.
|
||||
*/
|
||||
add_task(async function test_about_translations_language_swap() {
|
||||
const { runInPage, cleanup, resolveDownloads } = await openAboutTranslations({
|
||||
languagePairs: [
|
||||
{ fromLang: "en", toLang: "fr" },
|
||||
{ fromLang: "en", toLang: "it" },
|
||||
{ fromLang: "fr", toLang: "en" },
|
||||
],
|
||||
autoDownloadFromRemoteSettings: false,
|
||||
});
|
||||
|
||||
await runInPage(async ({ selectors }) => {
|
||||
const { document, window } = content;
|
||||
Cu.waiveXrays(window).DEBOUNCE_DELAY = 5; // Make the timer run faster for tests.
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => {
|
||||
return document.body.hasAttribute("ready");
|
||||
},
|
||||
"Waiting for the document to be ready.",
|
||||
100,
|
||||
200
|
||||
);
|
||||
|
||||
/** @type {HTMLSelectElement} */
|
||||
const fromSelect = document.querySelector(selectors.fromLanguageSelect);
|
||||
/** @type {HTMLSelectElement} */
|
||||
const toSelect = document.querySelector(selectors.toLanguageSelect);
|
||||
/** @type {HTMLTextAreaElement} */
|
||||
const translationFrom = document.querySelector(
|
||||
selectors.translationTextarea
|
||||
);
|
||||
/** @type {HTMLButtonElement} */
|
||||
const swapButton = document.querySelector(selectors.languageSwapButton);
|
||||
|
||||
// default option -> default option
|
||||
is(fromSelect.value, "detect");
|
||||
is(toSelect.value, "");
|
||||
is(swapButton.disabled, true, "The language swap button is disabled"); // disabled because from-language is equivalent to to-language
|
||||
|
||||
fromSelect.value = "en";
|
||||
fromSelect.dispatchEvent(new Event("input"));
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => fromSelect.value === "en",
|
||||
"en selected in fromSelect",
|
||||
100,
|
||||
200
|
||||
);
|
||||
|
||||
// en -> default option
|
||||
is(fromSelect.value, "en");
|
||||
is(toSelect.value, "");
|
||||
is(swapButton.disabled, false, "The language swap button is enabled");
|
||||
|
||||
translationFrom.value = "Translation text number 1.";
|
||||
translationFrom.dispatchEvent(new Event("input"));
|
||||
|
||||
// default option (detect: en) -> default option
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => swapButton.disabled === true,
|
||||
"The language swap button is disabled",
|
||||
100,
|
||||
200
|
||||
);
|
||||
is(swapButton.disabled, true, "The language swap button is disabled"); // disabled because swapping would wipe the input
|
||||
|
||||
translationFrom.value = "";
|
||||
translationFrom.dispatchEvent(new Event("input"));
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => swapButton.disabled === false,
|
||||
"The language swap button is enabled",
|
||||
100,
|
||||
200
|
||||
); // re-enabled after the input is cleared
|
||||
|
||||
swapButton.dispatchEvent(new Event("click"));
|
||||
|
||||
// default option -> en
|
||||
is(fromSelect.value, "detect");
|
||||
is(toSelect.value, "en");
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => swapButton.disabled === false,
|
||||
"The language swap button is enabled",
|
||||
100,
|
||||
200
|
||||
);
|
||||
is(swapButton.disabled, false, "The language swap button is enabled");
|
||||
|
||||
translationFrom.value = "Translation text number 1.";
|
||||
translationFrom.dispatchEvent(new Event("input"));
|
||||
|
||||
// default option (detect: en) -> en
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => swapButton.disabled === true,
|
||||
"The language swap button is disabled",
|
||||
100,
|
||||
200
|
||||
);
|
||||
is(swapButton.disabled, true, "The language swap button is disabled"); // disabled because from-language (detected as en) is equivalent to to-language
|
||||
|
||||
fromSelect.value = "fr";
|
||||
fromSelect.dispatchEvent(new Event("input"));
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => fromSelect.value === "fr",
|
||||
"fr selected in fromSelect",
|
||||
100,
|
||||
200
|
||||
);
|
||||
|
||||
translationFrom.value = "";
|
||||
translationFrom.dispatchEvent(new Event("input"));
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => swapButton.disabled === false,
|
||||
"The language swap button is enabled",
|
||||
100,
|
||||
200
|
||||
);
|
||||
is(swapButton.disabled, false, "The language swap button is enabled"); // both sides are empty
|
||||
});
|
||||
|
||||
await resolveDownloads(1); // fr -> en
|
||||
|
||||
await runInPage(async ({ selectors }) => {
|
||||
const { document, window } = content;
|
||||
Cu.waiveXrays(window).DEBOUNCE_DELAY = 5;
|
||||
|
||||
/** @type {HTMLSelectElement} */
|
||||
const fromSelect = document.querySelector(selectors.fromLanguageSelect);
|
||||
/** @type {HTMLSelectElement} */
|
||||
const toSelect = document.querySelector(selectors.toLanguageSelect);
|
||||
/** @type {HTMLTextAreaElement} */
|
||||
const translationFrom = document.querySelector(
|
||||
selectors.translationTextarea
|
||||
);
|
||||
/** @type {HTMLDivElement} */
|
||||
const translationTo = document.querySelector(selectors.translationResult);
|
||||
/** @type {HTMLButtonElement} */
|
||||
const swapButton = document.querySelector(selectors.languageSwapButton);
|
||||
|
||||
translationFrom.value = "Translation text number 1.";
|
||||
translationFrom.dispatchEvent(new Event("input"));
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => translationTo.innerText === "TRANSLATION TEXT NUMBER 1. [fr to en]",
|
||||
"translation from fr to en is complete",
|
||||
100,
|
||||
200
|
||||
);
|
||||
|
||||
// fr -> en
|
||||
is(fromSelect.value, "fr");
|
||||
is(toSelect.value, "en");
|
||||
is(translationFrom.value, "Translation text number 1.");
|
||||
is(translationTo.innerText, "TRANSLATION TEXT NUMBER 1. [fr to en]");
|
||||
is(swapButton.disabled, false, "The language swap button is enabled");
|
||||
|
||||
swapButton.dispatchEvent(new Event("click"));
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => swapButton.disabled === true,
|
||||
"The language swap button is disabled",
|
||||
100,
|
||||
200
|
||||
);
|
||||
is(swapButton.disabled, true, "The language swap button is disabled"); // after the swap, the input is not empty while the output is
|
||||
});
|
||||
|
||||
await resolveDownloads(1); // en -> fr
|
||||
|
||||
await runInPage(async ({ selectors }) => {
|
||||
const { document, window } = content;
|
||||
Cu.waiveXrays(window).DEBOUNCE_DELAY = 5;
|
||||
|
||||
/** @type {HTMLSelectElement} */
|
||||
const fromSelect = document.querySelector(selectors.fromLanguageSelect);
|
||||
/** @type {HTMLSelectElement} */
|
||||
const toSelect = document.querySelector(selectors.toLanguageSelect);
|
||||
/** @type {HTMLTextAreaElement} */
|
||||
const translationFrom = document.querySelector(
|
||||
selectors.translationTextarea
|
||||
);
|
||||
/** @type {HTMLDivElement} */
|
||||
const translationTo = document.querySelector(selectors.translationResult);
|
||||
/** @type {HTMLButtonElement} */
|
||||
const swapButton = document.querySelector(selectors.languageSwapButton);
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() =>
|
||||
translationTo.innerText ===
|
||||
"TRANSLATION TEXT NUMBER 1. [FR TO EN] [en to fr]",
|
||||
"translation from en to fr is complete",
|
||||
100,
|
||||
200
|
||||
);
|
||||
|
||||
// en -> fr
|
||||
is(fromSelect.value, "en");
|
||||
is(toSelect.value, "fr");
|
||||
is(translationFrom.value, "TRANSLATION TEXT NUMBER 1. [fr to en]");
|
||||
is(
|
||||
translationTo.innerText,
|
||||
"TRANSLATION TEXT NUMBER 1. [FR TO EN] [en to fr]"
|
||||
); // translating the original fr-to-en translation back to fr
|
||||
is(swapButton.disabled, false, "The language swap button is enabled");
|
||||
|
||||
toSelect.value = "it";
|
||||
toSelect.dispatchEvent(new Event("input"));
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => toSelect.value === "it",
|
||||
"it selected in toSelect",
|
||||
100,
|
||||
200
|
||||
);
|
||||
|
||||
is(swapButton.disabled, true, "The language swap button is disabled"); // after the toLanguage change, the input is not empty while the output is
|
||||
});
|
||||
|
||||
await resolveDownloads(1); // en -> it
|
||||
|
||||
await runInPage(async ({ selectors }) => {
|
||||
const { document, window } = content;
|
||||
Cu.waiveXrays(window).DEBOUNCE_DELAY = 5;
|
||||
|
||||
/** @type {HTMLSelectElement} */
|
||||
const fromSelect = document.querySelector(selectors.fromLanguageSelect);
|
||||
/** @type {HTMLSelectElement} */
|
||||
const toSelect = document.querySelector(selectors.toLanguageSelect);
|
||||
/** @type {HTMLTextAreaElement} */
|
||||
const translationFrom = document.querySelector(
|
||||
selectors.translationTextarea
|
||||
);
|
||||
/** @type {HTMLDivElement} */
|
||||
const translationTo = document.querySelector(selectors.translationResult);
|
||||
/** @type {HTMLButtonElement} */
|
||||
const swapButton = document.querySelector(selectors.languageSwapButton);
|
||||
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() =>
|
||||
translationTo.innerText ===
|
||||
"TRANSLATION TEXT NUMBER 1. [FR TO EN] [en to it]",
|
||||
"translation from en to it is complete",
|
||||
100,
|
||||
200
|
||||
);
|
||||
|
||||
// en -> it
|
||||
is(fromSelect.value, "en");
|
||||
is(toSelect.value, "it");
|
||||
is(translationFrom.value, "TRANSLATION TEXT NUMBER 1. [fr to en]");
|
||||
is(
|
||||
translationTo.innerText,
|
||||
"TRANSLATION TEXT NUMBER 1. [FR TO EN] [en to it]"
|
||||
);
|
||||
is(swapButton.disabled, true, "The language swap button is disabled"); // disabled because to-language (it) is not a valid from-language
|
||||
});
|
||||
|
||||
await cleanup();
|
||||
});
|
|
@ -118,6 +118,7 @@ async function openAboutTranslations({
|
|||
pageHeader: '[data-l10n-id="about-translations-header"]',
|
||||
fromLanguageSelect: "select#language-from",
|
||||
toLanguageSelect: "select#language-to",
|
||||
languageSwapButton: "button#language-swap",
|
||||
translationTextarea: "textarea#translation-from",
|
||||
translationResult: "#translation-to",
|
||||
translationResultBlank: "#translation-to-blank",
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
# The title of the about:translations page, referencing the translations feature.
|
||||
about-translations-title = Translations
|
||||
about-translations-header = { -translations-brand-name }
|
||||
# The title attribute for the swap languages button, that swaps the
|
||||
# source and target languages, reversing the translation direction.
|
||||
about-translations-swap-languages =
|
||||
.title = Swap languages
|
||||
about-translations-results-placeholder = Translation
|
||||
about-translations-translating-message = Translating…
|
||||
# Text displayed on from-language dropdown when no language is selected
|
||||
|
|
Загрузка…
Ссылка в новой задаче