add translation selector and fix translation of data-attributes

This commit is contained in:
schlagmichdoch 2023-08-30 14:57:40 +02:00
parent 19f56a8499
commit 17afa18d84
18 changed files with 312 additions and 68 deletions

View file

@ -44,6 +44,11 @@
<use xlink:href="#info-outline" /> <use xlink:href="#info-outline" />
</svg> </svg>
</a> </a>
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title" title="Select Language">
<svg class="icon">
<use xlink:href="#icon-language-selector" />
</svg>
</div>
<div id="theme-wrapper"> <div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" > <div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" >
<svg class="icon"> <svg class="icon">
@ -109,7 +114,7 @@
</svg> </svg>
<div> <div>
<span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span> <span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
<div id="display-name" data-i18n-key="footer.display-name" data-i18n-attrs="placeholder title" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div> <div id="display-name" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title" placeholder="Loading..." data-placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon"> <svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" /> <use xlink:href="#edit-pen-icon" />
</svg> </svg>
@ -125,6 +130,25 @@
</div> </div>
</div> </div>
</footer> </footer>
<!-- Language Select Dialog -->
<x-dialog id="language-select-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text">Select Language</h2>
<hr>
<div class="language-buttons">
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text">System Language</button>
<button class="button fw" value="en">English</button>
<button class="button fw" value="nb">Norsk</button>
<button class="button fw" value="ru">Русский язык</button>
<button class="button fw" value="zh-CN">中文</button>
</div>
<div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Pair Device Dialog --> <!-- Pair Device Dialog -->
<x-dialog id="pair-device-dialog"> <x-dialog id="pair-device-dialog">
<form action="#"> <form action="#">
@ -147,7 +171,7 @@
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device to continue.</div> <div class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device to continue.</div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button> <button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button> <button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div> </div>
@ -173,7 +197,7 @@
</span> </span>
</p> </p>
</div> </div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button> <button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div> </div>
</x-paper> </x-paper>
@ -199,7 +223,7 @@
<div class="row font-body2 file-size"></div> <div class="row font-body2 file-size"></div>
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button> <button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
</div> </div>
@ -224,7 +248,7 @@
<div class="row font-body2 file-size"></div> <div class="row font-body2 file-size"></div>
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" autofocus hidden>Share</button> <button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" autofocus hidden>Share</button>
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button> <button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button> <button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
@ -244,7 +268,7 @@
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button> <button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button> <button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div> </div>
@ -263,7 +287,7 @@
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button>
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button> <button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
</div> </div>
@ -392,6 +416,11 @@
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/> <path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
</symbol> </symbol>
<symbol id="icon-language-selector" viewBox="0 0 640 512">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"/>
</symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/localization.js"></script> <script src="scripts/localization.js"></script>

View file

@ -1,6 +1,7 @@
{ {
"header": { "header": {
"about_title": "About PairDrop", "about_title": "About PairDrop",
"language-selector_title": "Select Language",
"about_aria-label": "Open About PairDrop", "about_aria-label": "Open About PairDrop",
"theme-auto_title": "Adapt Theme to System", "theme-auto_title": "Adapt Theme to System",
"theme-light_title": "Always Use Light-Theme", "theme-light_title": "Always Use Light-Theme",
@ -24,7 +25,7 @@
}, },
"footer": { "footer": {
"known-as": "You are known as:", "known-as": "You are known as:",
"display-name_placeholder": "Loading…", "display-name_data-placeholder": "Loading…",
"display-name_title": "Edit your device name permanently", "display-name_title": "Edit your device name permanently",
"discovery-everyone": "You can be discovered by everyone", "discovery-everyone": "You can be discovered by everyone",
"on-this-network": "on this network", "on-this-network": "on this network",
@ -75,7 +76,9 @@
"title-image-plural": "Images", "title-image-plural": "Images",
"title-file-plural": "Files", "title-file-plural": "Files",
"receive-title": "{{descriptor}} Received", "receive-title": "{{descriptor}} Received",
"download-again": "Download again" "download-again": "Download again",
"language-selector-title": "Select Language",
"system-language": "System Language"
}, },
"about": { "about": {
"close-about_aria-label": "Close About PairDrop", "close-about_aria-label": "Close About PairDrop",

View file

@ -15,7 +15,7 @@
"discovery-everyone": "Du kan oppdages av alle", "discovery-everyone": "Du kan oppdages av alle",
"and-by": "og av", "and-by": "og av",
"webrtc": "hvis WebRTC ikke er tilgjengelig.", "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", "display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
"traffic": "Trafikken", "traffic": "Trafikken",
"on-this-network": "på dette nettverket", "on-this-network": "på dette nettverket",

View file

@ -24,7 +24,7 @@
}, },
"footer": { "footer": {
"discovery-everyone": "О вас может узнать каждый", "discovery-everyone": "О вас может узнать каждый",
"display-name_placeholder": "Загрузка…", "display-name_data-placeholder": "Загрузка…",
"routed": "направляется через сервер", "routed": "направляется через сервер",
"webrtc": ", если WebRTC недоступен.", "webrtc": ", если WebRTC недоступен.",
"traffic": "Трафик", "traffic": "Трафик",

View file

@ -15,7 +15,7 @@
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
}, },
"footer": { "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" "display-name_title": "Cihazının adını kalıcı olarak düzenle"
}, },
"dialogs": { "dialogs": {

View file

@ -26,7 +26,7 @@
"routed": "途径服务器", "routed": "途径服务器",
"webrtc": "如果 WebRTC 不可用。", "webrtc": "如果 WebRTC 不可用。",
"known-as": "你的名字是:", "known-as": "你的名字是:",
"display-name_placeholder": "加载中…", "display-name_data-placeholder": "加载中…",
"and-by": "和", "and-by": "和",
"display-name_title": "长久修改你的设备名", "display-name_title": "长久修改你的设备名",
"discovery-everyone": "你对所有人可见", "discovery-everyone": "你对所有人可见",

View file

@ -5,12 +5,19 @@ class Localization {
Localization.translations = {}; Localization.translations = {};
Localization.defaultTranslations = {}; 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(_ => { .then(_ => {
Localization.translatePage(); console.log("Initial translation successful.");
}) Events.fire("translation-loaded");
});
} }
static isSupported(locale) { static isSupported(locale) {
@ -21,11 +28,21 @@ class Localization {
return locales.find(Localization.isSupported) || Localization.defaultLocale; 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) { static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false; if (newLocale === Localization.locale) return false;
const isFirstTranslation = !Localization.locale
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
const newTranslations = await Localization.fetchTranslationsFor(newLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale);
@ -34,10 +51,14 @@ class Localization {
Localization.locale = newLocale; Localization.locale = newLocale;
Localization.translations = newTranslations; 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) { static async fetchTranslationsFor(newLocale) {
@ -48,7 +69,7 @@ class Localization {
return await response.json(); return await response.json();
} }
static translatePage() { static async translatePage() {
document document
.querySelectorAll("[data-i18n-key]") .querySelectorAll("[data-i18n-key]")
.forEach(element => Localization.translateElement(element)); .forEach(element => Localization.translateElement(element));
@ -63,10 +84,14 @@ class Localization {
if (attr === "text") { if (attr === "text") {
element.innerText = Localization.getTranslation(key); element.innerText = Localization.getTranslation(key);
} else { } else {
if (attr.startsWith("data-")) {
let dataAttr = attr.substring(5);
element.dataset.dataAttr = Localization.getTranslation(key, attr);
} {
element.setAttribute(attr, Localization.getTranslation(key, attr)); element.setAttribute(attr, Localization.getTranslation(key, attr));
} }
} }
}
} }
static getTranslation(key, attr, data, useDefault=false) { static getTranslation(key, attr, data, useDefault=false) {

View file

@ -45,6 +45,8 @@ class PeersUI {
this.$displayName = $('display-name'); this.$displayName = $('display-name');
this.$displayName.setAttribute("placeholder", this.$displayName.dataset.placeholder);
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e)); this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e)); this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText)); 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.$languageButtons.length; i++) {
const $btn = this.$languageButtons[i];
if ($btn.value === locale) {
$btn.focus();
break;
}
}
}
super.show();
}
selectLanguage(e) {
e.preventDefault()
let languageCode = e.target.value;
if (languageCode) {
localStorage.setItem('language-code', languageCode);
} else {
localStorage.removeItem('language-code');
}
Localization.setTranslation(languageCode)
.then(_ => this.hide());
}
}
class ReceiveDialog extends Dialog { class ReceiveDialog extends Dialog {
constructor(id) { constructor(id) {
super(id); super(id);
@ -2255,6 +2309,7 @@ class PairDrop {
const server = new ServerConnection(); const server = new ServerConnection();
const peers = new PeersManager(server); const peers = new PeersManager(server);
const peersUI = new PeersUI(); const peersUI = new PeersUI();
const languageSelectDialog = new LanguageSelectDialog();
const receiveFileDialog = new ReceiveFileDialog(); const receiveFileDialog = new ReceiveFileDialog();
const receiveRequestDialog = new ReceiveRequestDialog(); const receiveRequestDialog = new ReceiveRequestDialog();
const sendTextDialog = new SendTextDialog(); const sendTextDialog = new SendTextDialog();

View file

@ -23,6 +23,7 @@ body {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
transition: color 300ms;
} }
body { body {
@ -40,6 +41,10 @@ html {
min-height: fill-available; min-height: fill-available;
} }
.fw {
width: 100%;
}
.row-reverse { .row-reverse {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
@ -591,7 +596,6 @@ footer {
align-items: center; align-items: center;
padding: 0 0 16px 0; padding: 0 0 16px 0;
text-align: center; text-align: center;
transition: color 300ms;
cursor: default; cursor: default;
} }
@ -683,7 +687,6 @@ x-dialog x-paper {
top: max(50%, 350px); top: max(50%, 350px);
margin-top: -328.5px; margin-top: -328.5px;
width: calc(100vw - 20px); width: calc(100vw - 20px);
height: 625px;
} }
#pair-device-dialog ::-moz-selection, #pair-device-dialog ::-moz-selection,
@ -761,7 +764,7 @@ x-dialog a {
} }
x-dialog hr { x-dialog hr {
margin: 40px -24px 30px -24px; margin: 20px -24px 20px -24px;
border: solid 1.25px var(--border-color); border: solid 1.25px var(--border-color);
} }
@ -868,18 +871,18 @@ x-dialog .row {
} }
/* button row*/ /* button row*/
x-paper > div:last-child { x-paper > .button-row {
margin: auto -24px -15px; margin: 25px -24px -15px;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
height: 50px; height: 50px;
} }
x-paper > div:last-child > .button { x-paper > .button-row > .button {
height: 100%; height: 100%;
width: 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); border-left: solid 2.5px var(--border-color);
} }
@ -1044,6 +1047,11 @@ x-dialog .dialog-subheader {
opacity: 0.1; opacity: 0.1;
} }
.button[selected],
.icon-button[selected] {
opacity: 0.1;
}
#cancel-paste-mode { #cancel-paste-mode {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
@ -1301,7 +1309,7 @@ x-peers:empty~x-instructions {
x-dialog x-paper { x-dialog x-paper {
padding: 15px; padding: 15px;
} }
x-paper > div:last-child { x-paper > .button-row {
margin: auto -15px -15px; margin: auto -15px -15px;
} }
} }

View file

@ -44,6 +44,11 @@
<use xlink:href="#info-outline" /> <use xlink:href="#info-outline" />
</svg> </svg>
</a> </a>
<div id="language-selector" class="icon-button" data-i18n-key="header.language-selector" data-i18n-attrs="title" title="Select Language">
<svg class="icon">
<use xlink:href="#icon-language-selector" />
</svg>
</div>
<div id="theme-wrapper"> <div id="theme-wrapper">
<div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" > <div id="theme-auto" class="icon-button selected" data-i18n-key="header.theme-auto" data-i18n-attrs="title" title="Adapt Theme to System" >
<svg class="icon"> <svg class="icon">
@ -109,7 +114,7 @@
</svg> </svg>
<div> <div>
<span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span> <span data-i18n-key="footer.known-as" data-i18n-attrs="text">You are known as:</span>
<div id="display-name" data-i18n-key="footer.display-name" data-i18n-attrs="placeholder title" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div> <div id="display-name" data-i18n-key="footer.display-name" data-i18n-attrs="data-placeholder title" placeholder="Loading..." data-placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon"> <svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" /> <use xlink:href="#edit-pen-icon" />
</svg> </svg>
@ -130,6 +135,25 @@
<span data-i18n-key="footer.webrtc" data-i18n-attrs="text">if WebRTC is not available.</span> <span data-i18n-key="footer.webrtc" data-i18n-attrs="text">if WebRTC is not available.</span>
</div> </div>
</footer> </footer>
<!-- Language Select Dialog -->
<x-dialog id="language-select-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center" data-i18n-key="dialogs.language-selector-title" data-i18n-attrs="text">Select Language</h2>
<hr>
<div class="language-buttons">
<button class="button fw" data-i18n-key="dialogs.system-language" data-i18n-attrs="text">System Language</button>
<button class="button fw" value="en">English</button>
<button class="button fw" value="nb">Norsk</button>
<button class="button fw" value="ru">Русский язык</button>
<button class="button fw" value="zh-CN">中文</button>
</div>
<div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div>
</x-paper>
</x-background>
</x-dialog>
<!-- Pair Device Dialog --> <!-- Pair Device Dialog -->
<x-dialog id="pair-device-dialog"> <x-dialog id="pair-device-dialog">
<form action="#"> <form action="#">
@ -152,7 +176,7 @@
<input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" aria-label="pair-key-6" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device to continue.</div> <div class="font-subheading center text-center" data-i18n-key="dialogs.enter-key-from-another-device" data-i18n-attrs="text">Enter key from another device to continue.</div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button> <button class="button" type="submit" data-i18n-key="dialogs.pair" data-i18n-attrs="text" disabled>Pair</button>
<button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button> <button class="button" type="button" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div> </div>
@ -178,7 +202,7 @@
</span> </span>
</p> </p>
</div> </div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button> <button class="button" type="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
</div> </div>
</x-paper> </x-paper>
@ -204,7 +228,7 @@
<div class="row font-body2 file-size"></div> <div class="row font-body2 file-size"></div>
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" data-i18n-key="dialogs.accept" data-i18n-attrs="text" autofocus>Accept</button>
<button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button> <button id="decline-request" class="button" title="ESCAPE" data-i18n-key="dialogs.decline" data-i18n-attrs="text">Decline</button>
</div> </div>
@ -229,7 +253,7 @@
<div class="row font-body2 file-size"></div> <div class="row font-body2 file-size"></div>
</div> </div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" autofocus hidden>Share</button> <button id="share-btn" class="button" data-i18n-key="dialogs.share" data-i18n-attrs="text" autofocus hidden>Share</button>
<button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button> <button id="download-btn" class="button" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus>Download</button>
<button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button> <button class="button" data-i18n-key="dialogs.close" data-i18n-attrs="text" close>Close</button>
@ -249,7 +273,7 @@
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button> <button class="button" type="submit" title="CTRL/⌘ + ENTER" data-i18n-key="dialogs.send" data-i18n-attrs="text" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button> <button class="button" type="button" title="ESCAPE" data-i18n-key="dialogs.cancel" data-i18n-attrs="text" close>Cancel</button>
</div> </div>
@ -268,7 +292,7 @@
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="center row-reverse"> <div class="center row-reverse button-row">
<button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C" data-i18n-key="dialogs.copy" data-i18n-attrs="text">Copy</button>
<button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button> <button id="close" class="button" title="ESCAPE" data-i18n-key="dialogs.close" data-i18n-attrs="text">Close</button>
</div> </div>
@ -397,6 +421,11 @@
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> <!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/> <path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
</symbol> </symbol>
<symbol id="icon-language-selector" viewBox="0 0 640 512">
<!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"/>
</symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/localization.js"></script> <script src="scripts/localization.js"></script>

View file

@ -24,7 +24,7 @@
}, },
"footer": { "footer": {
"known-as": "You are known as:", "known-as": "You are known as:",
"display-name_placeholder": "Loading…", "display-name_data-placeholder": "Loading…",
"display-name_title": "Edit your device name permanently", "display-name_title": "Edit your device name permanently",
"discovery-everyone": "You can be discovered by everyone", "discovery-everyone": "You can be discovered by everyone",
"on-this-network": "on this network", "on-this-network": "on this network",

View file

@ -15,7 +15,7 @@
"discovery-everyone": "Du kan oppdages av alle", "discovery-everyone": "Du kan oppdages av alle",
"and-by": "og av", "and-by": "og av",
"webrtc": "hvis WebRTC ikke er tilgjengelig.", "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", "display-name_title": "Rediger det vedvarende enhetsnavnet ditt",
"traffic": "Trafikken", "traffic": "Trafikken",
"on-this-network": "på dette nettverket", "on-this-network": "på dette nettverket",

View file

@ -24,7 +24,7 @@
}, },
"footer": { "footer": {
"discovery-everyone": "О вас может узнать каждый", "discovery-everyone": "О вас может узнать каждый",
"display-name_placeholder": "Загрузка…", "display-name_data-placeholder": "Загрузка…",
"routed": "направляется через сервер", "routed": "направляется через сервер",
"webrtc": ", если WebRTC недоступен.", "webrtc": ", если WebRTC недоступен.",
"traffic": "Трафик", "traffic": "Трафик",

View file

@ -15,7 +15,7 @@
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın"
}, },
"footer": { "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" "display-name_title": "Cihazının adını kalıcı olarak düzenle"
}, },
"dialogs": { "dialogs": {

View file

@ -26,7 +26,7 @@
"routed": "途径服务器", "routed": "途径服务器",
"webrtc": "如果 WebRTC 不可用。", "webrtc": "如果 WebRTC 不可用。",
"known-as": "你的名字是:", "known-as": "你的名字是:",
"display-name_placeholder": "加载中…", "display-name_data-placeholder": "加载中…",
"and-by": "和", "and-by": "和",
"display-name_title": "长久修改你的设备名", "display-name_title": "长久修改你的设备名",
"discovery-everyone": "你对所有人可见", "discovery-everyone": "你对所有人可见",

View file

@ -5,12 +5,19 @@ class Localization {
Localization.translations = {}; Localization.translations = {};
Localization.defaultTranslations = {}; 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(_ => { .then(_ => {
Localization.translatePage(); console.log("Initial translation successful.");
}) Events.fire("translation-loaded");
});
} }
static isSupported(locale) { static isSupported(locale) {
@ -21,11 +28,21 @@ class Localization {
return locales.find(Localization.isSupported) || Localization.defaultLocale; 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) { static async setLocale(newLocale) {
if (newLocale === Localization.locale) return false; if (newLocale === Localization.locale) return false;
const isFirstTranslation = !Localization.locale
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
const newTranslations = await Localization.fetchTranslationsFor(newLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale);
@ -34,10 +51,14 @@ class Localization {
Localization.locale = newLocale; Localization.locale = newLocale;
Localization.translations = newTranslations; 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) { static async fetchTranslationsFor(newLocale) {
@ -48,7 +69,7 @@ class Localization {
return await response.json(); return await response.json();
} }
static translatePage() { static async translatePage() {
document document
.querySelectorAll("[data-i18n-key]") .querySelectorAll("[data-i18n-key]")
.forEach(element => Localization.translateElement(element)); .forEach(element => Localization.translateElement(element));
@ -63,10 +84,14 @@ class Localization {
if (attr === "text") { if (attr === "text") {
element.innerText = Localization.getTranslation(key); element.innerText = Localization.getTranslation(key);
} else { } else {
if (attr.startsWith("data-")) {
let dataAttr = attr.substring(5);
element.dataset.dataAttr = Localization.getTranslation(key, attr);
} {
element.setAttribute(attr, Localization.getTranslation(key, attr)); element.setAttribute(attr, Localization.getTranslation(key, attr));
} }
} }
}
} }
static getTranslation(key, attr, data, useDefault=false) { static getTranslation(key, attr, data, useDefault=false) {

View file

@ -45,6 +45,8 @@ class PeersUI {
this.$displayName = $('display-name'); this.$displayName = $('display-name');
this.$displayName.setAttribute("placeholder", this.$displayName.dataset.placeholder);
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e)); this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e)); this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText)); 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.$languageButtons.length; i++) {
const $btn = this.$languageButtons[i];
if ($btn.value === locale) {
$btn.focus();
break;
}
}
}
super.show();
}
selectLanguage(e) {
e.preventDefault()
let languageCode = e.target.value;
if (languageCode) {
localStorage.setItem('language-code', languageCode);
} else {
localStorage.removeItem('language-code');
}
Localization.setTranslation(languageCode)
.then(_ => this.hide());
}
}
class ReceiveDialog extends Dialog { class ReceiveDialog extends Dialog {
constructor(id) { constructor(id) {
super(id); super(id);
@ -2256,6 +2310,7 @@ class PairDrop {
const server = new ServerConnection(); const server = new ServerConnection();
const peers = new PeersManager(server); const peers = new PeersManager(server);
const peersUI = new PeersUI(); const peersUI = new PeersUI();
const languageSelectDialog = new LanguageSelectDialog();
const receiveFileDialog = new ReceiveFileDialog(); const receiveFileDialog = new ReceiveFileDialog();
const receiveRequestDialog = new ReceiveRequestDialog(); const receiveRequestDialog = new ReceiveRequestDialog();
const sendTextDialog = new SendTextDialog(); const sendTextDialog = new SendTextDialog();

View file

@ -24,6 +24,7 @@ body {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
user-select: none; user-select: none;
transition: color 300ms;
} }
body { body {
@ -41,6 +42,10 @@ html {
min-height: fill-available; min-height: fill-available;
} }
.fw {
width: 100%;
}
.row-reverse { .row-reverse {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
@ -452,7 +457,7 @@ x-no-peers::before {
} }
x-no-peers[drop-bg]::before { x-no-peers[drop-bg]::before {
content: "Release to select recipient"; content: attr(data-drop-bg);
} }
x-no-peers[drop-bg] * { x-no-peers[drop-bg] * {
@ -652,11 +657,13 @@ footer .font-body2 {
#on-this-network { #on-this-network {
border-bottom: solid 4px var(--primary-color); border-bottom: solid 4px var(--primary-color);
padding-bottom: 1px; padding-bottom: 1px;
word-break: keep-all;
} }
#paired-devices { #paired-devices {
border-bottom: solid 4px var(--paired-device-color); border-bottom: solid 4px var(--paired-device-color);
padding-bottom: 1px; padding-bottom: 1px;
word-break: keep-all;
} }
#display-name { #display-name {
@ -723,7 +730,6 @@ x-dialog x-paper {
top: max(50%, 350px); top: max(50%, 350px);
margin-top: -328.5px; margin-top: -328.5px;
width: calc(100vw - 20px); width: calc(100vw - 20px);
height: 625px;
} }
#pair-device-dialog ::-moz-selection, #pair-device-dialog ::-moz-selection,
@ -800,8 +806,12 @@ x-dialog .font-subheading {
margin: 16px; margin: 16px;
} }
#pair-instructions {
flex-direction: column;
}
x-dialog hr { x-dialog hr {
margin: 40px -24px 30px -24px; margin: 20px -24px 20px -24px;
border: solid 1.25px var(--border-color); border: solid 1.25px var(--border-color);
} }
@ -811,7 +821,7 @@ x-dialog hr {
/* Edit Paired Devices Dialog */ /* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before { .paired-devices-wrapper:empty:before {
content: "No paired devices."; content: attr(data-empty);
} }
.paired-devices-wrapper:empty { .paired-devices-wrapper:empty {
@ -908,18 +918,18 @@ x-dialog .row {
} }
/* button row*/ /* button row*/
x-paper > div:last-child { x-paper > .button-row {
margin: auto -24px -15px; margin: 25px -24px -15px;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
height: 50px; height: 50px;
} }
x-paper > div:last-child > .button { x-paper > .button-row > .button {
height: 100%; height: 100%;
width: 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); border-left: solid 2.5px var(--border-color);
} }
@ -1084,6 +1094,11 @@ x-dialog .dialog-subheader {
opacity: 0.1; opacity: 0.1;
} }
.button[selected],
.icon-button[selected] {
opacity: 0.1;
}
#cancel-paste-mode { #cancel-paste-mode {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
@ -1314,11 +1329,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before {
} }
x-instructions[drop-peer]: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 { x-instructions[drop-bg]:not([drop-peer]):before {
content: "Release to select recipient"; content: attr(data-drop-bg);
} }
x-instructions p { x-instructions p {
@ -1358,7 +1373,7 @@ x-peers:empty~x-instructions {
x-dialog x-paper { x-dialog x-paper {
padding: 15px; padding: 15px;
} }
x-paper > div:last-child { x-paper > .button-row {
margin: auto -15px -15px; margin: auto -15px -15px;
} }
} }