//Browser Locale conversion utilities //dates,numbers currency etc export default { //////////////////////////////////////////////////////// // attempt to determine user's preferred language settings // As of Jan 2020 all major browsers support // navigator.languages // but some use navigator.language (singular) to denote UI language preference // not browsing language preference // so the ideal way to do this is to use navigator.languages[0] for the preferred language // and ignore the singular property since we don't care about the actual browser UI language // only how the user expects to see the page itself // // also for sake of future proofing and edge cases need to have it be manually settable as well // //https://appmakers.dev/bcp-47-language-codes-list/ /////////////////////////////////////////// // Get users default language code // first check if overriden in useroptions // if not then use browsers own setting //if not that then final default of en-US getResolvedLanguage() { let l = window.$gz.store.state.userOptions.languageOverride; if (!window.$gz.util.stringIsNullOrEmpty(l)) { return l; } else { l = window.navigator.languages[0]; if (!window.$gz.util.stringIsNullOrEmpty(l)) { return l; } else { return "en-US"; } } }, /////////////////////////////////////////// // Get users default time zone // first check if overriden in useroptions // if not then use browsers own setting // if that is empty then final default of "America/New_York" //https://www.iana.org/time-zones //https://en.wikipedia.org/wiki/List_of_tz_database_time_zones getResolvedTimeZoneName() { let tz = window.$gz.store.state.userOptions.timeZoneOverride; if (!window.$gz.util.stringIsNullOrEmpty(tz)) { return tz; } else { tz = Intl.DateTimeFormat().resolvedOptions().timeZone; if (!window.$gz.util.stringIsNullOrEmpty(tz)) { return tz; } else { return "America/New_York"; } } }, ////////////////////////////////////////////////// // Get the user's chosen currency name //https://en.wikipedia.org/wiki/ISO_4217 //default to USD if nothing specified getCurrencyName() { const cur = window.$gz.store.state.userOptions.currencyName; if (!window.$gz.util.stringIsNullOrEmpty(cur)) { return cur; } else { return "USD"; } }, ////////////////////////////////////////////////// // Get the user's chosen 12hr clock // getHour12() { return window.$gz.store.state.userOptions.hour12; }, ///////////////////////////////////////////////////////////////////// // Turn a utc ISO date from server into a vuetify calendar // schedule control compatible (epoch) format // localized. // For ease of use in schedule the epoch (milliseconds) is the best format // "It must be a Date, number of seconds since Epoch, or a string in the format of YYYY-MM-DD or YYYY-MM-DD hh:mm. Zero-padding is optional and seconds are ignored."" // // utcDateToScheduleCompatibleFormatLocalized(value, timeZoneName) { //This function takes a UTC iso format date string, parses it into a date then converts that date to the User's configured time zone //outputs that in a format close to ISO, fixes the space in the middle of the output to match ISO 8601 format then returns as an //epoch //this is to support controls that are not time zone settable so they are always in local browser time zone of os, however user may be operating //ayanova in another desired time zone so this is all to support that scenario if (!value) { if (window.$gz.dev) { throw new Error( `locale::utcDateToScheduleCompatibleFormatLocalized - Value is empty` ); } return null; } return new Date( new Date(value) //convert to locale timezone and output in the closest thing to iso-8601 format .toLocaleString("sv-SE", { timeZone: timeZoneName }) .replace(" ", "T") //Safari can't parse the date from here because sv-SE puts a space between date and time and Safari will only parse if it has a T between ).getTime(); }, /////////////////////////////////////////////// // Convert a local schedule epoch timestamp // to specified time zone equivalent then // to UTC and output as ISO 8601 // // localScheduleFormatToUTC8601String(value, timeZoneName) { if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } //input: epoch in local browser time zone //output: transform to date and time string //convert to desired time zone but at same time and date //(i.e. if it browser is vancouver and 1pm is selected but desired is new york's 1pm // so convert the string as if it was new york then back to iso so that the time is adjusted forward // as if the user was in new york in their browser default) //parse in the time in to the specified timezone let ret = window.$gz.DateTime.fromISO( //output the sched epoch as local time string without zone new Date(value).toLocaleString("sv-SE").replace(" ", "T"), { zone: timeZoneName } ); ret = ret.setZone("utc"); //convert to UTC ret = ret.toISO(); //output as ISO 8601 return ret; }, /////////////////////////////////////////// // Turn a utc date into a displayable // short date and time // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString // utcDateToShortDateAndTimeLocalized( value, timeZoneName, languageName, hour12 ) { if (!value) { return ""; } if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } if (!languageName) { languageName = this.getResolvedLanguage(); } if (!hour12) { hour12 = this.getHour12(); } //parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z") const parsedDate = new Date(value); //is it a valid date? if (!(parsedDate instanceof Date && !isNaN(parsedDate))) { return "not valid"; } return parsedDate.toLocaleString(languageName, { timeZone: timeZoneName, dateStyle: "short", timeStyle: "short", hour12: hour12 }); }, /////////////////////////////////////////// // Turn a utc date into a displayable // date and time with specific formats // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString // utcDateToSpecifiedDateAndTimeLocalized( value, timeZoneName, languageName, hour12, dateStyle, timeStyle ) { if (!value) { return ""; } if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } if (!languageName) { languageName = this.getResolvedLanguage(); } if (!hour12) { hour12 = this.getHour12(); } //parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z") const parsedDate = new Date(value); //is it a valid date? if (!(parsedDate instanceof Date && !isNaN(parsedDate))) { return "not valid"; } return parsedDate.toLocaleString(languageName, { timeZone: timeZoneName, dateStyle: dateStyle, timeStyle: timeStyle, hour12: hour12 }); }, /////////////////////////////////////////// // Turn a utc date into a displayable // short date // utcDateToShortDateLocalized(value, timeZoneName, languageName) { if (!value) { return ""; } if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } if (!languageName) { languageName = this.getResolvedLanguage(); } //parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z") const parsedDate = new Date(value); //is it a valid date? if (!(parsedDate instanceof Date && !isNaN(parsedDate))) { return "not valid"; } return parsedDate.toLocaleDateString(languageName, { timeZone: timeZoneName, dateStyle: "short" }); }, /////////////////////////////////////////// // Turn a utc date into a displayable // short time // utcDateToShortTimeLocalized(value, timeZoneName, languageName, hour12) { if (!value) { return ""; } if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } if (!languageName) { languageName = this.getResolvedLanguage(); } if (!hour12) { hour12 = this.getHour12(); } //parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z") const parsedDate = new Date(value); //is it a valid date? if (!(parsedDate instanceof Date && !isNaN(parsedDate))) { return "not valid"; } return parsedDate.toLocaleTimeString(languageName, { timeZone: timeZoneName, timeStyle: "short", hour12: hour12 }); }, /////////////////////////////////////////// // Turn a duration value into a display // durationLocalized(value, hideSeconds) { if (value == null || value == "00:00:00") { return ""; } let theDays = 0; let theHours = 0; let theMinutes = 0; let theSeconds = 0; let ret = ""; const work = value.split(":"); //has days? if (work[0].includes(".")) { let dh = work[0].split("."); theDays = Number(dh[0]); theHours = Number(dh[1]); } else { theHours = Number(work[0]); } theMinutes = Number(work[1]); //has milliseconds? (ignore them) if (work[2].includes(".")) { let dh = work[2].split("."); theSeconds = Number(dh[0]); } else { theSeconds = Number(work[2]); } if (theDays != 0) { ret += theDays + " " + window.$gz.translation.get("TimeSpanDays") + " "; } if (theHours != 0) { ret += theHours + " " + window.$gz.translation.get("TimeSpanHours") + " "; } if (theMinutes != 0) { ret += theMinutes + " " + window.$gz.translation.get("TimeSpanMinutes") + " "; } if (!hideSeconds && theSeconds != 0) { ret += theSeconds + " " + window.$gz.translation.get("TimeSpanSeconds") + " "; } return ret; }, /////////////////////////////////////////////// // Convert a utc date to local time zone // and return time portion only in iso 8601 // format (used by time and date picker components) // utcDateStringToLocal8601TimeOnlyString(value, timeZoneName) { if (!value) { //if no value, return the current time as expected by the time picker } else { //ok, the reason for sv-SE is that it's a locale that returns the time already in ISO format and 24hr by default //that can change over time so if this breaks that's why //also fr-CA does as well as possibly en-CA //https://stackoverflow.com/a/58633686/8939 if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } return new Date(value).toLocaleTimeString("sv-SE", { timeZone: timeZoneName }); } }, /////////////////////////////////////////////// // Convert a local time only string with date string // to UTC and output as ISO 8601 // also converts to time zone specified if diff from browser // (used by time and date picker components) // localTimeDateStringToUTC8601String(value, timeZoneName) { //https://moment.github.io/luxon/docs/manual/zones.html#creating-datetimes-in-a-zone if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } //parse in the time in the currently used timezone let ret = window.$gz.DateTime.fromISO(value, { zone: timeZoneName }); ret = ret.setZone("utc"); //convert to UTC ret = ret.toISO(); //output as ISO 8601 return ret; }, /////////////////////////////////////////////// // UTC Now in api format // to UTC and output as ISO 8601 // (used to set defaults) // nowUTC8601String(timeZoneName) { if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } const ret = window.$gz.DateTime.local() .setZone(timeZoneName) .toUTC() .toString(); return ret; }, /////////////////////////////////////////////// // UTC ISO 8601 string add minutes // and return as UTC ISO 8601 string // (used to set automatic / default adjusted times) // addMinutesToUTC8601String(val, minutes) { if (!val || val == "" || minutes == null || minutes == 0) { return val; } //instantiate a luxon date object from val which is assumed to be an iso string let dt = window.$gz.DateTime.fromISO(val); if (!dt.isValid) { console.error("locale::addMinutes, input not valid:", { val: val, dt: dt }); return val; } //add minutes dt = dt.plus({ minutes: minutes }); return dt.toUTC().toString(); }, /////////////////////////////////////////////// // UTC ISO 8601 string add arbitrary value based // on luxon duration format // and return as UTC ISO 8601 string //https://moment.github.io/luxon/api-docs/index.html#datetimeplus // addDurationToUTC8601String(val, duration) { if ( !val || val == "" || duration == null || !typeof duration === "object" ) { return val; } //instantiate a luxon date object from val which is assumed to be an iso string let dt = window.$gz.DateTime.fromISO(val); if (!dt.isValid) { console.error("locale::addDurationToUTC8601String, input not valid:", { val: val, dt: dt }); return val; } //add minutes dt = dt.plus(duration); return dt.toUTC().toString(); }, /////////////////////////////////////////////// // parse UTC ISO 8601 strings, diff, return hours // diffHoursFromUTC8601String(start, stop) { if (!start || start == "" || !stop == null || stop == "") { return 0; } //instantiate a luxon date object from val which is assumed to be an iso string const startDate = window.$gz.DateTime.fromISO(start); if (!startDate.isValid) { console.error("locale::diffHours, start not valid:", { start: start, startDate: startDate }); return 0; } const stopDate = window.$gz.DateTime.fromISO(stop); if (!stopDate.isValid) { console.error("locale::diffHours, start not valid:", { stop: stop, stopDate: stopDate }); return 0; } // console.log( // "locale:diffhours...", // stopDate.diff(startDate, "hours").toObject().hours // ); // console.log( // "locale:diffhours.. ROUNDED.", // window.$gz.util.roundAccurately( // stopDate.diff(startDate, "hours").toObject().hours, // 2 // ) // ); return window.$gz.util.roundAccurately( stopDate.diff(startDate, "hours").toObject().hours, 2 ); }, /////////////////////////////////////////////// // Local now timestamp converted to timeZoneName // and output as ISO 8601 // (used to inform server of local client time) // clientLocalZoneTimeStamp(timeZoneName) { if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } const ret = window.$gz.DateTime.local() .setZone(timeZoneName) .toString(); return ret; }, /////////////////////////////////////////////// // Get default start date time in api format // (this is used to centralize and for future) defaultStartDateTime() { return { start: window.$gz.DateTime.local() .toUTC() .toString(), end: window.$gz.DateTime.local() .plus({ hours: 1 }) .toUTC() .toString() }; }, /////////////////////////////////////////////// // Convert a utc date to local time zone // and return date only portion only in iso 8601 // format (used by time and date picker components) // utcDateStringToLocal8601DateOnlyString(value, timeZoneName) { if (!value) { //if no value, return the current time as expected by the time picker } else { //ok, the reason for sv-SE is that it's a locale that returns the time already in ISO format and 24hr by default //that can change over time so if this breaks that's why //also fr-CA does as well as possibly en-CA //https://stackoverflow.com/a/58633686/8939 if (!timeZoneName) { timeZoneName = this.getResolvedTimeZoneName(); } return new Date(value).toLocaleDateString("sv-SE", { timeZone: timeZoneName }); } }, /////////////////////////////////////////////// // Date/time past or future evaluation // dateIsPast(value) { if (!value) { return false; } return new Date(value) < new Date(); }, /////////////////////////////////////////// // Turn a decimal number into a local // currency display // currencyLocalized(value, languageName, currencyName) { if (value == null) return ""; if (!languageName) { languageName = this.getResolvedLanguage(); } if (!currencyName) { currencyName = this.getCurrencyName(); } return new Intl.NumberFormat(languageName, { style: "currency", currency: currencyName }).format(value); }, /////////////////////////////////////////// // Turn a decimal number into a local // decimal format display // decimalLocalized(value, languageName) { if (value == null) return ""; if (!languageName) { languageName = this.getResolvedLanguage(); } //This forces 2 digits after the decimal // return new Intl.NumberFormat(languageName, { // minimumFractionDigits: 2 // }).format(value); //this goes with whatever is the local format which for dev testing turned out to be perfect: 1.00 displays as 1 and 1.75 displays as 1.75 //alignment goes out the window but it follows v7 format return new Intl.NumberFormat(languageName).format(value); }, /////////////////////////////////////////// // Turn a file / memory size number into a local // decimal format display and in reasonable human readable range // humanFileSize(bytes, languageName, si = false, dp = 1) { const thresh = si ? 1000 : 1024; if (Math.abs(bytes) < thresh) { return bytes + " B"; } const units = si ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; let u = -1; const r = 10 ** dp; do { bytes /= thresh; ++u; } while ( Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1 ); return ( this.decimalLocalized(bytes.toFixed(dp), languageName) + " " + units[u] ); } };