|
| 1 | +/** |
| 2 | + * Maps the Espruino datetime format to min and max character lengths. |
| 3 | + * Used when determining if a format can produce outputs that are too short or long. |
| 4 | + */ |
| 5 | +const datetime_length_map = { |
| 6 | + // %A, %a, %B, %b vary depending on the locale, so they are calculated later |
| 7 | + "%Y": [4, 4], |
| 8 | + "%y": [2, 2], |
| 9 | + "%m": [2, 2], |
| 10 | + "%-m": [1, 2], |
| 11 | + "%d": [2, 2], |
| 12 | + "%-d": [1, 2], |
| 13 | + "%HH": [2, 2], |
| 14 | + "%MM": [2, 2], |
| 15 | + "%SS": [2, 2], |
| 16 | +}; |
| 17 | + |
| 18 | +/** |
| 19 | + * Takes an Espruino datetime format string and returns the minumum and maximum possible length of characters that the format could use. |
| 20 | + * |
| 21 | + * @param {string} datetimeEspruino - The datetime Espruino format |
| 22 | + * @returns first the minimum possible length, second the maximum possible length. |
| 23 | + */ |
| 24 | +function getLengthOfDatetimeFormat(name, datetimeEspruino, locale, errors) { |
| 25 | + |
| 26 | + // Generate the length_map based on the actual names in the locale |
| 27 | + const length_map = {...datetime_length_map}; |
| 28 | + for(const [symbol, values] of [ |
| 29 | + ["%A", locale.day], |
| 30 | + ["%a", locale.abday], |
| 31 | + ["%B", locale.month], |
| 32 | + ["%b", locale.abmonth], |
| 33 | + ]){ |
| 34 | + const length = [Infinity, 0]; |
| 35 | + for(const value of values.split(",")){ |
| 36 | + if(length[0] > value.length) length[0] = value.length; |
| 37 | + if(length[1] < value.length) length[1] = value.length; |
| 38 | + } |
| 39 | + length_map[symbol] = length; |
| 40 | + } |
| 41 | + |
| 42 | + // Find the length of the output |
| 43 | + let formatLength = [0, 0]; |
| 44 | + let i = 0; |
| 45 | + while (i < datetimeEspruino.length) { |
| 46 | + if (datetimeEspruino[i] === "%") { |
| 47 | + let match; |
| 48 | + for(const symbolLength of [2, 3]){ |
| 49 | + const length = length_map[datetimeEspruino.substring(i, i+symbolLength)]; |
| 50 | + if(length){ |
| 51 | + match = { |
| 52 | + length, |
| 53 | + symbolLength, |
| 54 | + } |
| 55 | + } |
| 56 | + } |
| 57 | + if(match){ |
| 58 | + formatLength[0] += match.length[0]; |
| 59 | + formatLength[1] += match.length[1]; |
| 60 | + i += match.symbolLength; |
| 61 | + }else{ |
| 62 | + errors.push({name, value: datetimeEspruino, lang: locale.lang, error: `uses an unsupported format symbol: ${datetimeEspruino.substring(i, i+3)}`}); |
| 63 | + formatLength[0]++; |
| 64 | + formatLength[1]++; |
| 65 | + i++; |
| 66 | + } |
| 67 | + } else { |
| 68 | + formatLength[0]++; |
| 69 | + formatLength[1]++; |
| 70 | + i++; |
| 71 | + } |
| 72 | + } |
| 73 | + return formatLength; |
| 74 | +} |
| 75 | + |
| 76 | +/** |
| 77 | + * Checks that a locale conforms to some basic standards. |
| 78 | + * |
| 79 | + * @param {object} locale - The locale to test. |
| 80 | + * @param {object} meta - Meta information that is needed to check if locales are supported. |
| 81 | + * @param {object} meta.speedUnits - The table of speed units. |
| 82 | + * @param {object} meta.distanceUnits - The table of distance units. |
| 83 | + * @param {object} meta.codePages - Custom codepoint mappings. |
| 84 | + * @param {object} meta.CODEPAGE_CONVERSIONS - The table of custom codepoint conversions. |
| 85 | + * @returns an object with an array of errors and warnings. |
| 86 | + */ |
| 87 | +function checkLocale(locale, {speedUnits, distanceUnits, codePages, CODEPAGE_CONVERSIONS}){ |
| 88 | + const errors = []; |
| 89 | + const warnings = []; |
| 90 | + |
| 91 | + const speeds = Object.keys(speedUnits); |
| 92 | + const distances = Object.keys(distanceUnits); |
| 93 | + |
| 94 | + checkLength("lang", locale.lang, 5, undefined); |
| 95 | + checkLength("decimal point", locale.decimal_point, 1, 1); |
| 96 | + checkLength("thousands separator", locale.thousands_sep, 1, 1); |
| 97 | + checkLength("speed", locale.speed, 2, 4); |
| 98 | + checkIsIn("speed", locale.speed, "speedUnits", speeds); |
| 99 | + checkLength("distance", locale.distance["0"], 1, 3); |
| 100 | + checkLength("distance", locale.distance["1"], 1, 3); |
| 101 | + checkIsIn("distance", locale.distance["0"], "distanceUnits", distances); |
| 102 | + checkIsIn("distance", locale.distance["1"], "distanceUnits", distances); |
| 103 | + checkLength("temperature", locale.temperature, 1, 2); |
| 104 | + checkLength("meridian", locale.ampm["0"], 1, 3); |
| 105 | + checkLength("meridian", locale.ampm["1"], 1, 3); |
| 106 | + warnIfNot("long time format", locale.timePattern["0"], "%HH:%MM:%SS"); |
| 107 | + warnIfNot("short time format", locale.timePattern["1"], "%HH:%MM"); |
| 108 | + checkFormatLength("long time", locale.timePattern["0"], 8, 8); |
| 109 | + checkFormatLength("short time", locale.timePattern["1"], 5, 5); |
| 110 | + checkFormatLength("long date", locale.datePattern["0"], 6, 14); |
| 111 | + checkFormatLength("short date", locale.datePattern["1"], 6, 11); |
| 112 | + checkArrayLength("short months", locale.abmonth.split(","), 12, 12); |
| 113 | + checkArrayLength("long months", locale.month.split(","), 12, 12); |
| 114 | + checkArrayLength("short days", locale.abday.split(","), 7, 7); |
| 115 | + checkArrayLength("long days", locale.day.split(","), 7, 7); |
| 116 | + for (const abmonth of locale.abmonth.split(",")) { |
| 117 | + checkLength("short month", abmonth, 2, 4); |
| 118 | + } |
| 119 | + for (const month of locale.month.split(",")) { |
| 120 | + checkLength("month", month, 3, 11); |
| 121 | + } |
| 122 | + for (const abday of locale.abday.split(",")) { |
| 123 | + checkLength("short day", abday, 2, 4); |
| 124 | + } |
| 125 | + for (const day of locale.day.split(",")) { |
| 126 | + checkLength("day", day, 3, 13); |
| 127 | + } |
| 128 | + checkEncoding(locale); |
| 129 | + |
| 130 | + function checkLength(name, value, min, max) { |
| 131 | + if(typeof value !== "string"){ |
| 132 | + errors.push({name, value, lang: locale.lang, error: `must be defined and must be a string`}); |
| 133 | + return; |
| 134 | + } |
| 135 | + if (min && value.length < min) { |
| 136 | + errors.push({name, value, lang: locale.lang, error: `must be longer than ${min-1} characters`}); |
| 137 | + } |
| 138 | + if (max && value.length > max) { |
| 139 | + errors.push({name, value, lang: locale.lang, error: `must be shorter than ${max+1} characters`}); |
| 140 | + } |
| 141 | + } |
| 142 | + function checkArrayLength(name, value, min, max){ |
| 143 | + if(!Array.isArray(value)){ |
| 144 | + errors.push({name, value, lang: locale.lang, error: `must be defined and must be an array`}); |
| 145 | + return; |
| 146 | + } |
| 147 | + if (min && value.length < min) { |
| 148 | + errors.push({name, value, lang: locale.lang, error: `array must be longer than ${min-1} entries`}); |
| 149 | + } |
| 150 | + if (max && value.length > max) { |
| 151 | + errors.push({name, value, lang: locale.lang, error: `array must be shorter than ${max+1} entries`}); |
| 152 | + } |
| 153 | + } |
| 154 | + function checkFormatLength(name, value, min, max) { |
| 155 | + const length = getLengthOfDatetimeFormat(name, value, locale, errors); |
| 156 | + if (min && length[0] < min) { |
| 157 | + errors.push({name, value, lang: locale.lang, error: `output must be longer than ${min-1} characters`}); |
| 158 | + } |
| 159 | + if (max && length[1] > max) { |
| 160 | + errors.push({name, value, lang: locale.lang, error: `output must be shorter than ${max+1} characters`}); |
| 161 | + } |
| 162 | + } |
| 163 | + function checkIsIn(name, value, listName, list) { |
| 164 | + if (!list.includes(value)) { |
| 165 | + errors.push({name, value, lang: locale.lang, error: `must be included in the ${listName} map`}); |
| 166 | + } |
| 167 | + } |
| 168 | + function warnIfNot(name, value, expected) { |
| 169 | + if (value !== expected) { |
| 170 | + warnings.push({name, value, lang: locale.lang, error: `might not work in some apps if it is not "${expected}"`}); |
| 171 | + } |
| 172 | + } |
| 173 | + function checkEncoding(object) { |
| 174 | + if(!object){ |
| 175 | + return; |
| 176 | + }else if(typeof object === "string"){ |
| 177 | + for(const char of object){ |
| 178 | + const charCode = char.charCodeAt(); |
| 179 | + if (charCode >= 32 && charCode < 128) { |
| 180 | + // ASCII - fully supported |
| 181 | + continue; |
| 182 | + } else if (codePages["ISO8859-1"].map.indexOf(char) >= 0) { |
| 183 | + // At upload time, the char can be converted to a custom codepage |
| 184 | + continue; |
| 185 | + } else if (CODEPAGE_CONVERSIONS[char]) { |
| 186 | + // At upload time, the char can be converted to a similar supported char |
| 187 | + continue; |
| 188 | + } |
| 189 | + errors.push({name: `character ${char}`, value: char, lang: locale.lang, error: `is not supported by BangleJS`}); |
| 190 | + } |
| 191 | + }else{ |
| 192 | + for(const [key, value] of Object.entries(object)){ |
| 193 | + if(key === "icon") continue; |
| 194 | + checkEncoding(value); |
| 195 | + } |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + return {errors, warnings}; |
| 200 | +} |
| 201 | + |
| 202 | +/** |
| 203 | + * Checks that an array of locales conform to some basic standards. |
| 204 | + * |
| 205 | + * @param {object[]} locales - The locales to test. |
| 206 | + * @param {object} meta.speedUnits - The table of speed units. |
| 207 | + * @param {object} meta.distanceUnits - The table of distance units. |
| 208 | + * @param {object} meta.codePages - Custom codepoint mappings. |
| 209 | + * @param {object} meta.CODEPAGE_CONVERSIONS - The table of custom codepoint conversions. |
| 210 | + * @returns an object with an array of errors and warnings. |
| 211 | + */ |
| 212 | +function checkLocales(locales, meta){ |
| 213 | + let errors = []; |
| 214 | + let warnings = []; |
| 215 | + |
| 216 | + for(const locale of Object.values(locales)){ |
| 217 | + const result = checkLocale(locale, meta); |
| 218 | + errors = [...errors, ...result.errors]; |
| 219 | + warnings = [...warnings, ...result.warnings]; |
| 220 | + } |
| 221 | + |
| 222 | + return {errors, warnings}; |
| 223 | +} |
| 224 | + |
| 225 | +if(typeof module !== "undefined"){ |
| 226 | + module.exports = { |
| 227 | + checkLocale, |
| 228 | + checkLocales, |
| 229 | + }; |
| 230 | +}else{ |
| 231 | + globalThis.checkLocale = checkLocale; |
| 232 | + globalThis.checkLocales = checkLocales; |
| 233 | +} |
0 commit comments