[tbb-commits] [Git][tpo/applications/tor-browser][tor-browser-115.9.0esr-13.5-1] 5 commits: fixup! Bug 3455: Add DomainIsolator, for isolating circuit by domain.

richard (@richard) git at gitlab.torproject.org
Mon Mar 25 21:53:44 UTC 2024



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/b7fc915f3ed100830bd8574a62f9cd653c1ec250...966b7f3067cc7b71b56178af98f848451f9a7955

-- 
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/b7fc915f3ed100830bd8574a62f9cd653c1ec250...966b7f3067cc7b71b56178af98f848451f9a7955
You're receiving this email because of your account on gitlab.torproject.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tbb-commits/attachments/20240325/3c60b8ae/attachment-0001.htm>


More information about the tbb-commits mailing list