diff --git a/public/index.html b/public/index.html index cd81663..9625637 100644 --- a/public/index.html +++ b/public/index.html @@ -44,6 +44,11 @@ +
+ + + +
@@ -109,7 +114,7 @@
You are known as: -
+
@@ -125,6 +130,25 @@
+ + + + +

Select Language

+
+
+ + + + + +
+
+ +
+
+
+
@@ -147,7 +171,7 @@
Enter key from another device to continue.
-
+
@@ -173,7 +197,7 @@

-
+
@@ -199,7 +223,7 @@
-
+
@@ -224,7 +248,7 @@
-
+
@@ -244,7 +268,7 @@
-
+
@@ -263,7 +287,7 @@
-
+
@@ -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 @@ +
+ + + +
@@ -109,7 +114,7 @@
You are known as: -
+
@@ -130,6 +135,25 @@ if WebRTC is not available.
+ + + + +

Select Language

+
+
+ + + + + +
+
+ +
+
+
+
@@ -152,7 +176,7 @@
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; } }