2024-09-14 20:01:43 +00:00
|
|
|
/**
|
|
|
|
* Maps the Espruino datetime format to min and max character lengths.
|
|
|
|
* Used when determining if a format can produce outputs that are too short or long.
|
|
|
|
*/
|
|
|
|
const datetime_length_map = {
|
|
|
|
// %A, %a, %B, %b vary depending on the locale, so they are calculated later
|
2024-10-28 16:34:26 +00:00
|
|
|
"%Y": [4, 4, "2024", "2024"],
|
|
|
|
"%y": [2, 2, "24", "24"],
|
|
|
|
"%m": [2, 2, "10", "10"],
|
|
|
|
"%-m": [1, 2, "1", "10"],
|
|
|
|
"%d": [2, 2, "10", "10"],
|
|
|
|
"%-d": [1, 2, "1", "10"],
|
|
|
|
"%HH": [2, 2, "10", "10"],
|
|
|
|
"%MM": [2, 2, "10", "10"],
|
|
|
|
"%SS": [2, 2, "10", "10"],
|
2024-09-14 20:01:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Takes an Espruino datetime format string and returns the minumum and maximum possible length of characters that the format could use.
|
|
|
|
*
|
|
|
|
* @param {string} datetimeEspruino - The datetime Espruino format
|
|
|
|
* @returns first the minimum possible length, second the maximum possible length.
|
|
|
|
*/
|
|
|
|
function getLengthOfDatetimeFormat(name, datetimeEspruino, locale, errors) {
|
|
|
|
|
|
|
|
// Generate the length_map based on the actual names in the locale
|
|
|
|
const length_map = {...datetime_length_map};
|
|
|
|
for(const [symbol, values] of [
|
|
|
|
["%A", locale.day],
|
|
|
|
["%a", locale.abday],
|
|
|
|
["%B", locale.month],
|
|
|
|
["%b", locale.abmonth],
|
2024-10-28 16:34:26 +00:00
|
|
|
]) {
|
2024-09-14 20:01:43 +00:00
|
|
|
const length = [Infinity, 0];
|
|
|
|
for(const value of values.split(",")){
|
2024-10-28 16:34:26 +00:00
|
|
|
if(length[0] > value.length) { length[0] = value.length; length[2] = value; }
|
|
|
|
if(length[1] < value.length) { length[1] = value.length; length[3] = value; }
|
2024-09-14 20:01:43 +00:00
|
|
|
}
|
|
|
|
length_map[symbol] = length;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the length of the output
|
2024-10-28 16:34:26 +00:00
|
|
|
let formatLength = [0, 0, "", ""];
|
2024-09-14 20:01:43 +00:00
|
|
|
let i = 0;
|
|
|
|
while (i < datetimeEspruino.length) {
|
2024-10-28 16:34:26 +00:00
|
|
|
let ch = datetimeEspruino[i];
|
|
|
|
if (ch === "%") {
|
2024-09-14 20:01:43 +00:00
|
|
|
let match;
|
|
|
|
for(const symbolLength of [2, 3]){
|
|
|
|
const length = length_map[datetimeEspruino.substring(i, i+symbolLength)];
|
|
|
|
if(length){
|
|
|
|
match = {
|
|
|
|
length,
|
|
|
|
symbolLength,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(match){
|
|
|
|
formatLength[0] += match.length[0];
|
|
|
|
formatLength[1] += match.length[1];
|
2024-10-28 16:34:26 +00:00
|
|
|
formatLength[2] += match.length[2];
|
|
|
|
formatLength[3] += match.length[3];
|
2024-09-14 20:01:43 +00:00
|
|
|
i += match.symbolLength;
|
|
|
|
}else{
|
|
|
|
errors.push({name, value: datetimeEspruino, lang: locale.lang, error: `uses an unsupported format symbol: ${datetimeEspruino.substring(i, i+3)}`});
|
|
|
|
formatLength[0]++;
|
|
|
|
formatLength[1]++;
|
2024-10-28 16:34:26 +00:00
|
|
|
formatLength[2]+=" ";
|
|
|
|
formatLength[3]+=" ";
|
2024-09-14 20:01:43 +00:00
|
|
|
i++;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
formatLength[0]++;
|
|
|
|
formatLength[1]++;
|
2024-10-28 16:34:26 +00:00
|
|
|
formatLength[2]+=ch;
|
|
|
|
formatLength[3]+=ch;
|
2024-09-14 20:01:43 +00:00
|
|
|
i++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return formatLength;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks that a locale conforms to some basic standards.
|
|
|
|
*
|
|
|
|
* @param {object} locale - The locale to test.
|
|
|
|
* @param {object} meta - Meta information that is needed to check if locales are supported.
|
|
|
|
* @param {object} meta.speedUnits - The table of speed units.
|
|
|
|
* @param {object} meta.distanceUnits - The table of distance units.
|
|
|
|
* @param {object} meta.codePages - Custom codepoint mappings.
|
|
|
|
* @param {object} meta.CODEPAGE_CONVERSIONS - The table of custom codepoint conversions.
|
|
|
|
* @returns an object with an array of errors and warnings.
|
|
|
|
*/
|
|
|
|
function checkLocale(locale, {speedUnits, distanceUnits, codePages, CODEPAGE_CONVERSIONS}){
|
|
|
|
const errors = [];
|
|
|
|
const warnings = [];
|
|
|
|
|
|
|
|
const speeds = Object.keys(speedUnits);
|
|
|
|
const distances = Object.keys(distanceUnits);
|
|
|
|
|
2024-09-22 06:52:42 +00:00
|
|
|
checkLength("lang", locale.lang, 4, undefined);
|
2024-09-14 20:01:43 +00:00
|
|
|
checkLength("decimal point", locale.decimal_point, 1, 1);
|
|
|
|
checkLength("thousands separator", locale.thousands_sep, 1, 1);
|
|
|
|
checkLength("speed", locale.speed, 2, 4);
|
|
|
|
checkIsIn("speed", locale.speed, "speedUnits", speeds);
|
|
|
|
checkLength("distance", locale.distance["0"], 1, 3);
|
|
|
|
checkLength("distance", locale.distance["1"], 1, 3);
|
|
|
|
checkIsIn("distance", locale.distance["0"], "distanceUnits", distances);
|
|
|
|
checkIsIn("distance", locale.distance["1"], "distanceUnits", distances);
|
|
|
|
checkLength("temperature", locale.temperature, 1, 2);
|
|
|
|
checkLength("meridian", locale.ampm["0"], 1, 3);
|
|
|
|
checkLength("meridian", locale.ampm["1"], 1, 3);
|
|
|
|
warnIfNot("long time format", locale.timePattern["0"], "%HH:%MM:%SS");
|
|
|
|
warnIfNot("short time format", locale.timePattern["1"], "%HH:%MM");
|
|
|
|
checkFormatLength("long time", locale.timePattern["0"], 8, 8);
|
|
|
|
checkFormatLength("short time", locale.timePattern["1"], 5, 5);
|
|
|
|
checkFormatLength("long date", locale.datePattern["0"], 6, 14);
|
|
|
|
checkFormatLength("short date", locale.datePattern["1"], 6, 11);
|
|
|
|
checkArrayLength("short months", locale.abmonth.split(","), 12, 12);
|
|
|
|
checkArrayLength("long months", locale.month.split(","), 12, 12);
|
|
|
|
checkArrayLength("short days", locale.abday.split(","), 7, 7);
|
|
|
|
checkArrayLength("long days", locale.day.split(","), 7, 7);
|
|
|
|
for (const abmonth of locale.abmonth.split(",")) {
|
|
|
|
checkLength("short month", abmonth, 2, 4);
|
|
|
|
}
|
|
|
|
for (const month of locale.month.split(",")) {
|
|
|
|
checkLength("month", month, 3, 11);
|
|
|
|
}
|
|
|
|
for (const abday of locale.abday.split(",")) {
|
|
|
|
checkLength("short day", abday, 2, 4);
|
|
|
|
}
|
|
|
|
for (const day of locale.day.split(",")) {
|
|
|
|
checkLength("day", day, 3, 13);
|
|
|
|
}
|
|
|
|
checkEncoding(locale);
|
|
|
|
|
|
|
|
function checkLength(name, value, min, max) {
|
|
|
|
if(typeof value !== "string"){
|
|
|
|
errors.push({name, value, lang: locale.lang, error: `must be defined and must be a string`});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (min && value.length < min) {
|
|
|
|
errors.push({name, value, lang: locale.lang, error: `must be longer than ${min-1} characters`});
|
|
|
|
}
|
|
|
|
if (max && value.length > max) {
|
|
|
|
errors.push({name, value, lang: locale.lang, error: `must be shorter than ${max+1} characters`});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function checkArrayLength(name, value, min, max){
|
|
|
|
if(!Array.isArray(value)){
|
|
|
|
errors.push({name, value, lang: locale.lang, error: `must be defined and must be an array`});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (min && value.length < min) {
|
|
|
|
errors.push({name, value, lang: locale.lang, error: `array must be longer than ${min-1} entries`});
|
|
|
|
}
|
|
|
|
if (max && value.length > max) {
|
|
|
|
errors.push({name, value, lang: locale.lang, error: `array must be shorter than ${max+1} entries`});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function checkFormatLength(name, value, min, max) {
|
|
|
|
const length = getLengthOfDatetimeFormat(name, value, locale, errors);
|
|
|
|
if (min && length[0] < min) {
|
2024-10-28 16:34:26 +00:00
|
|
|
errors.push({name, value, lang: locale.lang, error: `output must be longer than ${min-1} characters (${length[2]} -> ${length[0]})`});
|
2024-09-14 20:01:43 +00:00
|
|
|
}
|
|
|
|
if (max && length[1] > max) {
|
2024-10-28 16:34:26 +00:00
|
|
|
errors.push({name, value, lang: locale.lang, error: `output must be shorter than ${max+1} characters (${length[3]} -> ${length[1]})`});
|
2024-09-14 20:01:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
function checkIsIn(name, value, listName, list) {
|
|
|
|
if (!list.includes(value)) {
|
|
|
|
errors.push({name, value, lang: locale.lang, error: `must be included in the ${listName} map`});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function warnIfNot(name, value, expected) {
|
|
|
|
if (value !== expected) {
|
|
|
|
warnings.push({name, value, lang: locale.lang, error: `might not work in some apps if it is not "${expected}"`});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function checkEncoding(object) {
|
|
|
|
if(!object){
|
|
|
|
return;
|
|
|
|
}else if(typeof object === "string"){
|
|
|
|
for(const char of object){
|
|
|
|
const charCode = char.charCodeAt();
|
|
|
|
if (charCode >= 32 && charCode < 128) {
|
|
|
|
// ASCII - fully supported
|
|
|
|
continue;
|
|
|
|
} else if (codePages["ISO8859-1"].map.indexOf(char) >= 0) {
|
|
|
|
// At upload time, the char can be converted to a custom codepage
|
|
|
|
continue;
|
|
|
|
} else if (CODEPAGE_CONVERSIONS[char]) {
|
|
|
|
// At upload time, the char can be converted to a similar supported char
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
errors.push({name: `character ${char}`, value: char, lang: locale.lang, error: `is not supported by BangleJS`});
|
|
|
|
}
|
|
|
|
}else{
|
|
|
|
for(const [key, value] of Object.entries(object)){
|
|
|
|
if(key === "icon") continue;
|
|
|
|
checkEncoding(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {errors, warnings};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks that an array of locales conform to some basic standards.
|
|
|
|
*
|
|
|
|
* @param {object[]} locales - The locales to test.
|
|
|
|
* @param {object} meta.speedUnits - The table of speed units.
|
|
|
|
* @param {object} meta.distanceUnits - The table of distance units.
|
|
|
|
* @param {object} meta.codePages - Custom codepoint mappings.
|
|
|
|
* @param {object} meta.CODEPAGE_CONVERSIONS - The table of custom codepoint conversions.
|
|
|
|
* @returns an object with an array of errors and warnings.
|
|
|
|
*/
|
|
|
|
function checkLocales(locales, meta){
|
|
|
|
let errors = [];
|
|
|
|
let warnings = [];
|
|
|
|
|
|
|
|
for(const locale of Object.values(locales)){
|
|
|
|
const result = checkLocale(locale, meta);
|
|
|
|
errors = [...errors, ...result.errors];
|
|
|
|
warnings = [...warnings, ...result.warnings];
|
|
|
|
}
|
|
|
|
|
|
|
|
return {errors, warnings};
|
|
|
|
}
|
|
|
|
|
|
|
|
if(typeof module !== "undefined"){
|
|
|
|
module.exports = {
|
|
|
|
checkLocale,
|
|
|
|
checkLocales,
|
|
|
|
};
|
|
|
|
}else{
|
|
|
|
globalThis.checkLocale = checkLocale;
|
|
|
|
globalThis.checkLocales = checkLocales;
|
|
|
|
}
|