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">
<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">
<use xlink:href="#info-outline" />
</svg>
</a>
<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">
<use xlink:href="#icon-theme-auto" />
</svg>
</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">
<use xlink:href="#icon-theme-light" />
</svg>
</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">
<use xlink:href="#icon-theme-dark" />
</svg>
</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">
<use xlink:href="#notifications" />
</svg>
</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">
<use xlink:href="#homescreen" />
</svg>
</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">
<use xlink:href="#pair-device-icon" />
</svg>
</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">
<use xlink:href="#edit-pair-devices-icon" />
</svg>
</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>
<!-- Center -->
<div id="center">
<!-- Peers -->
<div class="x-peers-filler"></div>
<x-peers class="center"></x-peers>
<x-no-peers>
<h2>Open PairDrop on other devices to send files</h2>
<div>Pair devices to be discoverable on other networks</div>
<x-no-peers data-i18n-key="instructions.no-peers" data-i18n-attrs="data-drop-bg" data-drop-bg="Release to select recipient">
<h2 data-i18n-key="instructions.no-peers-title" data-i18n-attrs="text">Open PairDrop on other devices to send files</h2>
<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-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>
</x-instructions>
</div>
@ -104,15 +108,21 @@
<use xlink:href="#wifi-tethering" />
</svg>
<div>
<span>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>
<span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
<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">
<use xlink:href="#edit-pen-icon" />
</svg>
</div>
<div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
<div>
<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>
</footer>
<!-- Pair Device Dialog -->
@ -120,10 +130,13 @@
<form action="#">
<x-background class="full center text-center">
<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>
<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>
<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>
@ -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-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</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">
<button class="button" type="submit" disabled>Pair</button>
<button class="button" type="button" close>Cancel</button>
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div>
</x-paper>
</x-background>
@ -147,13 +160,21 @@
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Edit Paired Devices</h2>
<div class="paired-devices-wrapper"></div>
<h2 class="center" data-i18n-key="dialogs.edit-paired-devices-title" data-i18n-attrs="text">Edit Paired Devices</h2>
<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">
<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 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>
</x-paper>
</x-background>
@ -167,7 +188,7 @@
<div class="center column file-description">
<div>
<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 class="row file-name" >
<span class="file-stem"></span>
@ -179,8 +200,8 @@
</div>
<div class="center file-preview"></div>
<div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE">Decline</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" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
</div>
</x-paper>
</x-background>
@ -193,7 +214,7 @@
<div class="center column file-description">
<div>
<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 class="row file-name" >
<span class="file-stem"></span>
@ -204,9 +225,9 @@
</div>
<div class="center file-preview"></div>
<div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</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" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div>
</x-paper>
</x-background>
@ -216,16 +237,16 @@
<form action="#">
<x-background class="full center">
<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">
<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>
</div>
<div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</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" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div>
</x-paper>
</x-background>
@ -235,16 +256,16 @@
<x-dialog id="receive-text-dialog">
<x-background class="full center">
<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">
<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 class="row-separator"></div>
<div id="text"></div>
<div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<button id="close" class="button" title="ESCAPE">Close</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" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
</div>
</x-paper>
</x-background>
@ -253,9 +274,9 @@
<x-dialog id="base64-paste-dialog">
<x-background class="full center">
<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>
<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-background>
</x-dialog>
@ -266,7 +287,7 @@
<!-- About Page -->
<x-about id="about" class="full center column">
<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">
<use xlink:href="#close-icon" />
</svg>
@ -280,7 +301,7 @@
<h1>PairDrop</h1>
<div class="font-subheading">v1.7.6</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">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
<svg class="icon">
@ -373,6 +394,7 @@
</symbol>
</svg>
<!-- Scripts -->
<script src="scripts/localization.js"></script>
<script src="scripts/theme.js"></script>
<script src="scripts/network.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() {
console.log('WS: server 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() {
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;
}
this.send({ type: 'pair-device-initiate' })
@ -107,7 +107,7 @@ class ServerConnection {
Events.fire('pair-device-canceled', msg.roomKey);
break;
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;
case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret);
@ -183,7 +183,7 @@ class ServerConnection {
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..');
Events.fire('notify-user', Localization.getTranslation("notifications.connecting"));
clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
Events.fire('ws-disconnected');
@ -488,7 +488,7 @@ class Peer {
_abortTransfer() {
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._requestAccepted = null;
this._digester = null;
@ -546,7 +546,7 @@ class Peer {
this._chunker = null;
if (!this._filesQueue.length) {
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 {
this._dequeueFile();
@ -558,7 +558,7 @@ class Peer {
Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'});
this._filesRequested = null;
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;
}
@ -568,7 +568,7 @@ class Peer {
}
_onMessageTransferCompleted() {
Events.fire('notify-user', 'Message transfer completed.');
Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed"));
}
sendText(text) {
@ -713,7 +713,7 @@ class RTCPeer extends Peer {
_onBeforeUnload(e) {
if (this._busy) {
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) {
PersistentStorage.set('editedDisplayName', newDisplayName)
.then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.');
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-permanently"));
})
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
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(_ => {
Events.fire('self-display-name-changed', newDisplayName);
@ -105,10 +105,9 @@ class PeersUI {
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
})
.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('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
@ -275,21 +274,22 @@ class PeersUI {
let descriptor;
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) {
descriptor = files[0].name;
noPeersMessage = `Open PairDrop on other devices to send<br><i>${descriptor}</i>`;
noPeersMessage = `${openPairDrop}<br><i>${files[0].name}</i>`;
} else if (files.length > 1) {
descriptor = `${files[0].name} and ${files.length-1} other files`;
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
noPeersMessage = `${openPairDrop}<br><i>${files[0].name}</i> ${andOtherFiles}`;
} else {
descriptor = "shared text";
noPeersMessage = `Open PairDrop on other devices to send<br>${descriptor}`;
noPeersMessage = `${openPairDrop}<br>${sharedText}`;
}
this.$xInstructions.querySelector('p').innerHTML = `<i>${descriptor}</i>`;
this.$xInstructions.querySelector('p').innerHTML = noPeersMessage;
this.$xInstructions.querySelector('p').style.display = 'block';
this.$xInstructions.setAttribute('desktop', `Click to send`);
this.$xInstructions.setAttribute('mobile', `Tap to send`);
this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.click-to-send"));
this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.tap-to-send"));
this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage;
@ -320,10 +320,10 @@ class PeersUI {
this.$xInstructions.querySelector('p').innerText = '';
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('mobile', 'Tap to send files or long tap to send a message');
this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.x-instructions", "desktop"));
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', "");
@ -368,9 +368,9 @@ class PeerUI {
let title;
let input = '';
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 {
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>';
}
this.$el.innerHTML = `
@ -392,7 +392,7 @@ class PeerUI {
<div class="name font-subheading"></div>
<div class="device-name 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>
</label>`;
@ -509,10 +509,23 @@ class PeerUI {
$progress.classList.remove('over50');
}
if (progress < 1) {
this.$el.setAttribute('status', status);
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.querySelector('.status').innerText = statusName;
this.currentStatus = status;
}
} else {
this.$el.removeAttribute('status');
this.$el.querySelector('.status').innerHTML = '';
progress = 0;
this.currentStatus = null;
}
const degrees = `rotate(${360 * progress}deg)`;
$progress.style.setProperty('--progress', degrees);
@ -595,7 +608,7 @@ class Dialog {
_onPeerDisconnected(peerId) {
if (this.isShown() && this.correspondingPeerId === peerId) {
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) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
let fileOther;
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 {
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;
@ -727,11 +744,15 @@ class ReceiveFileDialog extends ReceiveDialog {
let descriptor, url, filenameDownload;
if (files.length === 1) {
descriptor = imagesOnly ? 'Image' : 'File';
descriptor = imagesOnly
? Localization.getTranslation("dialogs.title-image")
: Localization.getTranslation("dialogs.title-file");
} 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});
if (canShare) {
@ -781,7 +802,7 @@ class ReceiveFileDialog extends ReceiveDialog {
}
}
this.$downloadBtn.innerText = "Download";
this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download");
this.$downloadBtn.onclick = _ => {
if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
@ -793,17 +814,18 @@ class ReceiveFileDialog extends ReceiveDialog {
}
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";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop`
: `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
@ -891,7 +913,7 @@ class ReceiveRequestDialog extends ReceiveDialog {
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");
this.show();
}
@ -1083,7 +1105,7 @@ class PairDeviceDialog extends Dialog {
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
this._cleanUp();
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;
}
@ -1129,7 +1151,7 @@ class PairDeviceDialog extends Dialog {
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
.then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-success"));
this._evaluateNumberRoomSecrets();
})
.finally(_ => {
@ -1137,13 +1159,13 @@ class PairDeviceDialog extends Dialog {
this.hide();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-not-persistent"));
PersistentStorage.logBrowserNotCapable();
});
}
_pairDeviceJoinKeyInvalid() {
Events.fire('notify-user', 'Key not valid');
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalid"));
}
_pairDeviceCancel() {
@ -1153,7 +1175,7 @@ class PairDeviceDialog extends Dialog {
}
_pairDeviceCanceled(roomKey) {
Events.fire('notify-user', `Key ${roomKey} invalidated.`);
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalidated", null, {key: roomKey}));
}
_cleanUp() {
@ -1260,7 +1282,7 @@ class EditPairedDevicesDialog extends Dialog {
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('room-secrets-deleted', roomSecrets);
Events.fire('evaluate-number-room-secrets');
Events.fire('notify-user', 'All Devices unpaired.');
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-cleared"));
this.hide();
})
});
@ -1415,14 +1437,14 @@ class ReceiveTextDialog extends Dialog {
_setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop'
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
? `${ Localization.getTranslation("document-titles.message-received") } - PairDrop`
: `${ Localization.getTranslation("document-titles.message-received-plural", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`;
}
async _onCopy() {
const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' ');
await navigator.clipboard.writeText(sanitizedText);
Events.fire('notify-user', 'Copied to clipboard');
Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard"));
this.hide();
}
@ -1449,13 +1471,13 @@ class Base64ZipDialog extends Dialog {
if (base64Text === "paste") {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
this.preparePasting("text");
this.preparePasting(Localization.getTranslation("dialogs.base64-text"));
} else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
@ -1465,7 +1487,7 @@ class Base64ZipDialog extends Dialog {
// base64 encoded string was part of url param (not recommended)
this.processBase64Text(base64Text)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
@ -1478,32 +1500,32 @@ class Base64ZipDialog extends Dialog {
// base64 encoded zip file is url hash which is never sent to the server
this.processBase64Zip(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'File content is incorrect.');
Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect"));
console.log("File content incorrect.");
}).finally(_ => {
this.hide();
});
} else {
// ?base64zip=paste || ?base64zip=true
this.preparePasting('files');
this.preparePasting(Localization.getTranslation("dialogs.base64-files"));
}
}
}
_setPasteBtnToProcessing() {
this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing");
}
preparePasting(type) {
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.$pasteBtn.addEventListener('click', _ => this._clickCallback());
} 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.")
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._inputCallback = _ => this.processInput(type);
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
@ -1543,7 +1565,7 @@ class Base64ZipDialog extends Dialog {
await this.processBase64Zip(base64);
}
} 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.")
}
this.hide();
@ -1626,7 +1648,7 @@ class Notifications {
Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error');
return;
}
Events.fire('notify-user', 'Notifications enabled.');
Events.fire('notify-user', Localization.getTranslation("notifications.notifications-enabled"));
this.$button.setAttribute('hidden', 1);
});
}
@ -1661,10 +1683,10 @@ class Notifications {
if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName();
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));
} 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));
}
}
@ -1679,13 +1701,23 @@ class Notifications {
break;
}
}
let title = files[0].name;
if (files.length >= 2) {
title += ` and ${files.length - 1} other `;
title += imagesOnly ? 'image' : 'file';
if (files.length > 2) title += "s";
let title;
if (files.length === 1) {
title = `${files[0].name}`;
} else {
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});
}
title = `${files[0].name} ${fileOther}`
}
const notification = this._notify(title, 'Click to download');
const notification = this._notify(title, Localization.getTranslation("notifications.click-to-download"));
this._bind(notification, _ => this._download(notification));
}
}
@ -1699,15 +1731,27 @@ class Notifications {
break;
}
}
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
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) {
if (navigator.clipboard.writeText(message)) {
notification.close();
this._notify('Copied text to clipboard');
this._notify(Localization.getTranslation("notifications.copied-text"));
} else {
this._notify('Writing to clipboard failed. Copy manually!');
this._notify(Localization.getTranslation("notifications.copied-text-error"));
}
}
@ -1746,11 +1789,11 @@ class NetworkStatusUI {
}
_showOfflineMessage() {
Events.fire('notify-user', 'You are offline');
Events.fire('notify-user', Localization.getTranslation("notifications.offline"));
}
_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 {
constructor() {
Events.on('load', _ => {
Events.on('translation-loaded', _ => {
const server = new ServerConnection();
const peers = new PeersManager(server);
const peersUI = new PeersUI();
@ -2232,6 +2275,7 @@ class PairDrop {
const persistentStorage = new PersistentStorage();
const pairDrop = new PairDrop();
const localization = new Localization();
if ('serviceWorker' in navigator) {

View file

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