Przeglądaj źródła

Address review: move send shortcut hint to template

- Render the send button's title and aria-keyshortcuts in the template.
  The title uses a new, descriptive message with the platform hotkey and
  instance name; it is appended untranslated to all translation files
  for Crowdin.
- Match the physical S key (event.code) so non-Latin layouts work, and
  split the conditions into named variables for readability.
- Note that Ctrl+Shift+S ("Save page as") is left to the browser.
- Update the SRI hash of privatebin.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vatsanbalaji-ossagent 1 miesiąc temu
rodzic
commit
6d4ad4a23c
43 zmienionych plików z 104 dodań i 111 usunięć
  1. 2 1
      i18n/ar.json
  2. 2 1
      i18n/bg.json
  3. 2 1
      i18n/ca.json
  4. 2 1
      i18n/co.json
  5. 2 1
      i18n/cs.json
  6. 2 1
      i18n/de.json
  7. 2 1
      i18n/el.json
  8. 2 1
      i18n/en.json
  9. 2 1
      i18n/es.json
  10. 2 1
      i18n/et.json
  11. 2 1
      i18n/fa.json
  12. 2 1
      i18n/fi.json
  13. 2 1
      i18n/fr.json
  14. 2 1
      i18n/he.json
  15. 2 1
      i18n/hi.json
  16. 2 1
      i18n/hu.json
  17. 2 1
      i18n/id.json
  18. 2 1
      i18n/it.json
  19. 2 1
      i18n/ja.json
  20. 2 1
      i18n/jbo.json
  21. 2 1
      i18n/ko.json
  22. 2 1
      i18n/ku.json
  23. 2 1
      i18n/la.json
  24. 2 1
      i18n/lt.json
  25. 2 1
      i18n/nl.json
  26. 2 1
      i18n/no.json
  27. 2 1
      i18n/oc.json
  28. 2 1
      i18n/pl.json
  29. 2 1
      i18n/pt.json
  30. 2 1
      i18n/ro.json
  31. 2 1
      i18n/ru.json
  32. 2 1
      i18n/sk.json
  33. 2 1
      i18n/sl.json
  34. 2 1
      i18n/sv.json
  35. 2 1
      i18n/th.json
  36. 2 1
      i18n/tr.json
  37. 2 1
      i18n/uk.json
  38. 2 1
      i18n/zh-tw.json
  39. 2 1
      i18n/zh.json
  40. 11 33
      js/privatebin.js
  41. 13 37
      js/test/TopNav.js
  42. 1 1
      lib/Configuration.php
  43. 1 1
      tpl/bootstrap5.php

+ 2 - 1
i18n/ar.json

@@ -234,5 +234,6 @@
     "Theme": "السمة",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/bg.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/ca.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/co.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Copy": "Cupià",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/cs.json

@@ -234,5 +234,6 @@
     "Theme": "Vzhled",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/de.json

@@ -234,5 +234,6 @@
     "Theme": "Design",
     "Copy": "Kopieren",
     "Cmd": "Cmd",
-    "Ctrl": "Strg"
+    "Ctrl": "Strg",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/el.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/en.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/es.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/et.json

@@ -234,5 +234,6 @@
     "Theme": "Teema",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/fa.json

@@ -234,5 +234,6 @@
     "Theme": "تم",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/fi.json

@@ -234,5 +234,6 @@
     "Theme": "Teema",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/fr.json

@@ -234,5 +234,6 @@
     "Theme": "Thème",
     "Copy": "Copier",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/he.json

@@ -234,5 +234,6 @@
     "Theme": "נושא",
     "Copy": "העתק",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/hi.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/hu.json

@@ -234,5 +234,6 @@
     "Theme": "Téma",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/id.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/it.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Copy": "Copiare",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/ja.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/jbo.json

@@ -234,5 +234,6 @@
     "Theme": "jvinu",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/ko.json

@@ -234,5 +234,6 @@
     "Theme": "테마",
     "Copy": "복사",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/ku.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/la.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/lt.json

@@ -234,5 +234,6 @@
     "Theme": "Apipavidalinimas",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/nl.json

@@ -234,5 +234,6 @@
     "Theme": "Thema",
     "Copy": "Kopieëren",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/no.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/oc.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/pl.json

@@ -234,5 +234,6 @@
     "Theme": "Motyw",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/pt.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/ro.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/ru.json

@@ -234,5 +234,6 @@
     "Theme": "Тема",
     "Copy": "Скопировать",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/sk.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/sl.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Copy": "Kopiraj",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/sv.json

@@ -234,5 +234,6 @@
     "Theme": "Tema",
     "Copy": "Kopiera",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/th.json

@@ -234,5 +234,6 @@
     "Theme": "ธีม ",
     "Copy": "คัดลอก",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/tr.json

@@ -234,5 +234,6 @@
     "Theme": "Theme",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/uk.json

@@ -234,5 +234,6 @@
     "Theme": "Тема",
     "Copy": "Copy",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/zh-tw.json

@@ -234,5 +234,6 @@
     "Theme": "佈景主題",
     "Copy": "複製",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 2 - 1
i18n/zh.json

@@ -234,5 +234,6 @@
     "Theme": "主题",
     "Copy": "复制",
     "Cmd": "Cmd",
-    "Ctrl": "Ctrl"
+    "Ctrl": "Ctrl",
+    "Hit %s+s to create the document, storing it on %s": "Hit %s+s to create the document, storing it on %s"
 }

+ 11 - 33
js/privatebin.js

@@ -4049,9 +4049,7 @@ window.PrivateBin = (function () {
          * sends the document on [Ctrl]+[S] or, on macOS, [Cmd]+[S]
          *
          * Only acts while the send button is shown, so the browser's own
-         * "save page" still works when viewing an existing document. Alt is
-         * excluded because AltGr is reported as Ctrl+Alt and some keyboard
-         * layouts type letters with AltGr+S.
+         * "save page" still works when viewing an existing document.
          *
          * @name   TopNav.sendOnShortcut
          * @private
@@ -4059,36 +4057,17 @@ window.PrivateBin = (function () {
          * @param  {KeyboardEvent} event
          */
         function sendOnShortcut(event) {
-            if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey ||
-                typeof event.key !== 'string' || event.key.toLowerCase() !== 's' ||
-                !sendButton || sendButton.classList.contains('hidden') || sendButton.disabled
-            ) {
-                return;
-            }
-            event.preventDefault();
-            sendButton.click();
-        }
-
-        /**
-         * shows the send shortcut as tooltip and exposes it to assistive technology
-         *
-         * The tooltip only uses the existing "Ctrl"/"Cmd" translations, so no
-         * new message ID is needed in the language files.
-         *
-         * @name   TopNav.showSendShortcutHint
-         * @private
-         * @function
-         */
-        function showSendShortcutHint() {
-            const isMac = /Mac/.test(navigator.userAgent);
-            function applyTitle() {
-                sendButton.setAttribute('title', I18n._(isMac ? 'Cmd' : 'Ctrl') + '+S');
+            // the physical S key, so non-Latin keyboard layouts work, too
+            const isSaveKey = event.code === 'KeyS';
+            // Alt is excluded because AltGr is reported as Ctrl+Alt and some
+            // layouts type letters with AltGr+S, Shift to leave "Save page as"
+            // ([Ctrl]+[Shift]+[S]) to the browser
+            const hasCorrectModifiers = (event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey;
+            const isButtonAvailable = sendButton && !sendButton.classList.contains('hidden') && !sendButton.disabled;
+            if (isSaveKey && hasCorrectModifiers && isButtonAvailable) {
+                event.preventDefault();
+                sendButton.click();
             }
-            sendButton.setAttribute('aria-keyshortcuts', isMac ? 'Meta+S' : 'Control+S');
-            // the language may still be loading, so register the re-translation
-            // first and then apply the current (possibly English) text
-            I18n.onLanguageLoaded(applyTitle);
-            applyTitle();
         }
 
         /**
@@ -4780,7 +4759,6 @@ window.PrivateBin = (function () {
             if (sendButton) {
                 sendButton.addEventListener('click', PasteEncrypter.sendPaste);
                 document.addEventListener('keydown', sendOnShortcut);
-                showSendShortcutHint();
             }
             if (cloneButton) {
                 cloneButton.addEventListener('click', Controller.clonePaste);

+ 13 - 37
js/test/TopNav.js

@@ -132,14 +132,9 @@ describe('TopNav', function () {
 
         // builds the create-document navbar and stubs the actual send, so no
         // server interaction happens
-        function setUpCreateMode(userAgent) {
+        function setUpCreateMode() {
             cleanup();
             sendCount = 0;
-            if (userAgent) {
-                Object.defineProperty(window.navigator, 'userAgent', {
-                    value: userAgent, configurable: true
-                });
-            }
             document.documentElement.innerHTML =
                 '<nav><div id="navbar"><ul><li><button id="newbutton" ' +
                 'type="button" class="hidden">New</button></li><li><a ' +
@@ -174,62 +169,43 @@ describe('TopNav', function () {
 
         it('sends the document on Ctrl+S and suppresses the browser save dialog', function () {
             setUpCreateMode();
-            const prevented = pressKey({key: 's', ctrlKey: true});
+            const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true});
             assert.strictEqual(sendCount, 1);
             assert.ok(prevented);
         });
 
         it('sends the document on Cmd+S (macOS)', function () {
             setUpCreateMode();
-            const prevented = pressKey({key: 's', metaKey: true});
+            const prevented = pressKey({key: 's', code: 'KeyS', metaKey: true});
             assert.strictEqual(sendCount, 1);
             assert.ok(prevented);
         });
 
-        it('still sends when caps lock turns the key into an upper case S', function () {
+        it('sends on the physical S key of non-Latin keyboard layouts', function () {
             setUpCreateMode();
-            pressKey({key: 'S', ctrlKey: true});
+            // on a Russian layout the S key produces "ы"
+            pressKey({key: 'ы', code: 'KeyS', ctrlKey: true});
             assert.strictEqual(sendCount, 1);
         });
 
         it('leaves Ctrl+S to the browser when viewing a document', function () {
             setUpCreateMode();
             PrivateBin.TopNav.hideCreateButtons();
-            const prevented = pressKey({key: 's', ctrlKey: true});
+            const prevented = pressKey({key: 's', code: 'KeyS', ctrlKey: true});
             assert.strictEqual(sendCount, 0);
             assert.ok(!prevented);
         });
 
-        it('ignores S without Ctrl/Cmd, with AltGr (Ctrl+Alt) or with Shift', function () {
+        it('ignores S without Ctrl/Cmd, with AltGr (Ctrl+Alt), with Shift, or other keys', function () {
             setUpCreateMode();
             const prevented = [
-                pressKey({key: 's'}),
-                pressKey({key: 's', ctrlKey: true, altKey: true}),
-                pressKey({key: 'S', ctrlKey: true, shiftKey: true})
+                pressKey({key: 's', code: 'KeyS'}),
+                pressKey({key: 's', code: 'KeyS', ctrlKey: true, altKey: true}),
+                pressKey({key: 'S', code: 'KeyS', ctrlKey: true, shiftKey: true}),
+                pressKey({key: 'd', code: 'KeyD', ctrlKey: true})
             ];
             assert.strictEqual(sendCount, 0);
-            assert.deepStrictEqual(prevented, [false, false, false]);
-        });
-
-        it('shows the Ctrl+S shortcut as tooltip and to assistive technology', function () {
-            setUpCreateMode();
-            const sendButton = query('#sendbutton');
-            assert.strictEqual(sendButton.getAttribute('title'), 'Ctrl+S');
-            assert.strictEqual(sendButton.getAttribute('aria-keyshortcuts'), 'Control+S');
-        });
-
-        it('shows Cmd+S on macOS', function () {
-            setUpCreateMode('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
-            const sendButton = query('#sendbutton');
-            assert.strictEqual(sendButton.getAttribute('title'), 'Cmd+S');
-            assert.strictEqual(sendButton.getAttribute('aria-keyshortcuts'), 'Meta+S');
-        });
-
-        it('translates the tooltip once the language has loaded', function () {
-            setUpCreateMode();
-            PrivateBin.I18n.reset('de', {Ctrl: 'Strg'});
-            document.dispatchEvent(new window.CustomEvent('languageLoaded'));
-            assert.strictEqual(query('#sendbutton').getAttribute('title'), 'Strg+S');
+            assert.deepStrictEqual(prevented, [false, false, false, false]);
         });
     });
 

+ 1 - 1
lib/Configuration.php

@@ -124,7 +124,7 @@ class Configuration
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-HvXCKJgjH+YD17gPj3c5d65is3nEaXMletNvdOarcoyGM+sbUpV75/pccSc8L1eDabEe+CYCvtFVqkrZm60MQw==',
+            'js/privatebin.js'       => 'sha512-HedKxoytOeYbMzjcN3x7/FW01Moba3U9SZI7Yi3V7CkO7B2wsvmgr8kGeurHW0hZssKxGA/3UZ+3tNdsO4o/UA==',
             'js/purify-3.4.12.js'    => 'sha512-Akf6HnAJZm0sWWWI4gp2GYff0NDnHUB02XJE5S7Hdq/Z5xtMjkuFacsDA8ZtViv1gi+onBxMhEMIaGyQeGxBng==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',

+ 1 - 1
tpl/bootstrap5.php

@@ -469,7 +469,7 @@ endif;
 					<li role="presentation" class="nav-item me-1"><a class="nav-link active" role="tab" id="messageedit" href="#"><?php echo I18n::_('Editor'); ?></a></li>
 					<li role="presentation" class="nav-item me-1"><a class="nav-link" role="tab" id="messagepreview" href="#"><?php echo I18n::_('Preview'); ?></a></li>
 					<li role="presentation" class="nav-item ms-auto">
-						<button id="sendbutton" type="button" tabindex="2" class="hidden btn btn-primary d-flex justify-content-center align-items-center gap-1">
+						<button id="sendbutton" type="button" tabindex="2" class="hidden btn btn-primary d-flex justify-content-center align-items-center gap-1" title="<?php echo I18n::_('Hit %s+s to create the document, storing it on %s', I18n::getCopyHotkey(), I18n::_($NAME)); ?>" aria-keyshortcuts="Control+S Meta+S">
 							<svg width="16" height="16" fill="currentColor" aria-hidden="true"><use href="img/bootstrap-icons.svg#cloud-upload" /></svg> <?php echo I18n::_('Create'), PHP_EOL; ?>
 						</button>
 					</li>