+
@@ -392,6 +416,11 @@
+
+
+
+
+
diff --git a/public/lang/en.json b/public/lang/en.json
index ff8294d..de26740 100644
--- a/public/lang/en.json
+++ b/public/lang/en.json
@@ -1,6 +1,7 @@
{
"header": {
"about_title": "About PairDrop",
+ "language-selector_title": "Select Language",
"about_aria-label": "Open About PairDrop",
"theme-auto_title": "Adapt Theme to System",
"theme-light_title": "Always Use Light-Theme",
@@ -24,7 +25,7 @@
},
"footer": {
"known-as": "You are known as:",
- "display-name_placeholder": "Loading…",
+ "display-name_data-placeholder": "Loading…",
"display-name_title": "Edit your device name permanently",
"discovery-everyone": "You can be discovered by everyone",
"on-this-network": "on this network",
@@ -75,7 +76,9 @@
"title-image-plural": "Images",
"title-file-plural": "Files",
"receive-title": "{{descriptor}} Received",
- "download-again": "Download again"
+ "download-again": "Download again",
+ "language-selector-title": "Select Language",
+ "system-language": "System Language"
},
"about": {
"close-about_aria-label": "Close About PairDrop",
diff --git a/public/lang/nb.json b/public/lang/nb.json
index b11b664..ee2bd64 100644
--- a/public/lang/nb.json
+++ b/public/lang/nb.json
@@ -15,7 +15,7 @@
"discovery-everyone": "Du kan oppdages av alle",
"and-by": "og av",
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
- "display-name_placeholder": "Laster inn …",
+ "display-name_data-placeholder": "Laster inn…",
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
"traffic": "Trafikken",
"on-this-network": "på dette nettverket",
diff --git a/public/lang/ru.json b/public/lang/ru.json
index 8617ec2..1c67504 100644
--- a/public/lang/ru.json
+++ b/public/lang/ru.json
@@ -24,7 +24,7 @@
},
"footer": {
"discovery-everyone": "О вас может узнать каждый",
- "display-name_placeholder": "Загрузка…",
+ "display-name_data-placeholder": "Загрузка…",
"routed": "направляется через сервер",
"webrtc": ", если WebRTC недоступен.",
"traffic": "Трафик",
diff --git a/public/lang/tr.json b/public/lang/tr.json
index 783e25b..87608f2 100644
--- a/public/lang/tr.json
+++ b/public/lang/tr.json
@@ -15,7 +15,7 @@
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
},
"footer": {
- "display-name_placeholder": "Yükleniyor…",
+ "display-name_data-placeholder": "Yükleniyor…",
"display-name_title": "Cihazının adını kalıcı olarak düzenle"
},
"dialogs": {
diff --git a/public/lang/zh-CN.json b/public/lang/zh-CN.json
index 4191c7d..d6af684 100644
--- a/public/lang/zh-CN.json
+++ b/public/lang/zh-CN.json
@@ -26,7 +26,7 @@
"routed": "途径服务器",
"webrtc": "如果 WebRTC 不可用。",
"known-as": "你的名字是:",
- "display-name_placeholder": "加载中…",
+ "display-name_data-placeholder": "加载中…",
"and-by": "和",
"display-name_title": "长久修改你的设备名",
"discovery-everyone": "你对所有人可见",
diff --git a/public/scripts/localization.js b/public/scripts/localization.js
index a833993..4510682 100644
--- a/public/scripts/localization.js
+++ b/public/scripts/localization.js
@@ -5,12 +5,19 @@ class Localization {
Localization.translations = {};
Localization.defaultTranslations = {};
- const initialLocale = Localization.supportedOrDefault(navigator.languages);
+ Localization.systemLocale = Localization.supportedOrDefault(navigator.languages);
- Localization.setLocale(initialLocale)
+ let storedLanguageCode = localStorage.getItem("language-code");
+
+ Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
+ ? storedLanguageCode
+ : Localization.systemLocale;
+
+ Localization.setTranslation(Localization.initialLocale)
.then(_ => {
- Localization.translatePage();
- })
+ console.log("Initial translation successful.");
+ Events.fire("translation-loaded");
+ });
}
static isSupported(locale) {
@@ -21,11 +28,21 @@ class Localization {
return locales.find(Localization.isSupported) || Localization.defaultLocale;
}
+ static async setTranslation(locale) {
+ if (!locale) locale = Localization.systemLocale;
+
+ await Localization.setLocale(locale)
+ await Localization.translatePage();
+
+ console.log("Page successfully translated",
+ `System language: ${Localization.systemLocale}`,
+ `Selected language: ${locale}`
+ );
+ }
+
static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false;
- const isFirstTranslation = !Localization.locale
-
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
@@ -34,10 +51,14 @@ class Localization {
Localization.locale = newLocale;
Localization.translations = newTranslations;
+ }
- if (isFirstTranslation) {
- Events.fire("translation-loaded");
- }
+ static getLocale() {
+ return Localization.locale;
+ }
+
+ static isSystemLocale() {
+ return !localStorage.getItem('language-code');
}
static async fetchTranslationsFor(newLocale) {
@@ -48,7 +69,7 @@ class Localization {
return await response.json();
}
- static translatePage() {
+ static async translatePage() {
document
.querySelectorAll("[data-i18n-key]")
.forEach(element => Localization.translateElement(element));
@@ -63,10 +84,14 @@ class Localization {
if (attr === "text") {
element.innerText = Localization.getTranslation(key);
} else {
- element.setAttribute(attr, Localization.getTranslation(key, attr));
+ if (attr.startsWith("data-")) {
+ let dataAttr = attr.substring(5);
+ element.dataset.dataAttr = Localization.getTranslation(key, attr);
+ } {
+ element.setAttribute(attr, Localization.getTranslation(key, attr));
+ }
}
}
-
}
static getTranslation(key, attr, data, useDefault=false) {
diff --git a/public/scripts/ui.js b/public/scripts/ui.js
index f3d08d8..0ab425f 100644
--- a/public/scripts/ui.js
+++ b/public/scripts/ui.js
@@ -45,6 +45,8 @@ class PeersUI {
this.$displayName = $('display-name');
+ this.$displayName.setAttribute("placeholder", this.$displayName.dataset.placeholder);
+
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
@@ -613,6 +615,58 @@ class Dialog {
}
}
+class LanguageSelectDialog extends Dialog {
+
+ constructor() {
+ super('language-select-dialog');
+
+ this.$languageSelectBtn = $('language-selector');
+ this.$languageSelectBtn.addEventListener('click', _ => this.show());
+
+ this.$languageButtons = this.$el.querySelectorAll(".language-buttons button");
+ this.$languageButtons.forEach($btn => {
+ $btn.addEventListener("click", e => this.selectLanguage(e));
+ })
+ Events.on('keydown', e => this._onKeyDown(e));
+ }
+
+ _onKeyDown(e) {
+ if (this.isShown() && e.code === "Escape") {
+ this.hide();
+ }
+ }
+
+ show() {
+ if (Localization.isSystemLocale()) {
+ this.$languageButtons[0].focus();
+ } else {
+ let locale = Localization.getLocale();
+ for (let i=0; i
this.hide());
+ }
+}
+
class ReceiveDialog extends Dialog {
constructor(id) {
super(id);
@@ -2255,6 +2309,7 @@ class PairDrop {
const server = new ServerConnection();
const peers = new PeersManager(server);
const peersUI = new PeersUI();
+ const languageSelectDialog = new LanguageSelectDialog();
const receiveFileDialog = new ReceiveFileDialog();
const receiveRequestDialog = new ReceiveRequestDialog();
const sendTextDialog = new SendTextDialog();
diff --git a/public/styles.css b/public/styles.css
index 1375b46..4b23974 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -23,6 +23,7 @@ body {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
+ transition: color 300ms;
}
body {
@@ -40,6 +41,10 @@ html {
min-height: fill-available;
}
+.fw {
+ width: 100%;
+}
+
.row-reverse {
display: flex;
flex-direction: row-reverse;
@@ -591,7 +596,6 @@ footer {
align-items: center;
padding: 0 0 16px 0;
text-align: center;
- transition: color 300ms;
cursor: default;
}
@@ -683,7 +687,6 @@ x-dialog x-paper {
top: max(50%, 350px);
margin-top: -328.5px;
width: calc(100vw - 20px);
- height: 625px;
}
#pair-device-dialog ::-moz-selection,
@@ -761,7 +764,7 @@ x-dialog a {
}
x-dialog hr {
- margin: 40px -24px 30px -24px;
+ margin: 20px -24px 20px -24px;
border: solid 1.25px var(--border-color);
}
@@ -868,18 +871,18 @@ x-dialog .row {
}
/* button row*/
-x-paper > div:last-child {
- margin: auto -24px -15px;
+x-paper > .button-row {
+ margin: 25px -24px -15px;
border-top: solid 2.5px var(--border-color);
height: 50px;
}
-x-paper > div:last-child > .button {
+x-paper > .button-row > .button {
height: 100%;
width: 100%;
}
-x-paper > div:last-child > .button:not(:last-child) {
+x-paper > .button-row > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
}
@@ -1044,6 +1047,11 @@ x-dialog .dialog-subheader {
opacity: 0.1;
}
+.button[selected],
+.icon-button[selected] {
+ opacity: 0.1;
+}
+
#cancel-paste-mode {
z-index: 2;
margin: 0;
@@ -1301,7 +1309,7 @@ x-peers:empty~x-instructions {
x-dialog x-paper {
padding: 15px;
}
- x-paper > div:last-child {
+ x-paper > .button-row {
margin: auto -15px -15px;
}
}
diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html
index a233aab..529bc1a 100644
--- a/public_included_ws_fallback/index.html
+++ b/public_included_ws_fallback/index.html
@@ -44,6 +44,11 @@
+
+
+
Enter key from another device to continue.
-
+
@@ -178,7 +202,7 @@
-
+
@@ -204,7 +228,7 @@
-
+
@@ -229,7 +253,7 @@
-
+
@@ -249,7 +273,7 @@
-
+
@@ -268,7 +292,7 @@
-
+
@@ -397,6 +421,11 @@
+
+
+
+
+
diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json
index ff8294d..4c88dae 100644
--- a/public_included_ws_fallback/lang/en.json
+++ b/public_included_ws_fallback/lang/en.json
@@ -24,7 +24,7 @@
},
"footer": {
"known-as": "You are known as:",
- "display-name_placeholder": "Loading…",
+ "display-name_data-placeholder": "Loading…",
"display-name_title": "Edit your device name permanently",
"discovery-everyone": "You can be discovered by everyone",
"on-this-network": "on this network",
diff --git a/public_included_ws_fallback/lang/nb.json b/public_included_ws_fallback/lang/nb.json
index b11b664..ee2bd64 100644
--- a/public_included_ws_fallback/lang/nb.json
+++ b/public_included_ws_fallback/lang/nb.json
@@ -15,7 +15,7 @@
"discovery-everyone": "Du kan oppdages av alle",
"and-by": "og av",
"webrtc": "hvis WebRTC ikke er tilgjengelig.",
- "display-name_placeholder": "Laster inn …",
+ "display-name_data-placeholder": "Laster inn…",
"display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
"traffic": "Trafikken",
"on-this-network": "på dette nettverket",
diff --git a/public_included_ws_fallback/lang/ru.json b/public_included_ws_fallback/lang/ru.json
index 8617ec2..1c67504 100644
--- a/public_included_ws_fallback/lang/ru.json
+++ b/public_included_ws_fallback/lang/ru.json
@@ -24,7 +24,7 @@
},
"footer": {
"discovery-everyone": "О вас может узнать каждый",
- "display-name_placeholder": "Загрузка…",
+ "display-name_data-placeholder": "Загрузка…",
"routed": "направляется через сервер",
"webrtc": ", если WebRTC недоступен.",
"traffic": "Трафик",
diff --git a/public_included_ws_fallback/lang/tr.json b/public_included_ws_fallback/lang/tr.json
index 783e25b..87608f2 100644
--- a/public_included_ws_fallback/lang/tr.json
+++ b/public_included_ws_fallback/lang/tr.json
@@ -15,7 +15,7 @@
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
},
"footer": {
- "display-name_placeholder": "Yükleniyor…",
+ "display-name_data-placeholder": "Yükleniyor…",
"display-name_title": "Cihazının adını kalıcı olarak düzenle"
},
"dialogs": {
diff --git a/public_included_ws_fallback/lang/zh-CN.json b/public_included_ws_fallback/lang/zh-CN.json
index 4191c7d..d6af684 100644
--- a/public_included_ws_fallback/lang/zh-CN.json
+++ b/public_included_ws_fallback/lang/zh-CN.json
@@ -26,7 +26,7 @@
"routed": "途径服务器",
"webrtc": "如果 WebRTC 不可用。",
"known-as": "你的名字是:",
- "display-name_placeholder": "加载中…",
+ "display-name_data-placeholder": "加载中…",
"and-by": "和",
"display-name_title": "长久修改你的设备名",
"discovery-everyone": "你对所有人可见",
diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js
index a833993..a447669 100644
--- a/public_included_ws_fallback/scripts/localization.js
+++ b/public_included_ws_fallback/scripts/localization.js
@@ -5,12 +5,19 @@ class Localization {
Localization.translations = {};
Localization.defaultTranslations = {};
- const initialLocale = Localization.supportedOrDefault(navigator.languages);
+ Localization.systemLocale = Localization.supportedOrDefault(navigator.languages);
- Localization.setLocale(initialLocale)
+ let storedLanguageCode = localStorage.getItem("language-code");
+
+ Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode)
+ ? storedLanguageCode
+ : Localization.systemLocale;
+
+ Localization.setTranslation(Localization.initialLocale)
.then(_ => {
- Localization.translatePage();
- })
+ console.log("Initial translation successful.");
+ Events.fire("translation-loaded");
+ });
}
static isSupported(locale) {
@@ -21,10 +28,20 @@ class Localization {
return locales.find(Localization.isSupported) || Localization.defaultLocale;
}
+ static async setTranslation(locale) {
+ if (!locale) locale = Localization.systemLocale;
+
+ await Localization.setLocale(locale)
+ await Localization.translatePage();
+
+ console.log("Page successfully translated",
+ `System language: ${Localization.systemLocale}`,
+ `Selected language: ${locale}`
+ );
+ }
+
static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false;
-
- const isFirstTranslation = !Localization.locale
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
@@ -34,10 +51,14 @@ class Localization {
Localization.locale = newLocale;
Localization.translations = newTranslations;
+ }
- if (isFirstTranslation) {
- Events.fire("translation-loaded");
- }
+ static getLocale() {
+ return Localization.locale;
+ }
+
+ static isSystemLocale() {
+ return !localStorage.getItem('language-code');
}
static async fetchTranslationsFor(newLocale) {
@@ -48,7 +69,7 @@ class Localization {
return await response.json();
}
- static translatePage() {
+ static async translatePage() {
document
.querySelectorAll("[data-i18n-key]")
.forEach(element => Localization.translateElement(element));
@@ -63,10 +84,14 @@ class Localization {
if (attr === "text") {
element.innerText = Localization.getTranslation(key);
} else {
- element.setAttribute(attr, Localization.getTranslation(key, attr));
+ if (attr.startsWith("data-")) {
+ let dataAttr = attr.substring(5);
+ element.dataset.dataAttr = Localization.getTranslation(key, attr);
+ } {
+ element.setAttribute(attr, Localization.getTranslation(key, attr));
+ }
}
}
-
}
static getTranslation(key, attr, data, useDefault=false) {
diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js
index b3afac4..edba81e 100644
--- a/public_included_ws_fallback/scripts/ui.js
+++ b/public_included_ws_fallback/scripts/ui.js
@@ -45,6 +45,8 @@ class PeersUI {
this.$displayName = $('display-name');
+ this.$displayName.setAttribute("placeholder", this.$displayName.dataset.placeholder);
+
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
@@ -614,6 +616,58 @@ class Dialog {
}
}
+class LanguageSelectDialog extends Dialog {
+
+ constructor() {
+ super('language-select-dialog');
+
+ this.$languageSelectBtn = $('language-selector');
+ this.$languageSelectBtn.addEventListener('click', _ => this.show());
+
+ this.$languageButtons = this.$el.querySelectorAll(".language-buttons button");
+ this.$languageButtons.forEach($btn => {
+ $btn.addEventListener("click", e => this.selectLanguage(e));
+ })
+ Events.on('keydown', e => this._onKeyDown(e));
+ }
+
+ _onKeyDown(e) {
+ if (this.isShown() && e.code === "Escape") {
+ this.hide();
+ }
+ }
+
+ show() {
+ if (Localization.isSystemLocale()) {
+ this.$languageButtons[0].focus();
+ } else {
+ let locale = Localization.getLocale();
+ for (let i=0; i
this.hide());
+ }
+}
+
class ReceiveDialog extends Dialog {
constructor(id) {
super(id);
@@ -2256,6 +2310,7 @@ class PairDrop {
const server = new ServerConnection();
const peers = new PeersManager(server);
const peersUI = new PeersUI();
+ const languageSelectDialog = new LanguageSelectDialog();
const receiveFileDialog = new ReceiveFileDialog();
const receiveRequestDialog = new ReceiveRequestDialog();
const sendTextDialog = new SendTextDialog();
diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css
index 2e8fbb8..b36dd69 100644
--- a/public_included_ws_fallback/styles.css
+++ b/public_included_ws_fallback/styles.css
@@ -24,6 +24,7 @@ body {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
+ transition: color 300ms;
}
body {
@@ -41,6 +42,10 @@ html {
min-height: fill-available;
}
+.fw {
+ width: 100%;
+}
+
.row-reverse {
display: flex;
flex-direction: row-reverse;
@@ -452,7 +457,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] * {
@@ -652,11 +657,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,7 +730,6 @@ x-dialog x-paper {
top: max(50%, 350px);
margin-top: -328.5px;
width: calc(100vw - 20px);
- height: 625px;
}
#pair-device-dialog ::-moz-selection,
@@ -800,8 +806,12 @@ x-dialog .font-subheading {
margin: 16px;
}
+#pair-instructions {
+ flex-direction: column;
+}
+
x-dialog hr {
- margin: 40px -24px 30px -24px;
+ margin: 20px -24px 20px -24px;
border: solid 1.25px var(--border-color);
}
@@ -811,7 +821,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 {
@@ -908,18 +918,18 @@ x-dialog .row {
}
/* button row*/
-x-paper > div:last-child {
- margin: auto -24px -15px;
+x-paper > .button-row {
+ margin: 25px -24px -15px;
border-top: solid 2.5px var(--border-color);
height: 50px;
}
-x-paper > div:last-child > .button {
+x-paper > .button-row > .button {
height: 100%;
width: 100%;
}
-x-paper > div:last-child > .button:not(:last-child) {
+x-paper > .button-row > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
}
@@ -1084,6 +1094,11 @@ x-dialog .dialog-subheader {
opacity: 0.1;
}
+.button[selected],
+.icon-button[selected] {
+ opacity: 0.1;
+}
+
#cancel-paste-mode {
z-index: 2;
margin: 0;
@@ -1314,11 +1329,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 {
@@ -1358,7 +1373,7 @@ x-peers:empty~x-instructions {
x-dialog x-paper {
padding: 15px;
}
- x-paper > div:last-child {
+ x-paper > .button-row {
margin: auto -15px -15px;
}
}