implement localization

This commit is contained in:
schlagmichdoch 2023-07-06 21:29:36 +02:00
parent 29b91cb17a
commit f50d7438b6
12 changed files with 883 additions and 286 deletions

View file

@ -39,62 +39,66 @@
<body translate="no"> <body translate="no">
<header class="row-reverse"> <header class="row-reverse">
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop"> <a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label" title="About PairDrop" aria-label="Open About PairDrop">
<svg class="icon"> <svg class="icon">
<use xlink:href="#info-outline" /> <use xlink:href="#info-outline" />
</svg> </svg>
</a> </a>
<div id="theme-wrapper"> <div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" > <div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" >
<svg class="icon"> <svg class="icon">
<use xlink:href="#icon-theme-auto" /> <use xlink:href="#icon-theme-auto" />
</svg> </svg>
</div> </div>
<div> <div>
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" > <div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title" title="Always Use Light-Theme" >
<svg class="icon"> <svg class="icon">
<use xlink:href="#icon-theme-light" /> <use xlink:href="#icon-theme-light" />
</svg> </svg>
</div> </div>
<div id="theme-dark" class="icon-button" title="Always Use Dark-Theme" > <div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title" title="Always Use Dark-Theme" >
<svg class="icon"> <svg class="icon">
<use xlink:href="#icon-theme-dark" /> <use xlink:href="#icon-theme-dark" />
</svg> </svg>
</div> </div>
</div> </div>
</div> </div>
<div id="notification" class="icon-button" title="Enable Notifications" hidden> <div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" title="Enable Notifications" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#notifications" /> <use xlink:href="#notifications" />
</svg> </svg>
</div> </div>
<div id="install" class="icon-button" title="Install PairDrop" hidden> <div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" title="Install PairDrop" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#homescreen" /> <use xlink:href="#homescreen" />
</svg> </svg>
</div> </div>
<div id="pair-device" class="icon-button" title="Pair Device" hidden> <div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title" title="Pair Device" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#pair-device-icon" /> <use xlink:href="#pair-device-icon" />
</svg> </svg>
</div> </div>
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden> <div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" title="Edit Paired Devices" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#edit-pair-devices-icon" /> <use xlink:href="#edit-pair-devices-icon" />
</svg> </svg>
</div> </div>
<div id="cancel-paste-mode" class="button" hidden>Done</div> <div id="cancel-paste-mode" class="button" data-i18n-key="header.done" data-i18n-attrs="text" hidden>Done</div>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center"> <div id="center">
<!-- Peers --> <!-- Peers -->
<div class="x-peers-filler"></div> <div class="x-peers-filler"></div>
<x-peers class="center"></x-peers> <x-peers class="center"></x-peers>
<x-no-peers> <x-no-peers data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg" data-drop-bg="Release to select recipient">
<h2>Open PairDrop on other devices to send files</h2> <h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text">Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div> <div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text">Pair devices to be discoverable on other networks</div>
</x-no-peers> </x-no-peers>
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message"> <x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg"
desktop="Click to send files or right click to send a message"
mobile="Tap to send files or long tap to send a message"
data-drop-peer="Release to send to peer"
data-drop-bg="Release to select recipient">
<p id="paste-filename"></p> <p id="paste-filename"></p>
</x-instructions> </x-instructions>
</div> </div>
@ -104,15 +108,21 @@
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
<div> <div>
<span>You are known as:</span> <span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div> <div id="display-name" data-i18n-key="footer.display-name" data-i18n-attrs="placeholder title" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon"> <svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" /> <use xlink:href="#edit-pen-icon" />
</svg> </svg>
</div> </div>
<div class="font-body2"> <div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span> <div>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span> <span data-i18n-key="footer.discovery-everyone" data-i18n-attrs="text">You can be discovered by everyone</span>
<span id="on-this-network" data-i18n-key="footer.on-this-network" data-i18n-attrs="text">on this network</span>
</div>
<div id="and-by-paired-devices" hidden>
<span id="and-by" data-i18n-key="footer.and-by" data-i18n-attrs="text">and by</span>
<span id="paired-devices" data-i18n-key="footer.paired-devices" data-i18n-attrs="text">paired devices</span>
</div>
</div> </div>
</footer> </footer>
<!-- Pair Device Dialog --> <!-- Pair Device Dialog -->
@ -120,10 +130,13 @@
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Pair Devices</h2> <h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text">Pair Devices</h2>
<div id="room-key-qr-code" class="center"></div> <div id="room-key-qr-code" class="center"></div>
<h1 id="room-key" class="center">000 000</h1> <h1 id="room-key" class="center">000 000</h1>
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div> <div id="pair-instructions" class="center text-center">
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text">Input this key on another device</span>
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
</div>
<hr> <hr>
<div id="key-input-container"> <div id="key-input-container">
<input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled> <input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
@ -133,10 +146,10 @@
<input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div> <div class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device to continue.</div>
<div class="center row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
<button class="button" type="button" close>Cancel</button> <button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -147,13 +160,21 @@
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Edit Paired Devices</h2> <h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text">Edit Paired Devices</h2>
<div class="paired-devices-wrapper"></div> <div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-empty" data-i18n-attrs="data-empty" data-empty="No paired devices."></div>
<div class="font-subheading center"> <div class="font-subheading center">
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p> <p>
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text">
Activate
</span>
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text">auto-accept</u>
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text">
to automatically accept all files sent from that device.
</span>
</p>
</div> </div>
<div class="center row-reverse"> <div class="center row-reverse">
<button class="button" type="button" close>Close</button> <button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -167,7 +188,7 @@
<div class="center column file-description"> <div class="center column file-description">
<div> <div>
<span class="display-name"></span> <span class="display-name"></span>
<span>would like to share</span> <span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text">would like to share</span>
</div> </div>
<div class="row file-name" > <div class="row file-name" >
<span class="file-stem"></span> <span class="file-stem"></span>
@ -179,8 +200,8 @@
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="center row-reverse"> <div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE">Decline</button> <button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -193,7 +214,7 @@
<div class="center column file-description"> <div class="center column file-description">
<div> <div>
<span class="display-name"></span> <span class="display-name"></span>
<span>has sent</span> <span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent</span>
</div> </div>
<div class="row file-name" > <div class="row file-name" >
<span class="file-stem"></span> <span class="file-stem"></span>
@ -204,9 +225,9 @@
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="center row-reverse"> <div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button> <button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button> <button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
<button class="button" close>Close</button> <button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -216,16 +237,16 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">Send Message</h2> <h2 class="text-center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text">Send Message</h2>
<div class="dialog-subheader text-center"> <div class="dialog-subheader text-center">
<span>Send a Message to</span> <span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text">Send a Message to</span>
<span class="display-name"></span> <span class="display-name"></span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button> <button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button> <button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -235,16 +256,16 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">Message Received</h2> <h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text">Message Received</h2>
<div class="text-center dialog-subheader"> <div class="text-center dialog-subheader">
<span class="display-name"></span> <span class="display-name"></span>
<span>has sent:</span> <span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent:</span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="center row-reverse"> <div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button>
<button id="close" class="button" title="ESCAPE">Close</button> <button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -253,9 +274,9 @@
<x-dialog id="base64-paste-dialog"> <x-dialog id="base64-paste-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button> <button class="button center" id="base64-paste-btn" title="Paste"></button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div> <div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button> <button class="button center" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</x-paper> </x-paper>
</x-background> </x-background>
</x-dialog> </x-dialog>
@ -266,7 +287,7 @@
<!-- About Page --> <!-- About Page -->
<x-about id="about" class="full center column"> <x-about id="about" class="full center column">
<header class="row-reverse fade-in"> <header class="row-reverse fade-in">
<a href="#" class="close icon-button" aria-label="Close About PairDrop"> <a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="text" aria-label="Close About PairDrop">
<svg class="icon"> <svg class="icon">
<use xlink:href="#close-icon" /> <use xlink:href="#close-icon" />
</svg> </svg>
@ -280,7 +301,7 @@
<h1>PairDrop</h1> <h1>PairDrop</h1>
<div class="font-subheading">v1.7.6</div> <div class="font-subheading">v1.7.6</div>
</div> </div>
<div class="font-subheading">The easiest way to transfer files across devices</div> <div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text">The easiest way to transfer files across devices</div>
<div class="row"> <div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer"> <a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
<svg class="icon"> <svg class="icon">
@ -373,6 +394,7 @@
</symbol> </symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/localization.js"></script>
<script src="scripts/theme.js"></script> <script src="scripts/theme.js"></script>
<script src="scripts/network.js"></script> <script src="scripts/network.js"></script>
<script src="scripts/ui.js"></script> <script src="scripts/ui.js"></script>

136
public/lang/en.json Normal file
View file

@ -0,0 +1,136 @@
{
"header": {
"about_title": "About PairDrop",
"about_aria-label": "Open About PairDrop",
"theme-auto_title": "Adapt Theme to System",
"theme-light_title": "Always Use Light-Theme",
"theme-dark_title": "Always Use Dark-Theme",
"notification_title": "Enable Notifications",
"install_title": "Install PairDrop",
"pair-device_title": "Pair Device",
"edit-paired-devices_title": "Edit Paired Devices",
"cancel-paste-mode": "Done"
},
"instructions": {
"no-peers_data-drop-bg": "Release to select recipient",
"no-peers-title": "Open PairDrop on other devices to send files",
"no-peers-subtitle": "Pair devices to be discoverable on other networks",
"x-instructions_desktop": "Click to send files or right click to send a message",
"x-instructions_mobile": "Tap to send files or long tap to send a message",
"x-instructions_data-drop-peer": "Release to send to peer",
"x-instructions_data-drop-bg": "Release to select recipient",
"click-to-send": "Click to send",
"tap-to-send": "Tap to send"
},
"footer": {
"known-as": "You are known as:",
"display-name_placeholder": "Loading...",
"display-name_title": "Edit your device name permanently",
"discovery-everyone": "You can be discovered by everyone",
"on-this-network": "on this network",
"and-by": "and by",
"paired-devices": "paired devices",
"traffic": "Traffic is",
"routed": "routed through the server",
"webrtc": "if WebRTC is not available."
},
"dialogs": {
"activate-paste-mode-base": "Open PairDrop on other devices to send",
"activate-paste-mode-and-other-files": "and {{count}} other files",
"activate-paste-mode-activate-paste-mode-shared-text": "shared text",
"pair-devices-title": "Pair Devices",
"input-key-on-this-device": "Input this key on another device",
"scan-qr-code": "or scan the QR-Code.",
"enter-key-from-another-device": "Enter key from another device to continue.",
"pair": "Pair",
"cancel": "Cancel",
"edit-paired-devices-title": "Edit Paired Devices",
"paired-devices-wrapper_data-empty": "No paired devices.",
"auto-accept-instructions-1": "Activate",
"auto-accept": "auto-accept",
"auto-accept-instructions-2": "to automatically accept all files sent from that device.",
"close": "Close",
"would-like-to-share": "would like to share",
"accept": "Accept",
"decline": "Decline",
"has-sent": "has sent:",
"share": "Share",
"download": "Download",
"send-message-title": "Send Message",
"send-message-to": "Send a Message to",
"send": "Send",
"receive-text-title": "Message Received",
"copy": "Copy",
"base64-processing": "Processing...",
"base64-tap-to-paste": "Tap here to paste {{type}}",
"base64-paste-to-send": "Paste here to send {{type}}",
"base64-text": "text",
"base64-files": "files",
"file-other-description-image": "and 1 other image",
"file-other-description-file": "and 1 other file",
"file-other-description-image-plural": "and {{count}} other images",
"file-other-description-file-plural": "and {{count}} other files",
"title-image": "Image",
"title-file": "File",
"title-image-plural": "Images",
"title-file-plural": "Files",
"receive-title": "{{descriptor}} Received",
"download-again": "Download again"
},
"about": {
"close-about-aria-label": "Close About PairDrop",
"claim": "The easiest way to transfer files across devices"
},
"notifications": {
"display-name-changed-permanently": "Display name is changed permanently.",
"display-name-changed-temporarily": "Display name is changed only for this session.",
"display-name-random-again": "Display name is randomly generated again.",
"download-successful": "{{descriptor}} downloaded successfully",
"pairing-tabs-error": "Pairing of two browser tabs is not possible.",
"pairing-success": "Devices paired successfully.",
"pairing-not-persistent": "Paired devices are not persistent.",
"pairing-key-invalid": "Key not valid",
"pairing-key-invalidated": "Key {{key}} invalidated.",
"pairing-cleared": "All Devices unpaired.",
"copied-to-clipboard": "Copied to clipboard",
"text-content-incorrect": "Text content is incorrect.",
"file-content-incorrect": "File content is incorrect.",
"clipboard-content-incorrect": "Clipboard content is incorrect.",
"notifications-enabled": "Notifications enabled.",
"link-received": "Link received by {{name}} - Click to open",
"message-received": "Message received by {{name}} - Click to copy",
"click-to-download": "Click to download",
"request-title": "{{name}} would like to transfer {{count}} {{descriptor}}",
"click-to-show": "Click to show",
"copied-text": "Copied text to clipboard",
"copied-text-error": "Writing to clipboard failed. Copy manually!",
"offline": "You are offline",
"online": "You are back online",
"connected": "Connected.",
"online-requirement": "You need to be online to pair devices.",
"connecting": "Connecting...",
"files-incorrect": "Files are incorrect.",
"file-transfer-completed": "File transfer completed.",
"ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once",
"message-transfer-completed": "Message transfer completed.",
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?",
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
"selected-peer-left": "Selected peer left."
},
"document-titles": {
"file-received": "File Received",
"file-received-plural": "{{count}} Files Received",
"file-transfer-requested": "File Transfer Requested",
"message-received": "Message Received",
"message-received-plural": "{{count}} Messages Received"
},
"peer-ui": {
"click-to-send-paste-mode": "Click to send {{descriptor}}",
"click-to-send": "Click to send files or right click to send a message",
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
"preparing": "Preparing...",
"waiting": "Waiting...",
"processing": "Processing...",
"transferring": "Transferring..."
}
}

View file

@ -0,0 +1,102 @@
class Localization {
constructor() {
Localization.defaultLocale = "en";
Localization.supportedLocales = ["en"];
Localization.translations = {};
const initialLocale = Localization.supportedOrDefault(Localization.browserLocales());
Localization.setLocale(initialLocale)
.then(_ => {
Localization.translatePage();
})
}
static isSupported(locale) {
return Localization.supportedLocales.indexOf(locale) > -1;
}
static supportedOrDefault(locales) {
return locales.find(Localization.isSupported) || Localization.defaultLocale;
}
static browserLocales() {
return navigator.languages.map(locale =>
locale.split("-")[0]
);
}
static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false;
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
if(!newTranslations) return false;
const firstTranslation = !Localization.locale
Localization.locale = newLocale;
Localization.translations = newTranslations;
if (firstTranslation) {
Events.fire("translation-loaded");
}
}
static async fetchTranslationsFor(newLocale) {
const response = await fetch(`lang/${newLocale}.json`)
if (response.redirected === true || response.status !== 200) return false;
return await response.json();
}
static translatePage() {
document
.querySelectorAll("[data-i18n-key]")
.forEach(element => Localization.translateElement(element));
}
static async translateElement(element) {
const key = element.getAttribute("data-i18n-key");
const attrs = element.getAttribute("data-i18n-attrs").split(" ");
for (let i in attrs) {
let attr = attrs[i];
if (attr === "text") {
element.innerText = await Localization.getTranslation(key);
} else {
element.attr = await Localization.getTranslation(key, attr);
}
}
}
static getTranslation(key, attr, data) {
const keys = key.split(".");
let translationCandidates = Localization.translations;
for (let i=0; i<keys.length-1; i++) {
translationCandidates = translationCandidates[keys[i]]
}
let lastKey = keys[keys.length-1];
if (attr) lastKey += "_" + attr;
let translation = translationCandidates[lastKey];
for (key in data) {
translation = translation.replace(`{{${key}}}`, data[key]);
}
return Localization.escapeHTML(translation);
}
static escapeHTML(unsafeText) {
let div = document.createElement('div');
div.innerText = unsafeText;
return div.innerHTML;
}
}

View file

@ -46,12 +46,12 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.'); if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
} }
_onPairDeviceInitiate() { _onPairDeviceInitiate() {
if (!this._isConnected()) { if (!this._isConnected()) {
Events.fire('notify-user', 'You need to be online to pair devices.'); Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement"));
return; return;
} }
this.send({ type: 'pair-device-initiate' }) this.send({ type: 'pair-device-initiate' })
@ -107,7 +107,7 @@ class ServerConnection {
Events.fire('pair-device-canceled', msg.roomKey); Events.fire('pair-device-canceled', msg.roomKey);
break; break;
case 'pair-device-join-key-rate-limit': case 'pair-device-join-key-rate-limit':
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.'); Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key"));
break; break;
case 'secret-room-deleted': case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret); Events.fire('secret-room-deleted', msg.roomSecret);
@ -183,7 +183,7 @@ class ServerConnection {
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..'); Events.fire('notify-user', Localization.getTranslation("notifications.connecting"));
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 1000); this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
@ -488,7 +488,7 @@ class Peer {
_abortTransfer() { _abortTransfer() {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
Events.fire('notify-user', 'Files are incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
this._filesReceived = []; this._filesReceived = [];
this._requestAccepted = null; this._requestAccepted = null;
this._digester = null; this._digester = null;
@ -546,7 +546,7 @@ class Peer {
this._chunker = null; this._chunker = null;
if (!this._filesQueue.length) { if (!this._filesQueue.length) {
this._busy = false; this._busy = false;
Events.fire('notify-user', 'File transfer completed.'); Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} else { } else {
this._dequeueFile(); this._dequeueFile();
@ -558,7 +558,7 @@ class Peer {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
this._filesRequested = null; this._filesRequested = null;
if (message.reason === 'ios-memory-limit') { if (message.reason === 'ios-memory-limit') {
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once"); Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit"));
} }
return; return;
} }
@ -568,7 +568,7 @@ class Peer {
} }
_onMessageTransferCompleted() { _onMessageTransferCompleted() {
Events.fire('notify-user', 'Message transfer completed.'); Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
} }
sendText(text) { sendText(text) {
@ -713,7 +713,7 @@ class RTCPeer extends Peer {
_onBeforeUnload(e) { _onBeforeUnload(e) {
if (this._busy) { if (this._busy) {
e.preventDefault(); e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?"; return Localization.getTranslation("notifications.unfinished-transfers-warning");
} }
} }

View file

@ -89,12 +89,12 @@ class PeersUI {
if (newDisplayName) { if (newDisplayName) {
PersistentStorage.set('editedDisplayName', newDisplayName) PersistentStorage.set('editedDisplayName', newDisplayName)
.then(_ => { .then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.'); Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-permanently"));
}) })
.catch(_ => { .catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead."); console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName); localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', 'Device name is changed only for this session.'); Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily"));
}) })
.finally(_ => { .finally(_ => {
Events.fire('self-display-name-changed', newDisplayName); Events.fire('self-display-name-changed', newDisplayName);
@ -105,10 +105,9 @@ class PeersUI {
.catch(_ => { .catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.") console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName'); localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
}) })
.finally(_ => { .finally(_ => {
Events.fire('notify-user', 'Device name is randomly generated again.'); Events.fire('notify-user', Localization.getTranslation("notifications.display-name-random-again"));
Events.fire('self-display-name-changed', ''); Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
}); });
@ -275,21 +274,22 @@ class PeersUI {
let descriptor; let descriptor;
let noPeersMessage; let noPeersMessage;
const openPairDrop = Localization.getTranslation("dialogs.activate-paste-mode-base");
const andOtherFiles = Localization.getTranslation("dialogs.activate-paste-mode-and-other-files", null, {count: files.length-1});
const sharedText = Localization.getTranslation("dialogs.activate-paste-mode-shared-text");
if (files.length === 1) { if (files.length === 1) {
descriptor = files[0].name; noPeersMessage = `${openPairDrop}<br><i>${files[0].name}</i>`;
noPeersMessage = `Open PairDrop on other devices to send<br><i>${descriptor}</i>`;
} else if (files.length > 1) { } else if (files.length > 1) {
descriptor = `${files[0].name} and ${files.length-1} other files`; noPeersMessage = `${openPairDrop}<br><i>${files[0].name}</i> ${andOtherFiles}`;
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
} else { } else {
descriptor = "shared text"; noPeersMessage = `${openPairDrop}<br>${sharedText}`;
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
} }
this.$xInstructions.querySelector('p').innerHTML = `<i>${descriptor}</i>`; this.$xInstructions.querySelector('p').innerHTML = noPeersMessage;
this.$xInstructions.querySelector('p').style.display = 'block'; this.$xInstructions.querySelector('p').style.display = 'block';
this.$xInstructions.setAttribute('desktop', `Click to send`); this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.click-to-send"));
this.$xInstructions.setAttribute('mobile', `Tap to send`); this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.tap-to-send"));
this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage; this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage;
@ -320,10 +320,10 @@ class PeersUI {
this.$xInstructions.querySelector('p').innerText = ''; this.$xInstructions.querySelector('p').innerText = '';
this.$xInstructions.querySelector('p').style.display = 'none'; this.$xInstructions.querySelector('p').style.display = 'none';
this.$xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message'); this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.x-instructions", "desktop"));
this.$xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message'); this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.x-instructions", "mobile"));
this.$xNoPeers.querySelector('h2').innerHTML = 'Open PairDrop on other devices to send files'; this.$xNoPeers.querySelector('h2').innerHTML = Localization.getTranslation("instructions.no-peers-title");
this.$cancelPasteModeBtn.setAttribute('hidden', ""); this.$cancelPasteModeBtn.setAttribute('hidden', "");
@ -368,9 +368,9 @@ class PeerUI {
let title; let title;
let input = ''; let input = '';
if (window.pasteMode.activated) { if (window.pasteMode.activated) {
title = `Click to send ${window.pasteMode.descriptor}`; title = Localization.getTranslation("peer-ui.click-to-send-paste-mode", null, {descriptor: window.pasteMode.descriptor});
} else { } else {
title = 'Click to send files or right click to send a message'; title = Localization.getTranslation("peer-ui.click-to-send");
input = '<input type="file" multiple>'; input = '<input type="file" multiple>';
} }
this.$el.innerHTML = ` this.$el.innerHTML = `
@ -392,7 +392,7 @@ class PeerUI {
<div class="name font-subheading"></div> <div class="name font-subheading"></div>
<div class="device-name font-body2"></div> <div class="device-name font-body2"></div>
<div class="status font-body2"></div> <div class="status font-body2"></div>
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span> <span class="connection-hash font-body2" title="${ Localization.getTranslation("peer-ui.connection-hash") }"></span>
</div> </div>
</label>`; </label>`;
@ -509,10 +509,23 @@ class PeerUI {
$progress.classList.remove('over50'); $progress.classList.remove('over50');
} }
if (progress < 1) { if (progress < 1) {
if (status !== this.currentStatus) {
let statusName = {
"prepare": Localization.getTranslation("peer-ui.preparing"),
"transfer": Localization.getTranslation("peer-ui.transferring"),
"process": Localization.getTranslation("peer-ui.processing"),
"wait": Localization.getTranslation("peer-ui.waiting")
}[status];
this.$el.setAttribute('status', status); this.$el.setAttribute('status', status);
this.$el.querySelector('.status').innerText = statusName;
this.currentStatus = status;
}
} else { } else {
this.$el.removeAttribute('status'); this.$el.removeAttribute('status');
this.$el.querySelector('.status').innerHTML = '';
progress = 0; progress = 0;
this.currentStatus = null;
} }
const degrees = `rotate(${360 * progress}deg)`; const degrees = `rotate(${360 * progress}deg)`;
$progress.style.setProperty('--progress', degrees); $progress.style.setProperty('--progress', degrees);
@ -595,7 +608,7 @@ class Dialog {
_onPeerDisconnected(peerId) { _onPeerDisconnected(peerId) {
if (this.isShown() && this.correspondingPeerId === peerId) { if (this.isShown() && this.correspondingPeerId === peerId) {
this.hide(); this.hide();
Events.fire('notify-user', 'Selected peer left.') Events.fire('notify-user', Localization.getTranslation("notifications.selected-peer-left"));
} }
} }
} }
@ -629,13 +642,17 @@ class ReceiveDialog extends Dialog {
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) { if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `; let fileOther;
if (files.length === 2) { if (files.length === 2) {
fileOtherText += imagesOnly ? 'image' : 'file'; fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image")
: Localization.getTranslation("dialogs.file-other-description-file");
} else { } else {
fileOtherText += imagesOnly ? 'images' : 'files'; fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1})
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
} }
this.$fileOther.innerText = fileOtherText; this.$fileOther.innerText = fileOther;
} }
const fileName = files[0].name; const fileName = files[0].name;
@ -727,11 +744,15 @@ class ReceiveFileDialog extends ReceiveDialog {
let descriptor, url, filenameDownload; let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
descriptor = imagesOnly ? 'Image' : 'File'; descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image")
: Localization.getTranslation("dialogs.title-file");
} else { } else {
descriptor = imagesOnly ? 'Images' : 'Files'; descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image-plural")
: Localization.getTranslation("dialogs.title-file-plural");
} }
this.$receiveTitle.innerText = `${descriptor} Received`; this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor});
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
if (canShare) { if (canShare) {
@ -781,7 +802,7 @@ class ReceiveFileDialog extends ReceiveDialog {
} }
} }
this.$downloadBtn.innerText = "Download"; this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download");
this.$downloadBtn.onclick = _ => { this.$downloadBtn.onclick = _ => {
if (downloadZipped) { if (downloadZipped) {
let tmpZipBtn = document.createElement("a"); let tmpZipBtn = document.createElement("a");
@ -793,17 +814,18 @@ class ReceiveFileDialog extends ReceiveDialog {
} }
if (!canShare) { if (!canShare) {
this.$downloadBtn.innerText = "Download again"; this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download-again");
} }
Events.fire('notify-user', `${descriptor} downloaded successfully`); Events.fire('notify-user', Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor}));
this.$downloadBtn.style.pointerEvents = "none"; this.$downloadBtn.style.pointerEvents = "none";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
}; };
document.title = files.length === 1 document.title = files.length === 1
? 'File received - PairDrop' ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop`
: `${files.length} Files received - PairDrop`; : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show(); this.show();
@ -891,7 +913,7 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`; document.title = `${ Localization.getTranslation("document-titles.file-transfer-requested") } - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show(); this.show();
} }
@ -1083,7 +1105,7 @@ class PairDeviceDialog extends Dialog {
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) { if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
this._cleanUp(); this._cleanUp();
this.hide(); this.hide();
Events.fire('notify-user', 'Pairing of two browser tabs is not possible.'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-tabs-error"));
return; return;
} }
@ -1129,7 +1151,7 @@ class PairDeviceDialog extends Dialog {
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName) PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
.then(_ => { .then(_ => {
Events.fire('notify-user', 'Devices paired successfully.'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-success"));
this._evaluateNumberRoomSecrets(); this._evaluateNumberRoomSecrets();
}) })
.finally(_ => { .finally(_ => {
@ -1137,13 +1159,13 @@ class PairDeviceDialog extends Dialog {
this.hide(); this.hide();
}) })
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-not-persistent"));
PersistentStorage.logBrowserNotCapable(); PersistentStorage.logBrowserNotCapable();
}); });
} }
_pairDeviceJoinKeyInvalid() { _pairDeviceJoinKeyInvalid() {
Events.fire('notify-user', 'Key not valid'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalid"));
} }
_pairDeviceCancel() { _pairDeviceCancel() {
@ -1153,7 +1175,7 @@ class PairDeviceDialog extends Dialog {
} }
_pairDeviceCanceled(roomKey) { _pairDeviceCanceled(roomKey) {
Events.fire('notify-user', `Key ${roomKey} invalidated.`); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalidated", null, {key: roomKey}));
} }
_cleanUp() { _cleanUp() {
@ -1260,7 +1282,7 @@ class EditPairedDevicesDialog extends Dialog {
PersistentStorage.clearRoomSecrets().finally(_ => { PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('room-secrets-deleted', roomSecrets); Events.fire('room-secrets-deleted', roomSecrets);
Events.fire('evaluate-number-room-secrets'); Events.fire('evaluate-number-room-secrets');
Events.fire('notify-user', 'All Devices unpaired.'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-cleared"));
this.hide(); this.hide();
}) })
}); });
@ -1415,14 +1437,14 @@ class ReceiveTextDialog extends Dialog {
_setDocumentTitleMessages() { _setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop' ? `${ Localization.getTranslation("document-titles.message-received") } - PairDrop`
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`; : `${ Localization.getTranslation("document-titles.message-received-plural", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`;
} }
async _onCopy() { async _onCopy() {
const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' ');
await navigator.clipboard.writeText(sanitizedText); await navigator.clipboard.writeText(sanitizedText);
Events.fire('notify-user', 'Copied to clipboard'); Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard"));
this.hide(); this.hide();
} }
@ -1449,13 +1471,13 @@ class Base64ZipDialog extends Dialog {
if (base64Text === "paste") { if (base64Text === "paste") {
// ?base64text=paste // ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard // base64 encoded string is ready to be pasted from clipboard
this.preparePasting("text"); this.preparePasting(Localization.getTranslation("dialogs.base64-text"));
} else if (base64Text === "hash") { } else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED // ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended) // base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash) this.processBase64Text(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect."); console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
@ -1465,7 +1487,7 @@ class Base64ZipDialog extends Dialog {
// base64 encoded string was part of url param (not recommended) // base64 encoded string was part of url param (not recommended)
this.processBase64Text(base64Text) this.processBase64Text(base64Text)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect."); console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
@ -1478,32 +1500,32 @@ class Base64ZipDialog extends Dialog {
// base64 encoded zip file is url hash which is never sent to the server // base64 encoded zip file is url hash which is never sent to the server
this.processBase64Zip(base64Hash) this.processBase64Zip(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'File content is incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect"));
console.log("File content incorrect."); console.log("File content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
} else { } else {
// ?base64zip=paste || ?base64zip=true // ?base64zip=paste || ?base64zip=true
this.preparePasting('files'); this.preparePasting(Localization.getTranslation("dialogs.base64-files"));
} }
} }
} }
_setPasteBtnToProcessing() { _setPasteBtnToProcessing() {
this.$pasteBtn.style.pointerEvents = "none"; this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing");
} }
preparePasting(type) { preparePasting(type) {
if (navigator.clipboard.readText) { if (navigator.clipboard.readText) {
this.$pasteBtn.innerText = `Tap here to paste ${type}`; this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", {type: type});
this._clickCallback = _ => this.processClipboard(type); this._clickCallback = _ => this.processClipboard(type);
this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
} else { } else {
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
this.$pasteBtn.setAttribute('hidden', ''); this.$pasteBtn.setAttribute('hidden', '');
this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); this.$fallbackTextarea.setAttribute('placeholder', Localization.getTranslation("dialogs.base64-paste-to-send", {type: type}));
this.$fallbackTextarea.removeAttribute('hidden'); this.$fallbackTextarea.removeAttribute('hidden');
this._inputCallback = _ => this.processInput(type); this._inputCallback = _ => this.processInput(type);
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
@ -1543,7 +1565,7 @@ class Base64ZipDialog extends Dialog {
await this.processBase64Zip(base64); await this.processBase64Zip(base64);
} }
} catch(_) { } catch(_) {
Events.fire('notify-user', 'Clipboard content is incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect"));
console.log("Clipboard content is incorrect.") console.log("Clipboard content is incorrect.")
} }
this.hide(); this.hide();
@ -1626,7 +1648,7 @@ class Notifications {
Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error');
return; return;
} }
Events.fire('notify-user', 'Notifications enabled.'); Events.fire('notify-user', Localization.getTranslation("notifications.notifications-enabled"));
this.$button.setAttribute('hidden', 1); this.$button.setAttribute('hidden', 1);
}); });
} }
@ -1661,10 +1683,10 @@ class Notifications {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName(); const peerDisplayName = $(peerId).ui._displayName();
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); const notification = this._notify(Localization.getTranslation("notifications.link-received", null, {name: peerDisplayName}), message);
this._bind(notification, _ => window.open(message, '_blank', null, true)); this._bind(notification, _ => window.open(message, '_blank', null, true));
} else { } else {
const notification = this._notify(`Message received by ${peerDisplayName} - Click to copy`, message); const notification = this._notify(Localization.getTranslation("notifications.message-received", null, {name: peerDisplayName}), message);
this._bind(notification, _ => this._copyText(message, notification)); this._bind(notification, _ => this._copyText(message, notification));
} }
} }
@ -1679,13 +1701,23 @@ class Notifications {
break; break;
} }
} }
let title = files[0].name; let title;
if (files.length >= 2) { if (files.length === 1) {
title += ` and ${files.length - 1} other `; title = `${files[0].name}`;
title += imagesOnly ? 'image' : 'file'; } else {
if (files.length > 2) title += "s"; let fileOther;
if (files.length === 2) {
fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image")
: Localization.getTranslation("dialogs.file-other-description-file");
} else {
fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1})
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
} }
const notification = this._notify(title, 'Click to download'); title = `${files[0].name} ${fileOther}`
}
const notification = this._notify(title, Localization.getTranslation("notifications.click-to-download"));
this._bind(notification, _ => this._download(notification)); this._bind(notification, _ => this._download(notification));
} }
} }
@ -1699,15 +1731,27 @@ class Notifications {
break; break;
} }
} }
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
let displayName = $(peerId).querySelector('.name').textContent let displayName = $(peerId).querySelector('.name').textContent
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
const notification = this._notify(title, 'Click to show'); let descriptor;
if (request.header.length === 1) {
descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image")
: Localization.getTranslation("dialogs.title-file");
} else {
descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image-plural")
: Localization.getTranslation("dialogs.title-file-plural");
}
let title = Localization.getTranslation("notifications.request-title", null, {
name: displayName,
count: request.header.length,
descriptor: descriptor.toLowerCase()
});
const notification = this._notify(title, Localization.getTranslation("notifications.click-to-show"));
} }
} }
@ -1719,10 +1763,9 @@ class Notifications {
_copyText(message, notification) { _copyText(message, notification) {
if (navigator.clipboard.writeText(message)) { if (navigator.clipboard.writeText(message)) {
notification.close(); notification.close();
this._notify('Copied text to clipboard'); this._notify(Localization.getTranslation("notifications.copied-text"));
} else { } else {
this._notify('Writing to clipboard failed. Copy manually!'); this._notify(Localization.getTranslation("notifications.copied-text-error"));
} }
} }
@ -1746,11 +1789,11 @@ class NetworkStatusUI {
} }
_showOfflineMessage() { _showOfflineMessage() {
Events.fire('notify-user', 'You are offline'); Events.fire('notify-user', Localization.getTranslation("notifications.offline"));
} }
_showOnlineMessage() { _showOnlineMessage() {
Events.fire('notify-user', 'You are back online'); Events.fire('notify-user', Localization.getTranslation("notifications.online"));
} }
} }
@ -2208,7 +2251,7 @@ class BrowserTabsConnector {
class PairDrop { class PairDrop {
constructor() { constructor() {
Events.on('load', _ => { Events.on('translation-loaded', _ => {
const server = new ServerConnection(); const server = new ServerConnection();
const peers = new PeersManager(server); const peers = new PeersManager(server);
const peersUI = new PeersUI(); const peersUI = new PeersUI();
@ -2232,6 +2275,7 @@ class PairDrop {
const persistentStorage = new PersistentStorage(); const persistentStorage = new PersistentStorage();
const pairDrop = new PairDrop(); const pairDrop = new PairDrop();
const localization = new Localization();
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {

View file

@ -442,7 +442,7 @@ x-no-peers::before {
} }
x-no-peers[drop-bg]::before { x-no-peers[drop-bg]::before {
content: "Release to select recipient"; content: attr(data-drop-bg);
} }
x-no-peers[drop-bg] * { x-no-peers[drop-bg] * {
@ -553,22 +553,6 @@ x-peer[status] x-icon {
white-space: nowrap; white-space: nowrap;
} }
x-peer[status=transfer] .status:before {
content: 'Transferring...';
}
x-peer[status=prepare] .status:before {
content: 'Preparing...';
}
x-peer[status=wait] .status:before {
content: 'Waiting...';
}
x-peer[status=process] .status:before {
content: 'Processing...';
}
x-peer:not([status]) .status, x-peer:not([status]) .status,
x-peer[status] .device-name { x-peer[status] .device-name {
display: none; display: none;
@ -626,11 +610,13 @@ footer .font-body2 {
#on-this-network { #on-this-network {
border-bottom: solid 4px var(--primary-color); border-bottom: solid 4px var(--primary-color);
padding-bottom: 1px; padding-bottom: 1px;
word-break: keep-all;
} }
#paired-devices { #paired-devices {
border-bottom: solid 4px var(--paired-device-color); border-bottom: solid 4px var(--paired-device-color);
padding-bottom: 1px; padding-bottom: 1px;
word-break: keep-all;
} }
#display-name { #display-name {
@ -723,10 +709,6 @@ x-dialog a {
color: var(--primary-color); color: var(--primary-color);
} }
x-dialog .font-subheading {
margin-bottom: 5px;
}
/* Pair Devices Dialog */ /* Pair Devices Dialog */
#key-input-container { #key-input-container {
@ -774,6 +756,10 @@ x-dialog .font-subheading {
margin: 16px; margin: 16px;
} }
#pair-instructions {
flex-direction: column;
}
x-dialog hr { x-dialog hr {
margin: 40px -24px 30px -24px; margin: 40px -24px 30px -24px;
border: solid 1.25px var(--border-color); border: solid 1.25px var(--border-color);
@ -785,7 +771,7 @@ x-dialog hr {
/* Edit Paired Devices Dialog */ /* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before { .paired-devices-wrapper:empty:before {
content: "No paired devices."; content: attr(data-empty);
} }
.paired-devices-wrapper:empty { .paired-devices-wrapper:empty {
@ -1288,11 +1274,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before {
} }
x-instructions[drop-peer]:before { x-instructions[drop-peer]:before {
content: "Release to send to peer"; content: attr(data-drop-peer);
} }
x-instructions[drop-bg]:not([drop-peer]):before { x-instructions[drop-bg]:not([drop-peer]):before {
content: "Release to select recipient"; content: attr(data-drop-bg);
} }
x-instructions p { x-instructions p {

View file

@ -39,62 +39,66 @@
<body translate="no"> <body translate="no">
<header class="row-reverse"> <header class="row-reverse">
<a href="#about" class="icon-button" title="About PairDrop" aria-label="Open About PairDrop"> <a href="#about" class="icon-button" data-i18n-key="header.about" data-i18n-attrs="title aria-label" title="About PairDrop" aria-label="Open About PairDrop">
<svg class="icon"> <svg class="icon">
<use xlink:href="#info-outline" /> <use xlink:href="#info-outline" />
</svg> </svg>
</a> </a>
<div id="theme-wrapper"> <div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" title="Adapt Theme to System" > <div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" >
<svg class="icon"> <svg class="icon">
<use xlink:href="#icon-theme-auto" /> <use xlink:href="#icon-theme-auto" />
</svg> </svg>
</div> </div>
<div> <div>
<div id="theme-light" class="icon-button" title="Always Use Light-Theme" > <div id="theme-light" class="icon-button" data-i18n-key="header.theme-light" data-i18n-attrs="title" title="Always Use Light-Theme" >
<svg class="icon"> <svg class="icon">
<use xlink:href="#icon-theme-light" /> <use xlink:href="#icon-theme-light" />
</svg> </svg>
</div> </div>
<div id="theme-dark" class="icon-button" title="Always Use Dark-Theme" > <div id="theme-dark" class="icon-button" data-i18n-key="header.theme-dark" data-i18n-attrs="title" title="Always Use Dark-Theme" >
<svg class="icon"> <svg class="icon">
<use xlink:href="#icon-theme-dark" /> <use xlink:href="#icon-theme-dark" />
</svg> </svg>
</div> </div>
</div> </div>
</div> </div>
<div id="notification" class="icon-button" title="Enable Notifications" hidden> <div id="notification" class="icon-button" data-i18n-key="header.notification" data-i18n-attrs="title" title="Enable Notifications" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#notifications" /> <use xlink:href="#notifications" />
</svg> </svg>
</div> </div>
<div id="install" class="icon-button" title="Install PairDrop" hidden> <div id="install" class="icon-button" data-i18n-key="header.install" data-i18n-attrs="title" title="Install PairDrop" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#homescreen" /> <use xlink:href="#homescreen" />
</svg> </svg>
</div> </div>
<div id="pair-device" class="icon-button" title="Pair Device" hidden> <div id="pair-device" class="icon-button" data-i18n-key="header.pair-device" data-i18n-attrs="title" title="Pair Device" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#pair-device-icon" /> <use xlink:href="#pair-device-icon" />
</svg> </svg>
</div> </div>
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden> <div id="edit-paired-devices" class="icon-button" data-i18n-key="header.edit-paired-devices" data-i18n-attrs="title" title="Edit Paired Devices" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#edit-pair-devices-icon" /> <use xlink:href="#edit-pair-devices-icon" />
</svg> </svg>
</div> </div>
<div id="cancel-paste-mode" class="button" hidden>Done</div> <div id="cancel-paste-mode" class="button" data-i18n-key="header.done" data-i18n-attrs="text" hidden>Done</div>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center"> <div id="center">
<!-- Peers --> <!-- Peers -->
<div class="x-peers-filler"></div> <div class="x-peers-filler"></div>
<x-peers class="center"></x-peers> <x-peers class="center"></x-peers>
<x-no-peers> <x-no-peers data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg" data-drop-bg="Release to select recipient">
<h2>Open PairDrop on other devices to send files</h2> <h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text">Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div> <div data-i18n-key="instructions.no-peers-subtitle" data-i18n-attrs="text">Pair devices to be discoverable on other networks</div>
</x-no-peers> </x-no-peers>
<x-instructions desktop="Click to send files or right click to send a message" mobile="Tap to send files or long tap to send a message"> <x-instructions data-i18n-key="instructions.x-instructions" data-i18n-attrs="desktop mobile data-drop-peer data-drop-bg"
desktop="Click to send files or right click to send a message"
mobile="Tap to send files or long tap to send a message"
data-drop-peer="Release to send to peer"
data-drop-bg="Release to select recipient">
<p id="paste-filename"></p> <p id="paste-filename"></p>
</x-instructions> </x-instructions>
</div> </div>
@ -104,18 +108,26 @@
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
<div> <div>
<span>You are known as:</span> <span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div> <div id="display-name" data-i18n-key="footer.display-name" data-i18n-attrs="placeholder title" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon"> <svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" /> <use xlink:href="#edit-pen-icon" />
</svg> </svg>
</div> </div>
<div class="font-body2"> <div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span> <div>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span> <span data-i18n-key="footer.discovery-everyone" data-i18n-attrs="text">You can be discovered by everyone</span>
<span id="on-this-network" data-i18n-key="footer.on-this-network" data-i18n-attrs="text">on this network</span>
</div>
<div id="and-by-paired-devices" hidden>
<span id="and-by" data-i18n-key="footer.and-by" data-i18n-attrs="text">and by</span>
<span id="paired-devices" data-i18n-key="footer.paired-devices" data-i18n-attrs="text">paired devices</span>
</div>
</div> </div>
<div id="websocket-fallback"> <div id="websocket-fallback">
<span>Traffic is <span>routed through the server</span> if WebRTC is not available.</span> <span data-i18n-key="footer.traffic" data-i18n-attrs="text">Traffic is</span>
<span data-i18n-key="footer.routed" data-i18n-attrs="text">routed through the server</span>
<span data-i18n-key="footer.webrtc" data-i18n-attrs="text">if WebRTC is not available.</span>
</div> </div>
</footer> </footer>
<!-- Pair Device Dialog --> <!-- Pair Device Dialog -->
@ -123,10 +135,13 @@
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Pair Devices</h2> <h2 class="center" data-i18n-key="dialogs.pair-devices-title" data-i18n-attrs="text">Pair Devices</h2>
<div id="room-key-qr-code" class="center"></div> <div id="room-key-qr-code" class="center"></div>
<h1 id="room-key" class="center">000 000</h1> <h1 id="room-key" class="center">000 000</h1>
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div> <div id="pair-instructions" class="center text-center">
<span class="font-subheading" data-i18n-key="dialogs.input-key-on-this-device" data-i18n-attrs="text">Input this key on another device</span>
<span class="font-subheading" data-i18n-key="dialogs.scan-qr-code" data-i18n-attrs="text">or scan the QR-Code.</span>
</div>
<hr> <hr>
<div id="key-input-container"> <div id="key-input-container">
<input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled> <input type="tel" class="textarea center" aria-label="pair-key-1" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
@ -136,10 +151,10 @@
<input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" aria-label="pair-key-5" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div> <div class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device to continue.</div>
<div class="center row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
<button class="button" type="button" close>Cancel</button> <button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -150,13 +165,21 @@
<form action="#"> <form action="#">
<x-background class="full center text-center"> <x-background class="full center text-center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Edit Paired Devices</h2> <h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text">Edit Paired Devices</h2>
<div class="paired-devices-wrapper"></div> <div class="paired-devices-wrapper" data-i18n-key="dialogs.paired-devices-empty" data-i18n-attrs="data-empty" data-empty="No paired devices."></div>
<div class="font-subheading center"> <div class="font-subheading center">
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p> <p>
<span data-i18n-key="dialogs.auto-accept-instructions-1" data-i18n-attrs="text">
Activate
</span>
<u data-i18n-key="dialogs.auto-accept" data-i18n-attrs="text">auto-accept</u>
<span data-i18n-key="dialogs.auto-accept-instructions-2" data-i18n-attrs="text">
to automatically accept all files sent from that device.
</span>
</p>
</div> </div>
<div class="center row-reverse"> <div class="center row-reverse">
<button class="button" type="button" close>Close</button> <button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -170,7 +193,7 @@
<div class="center column file-description"> <div class="center column file-description">
<div> <div>
<span class="display-name"></span> <span class="display-name"></span>
<span>would like to share</span> <span data-i18n-key="dialogs.would-like-to-share" data-i18n-attrs="text">would like to share</span>
</div> </div>
<div class="row file-name" > <div class="row file-name" >
<span class="file-stem"></span> <span class="file-stem"></span>
@ -182,8 +205,8 @@
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="center row-reverse"> <div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE">Decline</button> <button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -196,7 +219,7 @@
<div class="center column file-description"> <div class="center column file-description">
<div> <div>
<span class="display-name"></span> <span class="display-name"></span>
<span>has sent</span> <span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent</span>
</div> </div>
<div class="row file-name" > <div class="row file-name" >
<span class="file-stem"></span> <span class="file-stem"></span>
@ -207,9 +230,9 @@
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="center row-reverse"> <div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button> <button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button> <button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
<button class="button" close>Close</button> <button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -219,16 +242,16 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">Send Message</h2> <h2 class="text-center" data-i18n-key="dialogs.send-message-title" data-i18n-attrs="text">Send Message</h2>
<div class="dialog-subheader text-center"> <div class="dialog-subheader text-center">
<span>Send a Message to</span> <span data-i18n-key="dialogs.send-message-to" data-i18n-attrs="text">Send a Message to</span>
<span class="display-name"></span> <span class="display-name"></span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button> <button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button> <button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -238,16 +261,16 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">Message Received</h2> <h2 class="text-center" data-i18n-key="dialogs.receive-text-title" data-i18n-attrs="text">Message Received</h2>
<div class="text-center dialog-subheader"> <div class="text-center dialog-subheader">
<span class="display-name"></span> <span class="display-name"></span>
<span>has sent:</span> <span data-i18n-key="dialogs.has-sent" data-i18n-attrs="text">has sent:</span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="center row-reverse"> <div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button>
<button id="close" class="button" title="ESCAPE">Close</button> <button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -256,9 +279,9 @@
<x-dialog id="base64-paste-dialog"> <x-dialog id="base64-paste-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button> <button class="button center" id="base64-paste-btn" title="Paste"></button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div> <div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button> <button class="button center" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</x-paper> </x-paper>
</x-background> </x-background>
</x-dialog> </x-dialog>
@ -269,7 +292,7 @@
<!-- About Page --> <!-- About Page -->
<x-about id="about" class="full center column"> <x-about id="about" class="full center column">
<header class="row-reverse fade-in"> <header class="row-reverse fade-in">
<a href="#" class="close icon-button" aria-label="Close About PairDrop"> <a href="#" class="close icon-button" data-i18n-key="about.close-about" data-i18n-attrs="text" aria-label="Close About PairDrop">
<svg class="icon"> <svg class="icon">
<use xlink:href="#close-icon" /> <use xlink:href="#close-icon" />
</svg> </svg>
@ -283,7 +306,7 @@
<h1>PairDrop</h1> <h1>PairDrop</h1>
<div class="font-subheading">v1.7.6</div> <div class="font-subheading">v1.7.6</div>
</div> </div>
<div class="font-subheading">The easiest way to transfer files across devices</div> <div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text">The easiest way to transfer files across devices</div>
<div class="row"> <div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer"> <a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
<svg class="icon"> <svg class="icon">
@ -376,6 +399,7 @@
</symbol> </symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/localization.js"></script>
<script src="scripts/theme.js"></script> <script src="scripts/theme.js"></script>
<script src="scripts/network.js"></script> <script src="scripts/network.js"></script>
<script src="scripts/ui.js"></script> <script src="scripts/ui.js"></script>

View file

@ -0,0 +1,136 @@
{
"header": {
"about_title": "About PairDrop",
"about_aria-label": "Open About PairDrop",
"theme-auto_title": "Adapt Theme to System",
"theme-light_title": "Always Use Light-Theme",
"theme-dark_title": "Always Use Dark-Theme",
"notification_title": "Enable Notifications",
"install_title": "Install PairDrop",
"pair-device_title": "Pair Device",
"edit-paired-devices_title": "Edit Paired Devices",
"cancel-paste-mode": "Done"
},
"instructions": {
"no-peers_data-drop-bg": "Release to select recipient",
"no-peers-title": "Open PairDrop on other devices to send files",
"no-peers-subtitle": "Pair devices to be discoverable on other networks",
"x-instructions_desktop": "Click to send files or right click to send a message",
"x-instructions_mobile": "Tap to send files or long tap to send a message",
"x-instructions_data-drop-peer": "Release to send to peer",
"x-instructions_data-drop-bg": "Release to select recipient",
"click-to-send": "Click to send",
"tap-to-send": "Tap to send"
},
"footer": {
"known-as": "You are known as:",
"display-name_placeholder": "Loading...",
"display-name_title": "Edit your device name permanently",
"discovery-everyone": "You can be discovered by everyone",
"on-this-network": "on this network",
"and-by": "and by",
"paired-devices": "paired devices",
"traffic": "Traffic is",
"routed": "routed through the server",
"webrtc": "if WebRTC is not available."
},
"dialogs": {
"activate-paste-mode-base": "Open PairDrop on other devices to send",
"activate-paste-mode-and-other-files": "and {{count}} other files",
"activate-paste-mode-activate-paste-mode-shared-text": "shared text",
"pair-devices-title": "Pair Devices",
"input-key-on-this-device": "Input this key on another device",
"scan-qr-code": "or scan the QR-Code.",
"enter-key-from-another-device": "Enter key from another device to continue.",
"pair": "Pair",
"cancel": "Cancel",
"edit-paired-devices-title": "Edit Paired Devices",
"paired-devices-wrapper_data-empty": "No paired devices.",
"auto-accept-instructions-1": "Activate",
"auto-accept": "auto-accept",
"auto-accept-instructions-2": "to automatically accept all files sent from that device.",
"close": "Close",
"would-like-to-share": "would like to share",
"accept": "Accept",
"decline": "Decline",
"has-sent": "has sent:",
"share": "Share",
"download": "Download",
"send-message-title": "Send Message",
"send-message-to": "Send a Message to",
"send": "Send",
"receive-text-title": "Message Received",
"copy": "Copy",
"base64-processing": "Processing...",
"base64-tap-to-paste": "Tap here to paste {{type}}",
"base64-paste-to-send": "Paste here to send {{type}}",
"base64-text": "text",
"base64-files": "files",
"file-other-description-image": "and 1 other image",
"file-other-description-file": "and 1 other file",
"file-other-description-image-plural": "and {{count}} other images",
"file-other-description-file-plural": "and {{count}} other files",
"title-image": "Image",
"title-file": "File",
"title-image-plural": "Images",
"title-file-plural": "Files",
"receive-title": "{{descriptor}} Received",
"download-again": "Download again"
},
"about": {
"close-about-aria-label": "Close About PairDrop",
"claim": "The easiest way to transfer files across devices"
},
"notifications": {
"display-name-changed-permanently": "Display name is changed permanently.",
"display-name-changed-temporarily": "Display name is changed only for this session.",
"display-name-random-again": "Display name is randomly generated again.",
"download-successful": "{{descriptor}} downloaded successfully",
"pairing-tabs-error": "Pairing of two browser tabs is not possible.",
"pairing-success": "Devices paired successfully.",
"pairing-not-persistent": "Paired devices are not persistent.",
"pairing-key-invalid": "Key not valid",
"pairing-key-invalidated": "Key {{key}} invalidated.",
"pairing-cleared": "All Devices unpaired.",
"copied-to-clipboard": "Copied to clipboard",
"text-content-incorrect": "Text content is incorrect.",
"file-content-incorrect": "File content is incorrect.",
"clipboard-content-incorrect": "Clipboard content is incorrect.",
"notifications-enabled": "Notifications enabled.",
"link-received": "Link received by {{name}} - Click to open",
"message-received": "Message received by {{name}} - Click to copy",
"click-to-download": "Click to download",
"request-title": "{{name}} would like to transfer {{count}} {{descriptor}}",
"click-to-show": "Click to show",
"copied-text": "Copied text to clipboard",
"copied-text-error": "Writing to clipboard failed. Copy manually!",
"offline": "You are offline",
"online": "You are back online",
"connected": "Connected.",
"online-requirement": "You need to be online to pair devices.",
"connecting": "Connecting...",
"files-incorrect": "Files are incorrect.",
"file-transfer-completed": "File transfer completed.",
"ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once",
"message-transfer-completed": "Message transfer completed.",
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?",
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
"selected-peer-left": "Selected peer left."
},
"document-titles": {
"file-received": "File Received",
"file-received-plural": "{{count}} Files Received",
"file-transfer-requested": "File Transfer Requested",
"message-received": "Message Received",
"message-received-plural": "{{count}} Messages Received"
},
"peer-ui": {
"click-to-send-paste-mode": "Click to send {{descriptor}}",
"click-to-send": "Click to send files or right click to send a message",
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
"preparing": "Preparing...",
"waiting": "Waiting...",
"processing": "Processing...",
"transferring": "Transferring..."
}
}

View file

@ -0,0 +1,102 @@
class Localization {
constructor() {
Localization.defaultLocale = "en";
Localization.supportedLocales = ["en"];
Localization.translations = {};
const initialLocale = Localization.supportedOrDefault(Localization.browserLocales());
Localization.setLocale(initialLocale)
.then(_ => {
Localization.translatePage();
})
}
static isSupported(locale) {
return Localization.supportedLocales.indexOf(locale) > -1;
}
static supportedOrDefault(locales) {
return locales.find(Localization.isSupported) || Localization.defaultLocale;
}
static browserLocales() {
return navigator.languages.map(locale =>
locale.split("-")[0]
);
}
static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false;
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
if(!newTranslations) return false;
const firstTranslation = !Localization.locale
Localization.locale = newLocale;
Localization.translations = newTranslations;
if (firstTranslation) {
Events.fire("translation-loaded");
}
}
static async fetchTranslationsFor(newLocale) {
const response = await fetch(`lang/${newLocale}.json`)
if (response.redirected === true || response.status !== 200) return false;
return await response.json();
}
static translatePage() {
document
.querySelectorAll("[data-i18n-key]")
.forEach(element => Localization.translateElement(element));
}
static async translateElement(element) {
const key = element.getAttribute("data-i18n-key");
const attrs = element.getAttribute("data-i18n-attrs").split(" ");
for (let i in attrs) {
let attr = attrs[i];
if (attr === "text") {
element.innerText = await Localization.getTranslation(key);
} else {
element.attr = await Localization.getTranslation(key, attr);
}
}
}
static getTranslation(key, attr, data) {
const keys = key.split(".");
let translationCandidates = Localization.translations;
for (let i=0; i<keys.length-1; i++) {
translationCandidates = translationCandidates[keys[i]]
}
let lastKey = keys[keys.length-1];
if (attr) lastKey += "_" + attr;
let translation = translationCandidates[lastKey];
for (key in data) {
translation = translation.replace(`{{${key}}}`, data[key]);
}
return Localization.escapeHTML(translation);
}
static escapeHTML(unsafeText) {
let div = document.createElement('div');
div.innerText = unsafeText;
return div.innerHTML;
}
}

View file

@ -44,12 +44,12 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.'); if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
} }
_onPairDeviceInitiate() { _onPairDeviceInitiate() {
if (!this._isConnected()) { if (!this._isConnected()) {
Events.fire('notify-user', 'You need to be online to pair devices.'); Events.fire('notify-user', Localization.getTranslation("notifications.online-requirement"));
return; return;
} }
this.send({ type: 'pair-device-initiate' }) this.send({ type: 'pair-device-initiate' })
@ -105,7 +105,7 @@ class ServerConnection {
Events.fire('pair-device-canceled', msg.roomKey); Events.fire('pair-device-canceled', msg.roomKey);
break; break;
case 'pair-device-join-key-rate-limit': case 'pair-device-join-key-rate-limit':
Events.fire('notify-user', 'Rate limit reached. Wait 10 seconds and try again.'); Events.fire('notify-user', Localization.getTranslation("notifications.rate-limit-join-key"));
break; break;
case 'secret-room-deleted': case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret); Events.fire('secret-room-deleted', msg.roomSecret);
@ -200,7 +200,7 @@ class ServerConnection {
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..'); Events.fire('notify-user', Localization.getTranslation("notifications.connecting"));
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 1000); this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
@ -505,7 +505,7 @@ class Peer {
_abortTransfer() { _abortTransfer() {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
Events.fire('notify-user', 'Files are incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect"));
this._filesReceived = []; this._filesReceived = [];
this._requestAccepted = null; this._requestAccepted = null;
this._digester = null; this._digester = null;
@ -546,7 +546,7 @@ class Peer {
this._abortTransfer(); this._abortTransfer();
} }
// include for compatibility with Snapdrop for Android app // include for compatibility with 'Snapdrop & PairDrop for Android' app
Events.fire('file-received', fileBlob); Events.fire('file-received', fileBlob);
this._filesReceived.push(fileBlob); this._filesReceived.push(fileBlob);
@ -563,7 +563,8 @@ class Peer {
this._chunker = null; this._chunker = null;
if (!this._filesQueue.length) { if (!this._filesQueue.length) {
this._busy = false; this._busy = false;
Events.fire('notify-user', 'File transfer completed.'); Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} else { } else {
this._dequeueFile(); this._dequeueFile();
} }
@ -574,7 +575,7 @@ class Peer {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
this._filesRequested = null; this._filesRequested = null;
if (message.reason === 'ios-memory-limit') { if (message.reason === 'ios-memory-limit') {
Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once"); Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit"));
} }
return; return;
} }
@ -584,7 +585,7 @@ class Peer {
} }
_onMessageTransferCompleted() { _onMessageTransferCompleted() {
Events.fire('notify-user', 'Message transfer completed.'); Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
} }
sendText(text) { sendText(text) {
@ -729,7 +730,7 @@ class RTCPeer extends Peer {
_onBeforeUnload(e) { _onBeforeUnload(e) {
if (this._busy) { if (this._busy) {
e.preventDefault(); e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?"; return Localization.getTranslation("notifications.unfinished-transfers-warning");
} }
} }

View file

@ -89,12 +89,12 @@ class PeersUI {
if (newDisplayName) { if (newDisplayName) {
PersistentStorage.set('editedDisplayName', newDisplayName) PersistentStorage.set('editedDisplayName', newDisplayName)
.then(_ => { .then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.'); Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-permanently"));
}) })
.catch(_ => { .catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead."); console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName); localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', 'Device name is changed only for this session.'); Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily"));
}) })
.finally(_ => { .finally(_ => {
Events.fire('self-display-name-changed', newDisplayName); Events.fire('self-display-name-changed', newDisplayName);
@ -105,10 +105,9 @@ class PeersUI {
.catch(_ => { .catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.") console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName'); localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
}) })
.finally(_ => { .finally(_ => {
Events.fire('notify-user', 'Device name is randomly generated again.'); Events.fire('notify-user', Localization.getTranslation("notifications.display-name-random-again"));
Events.fire('self-display-name-changed', ''); Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
}); });
@ -275,21 +274,22 @@ class PeersUI {
let descriptor; let descriptor;
let noPeersMessage; let noPeersMessage;
const openPairDrop = Localization.getTranslation("dialogs.activate-paste-mode-base");
const andOtherFiles = Localization.getTranslation("dialogs.activate-paste-mode-and-other-files", null, {count: files.length-1});
const sharedText = Localization.getTranslation("dialogs.activate-paste-mode-shared-text");
if (files.length === 1) { if (files.length === 1) {
descriptor = files[0].name; noPeersMessage = `${openPairDrop}<br><i>${files[0].name}</i>`;
noPeersMessage = `Open PairDrop on other devices to send<br><i>${descriptor}</i>`;
} else if (files.length > 1) { } else if (files.length > 1) {
descriptor = `${files[0].name} and ${files.length-1} other files`; noPeersMessage = `${openPairDrop}<br><i>${files[0].name}</i> ${andOtherFiles}`;
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
} else { } else {
descriptor = "shared text"; noPeersMessage = `${openPairDrop}<br>${sharedText}`;
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
} }
this.$xInstructions.querySelector('p').innerHTML = `<i>${descriptor}</i>`; this.$xInstructions.querySelector('p').innerHTML = noPeersMessage;
this.$xInstructions.querySelector('p').style.display = 'block'; this.$xInstructions.querySelector('p').style.display = 'block';
this.$xInstructions.setAttribute('desktop', `Click to send`); this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.click-to-send"));
this.$xInstructions.setAttribute('mobile', `Tap to send`); this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.tap-to-send"));
this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage; this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage;
@ -320,10 +320,10 @@ class PeersUI {
this.$xInstructions.querySelector('p').innerText = ''; this.$xInstructions.querySelector('p').innerText = '';
this.$xInstructions.querySelector('p').style.display = 'none'; this.$xInstructions.querySelector('p').style.display = 'none';
this.$xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message'); this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.x-instructions", "desktop"));
this.$xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message'); this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.x-instructions", "mobile"));
this.$xNoPeers.querySelector('h2').innerHTML = 'Open PairDrop on other devices to send files'; this.$xNoPeers.querySelector('h2').innerHTML = Localization.getTranslation("instructions.no-peers-title");
this.$cancelPasteModeBtn.setAttribute('hidden', ""); this.$cancelPasteModeBtn.setAttribute('hidden', "");
@ -368,9 +368,9 @@ class PeerUI {
let title; let title;
let input = ''; let input = '';
if (window.pasteMode.activated) { if (window.pasteMode.activated) {
title = `Click to send ${window.pasteMode.descriptor}`; title = Localization.getTranslation("peer-ui.click-to-send-paste-mode", null, {descriptor: window.pasteMode.descriptor});
} else { } else {
title = 'Click to send files or right click to send a message'; title = Localization.getTranslation("peer-ui.click-to-send");
input = '<input type="file" multiple>'; input = '<input type="file" multiple>';
} }
this.$el.innerHTML = ` this.$el.innerHTML = `
@ -392,7 +392,7 @@ class PeerUI {
<div class="name font-subheading"></div> <div class="name font-subheading"></div>
<div class="device-name font-body2"></div> <div class="device-name font-body2"></div>
<div class="status font-body2"></div> <div class="status font-body2"></div>
<span class="connection-hash font-body2" title="To verify the security of the end-to-end encryption, compare this security number on both devices"></span> <span class="connection-hash font-body2" title="${ Localization.getTranslation("peer-ui.connection-hash") }"></span>
</div> </div>
</label>`; </label>`;
@ -510,10 +510,23 @@ class PeerUI {
$progress.classList.remove('over50'); $progress.classList.remove('over50');
} }
if (progress < 1) { if (progress < 1) {
if (status !== this.currentStatus) {
let statusName = {
"prepare": Localization.getTranslation("peer-ui.preparing"),
"transfer": Localization.getTranslation("peer-ui.transferring"),
"process": Localization.getTranslation("peer-ui.processing"),
"wait": Localization.getTranslation("peer-ui.waiting")
}[status];
this.$el.setAttribute('status', status); this.$el.setAttribute('status', status);
this.$el.querySelector('.status').innerText = statusName;
this.currentStatus = status;
}
} else { } else {
this.$el.removeAttribute('status'); this.$el.removeAttribute('status');
this.$el.querySelector('.status').innerHTML = '';
progress = 0; progress = 0;
this.currentStatus = null;
} }
const degrees = `rotate(${360 * progress}deg)`; const degrees = `rotate(${360 * progress}deg)`;
$progress.style.setProperty('--progress', degrees); $progress.style.setProperty('--progress', degrees);
@ -596,7 +609,7 @@ class Dialog {
_onPeerDisconnected(peerId) { _onPeerDisconnected(peerId) {
if (this.isShown() && this.correspondingPeerId === peerId) { if (this.isShown() && this.correspondingPeerId === peerId) {
this.hide(); this.hide();
Events.fire('notify-user', 'Selected peer left.') Events.fire('notify-user', Localization.getTranslation("notifications.selected-peer-left"));
} }
} }
} }
@ -630,13 +643,17 @@ class ReceiveDialog extends Dialog {
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) { if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `; let fileOther;
if (files.length === 2) { if (files.length === 2) {
fileOtherText += imagesOnly ? 'image' : 'file'; fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image")
: Localization.getTranslation("dialogs.file-other-description-file");
} else { } else {
fileOtherText += imagesOnly ? 'images' : 'files'; fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1})
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
} }
this.$fileOther.innerText = fileOtherText; this.$fileOther.innerText = fileOther;
} }
const fileName = files[0].name; const fileName = files[0].name;
@ -728,11 +745,15 @@ class ReceiveFileDialog extends ReceiveDialog {
let descriptor, url, filenameDownload; let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
descriptor = imagesOnly ? 'Image' : 'File'; descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image")
: Localization.getTranslation("dialogs.title-file");
} else { } else {
descriptor = imagesOnly ? 'Images' : 'Files'; descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image-plural")
: Localization.getTranslation("dialogs.title-file-plural");
} }
this.$receiveTitle.innerText = `${descriptor} Received`; this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor});
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
if (canShare) { if (canShare) {
@ -782,7 +803,7 @@ class ReceiveFileDialog extends ReceiveDialog {
} }
} }
this.$downloadBtn.innerText = "Download"; this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download");
this.$downloadBtn.onclick = _ => { this.$downloadBtn.onclick = _ => {
if (downloadZipped) { if (downloadZipped) {
let tmpZipBtn = document.createElement("a"); let tmpZipBtn = document.createElement("a");
@ -794,17 +815,18 @@ class ReceiveFileDialog extends ReceiveDialog {
} }
if (!canShare) { if (!canShare) {
this.$downloadBtn.innerText = "Download again"; this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download-again");
} }
Events.fire('notify-user', `${descriptor} downloaded successfully`); Events.fire('notify-user', Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor}));
this.$downloadBtn.style.pointerEvents = "none"; this.$downloadBtn.style.pointerEvents = "none";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
}; };
document.title = files.length === 1 document.title = files.length === 1
? 'File received - PairDrop' ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop`
: `${files.length} Files received - PairDrop`; : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show(); this.show();
@ -892,7 +914,7 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`; document.title = `${ Localization.getTranslation("document-titles.file-transfer-requested") } - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show(); this.show();
} }
@ -1084,7 +1106,7 @@ class PairDeviceDialog extends Dialog {
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) { if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
this._cleanUp(); this._cleanUp();
this.hide(); this.hide();
Events.fire('notify-user', 'Pairing of two browser tabs is not possible.'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-tabs-error"));
return; return;
} }
@ -1130,7 +1152,7 @@ class PairDeviceDialog extends Dialog {
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName) PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
.then(_ => { .then(_ => {
Events.fire('notify-user', 'Devices paired successfully.'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-success"));
this._evaluateNumberRoomSecrets(); this._evaluateNumberRoomSecrets();
}) })
.finally(_ => { .finally(_ => {
@ -1138,13 +1160,13 @@ class PairDeviceDialog extends Dialog {
this.hide(); this.hide();
}) })
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-not-persistent"));
PersistentStorage.logBrowserNotCapable(); PersistentStorage.logBrowserNotCapable();
}); });
} }
_pairDeviceJoinKeyInvalid() { _pairDeviceJoinKeyInvalid() {
Events.fire('notify-user', 'Key not valid'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalid"));
} }
_pairDeviceCancel() { _pairDeviceCancel() {
@ -1154,7 +1176,7 @@ class PairDeviceDialog extends Dialog {
} }
_pairDeviceCanceled(roomKey) { _pairDeviceCanceled(roomKey) {
Events.fire('notify-user', `Key ${roomKey} invalidated.`); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalidated", null, {key: roomKey}));
} }
_cleanUp() { _cleanUp() {
@ -1261,7 +1283,7 @@ class EditPairedDevicesDialog extends Dialog {
PersistentStorage.clearRoomSecrets().finally(_ => { PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('room-secrets-deleted', roomSecrets); Events.fire('room-secrets-deleted', roomSecrets);
Events.fire('evaluate-number-room-secrets'); Events.fire('evaluate-number-room-secrets');
Events.fire('notify-user', 'All Devices unpaired.'); Events.fire('notify-user', Localization.getTranslation("notifications.pairing-cleared"));
this.hide(); this.hide();
}) })
}); });
@ -1416,14 +1438,14 @@ class ReceiveTextDialog extends Dialog {
_setDocumentTitleMessages() { _setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop' ? `${ Localization.getTranslation("document-titles.message-received") } - PairDrop`
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`; : `${ Localization.getTranslation("document-titles.message-received-plural", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`;
} }
async _onCopy() { async _onCopy() {
const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' ');
await navigator.clipboard.writeText(sanitizedText); await navigator.clipboard.writeText(sanitizedText);
Events.fire('notify-user', 'Copied to clipboard'); Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard"));
this.hide(); this.hide();
} }
@ -1450,13 +1472,13 @@ class Base64ZipDialog extends Dialog {
if (base64Text === "paste") { if (base64Text === "paste") {
// ?base64text=paste // ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard // base64 encoded string is ready to be pasted from clipboard
this.preparePasting("text"); this.preparePasting(Localization.getTranslation("dialogs.base64-text"));
} else if (base64Text === "hash") { } else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED // ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended) // base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash) this.processBase64Text(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect."); console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
@ -1466,7 +1488,7 @@ class Base64ZipDialog extends Dialog {
// base64 encoded string was part of url param (not recommended) // base64 encoded string was part of url param (not recommended)
this.processBase64Text(base64Text) this.processBase64Text(base64Text)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect."); console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
@ -1479,32 +1501,32 @@ class Base64ZipDialog extends Dialog {
// base64 encoded zip file is url hash which is never sent to the server // base64 encoded zip file is url hash which is never sent to the server
this.processBase64Zip(base64Hash) this.processBase64Zip(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'File content is incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect"));
console.log("File content incorrect."); console.log("File content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
} else { } else {
// ?base64zip=paste || ?base64zip=true // ?base64zip=paste || ?base64zip=true
this.preparePasting('files'); this.preparePasting(Localization.getTranslation("dialogs.base64-files"));
} }
} }
} }
_setPasteBtnToProcessing() { _setPasteBtnToProcessing() {
this.$pasteBtn.style.pointerEvents = "none"; this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing");
} }
preparePasting(type) { preparePasting(type) {
if (navigator.clipboard.readText) { if (navigator.clipboard.readText) {
this.$pasteBtn.innerText = `Tap here to paste ${type}`; this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", {type: type});
this._clickCallback = _ => this.processClipboard(type); this._clickCallback = _ => this.processClipboard(type);
this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
} else { } else {
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
this.$pasteBtn.setAttribute('hidden', ''); this.$pasteBtn.setAttribute('hidden', '');
this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); this.$fallbackTextarea.setAttribute('placeholder', Localization.getTranslation("dialogs.base64-paste-to-send", {type: type}));
this.$fallbackTextarea.removeAttribute('hidden'); this.$fallbackTextarea.removeAttribute('hidden');
this._inputCallback = _ => this.processInput(type); this._inputCallback = _ => this.processInput(type);
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
@ -1544,7 +1566,7 @@ class Base64ZipDialog extends Dialog {
await this.processBase64Zip(base64); await this.processBase64Zip(base64);
} }
} catch(_) { } catch(_) {
Events.fire('notify-user', 'Clipboard content is incorrect.'); Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect"));
console.log("Clipboard content is incorrect.") console.log("Clipboard content is incorrect.")
} }
this.hide(); this.hide();
@ -1627,7 +1649,7 @@ class Notifications {
Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error');
return; return;
} }
Events.fire('notify-user', 'Notifications enabled.'); Events.fire('notify-user', Localization.getTranslation("notifications.notifications-enabled"));
this.$button.setAttribute('hidden', 1); this.$button.setAttribute('hidden', 1);
}); });
} }
@ -1662,10 +1684,10 @@ class Notifications {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName(); const peerDisplayName = $(peerId).ui._displayName();
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); const notification = this._notify(Localization.getTranslation("notifications.link-received", null, {name: peerDisplayName}), message);
this._bind(notification, _ => window.open(message, '_blank', null, true)); this._bind(notification, _ => window.open(message, '_blank', null, true));
} else { } else {
const notification = this._notify(`Message received by ${peerDisplayName} - Click to copy`, message); const notification = this._notify(Localization.getTranslation("notifications.message-received", null, {name: peerDisplayName}), message);
this._bind(notification, _ => this._copyText(message, notification)); this._bind(notification, _ => this._copyText(message, notification));
} }
} }
@ -1680,13 +1702,23 @@ class Notifications {
break; break;
} }
} }
let title = files[0].name; let title;
if (files.length >= 2) { if (files.length === 1) {
title += ` and ${files.length - 1} other `; title = `${files[0].name}`;
title += imagesOnly ? 'image' : 'file'; } else {
if (files.length > 2) title += "s"; let fileOther;
if (files.length === 2) {
fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image")
: Localization.getTranslation("dialogs.file-other-description-file");
} else {
fileOther = imagesOnly
? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1})
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
} }
const notification = this._notify(title, 'Click to download'); title = `${files[0].name} ${fileOther}`
}
const notification = this._notify(title, Localization.getTranslation("notifications.click-to-download"));
this._bind(notification, _ => this._download(notification)); this._bind(notification, _ => this._download(notification));
} }
} }
@ -1700,15 +1732,27 @@ class Notifications {
break; break;
} }
} }
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
let displayName = $(peerId).querySelector('.name').textContent let displayName = $(peerId).querySelector('.name').textContent
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
const notification = this._notify(title, 'Click to show'); let descriptor;
if (request.header.length === 1) {
descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image")
: Localization.getTranslation("dialogs.title-file");
} else {
descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image-plural")
: Localization.getTranslation("dialogs.title-file-plural");
}
let title = Localization.getTranslation("notifications.request-title", null, {
name: displayName,
count: request.header.length,
descriptor: descriptor.toLowerCase()
});
const notification = this._notify(title, Localization.getTranslation("notifications.click-to-show"));
} }
} }
@ -1720,10 +1764,9 @@ class Notifications {
_copyText(message, notification) { _copyText(message, notification) {
if (navigator.clipboard.writeText(message)) { if (navigator.clipboard.writeText(message)) {
notification.close(); notification.close();
this._notify('Copied text to clipboard'); this._notify(Localization.getTranslation("notifications.copied-text"));
} else { } else {
this._notify('Writing to clipboard failed. Copy manually!'); this._notify(Localization.getTranslation("notifications.copied-text-error"));
} }
} }
@ -1747,11 +1790,11 @@ class NetworkStatusUI {
} }
_showOfflineMessage() { _showOfflineMessage() {
Events.fire('notify-user', 'You are offline'); Events.fire('notify-user', Localization.getTranslation("notifications.offline"));
} }
_showOnlineMessage() { _showOnlineMessage() {
Events.fire('notify-user', 'You are back online'); Events.fire('notify-user', Localization.getTranslation("notifications.online"));
} }
} }
@ -2209,7 +2252,7 @@ class BrowserTabsConnector {
class PairDrop { class PairDrop {
constructor() { constructor() {
Events.on('load', _ => { Events.on('translation-loaded', _ => {
const server = new ServerConnection(); const server = new ServerConnection();
const peers = new PeersManager(server); const peers = new PeersManager(server);
const peersUI = new PeersUI(); const peersUI = new PeersUI();
@ -2233,6 +2276,7 @@ class PairDrop {
const persistentStorage = new PersistentStorage(); const persistentStorage = new PersistentStorage();
const pairDrop = new PairDrop(); const pairDrop = new PairDrop();
const localization = new Localization();
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {

View file

@ -1345,11 +1345,11 @@ x-peers:empty~x-instructions {
transition: opacity 300ms; transition: opacity 300ms;
} }
#websocket-fallback > span { #websocket-fallback {
margin: 2px; margin: 2px;
} }
#websocket-fallback > span > span { #websocket-fallback > span:nth-child(2) {
border-bottom: solid 4px var(--ws-peer-color); border-bottom: solid 4px var(--ws-peer-color);
} }