diff --git a/public/index.html b/public/index.html index 755673a..15b82da 100644 --- a/public/index.html +++ b/public/index.html @@ -39,62 +39,66 @@
- +
-
+
-
+
-
+
-
- -

Open PairDrop on other devices to send files

-
Pair devices to be discoverable on other networks
+ +

Open PairDrop on other devices to send files

+
Pair devices to be discoverable on other networks
- +

@@ -104,15 +108,21 @@
- You are known as: -
+ You are known as: +
- You can be discovered by everyone on this network - +
+ You can be discovered by everyone + on this network +
+
@@ -120,10 +130,13 @@
-

Pair Devices

+

Pair Devices

000 000

-
Input this key on another device
or scan the QR-Code.
+
+ Input this key on another device + or scan the QR-Code. +

@@ -133,10 +146,10 @@
-
Enter key from another device to continue.
+
Enter key from another device to continue.
- - + +
@@ -147,13 +160,21 @@ -

Edit Paired Devices

-
+

Edit Paired Devices

+
-

Activate auto-accept to automatically accept all files sent from that device.

+

+ + Activate + + auto-accept + + to automatically accept all files sent from that device. + +

- +
@@ -167,7 +188,7 @@
- would like to share + would like to share
@@ -179,8 +200,8 @@
- - + +
@@ -193,7 +214,7 @@
- has sent + has sent
@@ -204,9 +225,9 @@
- - - + + +
@@ -216,16 +237,16 @@ -

Send Message

+

Send Message

- Send a Message to + Send a Message to
- - + +
@@ -235,16 +256,16 @@ -

Message Received

+

Message Received

- has sent: + has sent:
- - + +
@@ -253,9 +274,9 @@ - + - + @@ -266,7 +287,7 @@
- + @@ -280,7 +301,7 @@

PairDrop

v1.7.6
-
The easiest way to transfer files across devices
+
The easiest way to transfer files across devices
@@ -373,6 +394,7 @@ + diff --git a/public/lang/en.json b/public/lang/en.json new file mode 100644 index 0000000..7ae2e56 --- /dev/null +++ b/public/lang/en.json @@ -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..." + } +} diff --git a/public/scripts/localization.js b/public/scripts/localization.js new file mode 100644 index 0000000..d09d5c0 --- /dev/null +++ b/public/scripts/localization.js @@ -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 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"); } } diff --git a/public/scripts/ui.js b/public/scripts/ui.js index b494f58..f3d08d8 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -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
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name}`; } else if (files.length > 1) { - descriptor = `${files[0].name} and ${files.length-1} other files`; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name} ${andOtherFiles}`; } else { - descriptor = "shared text"; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${sharedText}`; } - this.$xInstructions.querySelector('p').innerHTML = `${descriptor}`; + 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 = ''; } this.$el.innerHTML = ` @@ -392,7 +392,7 @@ class PeerUI {
- +
`; @@ -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) { diff --git a/public/styles.css b/public/styles.css index db86b60..1375b46 100644 --- a/public/styles.css +++ b/public/styles.css @@ -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 { diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 6beae65..e42f324 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -39,62 +39,66 @@
- +
-
+
-
+
-
+
-
- -

Open PairDrop on other devices to send files

-
Pair devices to be discoverable on other networks
+ +

Open PairDrop on other devices to send files

+
Pair devices to be discoverable on other networks
- +

@@ -104,18 +108,26 @@
- You are known as: -
+ You are known as: +
- You can be discovered by everyone on this network - +
+ You can be discovered by everyone + on this network +
+
- Traffic is routed through the server if WebRTC is not available. + Traffic is + routed through the server + if WebRTC is not available.
@@ -123,10 +135,13 @@ -

Pair Devices

+

Pair Devices

000 000

-
Input this key on another device
or scan the QR-Code.
+
+ Input this key on another device + or scan the QR-Code. +

@@ -136,10 +151,10 @@
-
Enter key from another device to continue.
+
Enter key from another device to continue.
- - + +
@@ -150,13 +165,21 @@ -

Edit Paired Devices

-
+

Edit Paired Devices

+
-

Activate auto-accept to automatically accept all files sent from that device.

+

+ + Activate + + auto-accept + + to automatically accept all files sent from that device. + +

- +
@@ -170,7 +193,7 @@
- would like to share + would like to share
@@ -182,8 +205,8 @@
- - + +
@@ -196,7 +219,7 @@
- has sent + has sent
@@ -207,9 +230,9 @@
- - - + + +
@@ -219,16 +242,16 @@ -

Send Message

+

Send Message

- Send a Message to + Send a Message to
- - + +
@@ -238,16 +261,16 @@ -

Message Received

+

Message Received

- has sent: + has sent:
- - + +
@@ -256,9 +279,9 @@ - + - + @@ -269,7 +292,7 @@
- + @@ -283,7 +306,7 @@

PairDrop

v1.7.6
-
The easiest way to transfer files across devices
+
The easiest way to transfer files across devices
@@ -376,6 +399,7 @@ + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json new file mode 100644 index 0000000..7ae2e56 --- /dev/null +++ b/public_included_ws_fallback/lang/en.json @@ -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..." + } +} diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js new file mode 100644 index 0000000..d09d5c0 --- /dev/null +++ b/public_included_ws_fallback/scripts/localization.js @@ -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 this._connect(), 1000); Events.fire('ws-disconnected'); @@ -505,7 +505,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._abortTransfer(); } - // include for compatibility with Snapdrop for Android app + // include for compatibility with 'Snapdrop & PairDrop for Android' app Events.fire('file-received', fileBlob); this._filesReceived.push(fileBlob); @@ -563,7 +563,8 @@ 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(); } @@ -574,7 +575,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; } @@ -584,7 +585,7 @@ class Peer { } _onMessageTransferCompleted() { - Events.fire('notify-user', 'Message transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed")); } sendText(text) { @@ -729,7 +730,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"); } } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index d355468..b3afac4 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -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
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name}`; } else if (files.length > 1) { - descriptor = `${files[0].name} and ${files.length-1} other files`; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name} ${andOtherFiles}`; } else { - descriptor = "shared text"; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${sharedText}`; } - this.$xInstructions.querySelector('p').innerHTML = `${descriptor}`; + 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 = ''; } this.$el.innerHTML = ` @@ -392,7 +392,7 @@ class PeerUI {
- +
`; @@ -510,10 +510,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); @@ -596,7 +609,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")); } } } @@ -630,13 +643,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; @@ -728,11 +745,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) { @@ -782,7 +803,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"); @@ -794,17 +815,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(); @@ -892,7 +914,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(); } @@ -1084,7 +1106,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; } @@ -1130,7 +1152,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(_ => { @@ -1138,13 +1160,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() { @@ -1154,7 +1176,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() { @@ -1261,7 +1283,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(); }) }); @@ -1416,14 +1438,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(); } @@ -1450,13 +1472,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(); @@ -1466,7 +1488,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(); @@ -1479,32 +1501,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()); @@ -1544,7 +1566,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(); @@ -1627,7 +1649,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); }); } @@ -1662,10 +1684,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)); } } @@ -1680,13 +1702,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)); } } @@ -1700,15 +1732,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")); } } @@ -1720,10 +1764,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")); } } @@ -1747,11 +1790,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")); } } @@ -2209,7 +2252,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(); @@ -2233,6 +2276,7 @@ class PairDrop { const persistentStorage = new PersistentStorage(); const pairDrop = new PairDrop(); +const localization = new Localization(); if ('serviceWorker' in navigator) { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index e384b51..2e8fbb8 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1345,11 +1345,11 @@ x-peers:empty~x-instructions { transition: opacity 300ms; } -#websocket-fallback > span { +#websocket-fallback { margin: 2px; } -#websocket-fallback > span > span { +#websocket-fallback > span:nth-child(2) { border-bottom: solid 4px var(--ws-peer-color); }