mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-21 15:26:17 -04:00
Merge branch 'master' into translate
This commit is contained in:
commit
b90924af68
6 changed files with 148 additions and 98 deletions
|
@ -739,10 +739,10 @@
|
|||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/localization.js"></script>
|
||||
<script src="scripts/persistent-storage.js"></script>
|
||||
<script src="scripts/ui-main.js"></script>
|
||||
<script src="scripts/main.js"></script>
|
||||
<script src="scripts/localization.js" defer></script>
|
||||
<script src="scripts/persistent-storage.js" defer></script>
|
||||
<script src="scripts/ui-main.js" defer></script>
|
||||
<script src="scripts/main.js" defer></script>
|
||||
<!-- Sounds -->
|
||||
<audio id="blop" autobuffer="true">
|
||||
<source src="sounds/blop.mp3" type="audio/mpeg">
|
||||
|
|
|
@ -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", "kn", "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) {
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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])');
|
||||
|
||||
|
|
|
@ -2420,14 +2420,9 @@ class Notifications {
|
|||
|
||||
_downloadNotification(files) {
|
||||
if (document.visibilityState !== 'visible') {
|
||||
let imagesOnly = true;
|
||||
for(let i=0; i<files.length; i++) {
|
||||
if (files[i].type.split('/')[0] !== 'image') {
|
||||
imagesOnly = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let imagesOnly = files.every(file => 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<request.header.length; i++) {
|
||||
if (request.header[i].mime.split('/')[0] !== 'image') {
|
||||
imagesOnly = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let displayName = $(peerId).querySelector('.name').textContent
|
||||
let imagesOnly = request.header.every(header => header.mime.split('/')[0] === 'image');
|
||||
let displayName = $(peerId).querySelector('.name').textContent;
|
||||
|
||||
let descriptor;
|
||||
if (request.header.length === 1) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue