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
-
56ce4cc1
by Henry Wilkes at 2024-03-25T09:42:30+00:00
-
76f869d5
by Henry Wilkes at 2024-03-25T09:42:31+00:00
-
724dfc5f
by Henry Wilkes at 2024-03-25T09:42:31+00:00
-
966b7f30
by Henry Wilkes at 2024-03-25T09:42:32+00:00
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:
... | ... | @@ -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"
|
... | ... | @@ -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"
|
... | ... | @@ -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"
|
... | ... | @@ -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 */
|
... | ... | @@ -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>
|
... | ... | @@ -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 | /**
|
... | ... | @@ -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 |
... | ... | @@ -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"> |
... | ... | @@ -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
|
... | ... | @@ -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(
|
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 | + ) |