mirror of https://github.com/espruino/BangleApps
467 lines
20 KiB
HTML
467 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<link rel="stylesheet" href="../../css/spectre.min.css">
|
|
<style>
|
|
table {width:100%;margin-top:3%;}
|
|
.table_t {font-weight:bold;width:40%;}
|
|
.form-group > * {display:block;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<p>Please choose a language from the following list:</p>
|
|
<div class="form-group">
|
|
<select id="languages" class="form-select">
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label><input id="translations" type="checkbox" /> Add common language translations like "Yes", "No", "On", "Off"<br/><i>(Not recommended. For translations use the option under <code>More...</code> in the app loader.</i></label>
|
|
<label><input id="customize" type="checkbox" /> Advanced: Customize the date and time formats.</label>
|
|
</div>
|
|
<p>
|
|
<table id="examples-short-long"></table>
|
|
<table id="examples"></table>
|
|
</p>
|
|
|
|
<p id="customize-warning"></p>
|
|
|
|
<p>Then click <button id="upload" class="btn btn-primary">Upload</button></p>
|
|
|
|
<script src="../../core/lib/customize.js"></script>
|
|
<script src="../../core/js/utils.js"></script>
|
|
<script src="sanitycheck.js"></script>
|
|
<script src="locales.js"></script>
|
|
|
|
<script>
|
|
/*
|
|
eg. the built-in en_GB at https://github.com/espruino/Espruino/blob/master/libs/js/banglejs/locale.js is:
|
|
|
|
function round(n, dp) {
|
|
if (dp===undefined) dp=1;
|
|
var p = Math.min(dp,dp - Math.floor(Math.log(n)/Math.log(10)));
|
|
return n.toFixed(p);
|
|
}
|
|
exports = { name : "system", currencySym:"£",
|
|
translate : str=>str, // as-is
|
|
date : (d,short) => short?("0"+d.getDate()).substr(-2)+"/"+("0"+(d.getMonth()+1)).substr(-2)+"/"+d.getFullYear():d.toString().substr(4,11), // Date to "Feb 28 2020" or "28/02/2020"(short)
|
|
time : (d,short) => { // Date to "4:15.28 pm" or "15:42"(short)
|
|
var h = d.getHours(), m = d.getMinutes()
|
|
if ((require('Storage').readJSON('setting.json',1)||{})["12hour"])
|
|
h = (h%12==0) ? 12 : h%12; // 12 hour
|
|
if (short)
|
|
return (" "+h).substr(-2)+":"+("0"+m).substr(-2);
|
|
else {
|
|
var r = "am";
|
|
if (h==0) { h=12; }
|
|
else if (h>=12) {
|
|
if (h>12) h-=12;
|
|
r = "pm";
|
|
}
|
|
return (" "+h).substr(-2)+":"+("0"+m).substr(-2)+"."+("0"+d.getSeconds()).substr(-2)+" "+r;
|
|
}
|
|
},
|
|
dow : (d,short) => "Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday".split(",")[d.getDay()].substr(0, short ? 3 : 10), // Date to "Monday" or "Mon"(short)
|
|
month : (d,short) => "January,February,March,April,May,June,July,August,September,October,November,December".split(",")[d.getMonth()].substr(0, short ? 3 : 10), // Date to "February" or "Feb"(short)
|
|
number: (n, dec) => {
|
|
if (dec == null) dec = 2;
|
|
var w = n.toFixed(dec),
|
|
k = w|0,
|
|
b = n < 0 ? 1 : 0,
|
|
u = Math.abs(w-k),
|
|
d = (''+u.toFixed(dec)).substr(2, dec),
|
|
s = ''+k,
|
|
i = s.length,
|
|
r = '';
|
|
while ((i-=3) > b)
|
|
r = ',' + s.substr(i, 3) + r;
|
|
return s.substr(0, i + 3) + r + (d ? '.' + d: '');
|
|
},
|
|
currency : n => {console.log("Warning: Currency information is deprecated");return "£"+n.toFixed(2)}, // number to "£1.00"
|
|
distance : (m,dp) => (m<1000)?round(m,dp)+"m":round(m/1000,dp)+"km", // meters to "123m" or "1.2km" depending on size
|
|
speed : (s,dp) => round(s/1.60934,dp)+"mph",// kph to "123mph"
|
|
temp : (t,dp) => round(t,dp)+"'C", // degrees C to degrees C
|
|
meridian: d => (d.getHours() <= 12) ? "am":"pm" // Date to am/pm
|
|
};
|
|
|
|
|
|
*/
|
|
function codePageLookup(lang, codePage, ch) {
|
|
var chCode = ch.charCodeAt();
|
|
if (chCode>=32 && chCode<128) return ch; // ASCII - copy it
|
|
// not normal ASCII
|
|
var n; // default is a space
|
|
if (codePage.map.indexOf(ch)>=0) // look up in char map, escape that
|
|
n = 128+codePage.map.indexOf(ch);
|
|
/*else if (chCode<256) // it's non-ascii, but <256 - just escape it
|
|
n = chCode;*/
|
|
else {
|
|
if (CODEPAGE_CONVERSIONS[ch]) return CODEPAGE_CONVERSIONS[ch];
|
|
console.error(`Locale ${lang}: Character ${ch} (${chCode}) is not in Code Page ${codePage.name}`);
|
|
return undefined;
|
|
}
|
|
// escape the char
|
|
return '\\x'+(n+256).toString(16).slice(-2);
|
|
}
|
|
|
|
function createLocaleModule() {
|
|
console.log(`Language ${lang}`);
|
|
|
|
const translations = document.getElementById('translations').checked;
|
|
console.log(`Translations: ${translations}`);
|
|
|
|
if (!locale) {
|
|
alert(`Locale not set for language ${lang}!`);
|
|
return;
|
|
}
|
|
|
|
if (!translations)
|
|
locale.trans = null;
|
|
|
|
const codePageName = "ISO8859-1";
|
|
if (locale.codePage)
|
|
codePageName = locale.codePage;
|
|
const codePage = codePages[codePageName];
|
|
if (!codePage) {
|
|
alert(`Code Page ${codePageName} not found!`);
|
|
return;
|
|
}
|
|
|
|
// Convert object to JSON, with codepage
|
|
function js(x) {
|
|
// do nortmal conversion
|
|
x = JSON.stringify(x);
|
|
/* assume any out of bounds character is going to
|
|
be inside a quoted string */
|
|
var s = '';
|
|
for (var i=0;i<x.length;i++) {
|
|
var ch = codePageLookup(lang, codePage, x[i]);
|
|
s += (ch===undefined) ? "?" : ch;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function unitConv(x) {
|
|
return x === 1 ? 'n' : 'n/' + x
|
|
}
|
|
|
|
var replaceList = {
|
|
"%Y": "d.getFullYear()",
|
|
"%y": "d.getFullYear().toString().slice(-2)",
|
|
"%m": "('0'+(d.getMonth()+1).toString()).slice(-2)",
|
|
"%-m": "d.getMonth()+1",
|
|
"%d": "('0'+d.getDate()).slice(-2)",
|
|
"%-d": "d.getDate()",
|
|
"%HH": "getHours(d)",
|
|
"%MM": "('0'+d.getMinutes()).slice(-2)",
|
|
"%SS": "('0'+d.getSeconds()).slice(-2)",
|
|
"%A": `${js(locale.day)}.split(',')[d.getDay()]`,
|
|
"%a": `${js(locale.abday)}.split(',')[d.getDay()]`,
|
|
"%B": `${js(locale.month)}.split(',')[d.getMonth()]`,
|
|
"%b": `${js(locale.abmonth)}.split(',')[d.getMonth()]`,
|
|
};
|
|
|
|
var timeN = patternToCode(locale.timePattern[0]);
|
|
var timeS = patternToCode(locale.timePattern[1]);
|
|
var dateN = patternToCode(locale.datePattern[0]);
|
|
var dateS = patternToCode(locale.datePattern[1]);
|
|
var temperature = locale.temperature=='°F' ? '(t*9/5)+32' : 't';
|
|
|
|
function getLocaleModule(isLocal) {
|
|
return `
|
|
function round(n, dp) {
|
|
if (dp===undefined) dp=0;
|
|
var p = Math.max(0,Math.min(dp,dp - Math.floor(Math.log(n)/Math.log(10))));
|
|
return n.toFixed(p);
|
|
}
|
|
var _is12Hours;
|
|
function is12Hours() {
|
|
if (_is12Hours === undefined) _is12Hours = ${isLocal ? "false" : `(require('Storage').readJSON('setting.json', 1) || {})["12hour"]`};
|
|
return _is12Hours;
|
|
}
|
|
function getHours(d) {
|
|
var h = d.getHours();
|
|
if (!is12Hours()) return ('0' + h).slice(-2);
|
|
return ((h % 12 == 0) ? 12 : h % 12).toString();
|
|
}
|
|
exports = {
|
|
name: ${js(locale.lang)},
|
|
currencySym: ${js("£")},
|
|
dow: (d,short) => ${js(locale.day + ',' + locale.abday)}.split(',')[d.getDay() + (short ? 7 : 0)],
|
|
month: (d,short) => ${js(locale.month + ',' + locale.abmonth)}.split(',')[d.getMonth() + (short ? 12 : 0)],
|
|
number: (n, dec) => {
|
|
if (dec == null) dec = 2;
|
|
var w = n.toFixed(dec),
|
|
k = w|0,
|
|
b = n < 0 ? 1 : 0,
|
|
u = Math.abs(w-k),
|
|
d = (''+u.toFixed(dec)).substr(2, dec),
|
|
s = ''+k,
|
|
i = s.length,
|
|
r = '';
|
|
while ((i-=3) > b)
|
|
r = '${locale.thousands_sep}' + s.substr(i, 3) + r;
|
|
return s.substr(0, i + 3) + r + (d ? '${locale.decimal_point}' + d: '');
|
|
},
|
|
currency: n => {console.log("Warning: Currency information is deprecated, see https://github.com/espruino/BangleApps/issues/3269");return ${js("£")}+exports.number(n)},
|
|
distance: (n,dp) => n < ${distanceUnits[locale.distance[1]]} ? round(${unitConv(distanceUnits[locale.distance[0]])},dp) + ${js(locale.distance[0])} : round(${unitConv(distanceUnits[locale.distance[1]])},dp) + ${js(locale.distance[1])},
|
|
speed: (n,dp) => round(${unitConv(speedUnits[locale.speed])},dp) + ${js(locale.speed)},
|
|
temp: (t,dp) => round(${temperature},dp) + ${js(locale.temperature)},
|
|
translate: s => ${locale.trans?`{var t=${js(locale.trans)};s=''+s;return t[s]||t[s.toLowerCase()]||s;}`:`s`},
|
|
date: (d,short) => short ? \`${dateS}\` : \`${dateN}\`,
|
|
time: (d,short) => short ? \`${timeS}\` : \`${timeN}\`,
|
|
meridian: (d,force) => (force||is12Hours()) ? d.getHours() < 12 ? ${js(locale.ampm[0])}:${js(locale.ampm[1])} : "",
|
|
is12Hours,
|
|
};
|
|
`.trim()
|
|
};
|
|
|
|
var exports;
|
|
eval(getLocaleModule(true));
|
|
console.log("exports:",exports);
|
|
|
|
function patternToCode(pattern){
|
|
for(const symbol of Object.keys(replaceList)){
|
|
pattern = pattern.replaceAll(symbol,"${"+replaceList[symbol]+"}");
|
|
}
|
|
return pattern;
|
|
}
|
|
function patternToOutput(pattern){
|
|
const code = patternToCode(pattern);
|
|
const result = eval(`let d = new Date();\`${code}\``);
|
|
return result;
|
|
}
|
|
function dataList(id, options, formatter){
|
|
let output = `<datalist id="${id}">`;
|
|
for(const option of options){
|
|
const formatted = formatter?.(option) || option;
|
|
output+=`\n<option value="${option}">${formatted}</option>`
|
|
}
|
|
output += "\n</datalist>";
|
|
return output;
|
|
}
|
|
|
|
var date = new Date();
|
|
document.getElementById("examples-short-long").innerHTML = `
|
|
<tr><td class="table_t"></td><td style="font-weight:bold">Short</td><td style="font-weight:bold">Long</td></tr>
|
|
<tr><td class="table_t">Day</td><td>${exports.dow(date,1)}</td><td>${exports.dow(date,0)}</td></tr>
|
|
<tr><td class="table_t">Month</td><td>${exports.month(date,1)}</td><td>${exports.month(date,0)}</td></tr>
|
|
<tr><td class="table_t">Date</td>
|
|
<td id="short-date-pattern-output">${exports.date(date,1)}</td>
|
|
<td id="long-date-pattern-output">${exports.date(date,0)}</td>
|
|
</tr>
|
|
${customizeLocale ? `<tr><td class="table_t">Date format</td>
|
|
<td>
|
|
<input type=text id="short-date-pattern" list="short-date-patterns" value="${locale?.datePattern["1"]}"/>
|
|
${dataList("short-date-patterns", [locale?.datePattern["1"], "%-d.%-m.%y", "%-d/%-m/%y", "%d/%m/%Y"], patternToOutput)}
|
|
</td>
|
|
<td>
|
|
<input type=text id="long-date-pattern" list="long-date-patterns" value="${locale?.datePattern["0"]}"/>
|
|
${dataList("long-date-patterns", [locale?.datePattern["0"], "%-d. %b %Y", "%b %d, %Y"], patternToOutput)}
|
|
</td>
|
|
</td>`
|
|
: ""}
|
|
<tr><td class="table_t">Time</td>
|
|
<td id="short-time-pattern-output">${exports.time(date,1)}</td>
|
|
<td id="long-time-pattern-output">${exports.time(date,0)}</td>
|
|
</tr>
|
|
${customizeLocale ? `<tr><td class="table_t">Time format</td>
|
|
<td>
|
|
<input type=text id="short-time-pattern" list="short-time-patterns" value="${locale?.timePattern["1"]}"/>
|
|
${dataList("short-time-patterns", [ "%HH.%MM", "%HH:%MM"], patternToOutput)}
|
|
</td>
|
|
<td>
|
|
<input type=text id="long-time-pattern" list="long-time-patterns" value="${locale?.timePattern["0"]}"/>
|
|
${dataList("long-time-patterns", [locale?.timePattern["0"], "%HH.%MM.%SS", "%HH:%MM:%SS"], patternToOutput)}
|
|
</td>
|
|
</td>`
|
|
: ""}
|
|
<tr><td class="table_t">Number</td><td>${exports.number(12.3456789)}</td><td>${exports.number(12.3456789,4)}</td></tr>
|
|
<tr><td class="table_t">Distance</td><td>${exports.distance(12.34,0)}</td><td>${exports.distance(12345.6,1)}</td></tr>
|
|
`;
|
|
document.getElementById("examples").innerHTML = `
|
|
<tr><td class="table_t">Meridian</td><td>
|
|
<span id="meridian-am-output">${exports.meridian(new Date(0), true)}</span> /
|
|
<span id="meridian-pm-output">${exports.meridian(new Date(43200000), true)}</span>
|
|
</td></tr>
|
|
${customizeLocale ? `<tr><td class="table_t">Meridian names</td>
|
|
<td>
|
|
<input type=text id="meridian-am" list="meridian-ams" value="${locale?.ampm["0"]}"/>
|
|
${dataList("meridian-ams", [locale?.ampm["0"], "AM"])}
|
|
</td>
|
|
<td>
|
|
<input type=text id="meridian-pm" list="meridian-pms" value="${locale?.ampm["1"]}"/>
|
|
${dataList("meridian-pms", [locale?.ampm["1"], "PM"])}
|
|
</td>
|
|
</tr>`
|
|
: ""}
|
|
<tr><td class="table_t">Speed</td><td>${exports.speed(123)}</td></tr>
|
|
<tr><td class="table_t">Temperature</td><td>${exports.temp(12,0)}</td></tr>
|
|
`;
|
|
|
|
if(customizeLocale){
|
|
document.querySelector("input#short-date-pattern").addEventListener("input", event => {
|
|
locale.datePattern["1"] = event.target.value;
|
|
document.querySelector("td#short-date-pattern-output").innerText = patternToOutput(event.target.value);
|
|
checkCustomLocale();
|
|
});
|
|
document.querySelector("input#long-date-pattern").addEventListener("input", event => {
|
|
locale.datePattern["0"] = event.target.value;
|
|
document.querySelector("td#long-date-pattern-output").innerText = patternToOutput(event.target.value);
|
|
checkCustomLocale();
|
|
});
|
|
document.querySelector("input#short-time-pattern").addEventListener("input", event => {
|
|
locale.timePattern["1"] = event.target.value;
|
|
document.querySelector("td#short-time-pattern-output").innerText = patternToOutput(event.target.value);
|
|
checkCustomLocale();
|
|
});
|
|
document.querySelector("input#long-time-pattern").addEventListener("input", event => {
|
|
locale.timePattern["0"] = event.target.value;
|
|
document.querySelector("td#long-time-pattern-output").innerText = patternToOutput(event.target.value);
|
|
checkCustomLocale();
|
|
});
|
|
document.querySelector("input#meridian-am").addEventListener("input", event => {
|
|
locale.ampm["0"] = event.target.value;
|
|
document.querySelector("span#meridian-am-output").innerText = event.target.value;
|
|
checkCustomLocale();
|
|
});
|
|
document.querySelector("input#meridian-pm").addEventListener("input", event => {
|
|
locale.ampm["1"] = event.target.value;
|
|
document.querySelector("span#meridian-pm-output").innerText = event.target.value;
|
|
checkCustomLocale();
|
|
});
|
|
|
|
let isCheckingLocale = false;
|
|
// Polyfill for WebKit:
|
|
const requestIdleCallback = globalThis.requestIdleCallback || ((func) => {func()});
|
|
// Check that a custom locale follows some basic standards
|
|
function checkCustomLocale(){
|
|
if(isCheckingLocale) return;
|
|
isCheckingLocale = true;
|
|
setTimeout(() => {
|
|
requestIdleCallback(() => {
|
|
isCheckingLocale = false;
|
|
const result = globalThis.checkLocale(locale, {speedUnits, distanceUnits, codePages, CODEPAGE_CONVERSIONS});
|
|
let text = "";
|
|
for(const w of [...result.errors, ...result.warnings]){
|
|
text += `⚠️ ${w.name} ${w.error}.\n`;
|
|
}
|
|
const element = document.getElementById("customize-warning");
|
|
if(text.length > 0){
|
|
text += "\nIf you upload this locale, some apps might no longer work.\nPlease try to resolve the issues before uploading."
|
|
element.classList.add("toast");
|
|
element.classList.add("toast-error");
|
|
}else{
|
|
element.classList.remove("toast");
|
|
element.classList.remove("toast-error");
|
|
}
|
|
element.innerText = text;
|
|
}, {timeout: 2000})
|
|
}, 500);
|
|
}
|
|
|
|
}
|
|
return getLocaleModule(false);
|
|
}
|
|
|
|
const lastUploadedLocaleID = "last-uploaded-locale";
|
|
let lastUploadedLocale;
|
|
try{
|
|
lastUploadedLocale = JSON.parse(localStorage?.getItem(lastUploadedLocaleID));
|
|
}catch(error){
|
|
console.warn("Unable to load last uploaded locale", error);
|
|
}
|
|
if(lastUploadedLocale){
|
|
if(!lastUploadedLocale.lang){
|
|
lastUploadedLocale = undefined;
|
|
console.warn("Unable to load last uploaded locale, it is missing the lang entry");
|
|
}else if(lastUploadedLocale.custom){
|
|
// Make sure to add any missing data from the original lang
|
|
// We don't know if fx a new entry has been added after the locale was last saved
|
|
const originalLocale = structuredClone(locales[lastUploadedLocale.lang]);
|
|
lastUploadedLocale = {...originalLocale, ...lastUploadedLocale};
|
|
|
|
// Add a special entry for the custom locale, put it first in the list
|
|
locales = {[lastUploadedLocaleID]: lastUploadedLocale, ...locales};
|
|
}
|
|
}
|
|
|
|
var lang;
|
|
var locale;
|
|
var customizeLocale = false;
|
|
var languageSelector = document.getElementById("languages");
|
|
var customizeSelector = document.getElementById('customize');
|
|
languageSelector.innerHTML = Object.keys(locales).map(l=>{
|
|
var locale = locales[l];
|
|
var name = l === lastUploadedLocaleID ? `Custom locale based on ${locale.lang}` : locale.lang;
|
|
var localeParts = locale.lang.split("_"); // en_GB -> ["en","GB"]
|
|
var icon = "";
|
|
// If we have a 2 char ISO country code, use it to get the unicode flag
|
|
if (locale.icon)
|
|
icon = locale.icon+" ";
|
|
else if (localeParts[1] && localeParts[1].length==2)
|
|
icon = localeParts[1].toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0)+127397) )+" ";
|
|
return `<option value="${l}">${icon}${name}${locale.notes?" - "+locale.notes:""}</option>`
|
|
}).join("\n");
|
|
if(lastUploadedLocale){
|
|
if(lastUploadedLocale.custom){
|
|
// If the last uploaded locale was customized, choose the custom locale as default value
|
|
languageSelector.value = lastUploadedLocaleID;
|
|
}else{
|
|
// If the last uploaded locale was not customized, choose the existing locale in the list as the default value
|
|
languageSelector.value = lastUploadedLocale.lang;
|
|
}
|
|
}
|
|
languageSelector.addEventListener('change', handleLanguageChange);
|
|
function handleLanguageChange(){
|
|
lang = languageSelector.value;
|
|
locale = structuredClone(locales[lang]);
|
|
// If the locale is customized, make sure the customization option is activated. If not, disable it.
|
|
if(Boolean(customizeSelector.checked) !== Boolean(locale.custom)){
|
|
customizeSelector.checked = Boolean(locale.custom);
|
|
handleCustomizeChange();
|
|
}else{
|
|
createLocaleModule();
|
|
}
|
|
const warningsElement = document.getElementById("customize-warning")
|
|
warningsElement.innerText = "";
|
|
warningsElement.classList.remove("toast");
|
|
warningsElement.classList.remove("toast-error");
|
|
}
|
|
customizeSelector.addEventListener('change', handleCustomizeChange);
|
|
function handleCustomizeChange(){
|
|
customizeLocale = customizeSelector.checked;
|
|
// If the user no longer wants to customize, make sure to return to the default lang entry
|
|
if(!customizeLocale){
|
|
languageSelector.value = locales[lang].lang;
|
|
handleLanguageChange();
|
|
}else{
|
|
createLocaleModule();
|
|
}
|
|
}
|
|
// set initial values
|
|
handleLanguageChange();
|
|
|
|
|
|
|
|
document.getElementById("upload").addEventListener("click", function() {
|
|
|
|
var localeModule = createLocaleModule();
|
|
|
|
// Save the locale data to make it easier to upload the same locale next time.
|
|
// If the locale is not customized, only save the lang. The rest of the data will be added when the page loads next time.
|
|
const savedLocaleData = customizeLocale ? {...locale, custom: true} : {lang: locale.lang};
|
|
localStorage?.setItem(lastUploadedLocaleID, JSON.stringify(savedLocaleData));
|
|
|
|
console.log("Locale Module is:",localeModule);
|
|
sendCustomizedApp({
|
|
storage:[
|
|
{name:"locale", url:"locale.js", content:localeModule}
|
|
]
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|