richard pushed to branch tor-browser-115.9.0esr-13.5-1 at The Tor Project / Applications / Tor Browser

Commits:

11 changed files:

Changes:

  • browser/base/content/appmenu-viewcache.inc.xhtml
    ... ... @@ -62,7 +62,7 @@
    62 62
           <toolbarbutton id="appMenuNewCircuit"
    
    63 63
                          class="subviewbutton"
    
    64 64
                          key="new-circuit-key"
    
    65
    -                     label="&torbutton.context_menu.new_circuit_sentence_case;"
    
    65
    +                     data-l10n-id="appmenuitem-new-tor-circuit"
    
    66 66
                          oncommand="TorDomainIsolator.newCircuitForBrowser(gBrowser);"/>
    
    67 67
           <toolbarseparator/>
    
    68 68
           <toolbarbutton id="appMenu-bookmarks-button"
    

  • browser/base/content/browser-menubar.inc
    ... ... @@ -32,9 +32,7 @@
    32 32
                     <menuitem id="menu_newIdentity"
    
    33 33
                               key="new-identity-key" data-l10n-id="menu-new-identity"/>
    
    34 34
                     <menuitem id="menu_newCircuit"
    
    35
    -                          accesskey="&torbutton.context_menu.new_circuit_key;"
    
    36
    -                          key="new-circuit-key"
    
    37
    -                          label="&torbutton.context_menu.new_circuit;"
    
    35
    +                          key="new-circuit-key" data-l10n-id="menu-new-tor-circuit"
    
    38 36
                               oncommand="TorDomainIsolator.newCircuitForBrowser(gBrowser);"/>
    
    39 37
                     <menuseparator/>
    
    40 38
                     <menuitem id="menu_openLocation"
    

  • browser/base/content/navigator-toolbox.inc.xhtml
    ... ... @@ -198,7 +198,7 @@
    198 198
                      role="button"
    
    199 199
                      class="identity-box-button"
    
    200 200
                      align="center"
    
    201
    -                 tooltiptext="&torbutton.circuit_display.title;"
    
    201
    +                 data-l10n-id="tor-circuit-urlbar-button"
    
    202 202
                      hidden="true">
    
    203 203
                   <image id="tor-circuit-button-icon"/>
    
    204 204
                 </box>
    
    ... ... @@ -621,9 +621,8 @@
    621 621
                        data-l10n-id="toolbar-new-identity"/>
    
    622 622
     
    
    623 623
         <toolbarbutton id="new-circuit-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
    
    624
    -                   label="&torbutton.context_menu.new_circuit;"
    
    625
    -                   oncommand="TorDomainIsolator.newCircuitForBrowser(gBrowser);"
    
    626
    -                   tooltiptext="&torbutton.context_menu.new_circuit;"/>
    
    624
    +                   data-l10n-id="toolbar-new-tor-circuit"
    
    625
    +                   oncommand="TorDomainIsolator.newCircuitForBrowser(gBrowser);"/>
    
    627 626
     
    
    628 627
         <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
    
    629 628
                        observes="View:FullScreen"
    

  • browser/components/torcircuit/content/torCircuitPanel.css
    ... ... @@ -120,6 +120,15 @@
    120 120
       background-repeat: no-repeat;
    
    121 121
     }
    
    122 122
     
    
    123
    +.tor-circuit-node-item:not([hidden]) {
    
    124
    +  display: flex;
    
    125
    +  align-items: baseline;
    
    126
    +}
    
    127
    +
    
    128
    +.tor-circuit-node-item > * {
    
    129
    +  flex: 0 0 auto;
    
    130
    +}
    
    131
    +
    
    123 132
     @media (prefers-color-scheme: dark) {
    
    124 133
       .tor-circuit-node-item {
    
    125 134
         /* Light Gray 90 */
    
    ... ... @@ -146,9 +155,9 @@
    146 155
     .tor-circuit-region-flag {
    
    147 156
       margin-inline-end: 0.5em;
    
    148 157
       height: 16px;
    
    149
    -  vertical-align: sub;
    
    158
    +  align-self: center;
    
    150 159
       /* Don't occupy any vertical height. */
    
    151
    -  margin-block-start: -16px;
    
    160
    +  margin-block: -8px;
    
    152 161
     }
    
    153 162
     
    
    154 163
     .tor-circuit-region-flag.no-region-flag-src {
    
    ... ... @@ -158,7 +167,7 @@
    158 167
     .tor-circuit-addresses {
    
    159 168
       font-size: smaller;
    
    160 169
       font-family: monospace;
    
    161
    -  margin-inline-start: 0.25em;
    
    170
    +  margin-inline-start: 0.75em;
    
    162 171
     }
    
    163 172
     
    
    164 173
     /* Footer buttons */
    

  • browser/components/torcircuit/content/torCircuitPanel.inc.xhtml
    ... ... @@ -19,44 +19,67 @@
    19 19
         <vbox class="panel-header">
    
    20 20
           <html:h1 id="tor-circuit-heading"></html:h1>
    
    21 21
           <html:div id="tor-circuit-alias" hidden="hidden">
    
    22
    -        <html:img src="chrome://browser/content/tor-circuit-redirect.svg"
    
    23
    -                  alt="" />
    
    22
    +        <html:img
    
    23
    +          src="chrome://browser/content/tor-circuit-redirect.svg"
    
    24
    +          alt=""
    
    25
    +        />
    
    24 26
             <html:p id="tor-circuit-alias-label">
    
    25
    -          <html:a />
    
    27
    +          <html:a class="tor-circuit-alias-link" data-l10n-name="alias-link" />
    
    26 28
             </html:p>
    
    27 29
           </html:div>
    
    28 30
         </vbox>
    
    29 31
         <toolbarseparator/>
    
    30 32
         <vbox id="tor-circuit-panel-body" class="panel-subview-body">
    
    31
    -      <html:p id="tor-circuit-node-list-name">&torbutton.circuit_display.title;</html:p>
    
    33
    +      <html:p
    
    34
    +        id="tor-circuit-node-list-name"
    
    35
    +        data-l10n-id="tor-circuit-panel-node-list-introduction"
    
    36
    +      ></html:p>
    
    32 37
           <html:ol id="tor-circuit-node-list">
    
    33
    -        <html:li id="tor-circuit-start-item"
    
    34
    -                 class="tor-circuit-node-item">
    
    35
    -        </html:li>
    
    36
    -        <html:li id="tor-circuit-relays-item"
    
    37
    -                 class="tor-circuit-node-item tor-circuit-relays-item">
    
    38
    -        </html:li>
    
    39
    -        <html:li id="tor-circuit-end-item"
    
    40
    -                 class="tor-circuit-node-item">
    
    41
    -        </html:li>
    
    38
    +        <html:li
    
    39
    +          id="tor-circuit-start-item"
    
    40
    +          class="tor-circuit-node-item"
    
    41
    +          data-l10n-id="tor-circuit-panel-node-browser"
    
    42
    +        ></html:li>
    
    43
    +        <html:li
    
    44
    +          id="tor-circuit-relays-item"
    
    45
    +          class="tor-circuit-node-item tor-circuit-relays-item"
    
    46
    +          data-l10n-id="tor-circuit-panel-node-onion-relays"
    
    47
    +        ></html:li>
    
    48
    +        <html:li
    
    49
    +          id="tor-circuit-end-item"
    
    50
    +          class="tor-circuit-node-item"
    
    51
    +        ></html:li>
    
    42 52
           </html:ol>
    
    53
    +      <html:template id="tor-circuit-node-item-template">
    
    54
    +        <html:li class="tor-circuit-node-item">
    
    55
    +          <html:img class="tor-circuit-region-flag" alt="" />
    
    56
    +          <html:span class="tor-circuit-node-name"></html:span>
    
    57
    +          <html:span class="tor-circuit-addresses"></html:span>
    
    58
    +        </html:li>
    
    59
    +      </html:template>
    
    43 60
         </vbox>
    
    44 61
         <toolbarseparator/>
    
    45 62
         <!-- NOTE: To fully benefit from the .subviewbutton styling, we need to use
    
    46 63
            - a xul:toolbarbutton rather than a html:button.
    
    47 64
            - By default, a xul:toolbarbutton is not focusable so we need to add
    
    48 65
            - tabindex. -->
    
    49
    -    <toolbarbutton id="tor-circuit-new-circuit"
    
    50
    -                   class="subviewbutton panel-subview-footer-button tor-circuit-button"
    
    51
    -                   tabindex="0"
    
    52
    -                   aria-labelledby="tor-circuit-new-circuit-label"
    
    53
    -                   aria-describedby="tor-circuit-new-circuit-description">
    
    66
    +    <toolbarbutton
    
    67
    +      id="tor-circuit-new-circuit"
    
    68
    +      class="subviewbutton panel-subview-footer-button tor-circuit-button"
    
    69
    +      tabindex="0"
    
    70
    +      aria-labelledby="tor-circuit-new-circuit-label"
    
    71
    +      aria-describedby="tor-circuit-new-circuit-description"
    
    72
    +    >
    
    54 73
           <vbox align="start">
    
    55
    -        <label id="tor-circuit-new-circuit-label"
    
    56
    -               class="toolbarbutton-text"
    
    57
    -               value="&torbutton.context_menu.new_circuit_sentence_case;"/>
    
    58
    -        <label id="tor-circuit-new-circuit-description"
    
    59
    -               class="tor-circuit-button-description"/>
    
    74
    +        <label
    
    75
    +          id="tor-circuit-new-circuit-label"
    
    76
    +          class="toolbarbutton-text"
    
    77
    +          data-l10n-id="tor-circuit-panel-new-button"
    
    78
    +        />
    
    79
    +        <label
    
    80
    +          id="tor-circuit-new-circuit-description"
    
    81
    +          class="tor-circuit-button-description"
    
    82
    +        />
    
    60 83
           </vbox>
    
    61 84
         </toolbarbutton>
    
    62 85
       </vbox>
    

  • browser/components/torcircuit/content/torCircuitPanel.js
    ... ... @@ -34,6 +34,12 @@ var gTorCircuitPanel = {
    34 34
        * @type {bool}
    
    35 35
        */
    
    36 36
       _isActive: false,
    
    37
    +  /**
    
    38
    +   * The template element for circuit nodes.
    
    39
    +   *
    
    40
    +   * @type {HTMLTemplateElement?}
    
    41
    +   */
    
    42
    +  _nodeItemTemplate: null,
    
    37 43
     
    
    38 44
       /**
    
    39 45
        * The topic on which circuit changes are broadcast.
    
    ... ... @@ -62,7 +68,6 @@ var gTorCircuitPanel = {
    62 68
           heading: document.getElementById("tor-circuit-heading"),
    
    63 69
           alias: document.getElementById("tor-circuit-alias"),
    
    64 70
           aliasLabel: document.getElementById("tor-circuit-alias-label"),
    
    65
    -      aliasLink: document.querySelector("#tor-circuit-alias-label a"),
    
    66 71
           aliasMenu: document.getElementById("tor-circuit-panel-alias-menu"),
    
    67 72
           list: document.getElementById("tor-circuit-node-list"),
    
    68 73
           relaysItem: document.getElementById("tor-circuit-relays-item"),
    
    ... ... @@ -73,30 +78,24 @@ var gTorCircuitPanel = {
    73 78
         };
    
    74 79
         this.toolbarButton = document.getElementById("tor-circuit-button");
    
    75 80
     
    
    76
    -    // TODO: These strings should be set in the HTML markup with fluent.
    
    77
    -
    
    78
    -    // NOTE: There is already whitespace before and after the link from the
    
    79
    -    // XHTML markup.
    
    80
    -    const [aliasBefore, aliasAfter] = this._getString(
    
    81
    -      "torbutton.circuit_display.connected-to-alias",
    
    82
    -      // Placeholder is replaced with the same placeholder. This is a bit of a
    
    83
    -      // hack since we want the inserted address to be the rich anchor
    
    84
    -      // element already in the DOM, rather than a plain address.
    
    85
    -      // We won't have to do this with fluent by using data-l10n-name on the
    
    86
    -      // anchor element.
    
    87
    -      ["%S"]
    
    88
    -    ).split("%S");
    
    89
    -    this._panelElements.aliasLabel.prepend(aliasBefore);
    
    90
    -    this._panelElements.aliasLabel.append(aliasAfter);
    
    91
    -
    
    92
    -    this._panelElements.aliasLink.addEventListener("click", event => {
    
    81
    +    // We add listeners for the .tor-circuit-alias-link.
    
    82
    +    // NOTE: We have to add the listeners to the parent element because the
    
    83
    +    // link (with data-l10n-name="alias-link") will be replaced with a new
    
    84
    +    // cloned instance every time the parent gets re-translated.
    
    85
    +    this._panelElements.aliasLabel.addEventListener("click", event => {
    
    86
    +      if (!this._aliasLink.contains(event.target)) {
    
    87
    +        return;
    
    88
    +      }
    
    93 89
           event.preventDefault();
    
    94 90
           if (event.button !== 0) {
    
    95 91
             return;
    
    96 92
           }
    
    97 93
           this._openAlias("tab");
    
    98 94
         });
    
    99
    -    this._panelElements.aliasLink.addEventListener("contextmenu", event => {
    
    95
    +    this._panelElements.aliasLabel.addEventListener("contextmenu", event => {
    
    96
    +      if (!this._aliasLink.contains(event.target)) {
    
    97
    +        return;
    
    98
    +      }
    
    100 99
           event.preventDefault();
    
    101 100
           this._panelElements.aliasMenu.openPopupAtScreen(
    
    102 101
             event.screenX,
    
    ... ... @@ -119,21 +118,15 @@ var gTorCircuitPanel = {
    119 118
         document
    
    120 119
           .getElementById("tor-circuit-panel-alias-menu-copy")
    
    121 120
           .addEventListener("command", () => {
    
    122
    -        if (!this._panelElements.aliasLink.href) {
    
    121
    +        const alias = this._aliasLink?.href;
    
    122
    +        if (!alias) {
    
    123 123
               return;
    
    124 124
             }
    
    125 125
             Cc["@mozilla.org/widget/clipboardhelper;1"]
    
    126 126
               .getService(Ci.nsIClipboardHelper)
    
    127
    -          .copyString(this._panelElements.aliasLink.href);
    
    127
    +          .copyString(alias);
    
    128 128
           });
    
    129 129
     
    
    130
    -    document.getElementById("tor-circuit-start-item").textContent =
    
    131
    -      this._getString("torbutton.circuit_display.this_browser");
    
    132
    -
    
    133
    -    this._panelElements.relaysItem.textContent = this._getString(
    
    134
    -      "torbutton.circuit_display.onion-site-relays"
    
    135
    -    );
    
    136
    -
    
    137 130
         // Button is a xul:toolbarbutton, so we use "command" rather than "click".
    
    138 131
         document
    
    139 132
           .getElementById("tor-circuit-new-circuit")
    
    ... ... @@ -176,6 +169,13 @@ var gTorCircuitPanel = {
    176 169
           this.show();
    
    177 170
         });
    
    178 171
     
    
    172
    +    this._nodeItemTemplate = document.getElementById(
    
    173
    +      "tor-circuit-node-item-template"
    
    174
    +    );
    
    175
    +    // Prepare the unknown region name for the current locale.
    
    176
    +    // NOTE: We expect this to complete before the first call to _updateBody.
    
    177
    +    this._localeChanged();
    
    178
    +
    
    179 179
         this._locationListener = {
    
    180 180
           onLocationChange: (webProgress, request, locationURI, flags) => {
    
    181 181
             if (
    
    ... ... @@ -194,6 +194,7 @@ var gTorCircuitPanel = {
    194 194
     
    
    195 195
         // Get notifications for circuit changes.
    
    196 196
         Services.obs.addObserver(this, this.TOR_CIRCUIT_TOPIC);
    
    197
    +    Services.obs.addObserver(this, "intl:app-locales-changed");
    
    197 198
       },
    
    198 199
     
    
    199 200
       /**
    
    ... ... @@ -203,15 +204,21 @@ var gTorCircuitPanel = {
    203 204
         this._isActive = false;
    
    204 205
         gBrowser.removeProgressListener(this._locationListener);
    
    205 206
         Services.obs.removeObserver(this, this.TOR_CIRCUIT_TOPIC);
    
    207
    +    Services.obs.removeObserver(this, "intl:app-locales-changed");
    
    206 208
       },
    
    207 209
     
    
    208 210
       /**
    
    209 211
        * Observe circuit changes.
    
    210 212
        */
    
    211 213
       observe(subject, topic, data) {
    
    212
    -    if (topic === this.TOR_CIRCUIT_TOPIC) {
    
    213
    -      // TODO: Maybe check if we actually need to do something earlier.
    
    214
    -      this._updateCurrentBrowser();
    
    214
    +    switch (topic) {
    
    215
    +      case this.TOR_CIRCUIT_TOPIC:
    
    216
    +        // TODO: Maybe check if we actually need to do something earlier.
    
    217
    +        this._updateCurrentBrowser();
    
    218
    +        break;
    
    219
    +      case "intl:app-locales-changed":
    
    220
    +        this._localeChanged();
    
    221
    +        break;
    
    215 222
         }
    
    216 223
       },
    
    217 224
     
    
    ... ... @@ -231,6 +238,19 @@ var gTorCircuitPanel = {
    231 238
         this.panel.hidePopup();
    
    232 239
       },
    
    233 240
     
    
    241
    +  /**
    
    242
    +   * Get the current alias link instance.
    
    243
    +   *
    
    244
    +   * Note that this element instance may change whenever its parent element
    
    245
    +   * (#tor-circuit-alias-label) is re-translated. Attributes should be copied to
    
    246
    +   * the new instance.
    
    247
    +   */
    
    248
    +  get _aliasLink() {
    
    249
    +    return this._panelElements.aliasLabel.querySelector(
    
    250
    +      ".tor-circuit-alias-link"
    
    251
    +    );
    
    252
    +  },
    
    253
    +
    
    234 254
       /**
    
    235 255
        * Open the onion alias present in the alias link.
    
    236 256
        *
    
    ... ... @@ -238,12 +258,13 @@ var gTorCircuitPanel = {
    238 258
        *   window.
    
    239 259
        */
    
    240 260
       _openAlias(where) {
    
    241
    -    if (!this._panelElements.aliasLink.href) {
    
    261
    +    const url = this._aliasLink?.href;
    
    262
    +    if (!url) {
    
    242 263
           return;
    
    243 264
         }
    
    244 265
         // We hide the panel before opening the link.
    
    245 266
         this.hide();
    
    246
    -    window.openWebLinkIn(this._panelElements.aliasLink.href, where);
    
    267
    +    window.openWebLinkIn(url, where);
    
    247 268
       },
    
    248 269
     
    
    249 270
       /**
    
    ... ... @@ -351,11 +372,6 @@ var gTorCircuitPanel = {
    351 372
     
    
    352 373
         this.toolbarButton.hidden = false;
    
    353 374
     
    
    354
    -    if (this.panel.state !== "open" && this.panel.state !== "showing") {
    
    355
    -      // Don't update the panel content if it is not open or about to open.
    
    356
    -      return;
    
    357
    -    }
    
    358
    -
    
    359 375
         this._updateCircuitPanel();
    
    360 376
       },
    
    361 377
     
    
    ... ... @@ -383,35 +399,15 @@ var gTorCircuitPanel = {
    383 399
         return alias;
    
    384 400
       },
    
    385 401
     
    
    386
    -  /**
    
    387
    -   * Get a string from the properties bundle.
    
    388
    -   *
    
    389
    -   * @param {string} name - The string name.
    
    390
    -   * @param {string[]} args - The arguments to pass to the string.
    
    391
    -   *
    
    392
    -   * @returns {string} The string.
    
    393
    -   */
    
    394
    -  _getString(name, args = []) {
    
    395
    -    if (!this._stringBundle) {
    
    396
    -      this._stringBundle = Services.strings.createBundle(
    
    397
    -        "chrome://torbutton/locale/torbutton.properties"
    
    398
    -      );
    
    399
    -    }
    
    400
    -    try {
    
    401
    -      return this._stringBundle.formatStringFromName(name, args);
    
    402
    -    } catch {}
    
    403
    -    if (!this._fallbackStringBundle) {
    
    404
    -      this._fallbackStringBundle = Services.strings.createBundle(
    
    405
    -        "resource://torbutton/locale/en-US/torbutton.properties"
    
    406
    -      );
    
    407
    -    }
    
    408
    -    return this._fallbackStringBundle.formatStringFromName(name, args);
    
    409
    -  },
    
    410
    -
    
    411 402
       /**
    
    412 403
        * Updates the circuit display in the panel to show the current browser data.
    
    413 404
        */
    
    414 405
       _updateCircuitPanel() {
    
    406
    +    if (this.panel.state !== "open" && this.panel.state !== "showing") {
    
    407
    +      // Don't update the panel content if it is not open or about to open.
    
    408
    +      return;
    
    409
    +    }
    
    410
    +
    
    415 411
         // NOTE: The _currentBrowserData.nodes data may be stale. In particular, the
    
    416 412
         // circuit may have expired already, or we're still waiting on the new
    
    417 413
         // circuit.
    
    ... ... @@ -426,6 +422,9 @@ var gTorCircuitPanel = {
    426 422
           this.hide();
    
    427 423
           return;
    
    428 424
         }
    
    425
    +
    
    426
    +    this._log.debug("Updating circuit panel");
    
    427
    +
    
    429 428
         let domain = this._currentBrowserData.domain;
    
    430 429
         const onionAlias = this._getOnionAlias(domain);
    
    431 430
     
    
    ... ... @@ -447,24 +446,31 @@ var gTorCircuitPanel = {
    447 446
        * @param {string?} scheme - The scheme in use for the current domain.
    
    448 447
        */
    
    449 448
       _updateHeading(domain, onionAlias, scheme) {
    
    450
    -    this._panelElements.heading.textContent = this._getString(
    
    451
    -      "torbutton.circuit_display.heading",
    
    449
    +    document.l10n.setAttributes(
    
    450
    +      this._panelElements.heading,
    
    451
    +      "tor-circuit-panel-heading",
    
    452 452
           // Only shorten the onion domain if it has no alias.
    
    453
    -      [TorUIUtils.shortenOnionAddress(domain)]
    
    453
    +      { host: TorUIUtils.shortenOnionAddress(domain) }
    
    454 454
         );
    
    455 455
     
    
    456 456
         if (onionAlias) {
    
    457
    -      this._panelElements.aliasLink.textContent =
    
    458
    -        TorUIUtils.shortenOnionAddress(onionAlias);
    
    459 457
           if (scheme === "http" || scheme === "https") {
    
    460 458
             // We assume the same scheme as the current page for the alias, which we
    
    461 459
             // expect to be either http or https.
    
    462 460
             // NOTE: The href property is partially presentational so that the link
    
    463 461
             // location appears on hover.
    
    464
    -        this._panelElements.aliasLink.href = `${scheme}://${onionAlias}`;
    
    462
    +        // NOTE: The href attribute should be copied to any new instances of
    
    463
    +        // .tor-circuit-alias-link (with data-l10n-name="alias-link") when the
    
    464
    +        // parent _panelElements.aliasLabel gets re-translated.
    
    465
    +        this._aliasLink.href = `${scheme}://${onionAlias}`;
    
    465 466
           } else {
    
    466
    -        this._panelElements.aliasLink.removeAttribute("href");
    
    467
    +        this._aliasLink.removeAttribute("href");
    
    467 468
           }
    
    469
    +      document.l10n.setAttributes(
    
    470
    +        this._panelElements.aliasLabel,
    
    471
    +        "tor-circuit-panel-alias",
    
    472
    +        { alias: TorUIUtils.shortenOnionAddress(onionAlias) }
    
    473
    +      );
    
    468 474
           this._showPanelElement(this._panelElements.alias, true);
    
    469 475
         } else {
    
    470 476
           this._showPanelElement(this._panelElements.alias, false);
    
    ... ... @@ -485,20 +491,40 @@ var gTorCircuitPanel = {
    485 491
        * @param {string} domain - The domain to show for the last node.
    
    486 492
        */
    
    487 493
       _updateBody(nodes, domain) {
    
    488
    -    // Clean up old items.
    
    489
    -    // NOTE: We do not expect focus within a removed node.
    
    490
    -    for (const nodeItem of this._nodeItems) {
    
    491
    -      nodeItem.remove();
    
    492
    -    }
    
    494
    +    // NOTE: Rather than re-creating the <li> nodes from scratch, we prefer
    
    495
    +    // updating existing <li> nodes so that the display does not "flicker" in
    
    496
    +    // width as we wait for Fluent DOM to fill the nodes with text content. I.e.
    
    497
    +    // the existing node and text will remain in place, occupying the same
    
    498
    +    // width, up until it is replaced by Fluent DOM.
    
    499
    +    for (let index = 0; index < nodes.length; index++) {
    
    500
    +      if (index >= this._nodeItems.length) {
    
    501
    +        const newItem =
    
    502
    +          this._nodeItemTemplate.content.children[0].cloneNode(true);
    
    503
    +        const flagEl = newItem.querySelector(".tor-circuit-region-flag");
    
    504
    +        // Hide region flag whenever the flag src does not exist.
    
    505
    +        flagEl.addEventListener("error", () => {
    
    506
    +          flagEl.classList.add("no-region-flag-src");
    
    507
    +          flagEl.removeAttribute("src");
    
    508
    +        });
    
    509
    +        this._panelElements.list.insertBefore(
    
    510
    +          newItem,
    
    511
    +          this._panelElements.relaysItem
    
    512
    +        );
    
    493 513
     
    
    494
    -    this._nodeItems = nodes.map((nodeData, index) => {
    
    495
    -      const nodeItem = this._createCircuitNodeItem(nodeData, index === 0);
    
    496
    -      this._panelElements.list.insertBefore(
    
    497
    -        nodeItem,
    
    498
    -        this._panelElements.relaysItem
    
    514
    +        this._nodeItems.push(newItem);
    
    515
    +      }
    
    516
    +      this._updateCircuitNodeItem(
    
    517
    +        this._nodeItems[index],
    
    518
    +        nodes[index],
    
    519
    +        index === 0
    
    499 520
           );
    
    500
    -      return nodeItem;
    
    501
    -    });
    
    521
    +    }
    
    522
    +
    
    523
    +    // Remove excess items.
    
    524
    +    // NOTE: We do not expect focus within a removed node.
    
    525
    +    while (nodes.length < this._nodeItems.length) {
    
    526
    +      this._nodeItems.pop().remove();
    
    527
    +    }
    
    502 528
     
    
    503 529
         this._showPanelElement(
    
    504 530
           this._panelElements.relaysItem,
    
    ... ... @@ -511,40 +537,49 @@ var gTorCircuitPanel = {
    511 537
     
    
    512 538
         // Button description text, depending on whether our first node was a
    
    513 539
         // bridge, or otherwise a guard.
    
    514
    -    this._panelElements.newCircuitDescription.value = this._getString(
    
    540
    +    document.l10n.setAttributes(
    
    541
    +      this._panelElements.newCircuitDescription,
    
    515 542
           nodes[0].bridgeType === null
    
    516
    -        ? "torbutton.circuit_display.new-circuit-guard-description"
    
    517
    -        : "torbutton.circuit_display.new-circuit-bridge-description"
    
    543
    +        ? "tor-circuit-panel-new-button-description-guard"
    
    544
    +        : "tor-circuit-panel-new-button-description-bridge"
    
    518 545
         );
    
    519 546
       },
    
    520 547
     
    
    521 548
       /**
    
    522
    -   * Create a node item for the given circuit node data.
    
    549
    +   * Update a node item for the given circuit node data.
    
    523 550
        *
    
    551
    +   * @param {Element} nodeItem - The item to update.
    
    524 552
        * @param {NodeData} node - The circuit node data to create an item for.
    
    525 553
        * @param {bool} isCircuitStart - Whether this is the first node in the
    
    526 554
        *   circuit.
    
    527 555
        */
    
    528
    -  _createCircuitNodeItem(node, isCircuitStart) {
    
    529
    -    let nodeName;
    
    530
    -    // We do not show a flag for bridge nodes.
    
    531
    -    let regionCode = null;
    
    556
    +  _updateCircuitNodeItem(nodeItem, node, isCircuitStart) {
    
    557
    +    const nameEl = nodeItem.querySelector(".tor-circuit-node-name");
    
    558
    +    let flagSrc = null;
    
    559
    +
    
    532 560
         if (node.bridgeType === null) {
    
    533
    -      regionCode = node.regionCode;
    
    534
    -      if (!regionCode) {
    
    535
    -        nodeName = this._getString("torbutton.circuit_display.unknown_region");
    
    536
    -      } else {
    
    537
    -        nodeName = Services.intl.getRegionDisplayNames(undefined, [
    
    538
    -          regionCode,
    
    539
    -        ])[0];
    
    540
    -      }
    
    561
    +      const regionCode = node.regionCode;
    
    562
    +      flagSrc = this._regionFlagSrc(regionCode);
    
    563
    +
    
    564
    +      const regionName = regionCode
    
    565
    +        ? Services.intl.getRegionDisplayNames(undefined, [regionCode])[0]
    
    566
    +        : this._unknownRegionName;
    
    567
    +
    
    541 568
           if (isCircuitStart) {
    
    542
    -        nodeName = this._getString(
    
    543
    -          "torbutton.circuit_display.region-guard-node",
    
    544
    -          [nodeName]
    
    569
    +        document.l10n.setAttributes(
    
    570
    +          nameEl,
    
    571
    +          "tor-circuit-panel-node-region-guard",
    
    572
    +          { region: regionName }
    
    545 573
             );
    
    574
    +      } else {
    
    575
    +        // Set the text content directly, rather than using Fluent.
    
    576
    +        nameEl.removeAttribute("data-l10n-id");
    
    577
    +        nameEl.removeAttribute("data-l10n-args");
    
    578
    +        nameEl.textContent = regionName;
    
    546 579
           }
    
    547 580
         } else {
    
    581
    +      // Do not show a flag for bridges.
    
    582
    +
    
    548 583
           let bridgeType = node.bridgeType;
    
    549 584
           if (bridgeType === "meek_lite") {
    
    550 585
             bridgeType = "meek";
    
    ... ... @@ -552,55 +587,72 @@ var gTorCircuitPanel = {
    552 587
             bridgeType = "";
    
    553 588
           }
    
    554 589
           if (bridgeType) {
    
    555
    -        nodeName = this._getString(
    
    556
    -          "torbutton.circuit_display.tor_typed_bridge",
    
    557
    -          [bridgeType]
    
    590
    +        document.l10n.setAttributes(
    
    591
    +          nameEl,
    
    592
    +          "tor-circuit-panel-node-typed-bridge",
    
    593
    +          { "bridge-type": bridgeType }
    
    558 594
             );
    
    559 595
           } else {
    
    560
    -        nodeName = this._getString("torbutton.circuit_display.tor_bridge");
    
    596
    +        document.l10n.setAttributes(nameEl, "tor-circuit-panel-node-bridge");
    
    561 597
           }
    
    562 598
         }
    
    563
    -    const nodeItem = document.createElement("li");
    
    564
    -    nodeItem.classList.add("tor-circuit-node-item");
    
    565
    -
    
    566
    -    const regionFlagEl = this._regionFlag(regionCode);
    
    567
    -    if (regionFlagEl) {
    
    568
    -      nodeItem.append(regionFlagEl);
    
    599
    +    const flagEl = nodeItem.querySelector(".tor-circuit-region-flag");
    
    600
    +    flagEl.classList.toggle("no-region-flag-src", !flagSrc);
    
    601
    +    if (flagSrc) {
    
    602
    +      flagEl.setAttribute("src", flagSrc);
    
    603
    +    } else {
    
    604
    +      flagEl.removeAttribute("src");
    
    569 605
         }
    
    570 606
     
    
    571
    -    // Add whitespace after name for the addresses.
    
    572
    -    nodeItem.append(nodeName + " ");
    
    573
    -
    
    574
    -    if (node.ipAddrs) {
    
    575
    -      const addressesEl = document.createElement("span");
    
    576
    -      addressesEl.classList.add("tor-circuit-addresses");
    
    577
    -      let firstAddr = true;
    
    578
    -      for (const ip of node.ipAddrs) {
    
    579
    -        if (firstAddr) {
    
    580
    -          firstAddr = false;
    
    581
    -        } else {
    
    582
    -          addressesEl.append(", ");
    
    583
    -        }
    
    584
    -        // We use a <code> element to give screen readers a hint that
    
    585
    -        // punctuation is different for IP addresses.
    
    586
    -        const ipEl = document.createElement("code");
    
    587
    -        // TODO: Current HTML-aam 1.0 specs map the <code> element to the "code"
    
    588
    -        // role.
    
    589
    -        // However, mozilla-central commented out this mapping in
    
    590
    -        // accessible/base/HTMLMarkupMap.h because the HTML-aam specs at the
    
    591
    -        // time did not do this.
    
    592
    -        // See hg.mozilla.org/mozilla-central/rev/51eebe7d6199#l2.12
    
    593
    -        // For now we explicitly add the role="code", but once this is fixed
    
    594
    -        // from mozilla-central we should remove this.
    
    595
    -        ipEl.setAttribute("role", "code");
    
    596
    -        ipEl.classList.add("tor-circuit-ip-address");
    
    597
    -        ipEl.textContent = ip;
    
    598
    -        addressesEl.append(ipEl);
    
    607
    +    const addressesEl = nodeItem.querySelector(".tor-circuit-addresses");
    
    608
    +    // Empty children.
    
    609
    +    addressesEl.replaceChildren();
    
    610
    +    let firstAddr = true;
    
    611
    +    for (const ip of node.ipAddrs) {
    
    612
    +      if (firstAddr) {
    
    613
    +        firstAddr = false;
    
    614
    +      } else {
    
    615
    +        addressesEl.append(", ");
    
    599 616
           }
    
    600
    -      nodeItem.append(addressesEl);
    
    617
    +      const ipEl = document.createElement("code");
    
    618
    +      // TODO: Current HTML-aam 1.0 specs map the <code> element to the "code"
    
    619
    +      // role.
    
    620
    +      // However, mozilla-central commented out this mapping in
    
    621
    +      // accessible/base/HTMLMarkupMap.h because the HTML-aam specs at the
    
    622
    +      // time did not do this.
    
    623
    +      // See hg.mozilla.org/mozilla-central/rev/51eebe7d6199#l2.12
    
    624
    +      //
    
    625
    +      // This was updated in mozilla bug 1834931, for ESR 128
    
    626
    +      //
    
    627
    +      // For now we explicitly add the role="code", but once this is fixed
    
    628
    +      // from mozilla-central we should remove this.
    
    629
    +      ipEl.setAttribute("role", "code");
    
    630
    +      ipEl.classList.add("tor-circuit-ip-address");
    
    631
    +      ipEl.textContent = ip;
    
    632
    +      addressesEl.append(ipEl);
    
    601 633
         }
    
    634
    +  },
    
    635
    +
    
    636
    +  /**
    
    637
    +   * The string to use for unknown region names.
    
    638
    +   *
    
    639
    +   * Will be updated to match the current locale.
    
    640
    +   *
    
    641
    +   * @type {string}
    
    642
    +   */
    
    643
    +  _unknownRegionName: "Unknown region",
    
    602 644
     
    
    603
    -    return nodeItem;
    
    645
    +  /**
    
    646
    +   * Update the name for regions to match the current locale.
    
    647
    +   */
    
    648
    +  _localeChanged() {
    
    649
    +    document.l10n
    
    650
    +      .formatValue("tor-circuit-panel-node-unknown-region")
    
    651
    +      .then(name => {
    
    652
    +        this._unknownRegionName = name;
    
    653
    +        // Update the panel for the new region names, if it is shown.
    
    654
    +        this._updateCircuitPanel();
    
    655
    +      });
    
    604 656
       },
    
    605 657
     
    
    606 658
       /**
    
    ... ... @@ -609,9 +661,9 @@ var gTorCircuitPanel = {
    609 661
        * @param {string?} regionCode - The code to convert. It should be an upper
    
    610 662
        *   case 2-letter BCP47 Region subtag to be converted into a flag.
    
    611 663
        *
    
    612
    -   * @returns {HTMLImgElement?} The emoji flag img, or null if there is no flag.
    
    664
    +   * @returns {src?} The emoji flag img src, or null if there is no flag.
    
    613 665
        */
    
    614
    -  _regionFlag(regionCode) {
    
    666
    +  _regionFlagSrc(regionCode) {
    
    615 667
         if (!regionCode?.match(/^[A-Z]{2}$/)) {
    
    616 668
           return null;
    
    617 669
         }
    
    ... ... @@ -624,20 +676,7 @@ var gTorCircuitPanel = {
    624 676
           .map(cp => cp.toString(16))
    
    625 677
           .join("-");
    
    626 678
     
    
    627
    -    const flagEl = document.createElement("img");
    
    628
    -    // Decorative.
    
    629
    -    flagEl.alt = "";
    
    630
    -    flagEl.classList.add("tor-circuit-region-flag");
    
    631
    -    // Remove self if there is no matching flag found.
    
    632
    -    flagEl.addEventListener(
    
    633
    -      "error",
    
    634
    -      () => {
    
    635
    -        flagEl.classList.add("no-region-flag-src");
    
    636
    -      },
    
    637
    -      { once: true }
    
    638
    -    );
    
    639
    -    flagEl.src = `chrome://browser/content/tor-circuit-flags/${flagName}.svg`;
    
    640
    -    return flagEl;
    
    679
    +    return `chrome://browser/content/tor-circuit-flags/${flagName}.svg`;
    
    641 680
       },
    
    642 681
     
    
    643 682
       /**
    

  • browser/locales/en-US/browser/tor-browser.ftl
    ... ... @@ -326,3 +326,63 @@ about-dialog-browser-license-link = Licensing Information
    326 326
     # "Tor" and "The Onion Logo" are trademark names, so should not be translated (not including the quote marks, which can be localized).
    
    327 327
     # "The Tor Project, Inc." is an organisation name.
    
    328 328
     about-dialog-trademark-statement = “Tor” and “The Onion Logo” are registered trademarks of The Tor Project, Inc.
    
    329
    +
    
    330
    +## New tor circuit.
    
    331
    +
    
    332
    +# Shown in the File menu.
    
    333
    +# Uses title case for English (US).
    
    334
    +menu-new-tor-circuit =
    
    335
    +    .label = New Tor Circuit for this Site
    
    336
    +    .accesskey = C
    
    337
    +
    
    338
    +# Shown in the application menu (hamburger menu).
    
    339
    +# Uses sentence case for English (US).
    
    340
    +appmenuitem-new-tor-circuit =
    
    341
    +    .label = New Tor circuit for this site
    
    342
    +
    
    343
    +# Toolbar button to trigger a new circuit, available through toolbar customization.
    
    344
    +# Uses sentence case for English (US).
    
    345
    +# ".label" is the accessible name, and is visible in the overflow menu and when
    
    346
    +# customizing the toolbar.
    
    347
    +# ".tooltiptext" will be identical to the label.
    
    348
    +toolbar-new-tor-circuit =
    
    349
    +    .label = New Tor circuit for this site
    
    350
    +    .tooltiptext = { toolbar-new-tor-circuit.label }
    
    351
    +
    
    352
    +## Tor circuit URL bar button.
    
    353
    +
    
    354
    +# The tooltip also acts as the accessible name.
    
    355
    +tor-circuit-urlbar-button =
    
    356
    +    .tooltiptext = Tor Circuit
    
    357
    +
    
    358
    +## Tor circuit panel.
    
    359
    +
    
    360
    +# $host (String) - The host name shown in the URL bar, potentially shortened.
    
    361
    +tor-circuit-panel-heading = Circuit for { $host }
    
    362
    +# Shown when the current address is a ".tor.onion" alias.
    
    363
    +# $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.
    
    364
    +tor-circuit-panel-alias = Connected to <a data-l10n-name="alias-link">{ $alias }</a>
    
    365
    +
    
    366
    +# Text just before the list of circuit nodes.
    
    367
    +tor-circuit-panel-node-list-introduction = Tor Circuit
    
    368
    +# First node in the list of circuit nodes. Refers to Tor Browser.
    
    369
    +tor-circuit-panel-node-browser = This browser
    
    370
    +# Represents a number of unknown relays that complete a connection to an ".onion" site.
    
    371
    +tor-circuit-panel-node-onion-relays = Onion site relays
    
    372
    +# Represents the bridge node used to connect to the Tor network.
    
    373
    +# $bridge-type (String) - The name for the type of bridge used: meek, obfs4, snowflake, etc.
    
    374
    +tor-circuit-panel-node-typed-bridge = Bridge: { $bridge-type }
    
    375
    +# Represents the bridge node used to connect to the Tor network when the bridge type is unknown.
    
    376
    +tor-circuit-panel-node-bridge = Bridge
    
    377
    +# Represents the initial guard node used for a tor circuit.
    
    378
    +# $region (String) - The region name for the guard node, already localized.
    
    379
    +tor-circuit-panel-node-region-guard = { $region } (guard)
    
    380
    +# Represents a circuit node with an unknown regional location.
    
    381
    +tor-circuit-panel-node-unknown-region = Unknown region
    
    382
    +
    
    383
    +# Uses sentence case for English (US).
    
    384
    +tor-circuit-panel-new-button = New Tor circuit for this site
    
    385
    +# Shown when the first node in the circuit is a guard node, rather than a bridge.
    
    386
    +tor-circuit-panel-new-button-description-guard = Your guard node may not change
    
    387
    +# Shown when the first node in the circuit is a bridge node.
    
    388
    +tor-circuit-panel-new-button-description-bridge = Your bridge may not change

  • toolkit/torbutton/chrome/locale/en-US/torbutton.dtd
    ... ... @@ -3,12 +3,6 @@
    3 3
        - License, v. 2.0. If a copy of the MPL was not distributed with this
    
    4 4
        - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
    
    5 5
     
    
    6
    -<!ENTITY torbutton.context_menu.new_circuit "New Tor Circuit for this Site">
    
    7
    -<!ENTITY torbutton.context_menu.new_circuit_sentence_case "New Tor circuit for this site">
    
    8
    -<!ENTITY torbutton.context_menu.new_circuit_key "C">
    
    9
    -
    
    10
    -<!ENTITY torbutton.circuit_display.title "Tor Circuit">
    
    11
    -
    
    12 6
     <!-- Onion services strings. Strings are kept here for ease of translation. -->
    
    13 7
     <!ENTITY torbutton.onionServices.authPrompt.tooltip "Open onion service client authentication prompt">
    
    14 8
     <!ENTITY torbutton.onionServices.authPrompt.persistCheckboxLabel "Remember this key">

  • toolkit/torbutton/chrome/locale/en-US/torbutton.properties
    ... ... @@ -3,25 +3,6 @@
    3 3
     # License, v. 2.0. If a copy of the MPL was not distributed with this
    
    4 4
     # file, You can obtain one at http://mozilla.org/MPL/2.0/.
    
    5 5
     
    
    6
    -# Circuit display
    
    7
    -# LOCALIZATION NOTE: %S will be the host name shown in the URL bar.
    
    8
    -torbutton.circuit_display.heading = Circuit for %S
    
    9
    -# LOCALIZATION NOTE: %S will be the alias onion address.
    
    10
    -torbutton.circuit_display.connected-to-alias = Connected to %S
    
    11
    -torbutton.circuit_display.this_browser = This browser
    
    12
    -torbutton.circuit_display.onion-site-relays = Onion site relays
    
    13
    -# LOCALIZATION NOTE: %S will be the bridge type name (meek, obfs4, snowflake,
    
    14
    -# etc).
    
    15
    -torbutton.circuit_display.tor_typed_bridge = Bridge: %S
    
    16
    -# LOCALIZATION NOTE: Used when the bridge type is unknown.
    
    17
    -torbutton.circuit_display.tor_bridge = Bridge
    
    18
    -# LOCALIZATION NOTE: Used when a circuit node's regional location is unknown.
    
    19
    -torbutton.circuit_display.unknown_region = Unknown region
    
    20
    -# LOCALIZATION NOTE: %S will be the localized region name for the guard node.
    
    21
    -torbutton.circuit_display.region-guard-node = %S (guard)
    
    22
    -torbutton.circuit_display.new-circuit-guard-description = Your guard node may not change
    
    23
    -torbutton.circuit_display.new-circuit-bridge-description = Your bridge may not change
    
    24
    -
    
    25 6
     # Download pane warning
    
    26 7
     torbutton.download.warning.title = Be careful opening downloads
    
    27 8
     # %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):
    253 253
                 if path not in self.localization_resources
    
    254 254
             )
    
    255 255
     
    
    256
    -    def tb_get_transformed(self, target_path, transform_id):
    
    256
    +    def tb_get_transform(self, target_path, transform_id):
    
    257 257
             """
    
    258 258
             Find the transformation node with the given id for the given path.
    
    259
    -
    
    260
    -        The node will be evaluated (converted to regular fluent.ast) before it
    
    261
    -        is returned.
    
    262 259
             """
    
    263 260
             for node in self.transforms[target_path]:
    
    264 261
                 if node.id.name == transform_id:
    
    265
    -                return self.evaluate(node)
    
    262
    +                return node
    
    266 263
             return None
    
    267 264
     
    
    268 265
         def tb_get_reference_entry(self, target_path, entry_id):
    
    ... ... @@ -330,6 +327,21 @@ class TorBrowserMigrator:
    330 327
     
    
    331 328
             ctx = self._get_migration_context(locale, locale_dir)
    
    332 329
     
    
    330
    +        # NOTE: We do not use the existing ctx.serialize_changeset method.
    
    331
    +        # The problem with this approach was that it would re-shuffle the order
    
    332
    +        # of already existing strings to match the en-US locale.
    
    333
    +        # But Weblate currently does not preserve the order of translated
    
    334
    +        # strings: https://github.com/WeblateOrg/weblate/issues/11134
    
    335
    +        # so this created extra noise in the diff.
    
    336
    +        # Instead, we just always append transformations to the end of the
    
    337
    +        # existing file.
    
    338
    +        # Moreover, it would inject group comments into the translated files,
    
    339
    +        # which Weblate does not handle well. Instead, we just do not add any
    
    340
    +        # comments.
    
    341
    +        #
    
    342
    +        # In case we want to use it again in the future, here is a reference
    
    343
    +        # to how it works:
    
    344
    +        #
    
    333 345
             # ctx.serialize_changeset expects a set of (path, identifier) of
    
    334 346
             # localization resources that can be used to evaluate the
    
    335 347
             # transformations.
    
    ... ... @@ -344,76 +356,115 @@ class TorBrowserMigrator:
    344 356
             # one step, so we want to fill the changeset with all required
    
    345 357
             # (path, identifier) pairs found in the localization resources.
    
    346 358
     
    
    347
    -        # Choose the transforms that are required and available.
    
    348
    -        changeset = set()
    
    349 359
             available_strings = ctx.tb_get_available_strings()
    
    350
    -        for (target_path, transform_id), dep_set in ctx.dependencies.items():
    
    351
    -            # ctx.dependencies is a dict of dependencies for all
    
    352
    -            # transformations
    
    353
    -            # { (target_path, transform_identifier): set(
    
    354
    -            #     (localization_path, string_identifier),
    
    355
    -            # )}
    
    356
    -            #
    
    357
    -            # e.g. if we want to create a new fluent Message called
    
    358
    -            # "new-string1", and it uses "oldString1" from "old-file1.dtd"
    
    359
    -            # and "oldString2" from "old-file2.dtd". And "new-string2" using
    
    360
    -            # "oldString3" from "old-file2.dtd", it would be
    
    361
    -            # {
    
    362
    -            #   ("new-file.ftl", "new-string1"): set(
    
    363
    -            #     ("old-file1.dtd", "oldString1"),
    
    364
    -            #     ("old-file2.dtd", "oldString2"),
    
    365
    -            #   ),
    
    366
    -            #   ("new-file.ftl", "new-string2"): set(
    
    367
    -            #     ("old-file2.dtd", "oldString3"),
    
    368
    -            #   ),
    
    369
    -            # }
    
    370
    -            can_transform = True
    
    371
    -            for dep in dep_set:
    
    372
    -                path, string_id = dep
    
    373
    -                if dep not in available_strings:
    
    374
    -                    can_transform = False
    
    375
    -                    self.logger.info(
    
    376
    -                        f"Skipping transform {target_path}:{transform_id} for "
    
    377
    -                        f"'{locale}' locale because it is missing the "
    
    378
    -                        f"string {path}:{string_id}."
    
    379
    -                    )
    
    380
    -                    break
    
    381
    -                # Strings in legacy formats might have an entry in the file
    
    382
    -                # that is just a copy of the en-US strings.
    
    383
    -                # For these we want to check the weblate metadata to ensure
    
    384
    -                # it is a translated string.
    
    385
    -                if not path.endswith(
    
    386
    -                    ".ftl"
    
    387
    -                ) and not self.weblate_metadata.is_translated(
    
    388
    -                    os.path.join("en-US", path),
    
    389
    -                    os.path.join(locale, path),
    
    390
    -                    string_id,
    
    391
    -                ):
    
    392
    -                    can_transform = False
    
    360
    +        wrote_file = False
    
    361
    +        errors = []
    
    362
    +
    
    363
    +        for target_path, reference in ctx.reference_resources.items():
    
    364
    +            translated_ids = [
    
    365
    +                entry.id.name
    
    366
    +                for entry in ctx.target_resources[target_path].body
    
    367
    +                if isinstance(entry, (ast.Message, ast.Term))
    
    368
    +                # NOTE: We're assuming that the Message and Term ids do not
    
    369
    +                # conflict with each other.
    
    370
    +            ]
    
    371
    +            new_entries = []
    
    372
    +
    
    373
    +            # Apply transfomations in the order they appear in the reference
    
    374
    +            # (en-US) file.
    
    375
    +            for entry in reference.body:
    
    376
    +                if not isinstance(entry, (ast.Message, ast.Term)):
    
    377
    +                    continue
    
    378
    +                transform_id = entry.id.name
    
    379
    +                transform = ctx.tb_get_transform(target_path, transform_id)
    
    380
    +                if not transform:
    
    381
    +                    # No transformation for this reference entry.
    
    382
    +                    continue
    
    383
    +
    
    384
    +                if transform_id in translated_ids:
    
    393 385
                         self.logger.info(
    
    394
    -                        f"Skipping transform {target_path}:{transform_id} for "
    
    395
    -                        f"'{locale}' locale because the string "
    
    396
    -                        f"{path}:{string_id} has not been translated on "
    
    397
    -                        "weblate."
    
    386
    +                        f"Skipping transform {target_path}:{transform_id} "
    
    387
    +                        f"for '{locale}' locale because it already has a "
    
    388
    +                        f"translation."
    
    398 389
                         )
    
    399
    -                    break
    
    400
    -            if can_transform:
    
    401
    -                changeset.update(dep_set)
    
    390
    +                    continue
    
    402 391
     
    
    403
    -        print("", file=sys.stderr)
    
    404
    -        wrote_file = False
    
    405
    -        errors = []
    
    406
    -        for path, fluent in ctx.serialize_changeset(changeset).items():
    
    407
    -            full_path = os.path.join(locale_dir, path)
    
    392
    +                # ctx.dependencies is a dict of dependencies for all
    
    393
    +                # transformations
    
    394
    +                # { (target_path, transform_identifier): set(
    
    395
    +                #     (localization_path, string_identifier),
    
    396
    +                # )}
    
    397
    +                #
    
    398
    +                # e.g. if we want to create a new fluent Message called
    
    399
    +                # "new-string1", and it uses "oldString1" from "old-file1.dtd"
    
    400
    +                # and "oldString2" from "old-file2.dtd". And "new-string2" using
    
    401
    +                # "oldString3" from "old-file2.dtd", it would be
    
    402
    +                # {
    
    403
    +                #   ("new-file.ftl", "new-string1"): set(
    
    404
    +                #     ("old-file1.dtd", "oldString1"),
    
    405
    +                #     ("old-file2.dtd", "oldString2"),
    
    406
    +                #   ),
    
    407
    +                #   ("new-file.ftl", "new-string2"): set(
    
    408
    +                #     ("old-file2.dtd", "oldString3"),
    
    409
    +                #   ),
    
    410
    +                # }
    
    411
    +                dep_set = ctx.dependencies[(target_path, transform_id)]
    
    412
    +                can_transform = True
    
    413
    +                for dep in dep_set:
    
    414
    +                    path, string_id = dep
    
    415
    +                    if dep not in available_strings:
    
    416
    +                        can_transform = False
    
    417
    +                        self.logger.info(
    
    418
    +                            f"Skipping transform {target_path}:{transform_id} "
    
    419
    +                            f"for '{locale}' locale because it is missing the "
    
    420
    +                            f"string {path}:{string_id}."
    
    421
    +                        )
    
    422
    +                        break
    
    423
    +                    # Strings in legacy formats might have an entry in the file
    
    424
    +                    # that is just a copy of the en-US strings.
    
    425
    +                    # For these we want to check the weblate metadata to ensure
    
    426
    +                    # it is a translated string.
    
    427
    +                    if not path.endswith(
    
    428
    +                        ".ftl"
    
    429
    +                    ) and not self.weblate_metadata.is_translated(
    
    430
    +                        os.path.join("en-US", path),
    
    431
    +                        os.path.join(locale, path),
    
    432
    +                        string_id,
    
    433
    +                    ):
    
    434
    +                        can_transform = False
    
    435
    +                        self.logger.info(
    
    436
    +                            f"Skipping transform {target_path}:{transform_id} "
    
    437
    +                            f"for '{locale}' locale because the string "
    
    438
    +                            f"{path}:{string_id} has not been translated on "
    
    439
    +                            "weblate."
    
    440
    +                        )
    
    441
    +                        break
    
    442
    +                if not can_transform:
    
    443
    +                    continue
    
    444
    +
    
    445
    +                # Run the transformation.
    
    446
    +                new_entries.append(ctx.evaluate(transform))
    
    447
    +
    
    448
    +            if not new_entries:
    
    449
    +                continue
    
    450
    +
    
    451
    +            full_path = os.path.join(locale_dir, target_path)
    
    452
    +            print("", file=sys.stderr)
    
    408 453
                 self.logger.info(f"Writing to {full_path}")
    
    409
    -            with open(full_path, "w") as file:
    
    410
    -                file.write(fluent)
    
    411
    -            wrote_file = True
    
    412 454
     
    
    455
    +            # For Fluent we can just serialize the transformations and append
    
    456
    +            # them to the end of the existing file.
    
    457
    +            resource = ast.Resource(new_entries)
    
    458
    +            with open(full_path, "a") as file:
    
    459
    +                file.write(serialize(resource))
    
    460
    +
    
    461
    +            with open(full_path, "r") as file:
    
    462
    +                full_content = file.read()
    
    463
    +            wrote_file = True
    
    413 464
                 # Collect any fluent parsing errors from the newly written file.
    
    414 465
                 errors.extend(
    
    415 466
                     (full_path, message, line, sample)
    
    416
    -                for message, line, sample in self._fluent_errors(fluent)
    
    467
    +                for message, line, sample in self._fluent_errors(full_content)
    
    417 468
                 )
    
    418 469
     
    
    419 470
             if not wrote_file:
    
    ... ... @@ -547,7 +598,7 @@ class TorBrowserMigrator:
    547 598
                     have_error = True
    
    548 599
                     continue
    
    549 600
     
    
    550
    -            transformed = ctx.tb_get_transformed(target_path, transform_id)
    
    601
    +            transformed = ctx.evaluate(ctx.tb_get_transform(target_path, transform_id))
    
    551 602
                 reference_entry = ctx.tb_get_reference_entry(target_path, transform_id)
    
    552 603
                 if reference_entry is None:
    
    553 604
                     self.logger.error(
    

  • tools/torbrowser/l10n/migrations/bug-42209-tor-circuit.py
    1
    +import fluent.syntax.ast as FTL
    
    2
    +from fluent.migrate.helpers import VARIABLE_REFERENCE, transforms_from
    
    3
    +from fluent.migrate.transforms import CONCAT, REPLACE
    
    4
    +
    
    5
    +
    
    6
    +def migrate(ctx):
    
    7
    +    legacy_dtd = "torbutton.dtd"
    
    8
    +    legacy_properties = "torbutton.properties"
    
    9
    +    ctx.add_transforms(
    
    10
    +        "tor-browser.ftl",
    
    11
    +        "tor-browser.ftl",
    
    12
    +        transforms_from(
    
    13
    +            """
    
    14
    +menu-new-tor-circuit =
    
    15
    +    .label = { COPY(dtd_path, "torbutton.context_menu.new_circuit") }
    
    16
    +    .accesskey = { COPY(dtd_path, "torbutton.context_menu.new_circuit_key") }
    
    17
    +appmenuitem-new-tor-circuit =
    
    18
    +    .label = { COPY(dtd_path, "torbutton.context_menu.new_circuit_sentence_case") }
    
    19
    +toolbar-new-tor-circuit =
    
    20
    +    .label = { COPY(dtd_path, "torbutton.context_menu.new_circuit_sentence_case") }
    
    21
    +    .tooltiptext = { toolbar-new-tor-circuit.label }
    
    22
    +
    
    23
    +tor-circuit-urlbar-button =
    
    24
    +    .tooltiptext = { COPY(dtd_path, "torbutton.circuit_display.title") }
    
    25
    +
    
    26
    +tor-circuit-panel-node-list-introduction = { COPY(dtd_path, "torbutton.circuit_display.title") }
    
    27
    +tor-circuit-panel-node-browser = { COPY(path, "torbutton.circuit_display.this_browser") }
    
    28
    +tor-circuit-panel-node-onion-relays = { COPY(path, "torbutton.circuit_display.onion-site-relays") }
    
    29
    +tor-circuit-panel-node-bridge = { COPY(path, "torbutton.circuit_display.tor_bridge") }
    
    30
    +tor-circuit-panel-node-unknown-region = { COPY(path, "torbutton.circuit_display.unknown_region") }
    
    31
    +
    
    32
    +tor-circuit-panel-new-button = { COPY(dtd_path, "torbutton.context_menu.new_circuit_sentence_case") }
    
    33
    +tor-circuit-panel-new-button-description-guard = { COPY(path, "torbutton.circuit_display.new-circuit-guard-description") }
    
    34
    +tor-circuit-panel-new-button-description-bridge = { COPY(path, "torbutton.circuit_display.new-circuit-bridge-description") }
    
    35
    +""",
    
    36
    +            dtd_path=legacy_dtd,
    
    37
    +            path=legacy_properties,
    
    38
    +        )
    
    39
    +        + [
    
    40
    +            # Replace "%S" with "{ $host }"
    
    41
    +            FTL.Message(
    
    42
    +                id=FTL.Identifier("tor-circuit-panel-heading"),
    
    43
    +                value=REPLACE(
    
    44
    +                    legacy_properties,
    
    45
    +                    "torbutton.circuit_display.heading",
    
    46
    +                    {"%1$S": VARIABLE_REFERENCE("host")},
    
    47
    +                ),
    
    48
    +            ),
    
    49
    +            # Replace "%S" with "<a data-l10n-name="alias-link">{ $alias }</a>"
    
    50
    +            FTL.Message(
    
    51
    +                id=FTL.Identifier("tor-circuit-panel-alias"),
    
    52
    +                value=REPLACE(
    
    53
    +                    legacy_properties,
    
    54
    +                    "torbutton.circuit_display.connected-to-alias",
    
    55
    +                    {
    
    56
    +                        "%1$S": CONCAT(
    
    57
    +                            FTL.TextElement('<a data-l10n-name="alias-link">'),
    
    58
    +                            VARIABLE_REFERENCE("alias"),
    
    59
    +                            FTL.TextElement("</a>"),
    
    60
    +                        )
    
    61
    +                    },
    
    62
    +                ),
    
    63
    +            ),
    
    64
    +            # Replace "%S" with "{ $region }"
    
    65
    +            FTL.Message(
    
    66
    +                id=FTL.Identifier("tor-circuit-panel-node-region-guard"),
    
    67
    +                value=REPLACE(
    
    68
    +                    legacy_properties,
    
    69
    +                    "torbutton.circuit_display.region-guard-node",
    
    70
    +                    {"%1$S": VARIABLE_REFERENCE("region")},
    
    71
    +                ),
    
    72
    +            ),
    
    73
    +            # Replace "%S" with "{ $bridge-type }"
    
    74
    +            FTL.Message(
    
    75
    +                id=FTL.Identifier("tor-circuit-panel-node-typed-bridge"),
    
    76
    +                value=REPLACE(
    
    77
    +                    legacy_properties,
    
    78
    +                    "torbutton.circuit_display.tor_typed_bridge",
    
    79
    +                    {"%1$S": VARIABLE_REFERENCE("bridge-type")},
    
    80
    +                ),
    
    81
    +            ),
    
    82
    +        ],
    
    83
    +    )