diff --git a/public/index.html b/public/index.html index 3c3c03a..0fa0e94 100644 --- a/public/index.html +++ b/public/index.html @@ -734,10 +734,10 @@ - - - - + + + + diff --git a/public/scripts/localization.js b/public/scripts/localization.js index b696ef9..d81d3e3 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -1,40 +1,49 @@ class Localization { constructor() { + Localization.$htmlRoot = document.querySelector('html'); + Localization.defaultLocale = "en"; Localization.supportedLocales = ["ar", "ca", "de", "en", "es", "fr", "id", "it", "ja", "nb", "nl", "pt-BR", "ro", "ru", "tr", "zh-CN"]; Localization.supportedLocalesRtl = ["ar"]; Localization.translations = {}; - Localization.defaultTranslations = {}; + Localization.translationsDefaultLocale = {}; - Localization.systemLocale = Localization.getSupportedOrDefault(navigator.languages); + Localization.systemLocale = Localization.getSupportedOrDefaultLocales(navigator.languages); let storedLanguageCode = localStorage.getItem('language_code'); - Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode) + Localization.initialLocale = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode) ? storedLanguageCode : Localization.systemLocale; } - static isSupported(locale) { + static localeIsSupported(locale) { return Localization.supportedLocales.indexOf(locale) > -1; } - static isRtlLanguage(locale) { + static localeIsRtl(locale) { return Localization.supportedLocalesRtl.indexOf(locale) > -1; } - static isCurrentLocaleRtl() { - return Localization.isRtlLanguage(Localization.locale); + static currentLocaleIsRtl() { + return Localization.localeIsRtl(Localization.locale); } - static getSupportedOrDefault(locales) { + static currentLocaleIsDefault() { + return Localization.locale === Localization.defaultLocale + } + + static getSupportedOrDefaultLocales(locales) { + // get generic locales not included in locales + // ["en-us", "de-CH", "fr"] --> ["en", "de"] let localesGeneric = locales .map(locale => locale.split("-")[0]) .filter(locale => locales.indexOf(locale) === -1); - return locales.find(Localization.isSupported) - || localesGeneric.find(Localization.isSupported) + // If there is no perfect match for browser locales, try generic locales first before resorting to the default locale + return locales.find(Localization.localeIsSupported) + || localesGeneric.find(Localization.localeIsSupported) || Localization.defaultLocale; } @@ -48,16 +57,14 @@ class Localization { await Localization.setLocale(locale) await Localization.translatePage(); - const htmlRootNode = document.querySelector('html'); - - if (Localization.isRtlLanguage(locale)) { - htmlRootNode.setAttribute('dir', 'rtl'); + if (Localization.localeIsRtl(locale)) { + Localization.$htmlRoot.setAttribute('dir', 'rtl'); } else { - htmlRootNode.removeAttribute('dir'); + Localization.$htmlRoot.removeAttribute('dir'); } - htmlRootNode.setAttribute('lang', locale); + Localization.$htmlRoot.setAttribute('lang', locale); console.log("Page successfully translated", @@ -111,75 +118,108 @@ class Localization { const key = element.getAttribute("data-i18n-key"); const attrs = element.getAttribute("data-i18n-attrs").split(" "); - for (let i in attrs) { - let attr = attrs[i]; + attrs.forEach(attr => { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { element.setAttribute(attr, Localization.getTranslation(key, attr)); } - } + }) } - static getTranslation(key, attr = null, data = {}, useDefault = false) { - const keys = key.split("."); - - let translationCandidates = useDefault - ? Localization.defaultTranslations - : Localization.translations; - + static getTranslationFromTranslationsObj(translationObj, key, attr) { let translation; - try { + const keys = key.split("."); + for (let i = 0; i < keys.length - 1; i++) { - translationCandidates = translationCandidates[keys[i]] + // iterate into translation object until last layer + translationObj = translationObj[keys[i]] } let lastKey = keys[keys.length - 1]; if (attr) lastKey += "_" + attr; - translation = translationCandidates[lastKey]; + translation = translationObj[lastKey]; - for (let j in data) { - if (translation.includes(`{{${j}}}`)) { - translation = translation.replace(`{{${j}}}`, data[j]); - } else { - console.warn(`Translation for your language ${Localization.locale.toUpperCase()} misses at least one data placeholder:`, key, attr, data); - Localization.logHelpCallKey(key); - Localization.logHelpCall(); - translation = ""; - break; - } - } } catch (e) { console.error(e); - translation = ""; } if (!translation) { - if (!useDefault) { - console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr); - Localization.logHelpCallKey(key); - Localization.logHelpCall(); - translation = this.getTranslation(key, attr, data, true); + throw new Error(`Translation misses entry. Key: ${key} Attribute: ${attr}`); + } + + return translation; + } + + static addDataToTranslation(translation, data) { + for (let j in data) { + if (!translation.includes(`{{${j}}}`)) { + throw new Error(`Translation misses data placeholder: ${j}`); + } + // Add data to translation + translation = translation.replace(`{{${j}}}`, data[j]); + } + return translation; + } + + static getTranslation(key, attr = null, data = {}, useDefault = false) { + let translationObj = useDefault + ? Localization.translationsDefaultLocale + : Localization.translations; + + let translation; + + try { + translation = Localization.getTranslationFromTranslationsObj(translationObj, key, attr); + translation = Localization.addDataToTranslation(translation, data); + } + catch (e) { + // Log warnings and help calls + console.warn(e); + Localization.logTranslationMissingOrBroken(key, attr, data, useDefault); + Localization.logHelpCallKey(key, attr); + Localization.logHelpCall(); + + if (useDefault || Localization.currentLocaleIsDefault()) { + // Is default locale already + // Use empty string as translation + translation = "" } else { - console.warn("Missing translation in default language:", key, attr); - Localization.logHelpCall(); + // Is not default locale yet + // Get translation for default language with same arguments + console.log(`Using default language ${Localization.defaultLocale.toUpperCase()} instead.`); + translation = this.getTranslation(key, attr, data, true); } } return Localization.escapeHTML(translation); } + static logTranslationMissingOrBroken(key, attr, data, useDefault) { + let usedLocale = useDefault + ? Localization.defaultLocale.toUpperCase() + : Localization.locale.toUpperCase(); + + console.warn(`Missing or broken translation for language ${usedLocale}.\n`, 'key:', key, 'attr:', attr, 'data:', data); + } + static logHelpCall() { console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/"); } - static logHelpCallKey(key) { - console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`); + static logHelpCallKey(key, attr) { + let locale = Localization.locale.toLowerCase(); + + let keyComplete = !attr || attr === "text" + ? key + : `${key}_${attr}`; + + console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`); } static escapeHTML(unsafeText) { diff --git a/public/scripts/main.js b/public/scripts/main.js index e6ff259..8960ed2 100644 --- a/public/scripts/main.js +++ b/public/scripts/main.js @@ -56,13 +56,16 @@ class PairDrop { await this.backgroundCanvas.fadeIn(); // Load deferred assets + console.log("Load deferred assets..."); await this.loadDeferredAssets(); console.log("Loading of deferred assets completed."); + console.log("Hydrate UI..."); await this.hydrate(); console.log("UI hydrated."); // Evaluate url params as soon as ws is connected + console.log("Evaluate URL params as soon as websocket connection is established."); Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true}); } @@ -102,36 +105,40 @@ class PairDrop { } } - async loadDeferredAssets() { - console.log("Load deferred assets"); - for (const url of this.deferredStyles) { - await this.loadAndApplyStylesheet(url); - } - for (const url of this.deferredScripts) { - await this.loadAndApplyScript(url); - } + loadDeferredAssets() { + const stylePromises = this.deferredStyles.map(url => this.loadAndApplyStylesheet(url)); + const scriptPromises = this.deferredScripts.map(url => this.loadAndApplyScript(url)); + + return Promise.all([...stylePromises, ...scriptPromises]); } loadStyleSheet(url) { return new Promise((resolve, reject) => { let stylesheet = document.createElement('link'); - stylesheet.rel = 'stylesheet'; + stylesheet.rel = 'preload'; + stylesheet.as = 'style'; stylesheet.href = url; - stylesheet.type = 'text/css'; - stylesheet.onload = resolve; + stylesheet.onload = _ => { + stylesheet.onload = null; + stylesheet.rel = 'stylesheet'; + resolve(); + }; stylesheet.onerror = reject; document.head.appendChild(stylesheet); }); } - async loadAndApplyStylesheet(url) { - try { - await this.loadStyleSheet(url); - console.log(`Stylesheet loaded successfully: ${url}`); - } catch (error) { - console.error('Error loading stylesheet:', error); - } + loadAndApplyStylesheet(url) { + return new Promise( async (resolve) => { + try { + await this.loadStyleSheet(url); + console.log(`Stylesheet loaded successfully: ${url}`); + resolve(); + } catch (error) { + console.error('Error loading stylesheet:', error); + } + }); } loadScript(url) { @@ -145,13 +152,16 @@ class PairDrop { }); } - async loadAndApplyScript(url) { - try { - await this.loadScript(url); - console.log(`Script loaded successfully: ${url}`); - } catch (error) { - console.error('Error loading script:', error); - } + loadAndApplyScript(url) { + return new Promise( async (resolve) => { + try { + await this.loadScript(url); + console.log(`Script loaded successfully: ${url}`); + resolve(); + } catch (error) { + console.error('Error loading script:', error); + } + }); } async hydrate() { @@ -223,6 +233,8 @@ class PairDrop { // remove url params from url const urlWithoutParams = getUrlWithoutArguments(); window.history.replaceState({}, "Rewrite URL", urlWithoutParams); + + console.log("URL params evaluated."); } } diff --git a/public/scripts/ui-main.js b/public/scripts/ui-main.js index 409a29c..c7a27e5 100644 --- a/public/scripts/ui-main.js +++ b/public/scripts/ui-main.js @@ -132,7 +132,7 @@ class HeaderUI { this.$header.classList.remove('overflow-expanded'); - const rtlLocale = Localization.isCurrentLocaleRtl(); + const rtlLocale = Localization.currentLocaleIsRtl(); let icon; const $headerIconsShown = document.querySelectorAll('body > header:first-of-type > *:not([hidden])'); diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 52fecbc..ae63a0e 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -2420,14 +2420,9 @@ class Notifications { _downloadNotification(files) { if (document.visibilityState !== 'visible') { - let imagesOnly = true; - for(let i=0; i file.type.split('/')[0] === 'image'); let title; + if (files.length === 1) { title = `${files[0].name}`; } @@ -2452,15 +2447,8 @@ class Notifications { _requestNotification(request, peerId) { if (document.visibilityState !== 'visible') { - let imagesOnly = true; - for(let i=0; i header.mime.split('/')[0] === 'image'); + let displayName = $(peerId).querySelector('.name').textContent; let descriptor; if (request.header.length === 1) { diff --git a/server/index.js b/server/index.js index 2134c67..9f5d7b8 100644 --- a/server/index.js +++ b/server/index.js @@ -32,10 +32,14 @@ process.on('unhandledRejection', (reason, promise) => { // Evaluate arguments for deployment with Docker and Node.js let conf = {}; + conf.debugMode = process.env.DEBUG_MODE === "true"; + conf.port = process.env.PORT || 3000; + conf.wsFallback = process.argv.includes('--include-ws-fallback') || process.env.WS_FALLBACK === "true"; -conf.rtcConfig = process.env.RTC_CONFIG + +conf.rtcConfig = process.env.RTC_CONFIG && process.env.RTC_CONFIG !== "false" ? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8')) : { "sdpSemantics": "unified-plan", @@ -47,7 +51,10 @@ conf.rtcConfig = process.env.RTC_CONFIG }; -conf.signalingServer = process.env.SIGNALING_SERVER || false; +conf.signalingServer = process.env.SIGNALING_SERVER && process.env.SIGNALING_SERVER !== "false" + ? process.env.SIGNALING_SERVER + : false; + conf.ipv6Localize = parseInt(process.env.IPV6_LOCALIZE) || false; let rateLimit = false; @@ -61,6 +68,7 @@ else { } } conf.rateLimit = rateLimit; + conf.buttons = { "donation_button": { "active": process.env.DONATION_BUTTON_ACTIVE, @@ -96,8 +104,10 @@ conf.buttons = { // Evaluate arguments for deployment with Node.js only conf.autoStart = process.argv.includes('--auto-restart'); + conf.localhostOnly = process.argv.includes('--localhost-only'); + // Validate configuration if (conf.ipv6Localize) { if (!(0 < conf.ipv6Localize && conf.ipv6Localize < 8)) {