- 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 @@
-
The easiest way to transfer files across devices
+
The easiest way to transfer files across devices
+
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 @@