Merge branch 'master' into translate

This commit is contained in:
schlagmichdoch 2023-12-15 23:51:11 +01:00
commit b90924af68
6 changed files with 148 additions and 98 deletions

View file

@ -739,10 +739,10 @@
</symbol> </symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/localization.js"></script> <script src="scripts/localization.js" defer></script>
<script src="scripts/persistent-storage.js"></script> <script src="scripts/persistent-storage.js" defer></script>
<script src="scripts/ui-main.js"></script> <script src="scripts/ui-main.js" defer></script>
<script src="scripts/main.js"></script> <script src="scripts/main.js" defer></script>
<!-- Sounds --> <!-- Sounds -->
<audio id="blop" autobuffer="true"> <audio id="blop" autobuffer="true">
<source src="sounds/blop.mp3" type="audio/mpeg"> <source src="sounds/blop.mp3" type="audio/mpeg">

View file

@ -1,40 +1,49 @@
class Localization { class Localization {
constructor() { constructor() {
Localization.$htmlRoot = document.querySelector('html');
Localization.defaultLocale = "en"; Localization.defaultLocale = "en";
Localization.supportedLocales = ["ar", "ca", "de", "en", "es", "fr", "id", "it", "ja", "kn", "nb", "nl", "pt-BR", "ro", "ru", "tr", "zh-CN"]; Localization.supportedLocales = ["ar", "ca", "de", "en", "es", "fr", "id", "it", "ja", "kn", "nb", "nl", "pt-BR", "ro", "ru", "tr", "zh-CN"];
Localization.supportedLocalesRtl = ["ar"]; Localization.supportedLocalesRtl = ["ar"];
Localization.translations = {}; Localization.translations = {};
Localization.defaultTranslations = {}; Localization.translationsDefaultLocale = {};
Localization.systemLocale = Localization.getSupportedOrDefault(navigator.languages); Localization.systemLocale = Localization.getSupportedOrDefaultLocales(navigator.languages);
let storedLanguageCode = localStorage.getItem('language_code'); let storedLanguageCode = localStorage.getItem('language_code');
Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode) Localization.initialLocale = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode)
? storedLanguageCode ? storedLanguageCode
: Localization.systemLocale; : Localization.systemLocale;
} }
static isSupported(locale) { static localeIsSupported(locale) {
return Localization.supportedLocales.indexOf(locale) > -1; return Localization.supportedLocales.indexOf(locale) > -1;
} }
static isRtlLanguage(locale) { static localeIsRtl(locale) {
return Localization.supportedLocalesRtl.indexOf(locale) > -1; return Localization.supportedLocalesRtl.indexOf(locale) > -1;
} }
static isCurrentLocaleRtl() { static currentLocaleIsRtl() {
return Localization.isRtlLanguage(Localization.locale); 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 let localesGeneric = locales
.map(locale => locale.split("-")[0]) .map(locale => locale.split("-")[0])
.filter(locale => locales.indexOf(locale) === -1); .filter(locale => locales.indexOf(locale) === -1);
return locales.find(Localization.isSupported) // If there is no perfect match for browser locales, try generic locales first before resorting to the default locale
|| localesGeneric.find(Localization.isSupported) return locales.find(Localization.localeIsSupported)
|| localesGeneric.find(Localization.localeIsSupported)
|| Localization.defaultLocale; || Localization.defaultLocale;
} }
@ -48,16 +57,14 @@ class Localization {
await Localization.setLocale(locale) await Localization.setLocale(locale)
await Localization.translatePage(); await Localization.translatePage();
const htmlRootNode = document.querySelector('html'); if (Localization.localeIsRtl(locale)) {
Localization.$htmlRoot.setAttribute('dir', 'rtl');
if (Localization.isRtlLanguage(locale)) {
htmlRootNode.setAttribute('dir', 'rtl');
} }
else { else {
htmlRootNode.removeAttribute('dir'); Localization.$htmlRoot.removeAttribute('dir');
} }
htmlRootNode.setAttribute('lang', locale); Localization.$htmlRoot.setAttribute('lang', locale);
console.log("Page successfully translated", console.log("Page successfully translated",
@ -111,75 +118,108 @@ class Localization {
const key = element.getAttribute("data-i18n-key"); const key = element.getAttribute("data-i18n-key");
const attrs = element.getAttribute("data-i18n-attrs").split(" "); const attrs = element.getAttribute("data-i18n-attrs").split(" ");
for (let i in attrs) { attrs.forEach(attr => {
let attr = attrs[i];
if (attr === "text") { if (attr === "text") {
element.innerText = Localization.getTranslation(key); element.innerText = Localization.getTranslation(key);
} }
else { else {
element.setAttribute(attr, Localization.getTranslation(key, attr)); element.setAttribute(attr, Localization.getTranslation(key, attr));
} }
} })
} }
static getTranslation(key, attr = null, data = {}, useDefault = false) { static getTranslationFromTranslationsObj(translationObj, key, attr) {
const keys = key.split(".");
let translationCandidates = useDefault
? Localization.defaultTranslations
: Localization.translations;
let translation; let translation;
try { try {
const keys = key.split(".");
for (let i = 0; i < keys.length - 1; i++) { 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]; let lastKey = keys[keys.length - 1];
if (attr) lastKey += "_" + attr; 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) { } catch (e) {
console.error(e); console.error(e);
translation = "";
} }
if (!translation) { if (!translation) {
if (!useDefault) { throw new Error(`Translation misses entry. Key: ${key} Attribute: ${attr}`);
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr); }
Localization.logHelpCallKey(key);
Localization.logHelpCall(); return translation;
translation = this.getTranslation(key, attr, data, true); }
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 { else {
console.warn("Missing translation in default language:", key, attr); // Is not default locale yet
Localization.logHelpCall(); // 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); 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() { static logHelpCall() {
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/"); console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
} }
static logHelpCallKey(key) { static logHelpCallKey(key, attr) {
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`); 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) { static escapeHTML(unsafeText) {

View file

@ -56,13 +56,16 @@ class PairDrop {
await this.backgroundCanvas.fadeIn(); await this.backgroundCanvas.fadeIn();
// Load deferred assets // Load deferred assets
console.log("Load deferred assets...");
await this.loadDeferredAssets(); await this.loadDeferredAssets();
console.log("Loading of deferred assets completed."); console.log("Loading of deferred assets completed.");
console.log("Hydrate UI...");
await this.hydrate(); await this.hydrate();
console.log("UI hydrated."); console.log("UI hydrated.");
// Evaluate url params as soon as ws is connected // 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}); Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true});
} }
@ -102,36 +105,40 @@ class PairDrop {
} }
} }
async loadDeferredAssets() { loadDeferredAssets() {
console.log("Load deferred assets"); const stylePromises = this.deferredStyles.map(url => this.loadAndApplyStylesheet(url));
for (const url of this.deferredStyles) { const scriptPromises = this.deferredScripts.map(url => this.loadAndApplyScript(url));
await this.loadAndApplyStylesheet(url);
} return Promise.all([...stylePromises, ...scriptPromises]);
for (const url of this.deferredScripts) {
await this.loadAndApplyScript(url);
}
} }
loadStyleSheet(url) { loadStyleSheet(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let stylesheet = document.createElement('link'); let stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet'; stylesheet.rel = 'preload';
stylesheet.as = 'style';
stylesheet.href = url; stylesheet.href = url;
stylesheet.type = 'text/css'; stylesheet.onload = _ => {
stylesheet.onload = resolve; stylesheet.onload = null;
stylesheet.rel = 'stylesheet';
resolve();
};
stylesheet.onerror = reject; stylesheet.onerror = reject;
document.head.appendChild(stylesheet); document.head.appendChild(stylesheet);
}); });
} }
async loadAndApplyStylesheet(url) { loadAndApplyStylesheet(url) {
try { return new Promise( async (resolve) => {
await this.loadStyleSheet(url); try {
console.log(`Stylesheet loaded successfully: ${url}`); await this.loadStyleSheet(url);
} catch (error) { console.log(`Stylesheet loaded successfully: ${url}`);
console.error('Error loading stylesheet:', error); resolve();
} } catch (error) {
console.error('Error loading stylesheet:', error);
}
});
} }
loadScript(url) { loadScript(url) {
@ -145,13 +152,16 @@ class PairDrop {
}); });
} }
async loadAndApplyScript(url) { loadAndApplyScript(url) {
try { return new Promise( async (resolve) => {
await this.loadScript(url); try {
console.log(`Script loaded successfully: ${url}`); await this.loadScript(url);
} catch (error) { console.log(`Script loaded successfully: ${url}`);
console.error('Error loading script:', error); resolve();
} } catch (error) {
console.error('Error loading script:', error);
}
});
} }
async hydrate() { async hydrate() {
@ -223,6 +233,8 @@ class PairDrop {
// remove url params from url // remove url params from url
const urlWithoutParams = getUrlWithoutArguments(); const urlWithoutParams = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", urlWithoutParams); window.history.replaceState({}, "Rewrite URL", urlWithoutParams);
console.log("URL params evaluated.");
} }
} }

View file

@ -132,7 +132,7 @@ class HeaderUI {
this.$header.classList.remove('overflow-expanded'); this.$header.classList.remove('overflow-expanded');
const rtlLocale = Localization.isCurrentLocaleRtl(); const rtlLocale = Localization.currentLocaleIsRtl();
let icon; let icon;
const $headerIconsShown = document.querySelectorAll('body > header:first-of-type > *:not([hidden])'); const $headerIconsShown = document.querySelectorAll('body > header:first-of-type > *:not([hidden])');

View file

@ -2420,14 +2420,9 @@ class Notifications {
_downloadNotification(files) { _downloadNotification(files) {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
let imagesOnly = true; let imagesOnly = files.every(file => file.type.split('/')[0] === 'image');
for(let i=0; i<files.length; i++) {
if (files[i].type.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let title; let title;
if (files.length === 1) { if (files.length === 1) {
title = `${files[0].name}`; title = `${files[0].name}`;
} }
@ -2452,15 +2447,8 @@ class Notifications {
_requestNotification(request, peerId) { _requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
let imagesOnly = true; let imagesOnly = request.header.every(header => header.mime.split('/')[0] === 'image');
for(let i=0; i<request.header.length; i++) { let displayName = $(peerId).querySelector('.name').textContent;
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let displayName = $(peerId).querySelector('.name').textContent
let descriptor; let descriptor;
if (request.header.length === 1) { if (request.header.length === 1) {

View file

@ -32,10 +32,14 @@ process.on('unhandledRejection', (reason, promise) => {
// Evaluate arguments for deployment with Docker and Node.js // Evaluate arguments for deployment with Docker and Node.js
let conf = {}; let conf = {};
conf.debugMode = process.env.DEBUG_MODE === "true"; conf.debugMode = process.env.DEBUG_MODE === "true";
conf.port = process.env.PORT || 3000; conf.port = process.env.PORT || 3000;
conf.wsFallback = process.argv.includes('--include-ws-fallback') || process.env.WS_FALLBACK === "true"; 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')) ? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
: { : {
"sdpSemantics": "unified-plan", "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; conf.ipv6Localize = parseInt(process.env.IPV6_LOCALIZE) || false;
let rateLimit = false; let rateLimit = false;
@ -61,6 +68,7 @@ else {
} }
} }
conf.rateLimit = rateLimit; conf.rateLimit = rateLimit;
conf.buttons = { conf.buttons = {
"donation_button": { "donation_button": {
"active": process.env.DONATION_BUTTON_ACTIVE, "active": process.env.DONATION_BUTTON_ACTIVE,
@ -96,8 +104,10 @@ conf.buttons = {
// Evaluate arguments for deployment with Node.js only // Evaluate arguments for deployment with Node.js only
conf.autoStart = process.argv.includes('--auto-restart'); conf.autoStart = process.argv.includes('--auto-restart');
conf.localhostOnly = process.argv.includes('--localhost-only'); conf.localhostOnly = process.argv.includes('--localhost-only');
// Validate configuration // Validate configuration
if (conf.ipv6Localize) { if (conf.ipv6Localize) {
if (!(0 < conf.ipv6Localize && conf.ipv6Localize < 8)) { if (!(0 < conf.ipv6Localize && conf.ipv6Localize < 8)) {