UNPKG

intl

Version:

Polyfill the ECMA-402 Intl API (except collation)

309 lines (277 loc) 11.5 kB
/* jslint esnext: true */ // Match these datetime components in a CLDR pattern, except those in single quotes let expDTComponents = /(?:[Eec]{1,6}|G{1,5}|[Qq]{1,5}|(?:[yYur]+|U{1,5})|[ML]{1,5}|d{1,2}|D{1,3}|F{1}|[abB]{1,5}|[hkHK]{1,2}|w{1,2}|W{1}|m{1,2}|s{1,2}|[zZOvVxX]{1,4})(?=([^']*'[^']*')*[^']*$)/g; // trim patterns after transformations let expPatternTrimmer = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; // Skip over patterns with these datetime components because we don't have data // to back them up: // timezone, weekday, amoung others let unwantedDTCs = /[rqQASjJgwWIQq]/; // xXVO were removed from this list in favor of computing matches with timeZoneName values but printing as empty string let dtKeys = ["weekday", "era", "year", "month", "day", "weekday", "quarter"]; let tmKeys = ["hour", "minute", "second", "hour12", "timeZoneName"]; function isDateFormatOnly(obj) { for (let i = 0; i < tmKeys.length; i += 1) { if (obj.hasOwnProperty(tmKeys[i])) { return false; } } return true; } function isTimeFormatOnly(obj) { for (let i = 0; i < dtKeys.length; i += 1) { if (obj.hasOwnProperty(dtKeys[i])) { return false; } } return true; } function joinDateAndTimeFormats(dateFormatObj, timeFormatObj) { let o = { _: {} }; for (let i = 0; i < dtKeys.length; i += 1) { if (dateFormatObj[dtKeys[i]]) { o[dtKeys[i]] = dateFormatObj[dtKeys[i]]; } if (dateFormatObj._[dtKeys[i]]) { o._[dtKeys[i]] = dateFormatObj._[dtKeys[i]]; } } for (let j = 0; j < tmKeys.length; j += 1) { if (timeFormatObj[tmKeys[j]]) { o[tmKeys[j]] = timeFormatObj[tmKeys[j]]; } if (timeFormatObj._[tmKeys[j]]) { o._[tmKeys[j]] = timeFormatObj._[tmKeys[j]]; } } return o; } function computeFinalPatterns(formatObj) { // From http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns: // 'In patterns, two single quotes represents a literal single quote, either // inside or outside single quotes. Text within single quotes is not // interpreted in any way (except for two adjacent single quotes).' formatObj.pattern12 = formatObj.extendedPattern.replace(/'([^']*)'/g, ($0, literal) => { return literal ? literal : "'"; }); // pattern 12 is always the default. we can produce the 24 by removing {ampm} formatObj.pattern = formatObj.pattern12.replace('{ampm}', '').replace(expPatternTrimmer, ''); return formatObj; } function expDTComponentsMeta($0, formatObj) { switch ($0.charAt(0)) { // --- Era case 'G': formatObj.era = [ 'short', 'short', 'short', 'long', 'narrow' ][$0.length-1]; return '{era}'; // --- Year case 'y': case 'Y': case 'u': case 'U': case 'r': formatObj.year = $0.length === 2 ? '2-digit' : 'numeric'; return '{year}'; // --- Quarter (not supported in this polyfill) case 'Q': case 'q': formatObj.quarter = [ 'numeric', '2-digit', 'short', 'long', 'narrow' ][$0.length-1]; return '{quarter}'; // --- Month case 'M': case 'L': formatObj.month = [ 'numeric', '2-digit', 'short', 'long', 'narrow' ][$0.length-1]; return '{month}'; // --- Week (not supported in this polyfill) case 'w': // week of the year formatObj.week = $0.length === 2 ? '2-digit' : 'numeric'; return '{weekday}'; case 'W': // week of the month formatObj.week = 'numeric'; return '{weekday}'; // --- Day case 'd': // day of the month formatObj.day = $0.length === 2 ? '2-digit' : 'numeric'; return '{day}'; case 'D': // day of the year case 'F': // day of the week case 'g': // 1..n: Modified Julian day formatObj.day = 'numeric'; return '{day}'; // --- Week Day case 'E': // day of the week formatObj.weekday = [ 'short', 'short', 'short', 'long', 'narrow', 'short' ][$0.length-1]; return '{weekday}'; case 'e': // local day of the week formatObj.weekday = [ 'numeric', '2-digit', 'short', 'long', 'narrow', 'short' ][$0.length-1]; return '{weekday}'; case 'c': // stand alone local day of the week formatObj.weekday = [ 'numeric', undefined, 'short', 'long', 'narrow', 'short' ][$0.length-1]; return '{weekday}'; // --- Period case 'a': // AM, PM case 'b': // am, pm, noon, midnight case 'B': // flexible day periods formatObj.hour12 = true; return '{ampm}'; // --- Hour case 'h': case 'H': formatObj.hour = $0.length === 2 ? '2-digit' : 'numeric'; return '{hour}'; case 'k': case 'K': formatObj.hour12 = true; // 12-hour-cycle time formats (using h or K) formatObj.hour = $0.length === 2 ? '2-digit' : 'numeric'; return '{hour}'; // --- Minute case 'm': formatObj.minute = $0.length === 2 ? '2-digit' : 'numeric'; return '{minute}'; // --- Second case 's': formatObj.second = $0.length === 2 ? '2-digit' : 'numeric'; return '{second}'; case 'S': case 'A': formatObj.second = 'numeric'; return '{second}'; // --- Timezone case 'z': // 1..3, 4: specific non-location format case 'Z': // 1..3, 4, 5: The ISO8601 varios formats case 'O': // 1, 4: miliseconds in day short, long case 'v': // 1, 4: generic non-location format case 'V': // 1, 2, 3, 4: time zone ID or city case 'X': // 1, 2, 3, 4: The ISO8601 varios formats case 'x': // 1, 2, 3, 4: The ISO8601 varios formats // this polyfill only supports much, for now, we are just doing something dummy formatObj.timeZoneName = $0.length < 4 ? 'short' : 'long'; return '{timeZoneName}'; } } /** * Converts the CLDR availableFormats into the objects and patterns required by * the ECMAScript Internationalization API specification. */ export function createDateTimeFormat(skeleton, pattern) { // we ignore certain patterns that are unsupported to avoid this expensive op. if (unwantedDTCs.test(pattern)) return undefined; let formatObj = { originalPattern: pattern, _: {}, }; // Replace the pattern string with the one required by the specification, whilst // at the same time evaluating it for the subsets and formats formatObj.extendedPattern = pattern.replace(expDTComponents, ($0) => { // See which symbol we're dealing with return expDTComponentsMeta($0, formatObj._); }); // Match the skeleton string with the one required by the specification // this implementation is based on the Date Field Symbol Table: // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table // Note: we are adding extra data to the formatObject even though this polyfill // might not support it. skeleton.replace(expDTComponents, ($0) => { // See which symbol we're dealing with return expDTComponentsMeta($0, formatObj); }); return computeFinalPatterns(formatObj); } /** * Processes DateTime formats from CLDR to an easier-to-parse format. * the result of this operation should be cached the first time a particular * calendar is analyzed. * * The specification requires we support at least the following subsets of * date/time components: * * - 'weekday', 'year', 'month', 'day', 'hour', 'minute', 'second' * - 'weekday', 'year', 'month', 'day' * - 'year', 'month', 'day' * - 'year', 'month' * - 'month', 'day' * - 'hour', 'minute', 'second' * - 'hour', 'minute' * * We need to cherry pick at least these subsets from the CLDR data and convert * them into the pattern objects used in the ECMA-402 API. */ export function createDateTimeFormats(formats) { let availableFormats = formats.availableFormats; let timeFormats = formats.timeFormats; let dateFormats = formats.dateFormats; let result = []; let skeleton, pattern, computed, i, j; let timeRelatedFormats = []; let dateRelatedFormats = []; // Map available (custom) formats into a pattern for createDateTimeFormats for (skeleton in availableFormats) { if (availableFormats.hasOwnProperty(skeleton)) { pattern = availableFormats[skeleton]; computed = createDateTimeFormat(skeleton, pattern); if (computed) { result.push(computed); // in some cases, the format is only displaying date specific props // or time specific props, in which case we need to also produce the // combined formats. if (isDateFormatOnly(computed)) { dateRelatedFormats.push(computed); } else if (isTimeFormatOnly(computed)) { timeRelatedFormats.push(computed); } } } } // Map time formats into a pattern for createDateTimeFormats for (skeleton in timeFormats) { if (timeFormats.hasOwnProperty(skeleton)) { pattern = timeFormats[skeleton]; computed = createDateTimeFormat(skeleton, pattern); if (computed) { result.push(computed); timeRelatedFormats.push(computed); } } } // Map date formats into a pattern for createDateTimeFormats for (skeleton in dateFormats) { if (dateFormats.hasOwnProperty(skeleton)) { pattern = dateFormats[skeleton]; computed = createDateTimeFormat(skeleton, pattern); if (computed) { result.push(computed); dateRelatedFormats.push(computed); } } } // combine custom time and custom date formats when they are orthogonals to complete the // formats supported by CLDR. // This Algo is based on section "Missing Skeleton Fields" from: // http://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems for (i = 0; i < timeRelatedFormats.length; i += 1) { for (j = 0; j < dateRelatedFormats.length; j += 1) { if (dateRelatedFormats[j].month === 'long') { pattern = dateRelatedFormats[j].weekday ? formats.full : formats.long; } else if (dateRelatedFormats[j].month === 'short') { pattern = formats.medium; } else { pattern = formats.short; } computed = joinDateAndTimeFormats(dateRelatedFormats[j], timeRelatedFormats[i]); computed.originalPattern = pattern; computed.extendedPattern = pattern .replace('{0}', timeRelatedFormats[i].extendedPattern) .replace('{1}', dateRelatedFormats[j].extendedPattern) .replace(/^[,\s]+|[,\s]+$/gi, ''); result.push(computeFinalPatterns(computed)); } } return result; }