From 6f585e1f1d6967b042fc406f5b882c5baee93f00 Mon Sep 17 00:00:00 2001 From: djmaze Date: Thu, 15 Oct 2020 00:26:40 +0200 Subject: [PATCH] Use Intl.DateTimeFormat instead of momentjs where we can Need to solve the Intl.RelativeTimeFormat to drop momentjs --- dev/Common/Momentor.js | 9 +- dev/Common/Translator.js | 60 ++---- dev/Screen/User/Settings.js | 8 +- dev/prototype.js | 200 +++++++----------- .../app/libraries/RainLoop/ServiceActions.php | 52 ++--- 5 files changed, 115 insertions(+), 214 deletions(-) diff --git a/dev/Common/Momentor.js b/dev/Common/Momentor.js index 8629cea22..c9a1d65ae 100644 --- a/dev/Common/Momentor.js +++ b/dev/Common/Momentor.js @@ -12,13 +12,14 @@ export function format(timeStampInUTC, formatStr) { case 'SHORT': { if (4 >= (now - time) / 3600000) return m.fromNow(); - const ymd = m.format('Ymd'), date = new Date; - if (date.format('Ymd') === ymd) + const mt = m.getTime(), date = new Date, + dt = date.setHours(0,0,0,0); + if (mt > dt) return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')}); - if (new Date(now - 86400000).format('Ymd') === ymd) + if (mt > dt - 86400000) return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')}); if (date.getFullYear() === m.getFullYear()) - return m.format('d M.'); + return m.format('d M'); return m.format('LL'); } case 'FULL': diff --git a/dev/Common/Translator.js b/dev/Common/Translator.js index 1ad401e0b..cf655c307 100644 --- a/dev/Common/Translator.js +++ b/dev/Common/Translator.js @@ -132,26 +132,11 @@ export function i18nToNodes(element) { , 1); } -const reloadData = () => { - if (window.rainloopI18N) { - I18N_DATA = window.rainloopI18N || {}; - - i18nToNodes(doc); - - dispatchEvent(new CustomEvent('reload-time')); - trigger(!trigger()); - } - - window.rainloopI18N = null; -}; - /** * @returns {void} */ export function initNotificationLanguage() { - I18N_NOTIFICATION_MAP.forEach((item) => { - I18N_NOTIFICATION_DATA[item[0]] = i18n(item[1]); - }); + I18N_NOTIFICATION_MAP.forEach(item => I18N_NOTIFICATION_DATA[item[0]] = i18n(item[1])); } /** @@ -159,22 +144,9 @@ export function initNotificationLanguage() { * @param {Function=} langCallback = null */ export function initOnStartOrLangChange(startCallback, langCallback = null) { - if (startCallback) { - startCallback(); - } - - if (langCallback) { - trigger.subscribe(() => { - if (startCallback) { - startCallback(); - } - if (langCallback) { - langCallback(); - } - }); - } else if (startCallback) { - trigger.subscribe(startCallback); - } + startCallback && startCallback(); + startCallback && trigger.subscribe(startCallback); + langCallback && trigger.subscribe(langCallback); } /** @@ -251,11 +223,8 @@ export function reload(admin, language) { return new Promise((resolve, reject) => { return fetch(langLink(language, admin), {cache: 'reload'}) .then(response => { - if (response.ok) { - const type = response.headers.get('Content-Type'); - if (type.includes('application/javascript')) { - return response.text(); - } + if (response.ok && response.headers.get('Content-Type').includes('application/javascript')) { + return response.text(); } reject(new Error('Invalid response')) }, error => { @@ -267,15 +236,14 @@ export function reload(admin, language) { doc.head.appendChild(script).remove(); setTimeout( () => { - reloadData(); - - const isRtl = ['ar', 'ar_sa', 'he', 'he_he', 'ur', 'ur_ir'].includes((language || '').toLowerCase()), - htmlCL = doc.documentElement.classList; - - htmlCL.remove('rl-rtl', 'rl-ltr'); - htmlCL.add(isRtl ? 'rl-rtl' : 'rl-ltr'); - // doc.documentElement.dir = isRtl ? 'rtl' : 'ltr' - + // reload the data + if (window.rainloopI18N) { + I18N_DATA = window.rainloopI18N || {}; + i18nToNodes(doc); + dispatchEvent(new CustomEvent('reload-time')); + trigger(!trigger()); + } + window.rainloopI18N = null; resolve(); }, 500 < Date.now() - start ? 1 : 500 diff --git a/dev/Screen/User/Settings.js b/dev/Screen/User/Settings.js index 265d8525e..b5a9713b3 100644 --- a/dev/Screen/User/Settings.js +++ b/dev/Screen/User/Settings.js @@ -31,12 +31,8 @@ class SettingsUserScreen extends AbstractSettingsScreen { super([SystemDropDownSettingsUserView, MenuSettingsUserView, PaneSettingsUserView]); initOnStartOrLangChange( - () => { - this.sSettingsTitle = i18n('TITLES/SETTINGS'); - }, - () => { - this.setSettingsTitle(); - } + () => this.sSettingsTitle = i18n('TITLES/SETTINGS'), + () => this.setSettingsTitle() ); } diff --git a/dev/prototype.js b/dev/prototype.js index b35ff52c9..7ed618861 100644 --- a/dev/prototype.js +++ b/dev/prototype.js @@ -8,28 +8,62 @@ // Import momentjs locales function w.moment = { - defineLocale: (name, config)=>{ - locale = config; - const m = config.monthsShort; - if (Array.isArray(m)) { - Date.shortMonths = m; - } else for (let i = 0; i < 12; ++i) { - Date.shortMonths[i] = config.monthsShort({month:()=>i}, '-MMM-'); - } - Date.longMonths = config.months, - Date.longDays = config.weekdays; - Date.shortDays = config.weekdaysMin; - } + defineLocale: (name, config) => locale = config }; - let locale = { - longDateFormat: { - LT : 'h:mm A', // 'g:i A', - L : 'MM/DD/YYYY', // 'Y-m-d', - LL : 'MMMM D, YYYY', // 'F j, Y' - LLL : 'MMMM D, YYYY h:mm A', // 'F j, Y g:i A' - LLLL : 'dddd, MMMM D, YYYY h:mm A' // 'l, F j, Y g:i A' - }, + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat + let formats = { + LT : {hour: 'numeric', minute: 'numeric'}, + L : {}, + LL : {year: 'numeric', month: 'short', day: 'numeric'}, + LLL : {year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric'}, + LLLL : {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric'}, + 'd M': {day: '2-digit', month: 'short'} + }, + phpFormats = { + // Day + d: {day: '2-digit'}, + D: {weekday: 'short'}, + j: {day: 'numeric'}, + l: {weekday: 'long'}, +// N: {}, +// w: {}, +// z: {}, + // Week +// W: {}, + // Month + F: {month: 'long'}, + m: {month: '2-digit'}, + M: {month: 'short'}, + n: {month: 'numeric'}, +// t: {}, + // Year +// L: {}, +// o: {}, + Y: {year: 'numeric'}, + y: {year: '2-digit'}, + // Time + a: {hour12: true}, + A: {hour12: true}, + g: {hour: 'numeric', hourCycle: 'h12'}, + G: {hour: 'numeric'}, + h: {hour: '2-digit', hourCycle: 'h12'}, + H: {hour: '2-digit'}, + i: {minute: '2-digit'}, + s: {second: '2-digit'}, + u: {fractionalSecondDigits: 3}, + // Timezone +// O: return UTC ? 'Z' : (d.Z > 0 ? '+' : '-') + pad2(Math.abs(d.Z / 60)) + '00'; +// P: return UTC ? 'Z' : (d.Z > 0 ? '+' : '-') + pad2(Math.abs(d.Z / 60)) + :' + pad2(Math.abs(d.Z % 60)); +// T: return UTC ? 'UTC' : new Date(d.Y, 0, 1).toTimeString().replace(/^.+ \(?([^)]+)\)?$/, '$1'); + Z: {timeZone: 'UTC'} + // Full Date/Time +// c: {}, +// r: {}, +// U: {}, +// options.timeZoneName = 'short'; + }, + locale = { relativeTime : { future : 'in %s', past : '%s ago', @@ -47,117 +81,29 @@ yy : '%d years' } }, - pad2 = v => 10 > v ? '0' + v : v, - getISODay = x => x.getDay() || 7, - getDayOfYear = x => Math.floor((Date.UTC(x.getFullYear(),x.getMonth(),x.getDate()) - - Date.UTC(x.getFullYear(),0,1)) / 86400000), - getWeek = x => { - let d = new Date(x.getFullYear(),0,1), - wd = getISODay(d), - w = Math.ceil((getDayOfYear(x)+wd) / 7); - /* ISO 8601 states that week 1 is the week with january 4th in it */ - if (4 < wd) --w; - return (1 > w - ? getWeek(new Date(x.getFullYear()-1,11,31)) /* previous year, last week */ - : (52 < w && 4 > getISODay(x) ? 1 /* next year, first week */ : w) ); - }; - - // Defining locale - Date.shortMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - Date.longMonths = ['January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December']; - Date.shortDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - Date.longDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + pad2 = v => 10 > v ? '0' + v : v; // Simulate PHP's date function Date.prototype.format = function (str, UTC) { - if (locale.longDateFormat[str]) { - str = locale.longDateFormat[str] - .replace('YYYY', 'Y') - .replace(/(^|[^M])M([^M]|$)/, '$1n$2') - .replace('MMMM', 'F') - .replace('MMM', 'M') - .replace('MM', 'm') - .replace(/(^|[^D])D([^D]|$)/, '$1j$2') - .replace('DD', 'd') - .replace('dddd', 'l') - .replace('ddd', 'D') - .replace('dd', 'D') - .replace(/(^|[^H])H([^H]|$)/, '$1G$2') - .replace('HH', 'H') - .replace('h', 'g') - .replace('hh', 'h') - .replace('mm', 'i'); + if ('Y-m-d\\TH:i:s' == str) { + return this.getFullYear() + '-' + pad2(1 + this.getMonth()) + '-' + pad2(this.getDate()) + + 'T' + pad2(this.getHours()) + ':' + pad2(this.getMinutes()) + ':' + pad2(this.getSeconds()); } - UTC = UTC || str.match(/\\Z$/); - let x = this, - d = UTC ? { - D: x.getUTCDay(), - Y: x.getUTCFullYear(), - m: x.getUTCMonth(), - d: x.getUTCDate(), - H: x.getUTCHours(), - Z: 0 - } : { - D: x.getDay(), - Y: x.getFullYear(), - m: x.getMonth(), - d: x.getDate(), - H: x.getHours(), - Z: -x.getTimezoneOffset() - }; - return str - ? str.replace(/\\?[a-zA-Z]/g, m => { - if (m[0] === '\\') { return m[1]; } - switch (m) { - // Day - case 'd': return pad2(d.d); - case 'D': return Date.shortDays[d.D]; - case 'j': return d.d; - case 'l': return Date.longDays[d.D]; - case 'N': return getISODay(x); - case 'w': return d.D; - case 'z': return getDayOfYear(x); - // Week - case 'W': return pad2(getWeek(x)); - // Month - case 'F': return Date.longMonths[d.m]; - case 'm': return pad2(d.m + 1); - case 'M': return Date.shortMonths[d.m]; - case 'n': return d.m + 1; - case 't': return 32 - new Date(x.getFullYear(), x.getMonth(), 32).getDate(); - // Year - case 'L': return (((d.Y%4===0)&&(d.Y%100 !== 0)) || (d.Y%400===0)) ? '1' : '0'; - case 'o': return new Date( - x.getFullYear(), - x.getMonth(), - x.getDate() - ((x.getDay() + 6) % 7) + 3 - ).getFullYear(); - case 'Y': return d.Y; - case 'y': return ('' + d.Y).substr(2); - // Time - case 'a': return d.H < 12 ? "am" : "pm"; - case 'A': return d.H < 12 ? "AM" : "PM"; - case 'g': return d.H % 12 || 12; - case 'G': return d.H; - case 'h': return pad2(d.H % 12 || 12); - case 'H': return pad2(d.H); - case 'i': return pad2(UTC?x.getUTCMinutes():x.getMinutes()); - case 's': return pad2(UTC?x.getUTCSeconds():x.getSeconds()); - case 'u': return (UTC?x.getUTCMilliseconds():x.getMilliseconds()).toString().padStart(3,'0'); - // Timezone - case 'O': return UTC ? 'Z' : (d.Z > 0 ? '+' : '-') + pad2(Math.abs(d.Z / 60)) + '00'; - case 'P': return UTC ? 'Z' : (d.Z > 0 ? '+' : '-') + pad2(Math.abs(d.Z / 60)) + ':' + pad2(Math.abs(d.Z % 60)); - case 'T': return UTC ? 'UTC' : new Date(d.Y, 0, 1).toTimeString().replace(/^.+ \(?([^)]+)\)?$/, '$1'); - case 'Z': return d.Z * 60; - // Full Date/Time - case 'c': return x.format("Y-m-d\\TH:i:sO"); - case 'r': return x.format("D, d M Y H:i:s O"); - case 'U': return x.getTime() / 1000; - } - return m; - }) - : x.toString(); + let options = {}; + if (formats[str]) { + options = formats[str]; + } else { + console.log('Date.format('+str+', '+(UTC?'true':'false')+')'); + if (UTC) { + str += 'Z'; + } + let i = str.length; + while (i--) { + phpFormats[str[i]] && Object.entries(phpFormats[str[i]]).forEach(([k,v])=>options[k]=v); + } + formats[str] = options; + } + return new Intl.DateTimeFormat(document.documentElement.lang, options).format(this); }; // Simulate momentjs fromNow function diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/rainloop/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index 34340cade..5267abce4 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -470,8 +470,8 @@ class ServiceActions if (!empty($this->aPaths[3])) { - $bAdmim = 'Admin' === (isset($this->aPaths[2]) ? (string) $this->aPaths[2] : 'App'); - $sLanguage = $this->oActions->ValidateLanguage($this->aPaths[3], '', $bAdmim); + $bAdmin = 'Admin' === (isset($this->aPaths[2]) ? (string) $this->aPaths[2] : 'App'); + $sLanguage = $this->oActions->ValidateLanguage($this->aPaths[3], '', $bAdmin); $bCacheEnabled = $this->Config()->Get('labs', 'cache_system_data', true); if (!empty($sLanguage) && $bCacheEnabled) @@ -483,14 +483,14 @@ class ServiceActions if ($bCacheEnabled) { $sCacheFileName = KeyPathHelper::LangCache( - $sLanguage, $bAdmim, $this->oActions->Plugins()->Hash()); + $sLanguage, $bAdmin, $this->oActions->Plugins()->Hash()); $sResult = $this->Cacher()->Get($sCacheFileName); } if (0 === \strlen($sResult)) { - $sResult = $this->compileLanguage($sLanguage, $bAdmim, false); + $sResult = $this->compileLanguage($sLanguage, $bAdmin); if ($bCacheEnabled && 0 < \strlen($sCacheFileName)) { $this->Cacher()->Set($sCacheFileName, $sResult); @@ -1041,28 +1041,10 @@ class ServiceActions return $bJsOutput ? 'rl.TEMPLATES='.\MailSo\Base\Utils::Php2js($sHtml, $this->Logger()).';' : $sHtml; } - private function convertLanguageNameToMomentLanguageName(string $sLanguage) : string - { - $aHelper = array('en_gb' => 'en-gb', 'fr_ca' => 'fr-ca', 'pt_br' => 'pt-br', - 'uk_ua' => 'ua', 'zh_cn' => 'zh-cn', 'zh_tw' => 'zh-tw', 'fa_ir' => 'fa'); - - return isset($aHelper[$sLanguage]) ? $aHelper[$sLanguage] : \substr($sLanguage, 0, 2); - } - - private function compileLanguage(string $sLanguage, bool $bAdmin = false, bool $bWrapByScriptTag = true) : string + private function compileLanguage(string $sLanguage, bool $bAdmin = false) : string { $aResultLang = array(); - $sMoment = 'window.moment && window.moment.locale && window.moment.locale(\'en\');'; - $sMomentFileName = APP_VERSION_ROOT_PATH.'app/localization/moment/'. - $this->convertLanguageNameToMomentLanguageName($sLanguage).'.js'; - - if (\is_file($sMomentFileName)) - { - $sMoment = \file_get_contents($sMomentFileName); - $sMoment = \preg_replace('/\/\/[^\n]+\n/', '', $sMoment); - } - Utils::ReadAndAddLang(APP_VERSION_ROOT_PATH.'app/localization/langs.yml', $aResultLang); Utils::ReadAndAddLang(APP_VERSION_ROOT_PATH.'app/localization/'. ($bAdmin ? 'admin' : 'webmail').'/_source.en.yml', $aResultLang); @@ -1071,7 +1053,7 @@ class ServiceActions $this->Plugins()->ReadLang($sLanguage, $aResultLang); - $sLangJs = ''; + $sResult = ''; $aLangKeys = \array_keys($aResultLang); foreach ($aLangKeys as $sKey) { @@ -1081,17 +1063,25 @@ class ServiceActions $sString = \implode("\n", $sString); } - $sLangJs .= '"'.\str_replace('"', '\\"', \str_replace('\\', '\\\\', $sKey)).'":' + $sResult .= '"'.\str_replace('"', '\\"', \str_replace('\\', '\\\\', $sKey)).'":' .'"'.\str_replace(array("\r", "\n", "\t"), array('\r', '\n', '\t'), \str_replace('"', '\\"', \str_replace('\\', '\\\\', $sString))).'",'; } + $sResult = $sResult ? '{'.\substr($sResult, 0, -1).'}' : 'null'; - $sResult = empty($sLangJs) ? 'null' : '{'.\substr($sLangJs, 0, -1).'}'; + $sLanguage = strtr($sLanguage, '_', '-'); - return - ($bWrapByScriptTag ? '' : '') - ; + $sMoment = 'window.moment && window.moment.locale && window.moment.locale(\'en\');'; + $options = [$sLanguage, \substr($sLanguage, 0, 2)]; + foreach ($options as $lang) { + $sMomentFileName = APP_VERSION_ROOT_PATH.'app/localization/moment/'.$lang.'.js'; + if (\is_file($sMomentFileName)) { + $sMoment = \file_get_contents($sMomentFileName); + $sMoment = \preg_replace('/\/\/[^\n]+\n/', '', $sMoment); + break; + } + } + + return 'document.documentElement.lang = "'.$sLanguage.'";window.rainloopI18N='.$sResult.';'.$sMoment; } }