richard pushed to branch tor-browser-115.9.0esr-13.5-1 at The Tor Project / Applications / Tor Browser
Commits: 99d4a1de by Henry Wilkes at 2024-03-25T09:42:29+00:00 fixup! Bug 3455: Add DomainIsolator, for isolating circuit by domain.
Bug 42209: Migrate tor circuit strings to Fluent.
- - - - - 56ce4cc1 by Henry Wilkes at 2024-03-25T09:42:30+00:00 fixup! Bug 41600: Add a tor circuit display panel.
Bug 42209: Migrate tor circuit strings to Fluent.
- - - - - 76f869d5 by Henry Wilkes at 2024-03-25T09:42:31+00:00 fixup! Tor Browser strings
Bug 42209: Migrate tor circuit strings to Fluent.
- - - - - 724dfc5f by Henry Wilkes at 2024-03-25T09:42:31+00:00 fixup! Add TorStrings module for localization
Bug 42209: Migrate tor circuit strings to Fluent.
- - - - - 966b7f30 by Henry Wilkes at 2024-03-25T09:42:32+00:00 fixup! Tor Browser localization migration scripts.
Bug 42209: Migrate tor circuit strings to Fluent.
- - - - -
11 changed files:
- browser/base/content/appmenu-viewcache.inc.xhtml - browser/base/content/browser-menubar.inc - browser/base/content/navigator-toolbox.inc.xhtml - browser/components/torcircuit/content/torCircuitPanel.css - browser/components/torcircuit/content/torCircuitPanel.inc.xhtml - browser/components/torcircuit/content/torCircuitPanel.js - browser/locales/en-US/browser/tor-browser.ftl - toolkit/torbutton/chrome/locale/en-US/torbutton.dtd - toolkit/torbutton/chrome/locale/en-US/torbutton.properties - tools/torbrowser/l10n/migrate.py - + tools/torbrowser/l10n/migrations/bug-42209-tor-circuit.py
Changes:
===================================== browser/base/content/appmenu-viewcache.inc.xhtml ===================================== @@ -62,7 +62,7 @@ <toolbarbutton id="appMenuNewCircuit" class="subviewbutton" key="new-circuit-key" - label="&torbutton.context_menu.new_circuit_sentence_case;" + data-l10n-id="appmenuitem-new-tor-circuit" oncommand="TorDomainIsolator.newCircuitForBrowser(gBrowser);"/> <toolbarseparator/> <toolbarbutton id="appMenu-bookmarks-button"
===================================== browser/base/content/browser-menubar.inc ===================================== @@ -32,9 +32,7 @@ <menuitem id="menu_newIdentity" key="new-identity-key" data-l10n-id="menu-new-identity"/> <menuitem id="menu_newCircuit" - accesskey="&torbutton.context_menu.new_circuit_key;" - key="new-circuit-key" - label="&torbutton.context_menu.new_circuit;" + key="new-circuit-key" data-l10n-id="menu-new-tor-circuit" oncommand="TorDomainIsolator.newCircuitForBrowser(gBrowser);"/> <menuseparator/> <menuitem id="menu_openLocation"
===================================== browser/base/content/navigator-toolbox.inc.xhtml ===================================== @@ -198,7 +198,7 @@ role="button" class="identity-box-button" align="center" - tooltiptext="&torbutton.circuit_display.title;" + data-l10n-id="tor-circuit-urlbar-button" hidden="true"> <image id="tor-circuit-button-icon"/> </box> @@ -621,9 +621,8 @@ data-l10n-id="toolbar-new-identity"/>
<toolbarbutton id="new-circuit-button" class="toolbarbutton-1 chromeclass-toolbar-additional" - label="&torbutton.context_menu.new_circuit;" - oncommand="TorDomainIsolator.newCircuitForBrowser(gBrowser);" - tooltiptext="&torbutton.context_menu.new_circuit;"/> + data-l10n-id="toolbar-new-tor-circuit" + oncommand="TorDomainIsolator.newCircuitForBrowser(gBrowser);"/>
<toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional" observes="View:FullScreen"
===================================== browser/components/torcircuit/content/torCircuitPanel.css ===================================== @@ -120,6 +120,15 @@ background-repeat: no-repeat; }
+.tor-circuit-node-item:not([hidden]) { + display: flex; + align-items: baseline; +} + +.tor-circuit-node-item > * { + flex: 0 0 auto; +} + @media (prefers-color-scheme: dark) { .tor-circuit-node-item { /* Light Gray 90 */ @@ -146,9 +155,9 @@ .tor-circuit-region-flag { margin-inline-end: 0.5em; height: 16px; - vertical-align: sub; + align-self: center; /* Don't occupy any vertical height. */ - margin-block-start: -16px; + margin-block: -8px; }
.tor-circuit-region-flag.no-region-flag-src { @@ -158,7 +167,7 @@ .tor-circuit-addresses { font-size: smaller; font-family: monospace; - margin-inline-start: 0.25em; + margin-inline-start: 0.75em; }
/* Footer buttons */
===================================== browser/components/torcircuit/content/torCircuitPanel.inc.xhtml ===================================== @@ -19,44 +19,67 @@ <vbox class="panel-header"> <html:h1 id="tor-circuit-heading"></html:h1> <html:div id="tor-circuit-alias" hidden="hidden"> - <html:img src="chrome://browser/content/tor-circuit-redirect.svg" - alt="" /> + <html:img + src="chrome://browser/content/tor-circuit-redirect.svg" + alt="" + /> <html:p id="tor-circuit-alias-label"> - <html:a /> + <html:a class="tor-circuit-alias-link" data-l10n-name="alias-link" /> </html:p> </html:div> </vbox> <toolbarseparator/> <vbox id="tor-circuit-panel-body" class="panel-subview-body"> - <html:p id="tor-circuit-node-list-name">&torbutton.circuit_display.title;</html:p> + <html:p + id="tor-circuit-node-list-name" + data-l10n-id="tor-circuit-panel-node-list-introduction" + ></html:p> <html:ol id="tor-circuit-node-list"> - <html:li id="tor-circuit-start-item" - class="tor-circuit-node-item"> - </html:li> - <html:li id="tor-circuit-relays-item" - class="tor-circuit-node-item tor-circuit-relays-item"> - </html:li> - <html:li id="tor-circuit-end-item" - class="tor-circuit-node-item"> - </html:li> + <html:li + id="tor-circuit-start-item" + class="tor-circuit-node-item" + data-l10n-id="tor-circuit-panel-node-browser" + ></html:li> + <html:li + id="tor-circuit-relays-item" + class="tor-circuit-node-item tor-circuit-relays-item" + data-l10n-id="tor-circuit-panel-node-onion-relays" + ></html:li> + <html:li + id="tor-circuit-end-item" + class="tor-circuit-node-item" + ></html:li> </html:ol> + <html:template id="tor-circuit-node-item-template"> + <html:li class="tor-circuit-node-item"> + <html:img class="tor-circuit-region-flag" alt="" /> + <html:span class="tor-circuit-node-name"></html:span> + <html:span class="tor-circuit-addresses"></html:span> + </html:li> + </html:template> </vbox> <toolbarseparator/> <!-- NOTE: To fully benefit from the .subviewbutton styling, we need to use - a xul:toolbarbutton rather than a html:button. - By default, a xul:toolbarbutton is not focusable so we need to add - tabindex. --> - <toolbarbutton id="tor-circuit-new-circuit" - class="subviewbutton panel-subview-footer-button tor-circuit-button" - tabindex="0" - aria-labelledby="tor-circuit-new-circuit-label" - aria-describedby="tor-circuit-new-circuit-description"> + <toolbarbutton + id="tor-circuit-new-circuit" + class="subviewbutton panel-subview-footer-button tor-circuit-button" + tabindex="0" + aria-labelledby="tor-circuit-new-circuit-label" + aria-describedby="tor-circuit-new-circuit-description" + > <vbox align="start"> - <label id="tor-circuit-new-circuit-label" - class="toolbarbutton-text" - value="&torbutton.context_menu.new_circuit_sentence_case;"/> - <label id="tor-circuit-new-circuit-description" - class="tor-circuit-button-description"/> + <label + id="tor-circuit-new-circuit-label" + class="toolbarbutton-text" + data-l10n-id="tor-circuit-panel-new-button" + /> + <label + id="tor-circuit-new-circuit-description" + class="tor-circuit-button-description" + /> </vbox> </toolbarbutton> </vbox>
===================================== browser/components/torcircuit/content/torCircuitPanel.js ===================================== @@ -34,6 +34,12 @@ var gTorCircuitPanel = { * @type {bool} */ _isActive: false, + /** + * The template element for circuit nodes. + * + * @type {HTMLTemplateElement?} + */ + _nodeItemTemplate: null,
/** * The topic on which circuit changes are broadcast. @@ -62,7 +68,6 @@ var gTorCircuitPanel = { heading: document.getElementById("tor-circuit-heading"), alias: document.getElementById("tor-circuit-alias"), aliasLabel: document.getElementById("tor-circuit-alias-label"), - aliasLink: document.querySelector("#tor-circuit-alias-label a"), aliasMenu: document.getElementById("tor-circuit-panel-alias-menu"), list: document.getElementById("tor-circuit-node-list"), relaysItem: document.getElementById("tor-circuit-relays-item"), @@ -73,30 +78,24 @@ var gTorCircuitPanel = { }; this.toolbarButton = document.getElementById("tor-circuit-button");
- // TODO: These strings should be set in the HTML markup with fluent. - - // NOTE: There is already whitespace before and after the link from the - // XHTML markup. - const [aliasBefore, aliasAfter] = this._getString( - "torbutton.circuit_display.connected-to-alias", - // Placeholder is replaced with the same placeholder. This is a bit of a - // hack since we want the inserted address to be the rich anchor - // element already in the DOM, rather than a plain address. - // We won't have to do this with fluent by using data-l10n-name on the - // anchor element. - ["%S"] - ).split("%S"); - this._panelElements.aliasLabel.prepend(aliasBefore); - this._panelElements.aliasLabel.append(aliasAfter); - - this._panelElements.aliasLink.addEventListener("click", event => { + // We add listeners for the .tor-circuit-alias-link. + // NOTE: We have to add the listeners to the parent element because the + // link (with data-l10n-name="alias-link") will be replaced with a new + // cloned instance every time the parent gets re-translated. + this._panelElements.aliasLabel.addEventListener("click", event => { + if (!this._aliasLink.contains(event.target)) { + return; + } event.preventDefault(); if (event.button !== 0) { return; } this._openAlias("tab"); }); - this._panelElements.aliasLink.addEventListener("contextmenu", event => { + this._panelElements.aliasLabel.addEventListener("contextmenu", event => { + if (!this._aliasLink.contains(event.target)) { + return; + } event.preventDefault(); this._panelElements.aliasMenu.openPopupAtScreen( event.screenX, @@ -119,21 +118,15 @@ var gTorCircuitPanel = { document .getElementById("tor-circuit-panel-alias-menu-copy") .addEventListener("command", () => { - if (!this._panelElements.aliasLink.href) { + const alias = this._aliasLink?.href; + if (!alias) { return; } Cc["@mozilla.org/widget/clipboardhelper;1"] .getService(Ci.nsIClipboardHelper) - .copyString(this._panelElements.aliasLink.href); + .copyString(alias); });
- document.getElementById("tor-circuit-start-item").textContent = - this._getString("torbutton.circuit_display.this_browser"); - - this._panelElements.relaysItem.textContent = this._getString( - "torbutton.circuit_display.onion-site-relays" - ); - // Button is a xul:toolbarbutton, so we use "command" rather than "click". document .getElementById("tor-circuit-new-circuit") @@ -176,6 +169,13 @@ var gTorCircuitPanel = { this.show(); });
+ this._nodeItemTemplate = document.getElementById( + "tor-circuit-node-item-template" + ); + // Prepare the unknown region name for the current locale. + // NOTE: We expect this to complete before the first call to _updateBody. + this._localeChanged(); + this._locationListener = { onLocationChange: (webProgress, request, locationURI, flags) => { if ( @@ -194,6 +194,7 @@ var gTorCircuitPanel = {
// Get notifications for circuit changes. Services.obs.addObserver(this, this.TOR_CIRCUIT_TOPIC); + Services.obs.addObserver(this, "intl:app-locales-changed"); },
/** @@ -203,15 +204,21 @@ var gTorCircuitPanel = { this._isActive = false; gBrowser.removeProgressListener(this._locationListener); Services.obs.removeObserver(this, this.TOR_CIRCUIT_TOPIC); + Services.obs.removeObserver(this, "intl:app-locales-changed"); },
/** * Observe circuit changes. */ observe(subject, topic, data) { - if (topic === this.TOR_CIRCUIT_TOPIC) { - // TODO: Maybe check if we actually need to do something earlier. - this._updateCurrentBrowser(); + switch (topic) { + case this.TOR_CIRCUIT_TOPIC: + // TODO: Maybe check if we actually need to do something earlier. + this._updateCurrentBrowser(); + break; + case "intl:app-locales-changed": + this._localeChanged(); + break; } },
@@ -231,6 +238,19 @@ var gTorCircuitPanel = { this.panel.hidePopup(); },
+ /** + * Get the current alias link instance. + * + * Note that this element instance may change whenever its parent element + * (#tor-circuit-alias-label) is re-translated. Attributes should be copied to + * the new instance. + */ + get _aliasLink() { + return this._panelElements.aliasLabel.querySelector( + ".tor-circuit-alias-link" + ); + }, + /** * Open the onion alias present in the alias link. * @@ -238,12 +258,13 @@ var gTorCircuitPanel = { * window. */ _openAlias(where) { - if (!this._panelElements.aliasLink.href) { + const url = this._aliasLink?.href; + if (!url) { return; } // We hide the panel before opening the link. this.hide(); - window.openWebLinkIn(this._panelElements.aliasLink.href, where); + window.openWebLinkIn(url, where); },
/** @@ -351,11 +372,6 @@ var gTorCircuitPanel = {
this.toolbarButton.hidden = false;
- if (this.panel.state !== "open" && this.panel.state !== "showing") { - // Don't update the panel content if it is not open or about to open. - return; - } - this._updateCircuitPanel(); },
@@ -383,35 +399,15 @@ var gTorCircuitPanel = { return alias; },
- /** - * Get a string from the properties bundle. - * - * @param {string} name - The string name. - * @param {string[]} args - The arguments to pass to the string. - * - * @returns {string} The string. - */ - _getString(name, args = []) { - if (!this._stringBundle) { - this._stringBundle = Services.strings.createBundle( - "chrome://torbutton/locale/torbutton.properties" - ); - } - try { - return this._stringBundle.formatStringFromName(name, args); - } catch {} - if (!this._fallbackStringBundle) { - this._fallbackStringBundle = Services.strings.createBundle( - "resource://torbutton/locale/en-US/torbutton.properties" - ); - } - return this._fallbackStringBundle.formatStringFromName(name, args); - }, - /** * Updates the circuit display in the panel to show the current browser data. */ _updateCircuitPanel() { + if (this.panel.state !== "open" && this.panel.state !== "showing") { + // Don't update the panel content if it is not open or about to open. + return; + } + // NOTE: The _currentBrowserData.nodes data may be stale. In particular, the // circuit may have expired already, or we're still waiting on the new // circuit. @@ -426,6 +422,9 @@ var gTorCircuitPanel = { this.hide(); return; } + + this._log.debug("Updating circuit panel"); + let domain = this._currentBrowserData.domain; const onionAlias = this._getOnionAlias(domain);
@@ -447,24 +446,31 @@ var gTorCircuitPanel = { * @param {string?} scheme - The scheme in use for the current domain. */ _updateHeading(domain, onionAlias, scheme) { - this._panelElements.heading.textContent = this._getString( - "torbutton.circuit_display.heading", + document.l10n.setAttributes( + this._panelElements.heading, + "tor-circuit-panel-heading", // Only shorten the onion domain if it has no alias. - [TorUIUtils.shortenOnionAddress(domain)] + { host: TorUIUtils.shortenOnionAddress(domain) } );
if (onionAlias) { - this._panelElements.aliasLink.textContent = - TorUIUtils.shortenOnionAddress(onionAlias); if (scheme === "http" || scheme === "https") { // We assume the same scheme as the current page for the alias, which we // expect to be either http or https. // NOTE: The href property is partially presentational so that the link // location appears on hover. - this._panelElements.aliasLink.href = `${scheme}://${onionAlias}`; + // NOTE: The href attribute should be copied to any new instances of + // .tor-circuit-alias-link (with data-l10n-name="alias-link") when the + // parent _panelElements.aliasLabel gets re-translated. + this._aliasLink.href = `${scheme}://${onionAlias}`; } else { - this._panelElements.aliasLink.removeAttribute("href"); + this._aliasLink.removeAttribute("href"); } + document.l10n.setAttributes( + this._panelElements.aliasLabel, + "tor-circuit-panel-alias", + { alias: TorUIUtils.shortenOnionAddress(onionAlias) } + ); this._showPanelElement(this._panelElements.alias, true); } else { this._showPanelElement(this._panelElements.alias, false); @@ -485,20 +491,40 @@ var gTorCircuitPanel = { * @param {string} domain - The domain to show for the last node. */ _updateBody(nodes, domain) { - // Clean up old items. - // NOTE: We do not expect focus within a removed node. - for (const nodeItem of this._nodeItems) { - nodeItem.remove(); - } + // NOTE: Rather than re-creating the <li> nodes from scratch, we prefer + // updating existing <li> nodes so that the display does not "flicker" in + // width as we wait for Fluent DOM to fill the nodes with text content. I.e. + // the existing node and text will remain in place, occupying the same + // width, up until it is replaced by Fluent DOM. + for (let index = 0; index < nodes.length; index++) { + if (index >= this._nodeItems.length) { + const newItem = + this._nodeItemTemplate.content.children[0].cloneNode(true); + const flagEl = newItem.querySelector(".tor-circuit-region-flag"); + // Hide region flag whenever the flag src does not exist. + flagEl.addEventListener("error", () => { + flagEl.classList.add("no-region-flag-src"); + flagEl.removeAttribute("src"); + }); + this._panelElements.list.insertBefore( + newItem, + this._panelElements.relaysItem + );
- this._nodeItems = nodes.map((nodeData, index) => { - const nodeItem = this._createCircuitNodeItem(nodeData, index === 0); - this._panelElements.list.insertBefore( - nodeItem, - this._panelElements.relaysItem + this._nodeItems.push(newItem); + } + this._updateCircuitNodeItem( + this._nodeItems[index], + nodes[index], + index === 0 ); - return nodeItem; - }); + } + + // Remove excess items. + // NOTE: We do not expect focus within a removed node. + while (nodes.length < this._nodeItems.length) { + this._nodeItems.pop().remove(); + }
this._showPanelElement( this._panelElements.relaysItem, @@ -511,40 +537,49 @@ var gTorCircuitPanel = {
// Button description text, depending on whether our first node was a // bridge, or otherwise a guard. - this._panelElements.newCircuitDescription.value = this._getString( + document.l10n.setAttributes( + this._panelElements.newCircuitDescription, nodes[0].bridgeType === null - ? "torbutton.circuit_display.new-circuit-guard-description" - : "torbutton.circuit_display.new-circuit-bridge-description" + ? "tor-circuit-panel-new-button-description-guard" + : "tor-circuit-panel-new-button-description-bridge" ); },
/** - * Create a node item for the given circuit node data. + * Update a node item for the given circuit node data. * + * @param {Element} nodeItem - The item to update. * @param {NodeData} node - The circuit node data to create an item for. * @param {bool} isCircuitStart - Whether this is the first node in the * circuit. */ - _createCircuitNodeItem(node, isCircuitStart) { - let nodeName; - // We do not show a flag for bridge nodes. - let regionCode = null; + _updateCircuitNodeItem(nodeItem, node, isCircuitStart) { + const nameEl = nodeItem.querySelector(".tor-circuit-node-name"); + let flagSrc = null; + if (node.bridgeType === null) { - regionCode = node.regionCode; - if (!regionCode) { - nodeName = this._getString("torbutton.circuit_display.unknown_region"); - } else { - nodeName = Services.intl.getRegionDisplayNames(undefined, [ - regionCode, - ])[0]; - } + const regionCode = node.regionCode; + flagSrc = this._regionFlagSrc(regionCode); + + const regionName = regionCode + ? Services.intl.getRegionDisplayNames(undefined, [regionCode])[0] + : this._unknownRegionName; + if (isCircuitStart) { - nodeName = this._getString( - "torbutton.circuit_display.region-guard-node", - [nodeName] + document.l10n.setAttributes( + nameEl, + "tor-circuit-panel-node-region-guard", + { region: regionName } ); + } else { + // Set the text content directly, rather than using Fluent. + nameEl.removeAttribute("data-l10n-id"); + nameEl.removeAttribute("data-l10n-args"); + nameEl.textContent = regionName; } } else { + // Do not show a flag for bridges. + let bridgeType = node.bridgeType; if (bridgeType === "meek_lite") { bridgeType = "meek"; @@ -552,55 +587,72 @@ var gTorCircuitPanel = { bridgeType = ""; } if (bridgeType) { - nodeName = this._getString( - "torbutton.circuit_display.tor_typed_bridge", - [bridgeType] + document.l10n.setAttributes( + nameEl, + "tor-circuit-panel-node-typed-bridge", + { "bridge-type": bridgeType } ); } else { - nodeName = this._getString("torbutton.circuit_display.tor_bridge"); + document.l10n.setAttributes(nameEl, "tor-circuit-panel-node-bridge"); } } - const nodeItem = document.createElement("li"); - nodeItem.classList.add("tor-circuit-node-item"); - - const regionFlagEl = this._regionFlag(regionCode); - if (regionFlagEl) { - nodeItem.append(regionFlagEl); + const flagEl = nodeItem.querySelector(".tor-circuit-region-flag"); + flagEl.classList.toggle("no-region-flag-src", !flagSrc); + if (flagSrc) { + flagEl.setAttribute("src", flagSrc); + } else { + flagEl.removeAttribute("src"); }
- // Add whitespace after name for the addresses. - nodeItem.append(nodeName + " "); - - if (node.ipAddrs) { - const addressesEl = document.createElement("span"); - addressesEl.classList.add("tor-circuit-addresses"); - let firstAddr = true; - for (const ip of node.ipAddrs) { - if (firstAddr) { - firstAddr = false; - } else { - addressesEl.append(", "); - } - // We use a <code> element to give screen readers a hint that - // punctuation is different for IP addresses. - const ipEl = document.createElement("code"); - // TODO: Current HTML-aam 1.0 specs map the <code> element to the "code" - // role. - // However, mozilla-central commented out this mapping in - // accessible/base/HTMLMarkupMap.h because the HTML-aam specs at the - // time did not do this. - // See hg.mozilla.org/mozilla-central/rev/51eebe7d6199#l2.12 - // For now we explicitly add the role="code", but once this is fixed - // from mozilla-central we should remove this. - ipEl.setAttribute("role", "code"); - ipEl.classList.add("tor-circuit-ip-address"); - ipEl.textContent = ip; - addressesEl.append(ipEl); + const addressesEl = nodeItem.querySelector(".tor-circuit-addresses"); + // Empty children. + addressesEl.replaceChildren(); + let firstAddr = true; + for (const ip of node.ipAddrs) { + if (firstAddr) { + firstAddr = false; + } else { + addressesEl.append(", "); } - nodeItem.append(addressesEl); + const ipEl = document.createElement("code"); + // TODO: Current HTML-aam 1.0 specs map the <code> element to the "code" + // role. + // However, mozilla-central commented out this mapping in + // accessible/base/HTMLMarkupMap.h because the HTML-aam specs at the + // time did not do this. + // See hg.mozilla.org/mozilla-central/rev/51eebe7d6199#l2.12 + // + // This was updated in mozilla bug 1834931, for ESR 128 + // + // For now we explicitly add the role="code", but once this is fixed + // from mozilla-central we should remove this. + ipEl.setAttribute("role", "code"); + ipEl.classList.add("tor-circuit-ip-address"); + ipEl.textContent = ip; + addressesEl.append(ipEl); } + }, + + /** + * The string to use for unknown region names. + * + * Will be updated to match the current locale. + * + * @type {string} + */ + _unknownRegionName: "Unknown region",
- return nodeItem; + /** + * Update the name for regions to match the current locale. + */ + _localeChanged() { + document.l10n + .formatValue("tor-circuit-panel-node-unknown-region") + .then(name => { + this._unknownRegionName = name; + // Update the panel for the new region names, if it is shown. + this._updateCircuitPanel(); + }); },
/** @@ -609,9 +661,9 @@ var gTorCircuitPanel = { * @param {string?} regionCode - The code to convert. It should be an upper * case 2-letter BCP47 Region subtag to be converted into a flag. * - * @returns {HTMLImgElement?} The emoji flag img, or null if there is no flag. + * @returns {src?} The emoji flag img src, or null if there is no flag. */ - _regionFlag(regionCode) { + _regionFlagSrc(regionCode) { if (!regionCode?.match(/^[A-Z]{2}$/)) { return null; } @@ -624,20 +676,7 @@ var gTorCircuitPanel = { .map(cp => cp.toString(16)) .join("-");
- const flagEl = document.createElement("img"); - // Decorative. - flagEl.alt = ""; - flagEl.classList.add("tor-circuit-region-flag"); - // Remove self if there is no matching flag found. - flagEl.addEventListener( - "error", - () => { - flagEl.classList.add("no-region-flag-src"); - }, - { once: true } - ); - flagEl.src = `chrome://browser/content/tor-circuit-flags/${flagName}.svg`; - return flagEl; + return `chrome://browser/content/tor-circuit-flags/${flagName}.svg`; },
/**
===================================== browser/locales/en-US/browser/tor-browser.ftl ===================================== @@ -326,3 +326,63 @@ about-dialog-browser-license-link = Licensing Information # "Tor" and "The Onion Logo" are trademark names, so should not be translated (not including the quote marks, which can be localized). # "The Tor Project, Inc." is an organisation name. about-dialog-trademark-statement = “Tor” and “The Onion Logo” are registered trademarks of The Tor Project, Inc. + +## New tor circuit. + +# Shown in the File menu. +# Uses title case for English (US). +menu-new-tor-circuit = + .label = New Tor Circuit for this Site + .accesskey = C + +# Shown in the application menu (hamburger menu). +# Uses sentence case for English (US). +appmenuitem-new-tor-circuit = + .label = New Tor circuit for this site + +# Toolbar button to trigger a new circuit, available through toolbar customization. +# Uses sentence case for English (US). +# ".label" is the accessible name, and is visible in the overflow menu and when +# customizing the toolbar. +# ".tooltiptext" will be identical to the label. +toolbar-new-tor-circuit = + .label = New Tor circuit for this site + .tooltiptext = { toolbar-new-tor-circuit.label } + +## Tor circuit URL bar button. + +# The tooltip also acts as the accessible name. +tor-circuit-urlbar-button = + .tooltiptext = Tor Circuit + +## Tor circuit panel. + +# $host (String) - The host name shown in the URL bar, potentially shortened. +tor-circuit-panel-heading = Circuit for { $host } +# Shown when the current address is a ".tor.onion" alias. +# $alias (String) - The alias onion address. This should be wrapped in '<a data-l10n-name="alias-link">' and '</a>', which will link to the corresponding address. +tor-circuit-panel-alias = Connected to <a data-l10n-name="alias-link">{ $alias }</a> + +# Text just before the list of circuit nodes. +tor-circuit-panel-node-list-introduction = Tor Circuit +# First node in the list of circuit nodes. Refers to Tor Browser. +tor-circuit-panel-node-browser = This browser +# Represents a number of unknown relays that complete a connection to an ".onion" site. +tor-circuit-panel-node-onion-relays = Onion site relays +# Represents the bridge node used to connect to the Tor network. +# $bridge-type (String) - The name for the type of bridge used: meek, obfs4, snowflake, etc. +tor-circuit-panel-node-typed-bridge = Bridge: { $bridge-type } +# Represents the bridge node used to connect to the Tor network when the bridge type is unknown. +tor-circuit-panel-node-bridge = Bridge +# Represents the initial guard node used for a tor circuit. +# $region (String) - The region name for the guard node, already localized. +tor-circuit-panel-node-region-guard = { $region } (guard) +# Represents a circuit node with an unknown regional location. +tor-circuit-panel-node-unknown-region = Unknown region + +# Uses sentence case for English (US). +tor-circuit-panel-new-button = New Tor circuit for this site +# Shown when the first node in the circuit is a guard node, rather than a bridge. +tor-circuit-panel-new-button-description-guard = Your guard node may not change +# Shown when the first node in the circuit is a bridge node. +tor-circuit-panel-new-button-description-bridge = Your bridge may not change
===================================== toolkit/torbutton/chrome/locale/en-US/torbutton.dtd ===================================== @@ -3,12 +3,6 @@ - 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/. -->
-<!ENTITY torbutton.context_menu.new_circuit "New Tor Circuit for this Site"> -<!ENTITY torbutton.context_menu.new_circuit_sentence_case "New Tor circuit for this site"> -<!ENTITY torbutton.context_menu.new_circuit_key "C"> - -<!ENTITY torbutton.circuit_display.title "Tor Circuit"> - <!-- Onion services strings. Strings are kept here for ease of translation. --> <!ENTITY torbutton.onionServices.authPrompt.tooltip "Open onion service client authentication prompt"> <!ENTITY torbutton.onionServices.authPrompt.persistCheckboxLabel "Remember this key">
===================================== toolkit/torbutton/chrome/locale/en-US/torbutton.properties ===================================== @@ -3,25 +3,6 @@ # 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/.
-# Circuit display -# LOCALIZATION NOTE: %S will be the host name shown in the URL bar. -torbutton.circuit_display.heading = Circuit for %S -# LOCALIZATION NOTE: %S will be the alias onion address. -torbutton.circuit_display.connected-to-alias = Connected to %S -torbutton.circuit_display.this_browser = This browser -torbutton.circuit_display.onion-site-relays = Onion site relays -# LOCALIZATION NOTE: %S will be the bridge type name (meek, obfs4, snowflake, -# etc). -torbutton.circuit_display.tor_typed_bridge = Bridge: %S -# LOCALIZATION NOTE: Used when the bridge type is unknown. -torbutton.circuit_display.tor_bridge = Bridge -# LOCALIZATION NOTE: Used when a circuit node's regional location is unknown. -torbutton.circuit_display.unknown_region = Unknown region -# LOCALIZATION NOTE: %S will be the localized region name for the guard node. -torbutton.circuit_display.region-guard-node = %S (guard) -torbutton.circuit_display.new-circuit-guard-description = Your guard node may not change -torbutton.circuit_display.new-circuit-bridge-description = Your bridge may not change - # Download pane warning torbutton.download.warning.title = Be careful opening downloads # %S will be a link to the Tails operating system website. With the content given by torbutton.download.warning.tails_brand_name
===================================== tools/torbrowser/l10n/migrate.py ===================================== @@ -253,16 +253,13 @@ class TorBrowserMigrationContext(MigrationContext): if path not in self.localization_resources )
- def tb_get_transformed(self, target_path, transform_id): + def tb_get_transform(self, target_path, transform_id): """ Find the transformation node with the given id for the given path. - - The node will be evaluated (converted to regular fluent.ast) before it - is returned. """ for node in self.transforms[target_path]: if node.id.name == transform_id: - return self.evaluate(node) + return node return None
def tb_get_reference_entry(self, target_path, entry_id): @@ -330,6 +327,21 @@ class TorBrowserMigrator:
ctx = self._get_migration_context(locale, locale_dir)
+ # NOTE: We do not use the existing ctx.serialize_changeset method. + # The problem with this approach was that it would re-shuffle the order + # of already existing strings to match the en-US locale. + # But Weblate currently does not preserve the order of translated + # strings: https://github.com/WeblateOrg/weblate/issues/11134 + # so this created extra noise in the diff. + # Instead, we just always append transformations to the end of the + # existing file. + # Moreover, it would inject group comments into the translated files, + # which Weblate does not handle well. Instead, we just do not add any + # comments. + # + # In case we want to use it again in the future, here is a reference + # to how it works: + # # ctx.serialize_changeset expects a set of (path, identifier) of # localization resources that can be used to evaluate the # transformations. @@ -344,76 +356,115 @@ class TorBrowserMigrator: # one step, so we want to fill the changeset with all required # (path, identifier) pairs found in the localization resources.
- # Choose the transforms that are required and available. - changeset = set() available_strings = ctx.tb_get_available_strings() - for (target_path, transform_id), dep_set in ctx.dependencies.items(): - # ctx.dependencies is a dict of dependencies for all - # transformations - # { (target_path, transform_identifier): set( - # (localization_path, string_identifier), - # )} - # - # e.g. if we want to create a new fluent Message called - # "new-string1", and it uses "oldString1" from "old-file1.dtd" - # and "oldString2" from "old-file2.dtd". And "new-string2" using - # "oldString3" from "old-file2.dtd", it would be - # { - # ("new-file.ftl", "new-string1"): set( - # ("old-file1.dtd", "oldString1"), - # ("old-file2.dtd", "oldString2"), - # ), - # ("new-file.ftl", "new-string2"): set( - # ("old-file2.dtd", "oldString3"), - # ), - # } - can_transform = True - for dep in dep_set: - path, string_id = dep - if dep not in available_strings: - can_transform = False - self.logger.info( - f"Skipping transform {target_path}:{transform_id} for " - f"'{locale}' locale because it is missing the " - f"string {path}:{string_id}." - ) - break - # Strings in legacy formats might have an entry in the file - # that is just a copy of the en-US strings. - # For these we want to check the weblate metadata to ensure - # it is a translated string. - if not path.endswith( - ".ftl" - ) and not self.weblate_metadata.is_translated( - os.path.join("en-US", path), - os.path.join(locale, path), - string_id, - ): - can_transform = False + wrote_file = False + errors = [] + + for target_path, reference in ctx.reference_resources.items(): + translated_ids = [ + entry.id.name + for entry in ctx.target_resources[target_path].body + if isinstance(entry, (ast.Message, ast.Term)) + # NOTE: We're assuming that the Message and Term ids do not + # conflict with each other. + ] + new_entries = [] + + # Apply transfomations in the order they appear in the reference + # (en-US) file. + for entry in reference.body: + if not isinstance(entry, (ast.Message, ast.Term)): + continue + transform_id = entry.id.name + transform = ctx.tb_get_transform(target_path, transform_id) + if not transform: + # No transformation for this reference entry. + continue + + if transform_id in translated_ids: self.logger.info( - f"Skipping transform {target_path}:{transform_id} for " - f"'{locale}' locale because the string " - f"{path}:{string_id} has not been translated on " - "weblate." + f"Skipping transform {target_path}:{transform_id} " + f"for '{locale}' locale because it already has a " + f"translation." ) - break - if can_transform: - changeset.update(dep_set) + continue
- print("", file=sys.stderr) - wrote_file = False - errors = [] - for path, fluent in ctx.serialize_changeset(changeset).items(): - full_path = os.path.join(locale_dir, path) + # ctx.dependencies is a dict of dependencies for all + # transformations + # { (target_path, transform_identifier): set( + # (localization_path, string_identifier), + # )} + # + # e.g. if we want to create a new fluent Message called + # "new-string1", and it uses "oldString1" from "old-file1.dtd" + # and "oldString2" from "old-file2.dtd". And "new-string2" using + # "oldString3" from "old-file2.dtd", it would be + # { + # ("new-file.ftl", "new-string1"): set( + # ("old-file1.dtd", "oldString1"), + # ("old-file2.dtd", "oldString2"), + # ), + # ("new-file.ftl", "new-string2"): set( + # ("old-file2.dtd", "oldString3"), + # ), + # } + dep_set = ctx.dependencies[(target_path, transform_id)] + can_transform = True + for dep in dep_set: + path, string_id = dep + if dep not in available_strings: + can_transform = False + self.logger.info( + f"Skipping transform {target_path}:{transform_id} " + f"for '{locale}' locale because it is missing the " + f"string {path}:{string_id}." + ) + break + # Strings in legacy formats might have an entry in the file + # that is just a copy of the en-US strings. + # For these we want to check the weblate metadata to ensure + # it is a translated string. + if not path.endswith( + ".ftl" + ) and not self.weblate_metadata.is_translated( + os.path.join("en-US", path), + os.path.join(locale, path), + string_id, + ): + can_transform = False + self.logger.info( + f"Skipping transform {target_path}:{transform_id} " + f"for '{locale}' locale because the string " + f"{path}:{string_id} has not been translated on " + "weblate." + ) + break + if not can_transform: + continue + + # Run the transformation. + new_entries.append(ctx.evaluate(transform)) + + if not new_entries: + continue + + full_path = os.path.join(locale_dir, target_path) + print("", file=sys.stderr) self.logger.info(f"Writing to {full_path}") - with open(full_path, "w") as file: - file.write(fluent) - wrote_file = True
+ # For Fluent we can just serialize the transformations and append + # them to the end of the existing file. + resource = ast.Resource(new_entries) + with open(full_path, "a") as file: + file.write(serialize(resource)) + + with open(full_path, "r") as file: + full_content = file.read() + wrote_file = True # Collect any fluent parsing errors from the newly written file. errors.extend( (full_path, message, line, sample) - for message, line, sample in self._fluent_errors(fluent) + for message, line, sample in self._fluent_errors(full_content) )
if not wrote_file: @@ -547,7 +598,7 @@ class TorBrowserMigrator: have_error = True continue
- transformed = ctx.tb_get_transformed(target_path, transform_id) + transformed = ctx.evaluate(ctx.tb_get_transform(target_path, transform_id)) reference_entry = ctx.tb_get_reference_entry(target_path, transform_id) if reference_entry is None: self.logger.error(
===================================== tools/torbrowser/l10n/migrations/bug-42209-tor-circuit.py ===================================== @@ -0,0 +1,83 @@ +import fluent.syntax.ast as FTL +from fluent.migrate.helpers import VARIABLE_REFERENCE, transforms_from +from fluent.migrate.transforms import CONCAT, REPLACE + + +def migrate(ctx): + legacy_dtd = "torbutton.dtd" + legacy_properties = "torbutton.properties" + ctx.add_transforms( + "tor-browser.ftl", + "tor-browser.ftl", + transforms_from( + """ +menu-new-tor-circuit = + .label = { COPY(dtd_path, "torbutton.context_menu.new_circuit") } + .accesskey = { COPY(dtd_path, "torbutton.context_menu.new_circuit_key") } +appmenuitem-new-tor-circuit = + .label = { COPY(dtd_path, "torbutton.context_menu.new_circuit_sentence_case") } +toolbar-new-tor-circuit = + .label = { COPY(dtd_path, "torbutton.context_menu.new_circuit_sentence_case") } + .tooltiptext = { toolbar-new-tor-circuit.label } + +tor-circuit-urlbar-button = + .tooltiptext = { COPY(dtd_path, "torbutton.circuit_display.title") } + +tor-circuit-panel-node-list-introduction = { COPY(dtd_path, "torbutton.circuit_display.title") } +tor-circuit-panel-node-browser = { COPY(path, "torbutton.circuit_display.this_browser") } +tor-circuit-panel-node-onion-relays = { COPY(path, "torbutton.circuit_display.onion-site-relays") } +tor-circuit-panel-node-bridge = { COPY(path, "torbutton.circuit_display.tor_bridge") } +tor-circuit-panel-node-unknown-region = { COPY(path, "torbutton.circuit_display.unknown_region") } + +tor-circuit-panel-new-button = { COPY(dtd_path, "torbutton.context_menu.new_circuit_sentence_case") } +tor-circuit-panel-new-button-description-guard = { COPY(path, "torbutton.circuit_display.new-circuit-guard-description") } +tor-circuit-panel-new-button-description-bridge = { COPY(path, "torbutton.circuit_display.new-circuit-bridge-description") } +""", + dtd_path=legacy_dtd, + path=legacy_properties, + ) + + [ + # Replace "%S" with "{ $host }" + FTL.Message( + id=FTL.Identifier("tor-circuit-panel-heading"), + value=REPLACE( + legacy_properties, + "torbutton.circuit_display.heading", + {"%1$S": VARIABLE_REFERENCE("host")}, + ), + ), + # Replace "%S" with "<a data-l10n-name="alias-link">{ $alias }</a>" + FTL.Message( + id=FTL.Identifier("tor-circuit-panel-alias"), + value=REPLACE( + legacy_properties, + "torbutton.circuit_display.connected-to-alias", + { + "%1$S": CONCAT( + FTL.TextElement('<a data-l10n-name="alias-link">'), + VARIABLE_REFERENCE("alias"), + FTL.TextElement("</a>"), + ) + }, + ), + ), + # Replace "%S" with "{ $region }" + FTL.Message( + id=FTL.Identifier("tor-circuit-panel-node-region-guard"), + value=REPLACE( + legacy_properties, + "torbutton.circuit_display.region-guard-node", + {"%1$S": VARIABLE_REFERENCE("region")}, + ), + ), + # Replace "%S" with "{ $bridge-type }" + FTL.Message( + id=FTL.Identifier("tor-circuit-panel-node-typed-bridge"), + value=REPLACE( + legacy_properties, + "torbutton.circuit_display.tor_typed_bridge", + {"%1$S": VARIABLE_REFERENCE("bridge-type")}, + ), + ), + ], + )
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/b7fc915...