UNPKG

@brightspace-ui/intl

Version:

Internationalization APIs for number, date, time and file size formatting and parsing in D2L Brightspace.

1,268 lines (1,151 loc) 38.2 kB
import { getDocumentLocaleSettings, getLanguage, merge } from './common.js'; // to-do: These should be based on region, not language const hour24locales = ['cy', 'da', 'de', 'es', 'fr', 'nl', 'pt', 'sv', 'tr', 'zh']; const mondayFirstDayLocales = ['cy', 'da', 'de', 'fr', 'hi', 'mi', 'nl', 'sv', 'tr']; // timezone abbreviations and offsets from https://www.timeanddate.com/time/zones/ const timezoneOffsetMap = { 'ACDT': '+1030', 'ACST': '+0930', 'ACT': '-0500', 'ACWST': '+0845', 'AEDT': '+1100', 'AEST': '+1000', 'AET': '+1000', 'AFT': '+0430', 'AKDT': '-0800', 'AKST': '-0900', 'ALMT': '+0600', 'ANAST': '+1200', 'ANAT': '+1200', 'AQTT': '+0500', 'ART': '-0300', 'AWDT': '+0900', 'AWST': '+0800', 'AZODT': '+0000', 'AZOST': '+0000', 'AZOT': '-0100', 'AZST': '+0500', 'AZT': '+0400', 'B': '+0200', 'BDST': '+0100', 'BIOT': '+0600', 'BIT': '-1200', 'BNT': '+0800', 'BOT': '-0400', 'BRST': '-0200', 'BRT': '-0300', 'BT': '-0300', 'BTT': '+0600', 'C': '+0300', 'CAST': '+0800', 'CAT': '+0200', 'CCT': '+0630', 'CEDT': '+0200', 'CEST': '+0200', 'CET': '+0100', 'CHADT': '+1345', 'CHAST': '+1245', 'CHOT': '+0800', 'CHODT': '+0900', 'CHODST': '+0900', 'CHOST': '+0900', 'CHST': '+1000', 'CHUT': '+1000', 'CIDST': '-0400', 'CIST': '-0500', 'CIT': '-0500', 'CKT': '-1000', 'CLDT': '-0300', 'CLST': '-0300', 'CLT': '-0400', 'COST': '-0400', 'COT': '-0500', 'CT': '-0600', 'CVT': '-0100', 'CWST': '+0845', 'CXT': '+0700', 'D': '+0400', 'DAVT': '+0700', 'DDUT': '+1000', 'DFT': '+0100', 'E': '+0500', 'EADT': '-0500', 'EASST': '-0500', 'EAST': '-0600', 'EAT': '+0300', 'ECST': '+0200', 'EEDT': '+0300', 'EEST': '+0300', 'EET': '+0200', 'EFATE': '+1100', 'EGST': '+0000', 'EGT': '-0100', 'EIT': '+0900', 'ET': '-0500', 'F': '+0600', 'FET': '+0300', 'FJDT': '+1300', 'FJST': '+1300', 'FJT': '+1200', 'FKDT': '-0300', 'FKST': '-0300', 'FKT': '-0400', 'FNT': '-0200', 'G': '+0700', 'GALT': '-0600', 'GAMT': '-0900', 'GET': '+0400', 'GFT': '-0300', 'GILT': '+1200', 'GIT': '-0900', 'GMT': '+0000', 'GT': '+0000', 'GYT': '-0400', 'HAC': '-0500', 'HAR': '-0600', 'HNA': '-0400', 'HNC': '-0600', 'HNP': '-0800', 'HNR': '-0700', 'H': '+0800', 'HAA': '-0300', 'HADT': '-0900', 'HAE': '-0400', 'HAEC': '+0200', 'HAP': '-0700', 'HAST': '-1000', 'HAT': '-0230', 'HDT': '-0900', 'HKT': '+0800', 'HLV': '-0400', 'HMT': '+0500', 'HNE': '-0500', 'HNT': '-0330', 'HOVDT': '+0800', 'HOVDST': '+0800', 'HOVST': '+0800', 'HOVT': '+0700', 'HST': '-1000', 'I': '+0900', 'ICT': '+0700', 'IDLW': '-1200', 'IOT': '+0600', 'IRDT': '+0430', 'IRKST': '+0900', 'IRKT': '+0800', 'IRST': '+0330', 'JST': '+0900', 'K': '+1000', 'KALT': '+0200', 'KGT': '+0600', 'KIT': '+0500', 'KOST': '+1100', 'KRAST': '+0800', 'KRAT': '+0700', 'KST': '+0900', 'KT': '+0900', 'KUYT': '+0400', 'L': '+1100', 'LHDT': '+1100', 'LHST': '+1030', 'LINT': '+1400', 'M': '+1200', 'MAGST': '+1200', 'MAGT': '+1100', 'MART': '-0930', 'MAWT': '+0500', 'MCK': '+0300', 'MEST': '+0200', 'MESZ': '+0200', 'MET': '+0100', 'MEZ': '+0100', 'MDST': '-0600', 'MDT': '-0600', 'MHT': '+1200', 'MIST': '+1100', 'MIT': '-0930', 'MMT': '+0630', 'MSD': '+0400', 'MSK': '+0300', 'MT': '-0700', 'MUT': '+0400', 'MVT': '+0500', 'MYT': '+0800', 'N': '-0100', 'NACDT': '-0500', 'NACST': '-0600', 'NAEDT': '-0400', 'NAEST': '-0500', 'NAMDT': '-0600', 'NAMST': '-0700', 'NAPDT': '-0700', 'NAPST': '-0800', 'NCT': '+1100', 'NDT': '-0230', 'NFDT': '+1200', 'NFT': '+1100', 'NOVST': '+0700', 'NOVT': '+0700', 'NPT': '+0545', 'NRT': '+1200', 'NST': '-0330', 'NT': '-0330', 'NUT': '-1100', 'NZDT': '+1300', 'NZST': '+1200', 'O': '-0200', 'OESZ': '+0300', 'OEZ': '+0200', 'OMSST': '+0700', 'OMST': '+0600', 'ORAT': '+0500', 'PDST': '-0700', 'PDT': '-0700', 'PET': '-0500', 'PETST': '+1200', 'PETT': '+1200', 'PGT': '+1000', 'PHOT': '+1300', 'PHT': '+0800', 'PKT': '+0500', 'PMDT': '-0200', 'PMST': '-0300', 'PONT': '+1100', 'PT': '-0800', 'PWT': '+0900', 'Q': '-0400', 'QYZT': '+0600', 'R': '-0500', 'RET': '+0400', 'ROTT': '-0300', 'S': '-0600', 'SAKT': '+1100', 'SAMST': '+0400', 'SAMT': '+0400', 'SAST': '+0200', 'SBT': '+1100', 'SCT': '+0400', 'SDT': '-1000', 'SGT': '+0800', 'SLST': '+0530', 'SRET': '+1100', 'SRT': '-0300', 'ST': '+1400', 'SYOT': '+0300', 'T': '-0700', 'TAHT': '-1000', 'THA': '+0700', 'TFT': '+0500', 'TJT': '+0500', 'TKT': '+1300', 'TLT': '+0900', 'TMT': '+0500', 'TOST': '+1400', 'TOT': '+1300', 'TRT': '+0300', 'TVT': '+1200', 'U': '-0800', 'ULAST': '+0900', 'ULAT': '+0800', 'UTC': '+0000', 'UYST': '-0200', 'UYT': '-0300', 'UZT': '+0500', 'V': '-0900', 'VET': '-0400', 'VLAST': '+1100', 'VLAT': '+1000', 'VOLT': '+0400', 'VOST': '+0600', 'VUT': '+1100', 'W': '-1000', 'WAKT': '+1200', 'WARST': '-0300', 'WAST': '+0200', 'WDT': '+0900', 'WEDT': '+0100', 'WEST': '+0100', 'WESZ': '+0100', 'WET': '+0000', 'WEZ': '+0000', 'WFT': '+1200', 'WGST': '-0200', 'WGT': '-0300', 'WIB': '+0700', 'WIT': '+0900', 'WITA': '+0800', 'WT': '+0000', 'X': '-1100', 'Y': '-1200', 'YAKST': '+1000', 'YAKT': '+0900', 'YAPT': '+1000', 'YEKST': '+0600', 'YEKT': '+0500', 'Z': '+0000' }; function buildDayPeriodRe(part) { let re = ''; let or = ''; for (let i = 0; i < part.length; i++) { re += or + part.substr(0, i + 1); or = '|'; } return new RegExp(re, 'i'); } function convertJsDateToLocalDateTime(utcDate) { const formattedDateTime = formatDateString(utcDate); const re = /([0-9]{1,2})\/([0-9]{1,2})\/([0-9]{4})( |, )([0-9]{1,2}):([0-9]{2}):([0-9]{2})/; const match = formattedDateTime.match(re); if (!match || match.length !== 8) { return null; } return { month: parseInt(match[1]), date: parseInt(match[2]), year: parseInt(match[3]), hours: parseInt(match[5]), minutes: parseInt(match[6]), seconds: parseInt(match[7]) }; } function formatDateString(date) { const timezone = getDocumentLocaleSettings().timezone.identifier; const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' }; if (timezone) { options.timeZone = timezone; options.timeZoneName = 'short'; } const formatter = new Intl.DateTimeFormat('en-US', options); return formatter.format(date); } function getDateStringWithTimezone(date, timezone) { let dateString = `${date.month}/${date.date}/${date.year}, ${date.hours}:${date.minutes}:${date.seconds} ${timezone}`; if (timezone.includes('-') || timezone.includes('+')) { const re = /([A-Z]{3})?(\+|-)([0-9]{1,2})(:)?([0-9]{2})?/; const match = timezone.match(re); if (match && match.length === 6) { // YYYY-MM-DDTHH:mm:ss(+|-)HH:MM const mins = match[5] ? prePadByZero(match[5], 2) : '00'; dateString = `${date.year}-${date.month}-${date.date}T${date.hours}:${date.minutes}:${date.seconds}${match[2]}${prePadByZero(match[3], 2)}:${mins}`; } } return dateString; } function getParts() { const descriptor = getDateTimeDescriptor(); const result = []; const separator = getSeparator(); const parts = descriptor.formats.dateFormats.short.split(separator); for (let i = 0; i < parts.length; i++) { const part = parts[i].trim(); switch (part) { case 'dd': case 'd': result.push('d'); break; case 'MM': case 'M': result.push('M'); break; case 'yyyy': result.push('yyyy'); break; } } if (result.length !== 3) { return ['M', 'd', 'yyyy']; } return result; } const reSeparator = new RegExp('\\W'); function getSeparator() { const descriptor = getDateTimeDescriptor(); const match = reSeparator.exec(descriptor.formats.dateFormats.short); if (match !== null) { return match[0]; } return '/'; } function getTimeFormat(hour24, language, baseLanguage) { if (hour24 && baseLanguage === 'fr') { return 'HH\' h \'mm'; } let timeFormat = hour24 ? 'HH:mm' : 'h:mm'; // non-zero padded 24-hour clocks and zero-padded 12-hour clocks if (hour24 && (baseLanguage === 'ja' || language === 'pt-br' || (baseLanguage === 'zh' && language !== 'zh-tw'))) { timeFormat = 'H:mm'; } else if (!hour24 && language === 'zh-tw') { timeFormat = 'hh:mm'; } if (!hour24) { // AM/PM before vs. after if (baseLanguage === 'ko' || baseLanguage === 'zh') { timeFormat = `tt ${timeFormat}`; } else { timeFormat = `${timeFormat} tt`; } } return timeFormat; } function getTimezoneFromDuplicatedAbbreviation(abbrTimezone) { const longTimezone = getDocumentLocaleSettings().timezone.identifier; switch (abbrTimezone) { case 'ADST': // Alaska Daylight Saving Time -0800 (America/Anchorage, America/Juneau, America/Nome, America/Sitka, America/Yakutat) // Atlantic Daylight Saving TIme -0300 (America) if (longTimezone === 'America/Anchorage' || longTimezone === 'America/Juneau' || longTimezone === 'America/Nome' || longTimezone === 'America/Sitka' || longTimezone === 'America/Yakutat' ) { return '-0800'; } else return '-0300'; case 'ADT': // Arabia Daylight Time +0400 (Asia) // Atlantic Daylight Time -0300 (America) if (longTimezone.includes('America') || longTimezone.includes('Atlantic')) return '-0300'; else return '+0400'; case 'AMT': // Amazon Time (Brazil) -0400 (America) // Armenia Time +0400 (Asia) return longTimezone.includes('Asia') ? '+0400' : '-0400'; case 'AMST': // Amazon Summer Time -0300 (America) // Armenia Summer Time +0500 (Asia) return longTimezone.includes('Asia') ? '+0500' : '-0300'; case 'AST': // Atlantic Standard Time -0400 (America) // Arabia Standard Time +0300 (Asia) if (longTimezone.includes('America') || longTimezone.includes('Atlantic')) return '-0400'; else return '+0300'; case 'AT': // Alaska Time -0900 (America/Anchorage, America/Juneau, America/Nome, America/Sitka, America/Yakutat) // Atlantic Time -0400 (America) if (longTimezone === 'America/Anchorage' || longTimezone === 'America/Juneau' || longTimezone === 'America/Nome' || longTimezone === 'America/Sitka' || longTimezone === 'America/Yakutat' ) { return '-0900'; } else return '-0400'; case 'BDT': // British Daylight Time +0100 (Europe) // Brunei Time +0800 (Asia) return longTimezone.includes('Asia') ? '+0800' : '+0100'; case 'BST': // Bangladesh Standard Time +0600 (Asia) // British Summer Time +0100 (Europe) // Bougainville Standard Time +1100 (Pacific) // Brazilian Summer Time -0200 (America) if (longTimezone.includes('Asia')) return '+0600'; else if (longTimezone.includes('Pacific')) return '+1100'; else if (longTimezone.includes('Europe')) return '+0100'; else return '-0200'; case 'CDST': // Central Daylight Savings Time +1030 (Australia) // Central Daylight Saving Time -0500 (America) return longTimezone.includes('Australia') ? '+1030' : '-0500'; case 'CDT': // Central Daylight Time -0500 (America) // Cuba Daylight Time -0400 (America/Havana) // Central Daylight Time +1030 (Australia) if (longTimezone === 'America/Havana') return '-0400'; else if (longTimezone.includes('Australia')) return '+1030'; else return '-0500'; case 'CST': // Cuba Standard Time -0500 (America/Havana) // Central Standard Time -0600 (America) // China Standard Time +0800 (Asia) // Australian Central Standard Time +0930 (Australia) if (longTimezone === 'America/Havana') return '-0500'; else if (longTimezone.includes('Asia')) return '+0800'; else if (longTimezone.includes('Australia')) return '+0930'; else return '-0600'; case 'ECT': // European Central Time +0100 (Europe, Africa) // Ecuador Time -0500 (America) return longTimezone.includes('America') ? '-0500' : '+0100'; case 'EDT': case 'EDST': // Eastern Daylight Time +1100 (Australia) // Eastern Daylight Time -0400 (America) if (longTimezone.includes('Antarctica') || longTimezone.includes('Australia')) return '+1100'; else return '-0400'; case 'EST': // Eastern Standard Time +1000 (Australia) // Eastern Standard Time -0500 (America) return longTimezone.includes('Australia') ? '+1000' : '-0500'; case 'GST': // South Georgia and the South Sandwich Islands Time -0200 (Atlantic) // Gulf Standard Time +0400 (Asia) // Guam Standard Time +1000 (Pacific) if (longTimezone.includes('Atlantic')) return '-0200'; else if (longTimezone.includes('Pacific')) return '+1000'; else return '+0400'; case 'IDT': // Iran Daylight Time +0430 (Asia/Tehran) // Israel Daylight Time +0300 (Asia) return longTimezone === 'Asia/Tehran' ? '+0430' : '+0300'; case 'IST': // Indian Standard Time +0530 (Asia/Calcutta & Asia/Colombo) // Irish Standard Time +0100 (Europe) // Israel Standard Time +0200 (Asia) if (longTimezone === 'Asia/Calcutta' || longTimezone === 'Asia/Colombo') return '+0530'; else if (longTimezone.includes('Asia')) return '+0200'; else return '+0100'; case 'MST': // Malaysia Standard Time +0800 (Asia) // Mountain Standard Time -0700 (America) return longTimezone.includes('Asia') ? '+0800' : '-0700'; case 'PST': // Pacific Standard Time -0800 (America) // Philippine Standard Time +0800 (Asia) return longTimezone.includes('Asia') ? '+0800' : '-0800'; case 'PYST': // Pyongyan Time +0830 (Asia) // Paraguay Summer Time -0300 (America) return longTimezone.includes('Asia') ? '+0830' : '-0300'; case 'PYT': // Paraguary Time -0400 (South America) // Pyongyang Time +0830 (Asia) return longTimezone.includes('Asia') ? '+0830' : '-0400'; case 'SST': // Singapore Standard Time +0800 (Asia) // Samoa Standard Time -1100 (Pacific) return longTimezone.includes('Pacific') ? '-1100' : '+0800'; case 'WAT': // West Africa Time +0100 (Africa) // Western Australia Time +0800 (Australia) return longTimezone.includes('Australia') ? '+0800' : '+0100'; case 'WST': // West Samoa Time +1400 (Pacific) // Western Sahara Summer Time +0100 (Africa) // Western Standard Time +0800 (Australia) if (longTimezone.includes('Pacific')) return '+1400'; else if (longTimezone.includes('Australia')) return '+0800'; else return '+0100'; default: throw new Error(`Invalid timezone: unable to retrieve timezone offset for ${longTimezone}`); } } function isDateValid(year, month, day) { if (isNaN(year) || year < 1753 || year > 9999) { return false; } if (isNaN(month) || month < 1 || month > 12) { return false; } if (isNaN(day) || day < 1 || day > 31) { return false; } let allowedDays = 31; if (month === 2) { if ((year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0))) { allowedDays = 29; } else { allowedDays = 28; } } else if (month === 4 || month === 6 || month === 9 || month === 11) { allowedDays = 30; } if (day > allowedDays) { return false; } return true; } function prePadByZero(input, maxNum) { input = input.toString(); const zero = '0'; while (zero.length > 0 && input.length < maxNum) { input = zero + input; } return input; } function processPattern(pattern, replacements) { let reStr = ''; Object.keys(replacements).forEach((key) => { reStr += ((reStr === '') ? '' : '|') + key; }); const re = new RegExp(reStr, 'g'); const doReplacements = function(buf) { return buf.replace(re, (m) => { return replacements[m]; }); }; let escape = false; let buffer = ''; let value = ''; for (let i = 0; i < pattern.length; i++) { const c = pattern.charAt(i); if (c === "'") { if (!escape) { value += doReplacements(buffer); buffer = ''; } escape = !escape; } else if (escape) { value += c; } else { buffer += c; } } value += doReplacements(buffer); return value; } export function convertLocalToUTCDateTime(date) { if (!getDocumentLocaleSettings().timezone.identifier) { return date; } const dateDate = new Date(date.year, date.month - 1, date.date, date.hours, date.minutes, date.seconds); const datePrePad = { year: date.year, month: prePadByZero(date.month, 2), date: prePadByZero(date.date, 2), hours: prePadByZero(date.hours, 2), minutes: prePadByZero(date.minutes, 2), seconds: prePadByZero(date.seconds, 2) }; let timezone = formatDateString(dateDate).split(' ')[2]; let dateString = getDateStringWithTimezone(datePrePad, timezone); let parsedDateString = new Date(Date.parse(dateString)); if (isNaN(parsedDateString.getTime())) { timezone = timezoneOffsetMap[timezone] || getTimezoneFromDuplicatedAbbreviation(timezone); dateString = getDateStringWithTimezone(datePrePad, timezone); parsedDateString = new Date(Date.parse(dateString)); } // run again in case of DST (e.g., if timezone is CST for dateDate but local time is after CST is over, timezone is incorrect) let utcCorrectedTimezone = formatDateString(parsedDateString).split(' ')[2]; let dateStringInTimezone = getDateStringWithTimezone(datePrePad, utcCorrectedTimezone); let utcCorrectedDate = new Date(Date.parse(dateStringInTimezone)); if (isNaN(utcCorrectedDate.getTime())) { utcCorrectedTimezone = timezoneOffsetMap[utcCorrectedTimezone] || getTimezoneFromDuplicatedAbbreviation(utcCorrectedTimezone); dateStringInTimezone = getDateStringWithTimezone(datePrePad, utcCorrectedTimezone); utcCorrectedDate = new Date(Date.parse(dateStringInTimezone)); } return { month: utcCorrectedDate.getUTCMonth() + 1, date: utcCorrectedDate.getUTCDate(), year: utcCorrectedDate.getUTCFullYear(), hours: utcCorrectedDate.getUTCHours(), minutes: utcCorrectedDate.getUTCMinutes(), seconds: utcCorrectedDate.getUTCSeconds() }; } export function convertUTCToLocalDateTime(date) { if (!getDocumentLocaleSettings().timezone.identifier) { return date; } const utcDate = new Date(Date.UTC(date.year, date.month - 1, date.date, date.hours, date.minutes, date.seconds)); return convertJsDateToLocalDateTime(utcDate) || date; } export function getDateTimeDescriptor() { const settings = getDocumentLocaleSettings(); return settings.getCacheItem('dateTimeDescriptor', () => { const language = getLanguage(); const subtags = language.split('-'); const baseLanguage = subtags[0]; let hour24 = (hour24locales.indexOf(baseLanguage) > -1); if (language === 'zh-tw') { hour24 = false; } if (settings.overrides.date && settings.overrides.date.hour24 !== undefined) { hour24 = settings.overrides.date.hour24; } const timeFormat = getTimeFormat(hour24, language, baseLanguage); let dateFormats = ['dddd, MMMM d, yyyy', 'MMM d, yyyy', 'M/d/yyyy', 'MMMM yyyy', 'MMMM d', 'MMM d']; const fullTimeFormat = (baseLanguage === 'zh' && language !== 'zh-tw') ? `ZZZ ${timeFormat}` : `${timeFormat} ZZZ`; let dayPeriods = ['AM', 'PM']; let months = [ ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] ]; let days = [ ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], ['S', 'M', 'T', 'W', 'T', 'F', 'S'] ]; let firstDayOfWeek = (mondayFirstDayLocales.indexOf(baseLanguage) > -1) ? 1 : 0; let weekendStartDay = 6; let weekendEndDay = 0; switch (baseLanguage) { case 'ar': dateFormats = ['dddd, d MMMM, yyyy', 'dd MMMM, yyyy', 'dd/MM/yyyy', 'MMMM, yyyy', 'd MMMM', 'd MMM']; dayPeriods = ['ص', 'م']; months[0] = months[1] = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر']; days = [ ['الأحد', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'], ['أحد', 'إثنين', 'ثلاثاء', 'أربعاء', 'خميس', 'جمعة', 'سبت'], ['أ', 'إ', 'ث', 'أر', 'خ', 'ج', 'س'] ]; firstDayOfWeek = 6; weekendStartDay = 4; weekendEndDay = 5; break; case 'cy': dateFormats = ['dddd, d MMMM yyyy', 'dd MMMM yyyy', 'dd/MM/yyyy', 'MMMM yyyy', 'd MMMM', 'd MMM']; months = [ ['Ionawr', 'Chwefror', 'Mawrth', 'Ebrill', 'Mai', 'Mehefin', 'Gorffennaf', 'Awst', 'Medi', 'Hydref', 'Tachwedd', 'Rhagfyr'], ['Ion', 'Chwe', 'Maw', 'Ebr', 'Mai', 'Meh', 'Gor', 'Awst', 'Medi', 'Hyd', 'Tach', 'Rhag'] ]; days = [ ['Dydd Sul', 'Dydd Llun', 'Dydd Mawrth', 'Dydd Mercher', 'Dydd Iau', 'Dydd Gwener', 'Dydd Sadwrn'], ['Sul', 'Llun', 'Maw', 'Mer', 'Iau', 'Gwe', 'Sad'], ['Su', 'Ll', 'Ma', 'Me', 'Ia', 'Gw', 'Sa'] ]; break; case 'da': dateFormats = ['dddd \'den\' d. MMMM yyyy', 'd. MMM. yyyy', 'dd.MM.yyyy', 'MMMM yyyy', 'd. MMMM', 'd. MMM']; months = [ ['januar', 'februar', 'marts', 'april', 'maj', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'december'], ['jan.', 'feb.', 'mar.', 'apr.', 'maj', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'] ]; days = [ ['søndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag'], ['søn.', 'man.', 'tir.', 'ons.', 'tor.', 'fre.', 'lør.'], ['S', 'M', 'T', 'O', 'T', 'F', 'L'] ]; break; case 'de': dateFormats = ['dddd d. MMMM yyyy', 'd. MMMM yyyy', 'dd.MM.yyyy', 'MMMM yyyy', 'd. MMMM', 'd. MMM']; months = [ ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], ['Jan.', 'Feb.', 'März', 'Apr.', 'Mai', 'Juni', 'Juli', 'Aug.', 'Sept.', 'Okt.', 'Nov.', 'Dez.'] ]; days = [ ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], ['S', 'M', 'D', 'M', 'D', 'F', 'S'] ]; break; case 'es': dateFormats = ['dddd d\' de \'MMMM\' de \'yyyy', 'd\' de \'MMMM\' de \'yyyy', 'dd/MM/yyyy', 'MMMM yyyy', 'd\' de \'MMMM', 'd\' de \'MMM']; dayPeriods = ['a. m.', 'p. m.']; months = [ ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'], ['ene.', 'feb.', 'mar.', 'abr.', 'may.', 'jun.', 'jul.', 'ago.', 'sep.', 'oct.', 'nov.', 'dic.'] ]; days = [ ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'], ['dom.', 'lun.', 'mar.', 'mié.', 'jue.', 'vie.', 'sáb.'], ['D', 'L', 'M', 'M', 'J', 'V', 'S'] ]; break; case 'fr': dateFormats = ['dddd d MMMM yyyy', 'd MMM yyyy', 'dd/MM/yyyy', 'MMMM yyyy', 'd MMMM', 'd MMM']; months = [ ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'], ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'] ]; days = [ ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'], ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'], ['D', 'L', 'M', 'M', 'J', 'V', 'S'] ]; break; case 'haw': dateFormats = ['dddd, d MMMM yyyy', 'd MMM yyyy', 'd/M/yyyy', 'yyyy MMMM', 'd MMMM', 'd MMM']; months = [ ['Ianuali', 'Pepeluali', 'Malaki', 'ʻApelila', 'Mei', 'Iune', 'Iulai', 'ʻAukake', 'Kepakemapa', 'ʻOkakopa', 'Nowemapa', 'Kekemapa'], ['Ian.', 'Pep.', 'Mal.', 'ʻAp.', 'Mei', 'Iun.', 'Iul.', 'ʻAu.', 'Kep.', 'ʻOk.', 'Now.', 'Kek.'] ]; days = [ ['Lāpule', 'Poʻakahi', 'Poʻalua', 'Poʻakolu', 'Poʻahā', 'Poʻalima', 'Poʻaono'], ['LP', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6'], ['S', 'M', 'T', 'W', 'T', 'F', 'S'] ]; break; case 'hi': dateFormats = ['dddd, d MMMM yyyy', 'd MMMM yyyy', 'dd-MM-yyyy', 'MMMM yyyy', 'd MMMM', 'd MMM']; dayPeriods = ['पूर्वाह्न', 'अपराह्न']; months = [ ['जनवरी', 'फरवरी', 'मार्च', 'अप्रैल', 'मई', 'जून', 'जुलाई', 'अगस्त', 'सितंबर', 'अक्टूबर', 'नवंबर', 'दिसंबर'], ['जन', 'फर', 'मार्च', 'अप्रैल', 'मई', 'जून', 'जुलाई', 'अग', 'सितं', 'अक्टू', 'नवं', 'दिसं'] ]; days = [ ['रविवार', 'सोमवार', 'मंगलवार', 'बुधवार', 'गुरूवार', 'शुक्रवार', 'शनिवार'], ['रवि', 'सोम', 'मंगल', 'बुध', 'गुरु', 'शुक्र', 'शनि'], ['र', 'सो', 'मं', 'बु', 'गु', 'शु', 'श'] ]; break; case 'ja': dateFormats = ['yyyy年M月d日', 'yyyy年M月d日', 'yyyy/MM/dd', 'yyyy年M月', 'M月d日', 'M月d日']; dayPeriods = ['午前', '午後']; months[0] = months[1] = ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月']; days[0] = days[1] = days[2] = ['日', '月', '火', '水', '木', '金', '土']; break; case 'ko': dateFormats = ['yyyy년 M월 d일 dddd', 'yyyy년 M월 d일', 'yyyy-MM-dd', 'yyyy년 M월', 'M월 d일', 'MMM d일']; dayPeriods = ['오전', '오후']; months[0] = months[1] = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; days[0] = ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일']; days[1] = days[2] = ['일', '월', '화', '수', '목', '금', '토']; break; case 'mi': dateFormats = ['dddd, d MMMM y', 'd MMMM y', 'dd-MM-y', 'MMMM yyyy', 'd MMMM', 'd MMM']; months = [ ['Kohitātea', 'Huitanguru', 'Poutūterangi', 'Paengawhāwhā', 'Haratua', 'Pipiri', 'Hōngongoi', 'Hereturikōkā', 'Mahuru', 'Whiringa-ā-nuku', 'Whiringa-ā-rangi', 'Hakihea'], ['Kohi', 'Hui', 'Pou', 'Pae', 'Hara', 'Pipi', 'Hōngo', 'Here', 'Mahu', 'Nuku', 'Rangi', 'Haki'] ]; days = [ ['Rātapu', 'Rāhina', 'Rātū', 'Rāapa', 'Rāpare', 'Rāmere', 'Rāhoroi'], ['Tap', 'Hin', 'Tū', 'Apa', 'Par', 'Mer', 'Hor'], ['T', 'H', 'T', 'A', 'P', 'M', 'H'] ]; break; case 'nl': dateFormats = ['dddd d MMMM yyyy', 'd MMMM yyyy', 'dd-MM-yyyy', 'MMMM yyyy', 'd MMMM', 'd MMM']; dayPeriods = ['a.m.', 'p.m.']; months = [ ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'], ['jan.', 'feb.', 'mrt.', 'apr.', 'mei', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'] ]; days = [ ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'], ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'], ['Z', 'M', 'D', 'W', 'D', 'V', 'Z'] ]; break; case 'pt': dateFormats = ['dddd, d\' de \'MMMM\' de \'yyyy', 'd\' de \'MMMM\' de \'yyyy', 'dd/MM/yyyy', 'MMMM\' de \'yyyy', 'dd\' de \'MMMM', 'dd\' de \'MMM']; months = [ ['janeiro', 'fevereiro', 'março', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'], ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'] ]; days = [ ['domingo', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 'sábado'], ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 'sáb'], ['D', 'S', 'T', 'Q', 'Q', 'S', 'S'] ]; break; case 'sv': dateFormats = ['dddd \'den\' d MMMM yyyy', 'd MMMM yyyy', 'yyyy-MM-dd', 'MMMM yyyy', 'dd MMMM', 'dd MMM']; dayPeriods = ['fm', 'em']; months = [ ['januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december'], ['jan.', 'feb.', 'mars', 'apr.', 'maj', 'juni', 'juli', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'] ]; days = [ ['Söndag', 'Måndag', 'Tisdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lördag'], ['Sön', 'Mån', 'Tis', 'Ons', 'Tor', 'Fre', 'Lör'], ['S', 'M', 'T', 'O', 'T', 'F', 'L'] ]; break; case 'tr': dateFormats = ['dd MMMM yyyy dddd', 'dd MMMM yyyy', 'dd.MM.yyyy', 'MMMM yyyy', 'dd MMMM', 'dd MMM']; dayPeriods = ['ÖÖ', 'ÖS']; months = [ ['Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'], ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'Ağu', 'Eyl', 'Ek', 'Kas', 'Ara'] ]; days = [ ['Pazar', 'Pazartesi', 'Salı', 'Çarşamba', 'Perşembe', 'Cuma', 'Cumartesi'], ['Paz', 'Pzt', 'Sal', 'Çar', 'Per', 'Cum', 'Cmt'], ['P', 'P', 'S', 'Ç', 'P', 'C', 'C'] ]; break; case 'zh': dateFormats = ['yyyy年M月d日', 'yyyy年M月d日', 'yyyy/M/d', 'yyyy年M月', 'M月d日', 'M月d日']; dayPeriods = ['上午', '下午']; months[0] = months[1] = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']; days[0] = days[1] = ['週日', '週一', '週二', '週三', '週四', '週五', '週六']; days[2] = ['日', '一', '二', '三', '四', '五', '六']; break; } switch (language) { case 'en-gb': dateFormats = ['dddd, d MMMM yyyy', 'dd MMMM yyyy', 'dd/MM/yyyy', 'MMMM yyyy', 'd MMMM', 'd MMM']; break; case 'fr-ca': dateFormats[1] = 'MMM d yyyy'; dateFormats[2] = 'yyyy-MM-dd'; dateFormats[4] = 'MMMM d'; dateFormats[5] = 'MMM d'; firstDayOfWeek = 0; break; case 'fr-on': dateFormats[0] = 'dddd\' le \'d MMMM yyyy'; dateFormats[1] = 'MMM d yyyy'; dateFormats[2] = 'yyyy-MM-dd'; firstDayOfWeek = 0; break; case 'zh-tw': days[0] = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; break; } const descriptor = { hour24: hour24, formats: { dateFormats: { 'full': dateFormats[0], 'medium': dateFormats[1], 'short': dateFormats[2], 'monthYear': dateFormats[3], 'monthDay': dateFormats[4], 'shortMonthDay': dateFormats[5], 'longDayOfWeek': 'dddd', 'shortDayOfWeek': 'ddd', 'longMonth': 'MMMM', 'shortMonth': 'MMM' }, timeFormats: { 'full': fullTimeFormat, 'medium': timeFormat, 'short': timeFormat } }, calendar: { firstDayOfWeek: firstDayOfWeek, weekendStartDay: weekendStartDay, weekendEndDay: weekendEndDay, months: { short: months[1], long: months[0] }, days: { narrow: days[2], short: days[1], long: days[0] }, dayPeriods: { am: dayPeriods[0], pm: dayPeriods[1] } } }; if (settings.overrides.date) { merge(descriptor, settings.overrides.date); } return descriptor; }); } export function formatTime(date, options) { const descriptor = getDateTimeDescriptor(); const settings = getDocumentLocaleSettings(); options = options || {}; const timezone = options.timezone || settings.timezone.name; const format = descriptor.formats.timeFormats[options.format] || options.format || descriptor.formats.timeFormats['short']; const hour = date.getHours(); let hour12 = hour % 12; if (hour12 === 0) { hour12 = 12; } const replacements = { 'HH': prePadByZero(date.getHours(), 2), 'H': date.getHours().toString(), 'hh': prePadByZero(hour12, 2), 'h': hour12, 'mm': prePadByZero(date.getMinutes(), 2), 'tt': (hour > 11) ? descriptor.calendar.dayPeriods.pm : descriptor.calendar.dayPeriods.am, 'ZZZ': timezone }; const value = processPattern(format, replacements); return value; } export function parseTime(input, options) { if (input === undefined || input === null || input === '') { return null; } const descriptor = getDateTimeDescriptor(); const reDigits = new RegExp('(\\d+)', 'g'); const match = input.match(reDigits); if (match === null) { return null; } options = options || {}; const nowProvider = options.nowProvider || function() { return new Date(); }; const reAm = buildDayPeriodRe(descriptor.calendar.dayPeriods.am); const rePm = buildDayPeriodRe(descriptor.calendar.dayPeriods.pm); const today = nowProvider(); const isMorning = (today.getHours() < 12); const digits = match.join(''); const leadingZero = (digits.substr(0, 1) === '0'); let hour = 0; let minute = 0; switch (digits.length) { case 1: hour = digits.substr(0, 1); break; case 2: hour = digits.substr(0, 2); break; case 3: hour = digits.substr(0, 1); minute = digits.substr(1, 2); break; default: hour = parseInt(digits.substr(0, 2)); minute = parseInt(digits.substr(2, 2)); break; } hour = Math.min(Math.max(parseInt(hour, 10), 0), 23); minute = Math.min(Math.max(parseInt(minute, 10), 0), 59); if (!descriptor.hour24 && hour < 13) { const matchPm = input.match(rePm); const matchAm = input.match(reAm); const noAmPm = (matchAm === null && matchPm === null); if (matchPm !== null || (noAmPm && !isMorning && !leadingZero)) { hour += 12; if (hour === 24) { hour = 12; } } else if (hour === 12) { hour = 0; } } const time = new Date(today.getFullYear(), today.getMonth(), today.getDate(), hour, minute, 0); return time; } export function formatDate(date, options) { const descriptor = getDateTimeDescriptor(); options = options || {}; options.format = options.format || 'short'; let format = descriptor.formats.dateFormats[options.format]; if (format === undefined) { format = options.format; } const replacements = { 'dddd': descriptor.calendar.days.long[date.getDay()], 'ddd': descriptor.calendar.days.short[date.getDay()], 'dd': prePadByZero(date.getDate(), 2), 'd': date.getDate().toString(), 'MMMM': descriptor.calendar.months.long[date.getMonth()], 'MMM': descriptor.calendar.months.short[date.getMonth()], 'MM': prePadByZero((date.getMonth() + 1), 2), 'M': (date.getMonth() + 1).toString(), 'yyyy': date.getFullYear().toString() }; const value = processPattern(format, replacements); return value; } export function parseDate(input) { if (input === undefined || input === null) { input = ''; } input = input.toString().trim(); let year = null; let month = null; let day = null; const separator = getSeparator(); const dateFormatParts = getParts(); const dateParts = input.split(separator); if (dateParts.length !== dateFormatParts.length) { throw new Error('Invalid input date: not enough parts'); } for (let i = 0; i < dateFormatParts.length; i++) { const dateFormatPart = dateFormatParts[i]; const partValue = parseInt(dateParts[i]); if (isNaN(partValue)) { throw new Error('Invalid input date: part number value'); } switch (dateFormatPart) { case 'yyyy': year = partValue; break; case 'M': month = partValue; break; case 'd': day = partValue; break; } } if (!isDateValid(year, month, day)) { throw new Error('Invalid input date: part range value'); } const date = new Date(year, month - 1, day, 0, 0, 0); return date; } export function formatDateTime(date, options) { options = options || {}; const format = options.format || 'short'; switch (format) { case 'full': case 'medium': case 'short': return `${formatDate(date, options)} ${formatTime(date, options)}`; } return formatDate(date, options); } function parseLocalDateTimeFromTimestamp(timestamp) { const utcDate = new Date(timestamp); const local = convertJsDateToLocalDateTime(utcDate); if (!local) return utcDate; return new Date(local.year, local.month - 1, local.date, local.hours, local.minutes, local.seconds); } export function formatDateTimeFromTimestamp(timestamp, options) { const date = parseLocalDateTimeFromTimestamp(timestamp); return formatDateTime(date, options); } export function formatDateFromTimestamp(timestamp, options) { const date = parseLocalDateTimeFromTimestamp(timestamp); return formatDate(date, options); } export function formatTimeFromTimestamp(timestamp, options) { const date = parseLocalDateTimeFromTimestamp(timestamp); return formatTime(date, options); } export function formatRelativeDateTime(then) { const now = new Date(); const { firstDayOfWeek } = getDateTimeDescriptor().calendar; if (!Intl.RelativeTimeFormat) { return now.toDateString() === then.toDateString() ? formatTime(then) : formatDate(then); } const delta = (then.getTime() - now.getTime()) / 1000; const unitCutoffs = { second: 60, minute: 60, hour: 24, day: 7, week: 4.348, month: 12, year: 0 }; let { value, unit } = Object.entries(unitCutoffs).reduce(({ value }, [ unit, cutoff ], idx, arr) => { if (cutoff && Math.round(Math.abs(value)) >= Math.round(cutoff)) return { value: value / cutoff }; arr.splice(idx + 1); // short-circuit return { value, unit }; }, { value: delta }); let numeric = 'always'; const thenTS = then.getTime(); if (unit === 'week' || unit === 'day' && Math.round(Math.abs(value)) >= 4) { const fullWeek = 7 * 24 * 60 * 60 * 1000; const thisWeek = (d => { d.setDate(d.getDate() - d.getDay() + firstDayOfWeek); d.setHours(0, 0, 0, 0); return d.getTime(); })(new Date(now)); const lastWeek = thenTS < thisWeek && thenTS >= (thisWeek - fullWeek); const nextWeek = thenTS >= thisWeek + fullWeek && thenTS < (thisWeek + fullWeek * 2); if (lastWeek || nextWeek) { numeric = 'auto'; unit = 'week'; value = Math.sign(value); } } else if (unit === 'day' || unit === 'hour' && Math.round(Math.abs(value)) >= 6) { const fullDay = 24 * 60 * 60 * 1000; const today = new Date(now.toDateString()).getTime(); const yesterday = thenTS < today && thenTS >= (today - fullDay); const tomorrow = thenTS >= today + fullDay && thenTS < (today + fullDay * 2); if (yesterday || tomorrow) { numeric = 'auto'; unit = 'day'; value = Math.sign(value); } } const rtf = new Intl.RelativeTimeFormat(getLanguage(), { localeMatcher: 'best fit', style: 'long', numeric }); return rtf.format(Math.round(value), unit); }