2022-04-10 10:17:14 +00:00
#!/usr/bin/env node
2021-12-14 10:57:48 +00:00
/ * S c a n s f o r s t r i n g s t h a t m a y b e i n E n g l i s h i n e a c h a p p , a n d
outputs a list of strings that have been found .
2022-01-18 16:21:30 +00:00
See https : //github.com/espruino/BangleApps/issues/1311
2021-12-14 10:57:48 +00:00
* /
2022-04-10 14:04:31 +00:00
var childProcess = require ( 'child_process' ) ;
2022-04-10 10:17:14 +00:00
let refresh = false ;
function handleCliParameters ( )
{
let usage = "USAGE: language_scan.js [options]" ;
let die = function ( message ) {
console . log ( usage ) ;
console . log ( message ) ;
process . exit ( 3 ) ;
} ;
let hadTURL = false ,
hadDEEPL = false ;
for ( let i = 2 ; i < process . argv . length ; i ++ )
{
const param = process . argv [ i ] ;
switch ( param )
{
case '-r' :
case '--refresh' :
refresh = true ;
break ;
case '--deepl' :
i ++ ;
let KEY = process . argv [ i ] ;
if ( KEY === '' || KEY === null || KEY === undefined )
{
die ( '--deepl requires a parameter: the API key to use' ) ;
}
process . env . DEEPL = KEY ;
hadDEEPL = true ;
break ;
case '--turl' :
i ++ ;
let URL = process . argv [ i ] ;
if ( URL === '' || URL === null || URL === undefined )
{
die ( '--turl requires a parameter: the URL to use' ) ;
}
process . env . TURL = URL ;
hadTURL = true ;
break ;
case '-h' :
case '--help' :
console . log ( usage + "\n" ) ;
console . log ( "Parameters:" ) ;
console . log ( " -h, --help Output this help text and exit" ) ;
console . log ( " -r, --refresh Auto-add new strings into lang/*.json" ) ;
console . log ( ' --deepl KEY Enable DEEPL as auto-translation engine and' ) ;
console . log ( ' use KEY as its API key. You also need to provide --turl' ) ;
console . log ( ' --turl URL In combination with --deepl, use URL as the API base URL' ) ;
process . exit ( 0 ) ;
default :
2023-02-20 13:24:28 +00:00
die ( "Unknown parameter: " + param + ", use --help for options" ) ;
2022-04-10 10:17:14 +00:00
}
}
if ( ( hadTURL !== false || hadDEEPL !== false ) && hadTURL !== hadDEEPL )
{
die ( "Use of deepl requires both a --deepl API key and --turl URL" ) ;
}
}
handleCliParameters ( ) ;
2022-02-09 23:52:49 +00:00
let translate = false ;
if ( process . env . DEEPL ) {
2022-02-10 13:30:00 +00:00
// Requires translate
// npm i translate
2022-02-09 23:52:49 +00:00
translate = require ( "translate" ) ;
translate . engine = "deepl" ; // Or "yandex", "libre", "deepl"
2022-02-10 13:30:00 +00:00
translate . key = process . env . DEEPL ; // Requires API key (which are free)
translate . url = process . env . TURL ;
2022-02-09 23:52:49 +00:00
}
2022-01-18 16:21:30 +00:00
var IGNORE _STRINGS = [
2022-01-19 11:30:18 +00:00
"5x5" , "6x8" , "6x8:2" , "4x6" , "12x20" , "6x15" , "5x9Numeric7Seg" , "Vector" , // fonts
"---" , "..." , "*" , "##" , "00" , "GPS" , "ram" ,
"12hour" , "rising" , "falling" , "title" ,
"sortorder" , "tl" , "tr" ,
"function" , "object" , // typeof===
"txt" , // layout styles
2022-02-09 23:52:49 +00:00
"play" , "stop" , "pause" , "volumeup" , "volumedown" , // music state
"${hours}:${minutes}:${seconds}" , "${hours}:${minutes}" ,
"BANGLEJS" ,
2022-02-10 13:30:00 +00:00
"fgH" , "bgH" , "m/s" ,
"undefined" , "kbmedia" , "NONE" ,
2022-01-19 11:09:12 +00:00
] ;
var IGNORE _FUNCTION _PARAMS = [
"read" ,
"readJSON" ,
"require" ,
2022-01-19 11:30:18 +00:00
"setFont" , "setUI" , "setLCDMode" ,
2022-01-19 11:09:12 +00:00
"on" ,
2022-01-19 11:30:18 +00:00
"RegExp" , "sendCommand" ,
"print" , "log"
] ;
var IGNORE _ARRAY _ACCESS = [
"WIDGETS"
2022-01-18 16:21:30 +00:00
] ;
2021-12-14 10:57:48 +00:00
var BASEDIR = _ _dirname + "/../" ;
Espruino = require ( BASEDIR + "core/lib/espruinotools.js" ) ;
var fs = require ( "fs" ) ;
var APPSDIR = BASEDIR + "apps/" ;
2022-01-18 16:21:30 +00:00
2021-12-14 10:57:48 +00:00
function ERROR ( s ) {
console . error ( "ERROR: " + s ) ;
process . exit ( 1 ) ;
}
function WARN ( s ) {
console . log ( "Warning: " + s ) ;
}
2022-01-18 16:21:30 +00:00
function log ( s ) {
console . log ( s ) ;
}
2021-12-14 10:57:48 +00:00
2023-02-20 13:23:14 +00:00
var apploader = require ( "./lib/apploader.js" ) ;
apploader . init ( {
DEVICEID : "BANGLEJS2"
} ) ;
var apps = apploader . apps ;
2021-12-14 10:57:48 +00:00
// Given a string value, work out if it's obviously not a text string
2022-01-19 11:30:18 +00:00
function isNotString ( s , wasFnCall , wasArrayAccess ) {
if ( s == "" ) return true ;
2022-01-19 11:09:12 +00:00
// wasFnCall is set to the function name if 's' is the first argument to a function
if ( wasFnCall && IGNORE _FUNCTION _PARAMS . includes ( wasFnCall ) ) return true ;
2022-01-19 11:30:18 +00:00
if ( wasArrayAccess && IGNORE _ARRAY _ACCESS . includes ( wasArrayAccess ) ) return true ;
2022-01-19 11:09:12 +00:00
if ( s == "Storage" ) console . log ( "isNotString" , s , wasFnCall ) ;
2022-02-09 23:52:49 +00:00
if ( s . length < 3 ) return true ; // too short
2021-12-14 10:57:48 +00:00
if ( s . length > 40 ) return true ; // too long
if ( s [ 0 ] == "#" ) return true ; // a color
2022-02-09 23:52:49 +00:00
if ( s . endsWith ( '.log' ) || s . endsWith ( '.js' ) || s . endsWith ( ".info" ) || s . endsWith ( ".csv" ) || s . endsWith ( ".json" ) || s . endsWith ( ".img" ) || s . endsWith ( ".txt" ) ) return true ; // a filename
2021-12-14 10:57:48 +00:00
if ( s . endsWith ( "=" ) ) return true ; // probably base64
if ( s . startsWith ( "BTN" ) ) return true ; // button name
2022-01-18 16:21:30 +00:00
if ( IGNORE _STRINGS . includes ( s ) ) return true ; // one we know to ignore
2022-02-09 23:52:49 +00:00
if ( ! isNaN ( parseFloat ( s ) ) && isFinite ( s ) ) return true ; //is number
if ( s . match ( /^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/ ) ) return true ; //roman number
if ( ! s . match ( /.*[A-Z].*/i ) ) return true ; // No letters
if ( s . match ( /.*[0-9].*/i ) ) return true ; // No letters
if ( s . match ( /.*\(.*\).*/ ) ) return true ; // is function
if ( s . match ( /[A-Za-z]+[A-Z]([A-Z]|[a-z])*/ ) ) return true ; // is camel case
if ( s . includes ( '_' ) ) return true ;
2021-12-14 10:57:48 +00:00
return false ;
}
2022-01-19 11:09:12 +00:00
function getTextFromString ( s ) {
2022-01-19 11:30:18 +00:00
return s . replace ( /^([.<>\-\n ]*)([^<>\!\?]*?)([.<>\!\?\-\n ]*)$/ , "$2" ) ;
2022-01-19 11:09:12 +00:00
}
2022-01-18 16:21:30 +00:00
// A string that *could* be translated?
var untranslatedStrings = [ ] ;
// Strings that are marked with 'LANG'
var translatedStrings = [ ] ;
2021-12-14 10:57:48 +00:00
2022-01-19 11:09:12 +00:00
function addString ( list , str , file ) {
str = getTextFromString ( str ) ;
var entry = list . find ( e => e . str == str ) ;
if ( ! entry ) {
entry = { str : str , uses : 0 , files : [ ] } ;
list . push ( entry ) ;
}
entry . uses ++ ;
if ( ! entry . files . includes ( file ) )
entry . files . push ( file )
}
2022-01-18 16:21:30 +00:00
console . log ( "Scanning apps..." ) ;
2022-01-19 11:09:12 +00:00
//apps = apps.filter(a=>a.id=="wid_edit");
2021-12-14 10:57:48 +00:00
apps . forEach ( ( app , appIdx ) => {
var appDir = APPSDIR + app . id + "/" ;
app . storage . forEach ( ( file ) => {
if ( ! file . url || ! file . name . endsWith ( ".js" ) ) return ;
2022-01-19 11:09:12 +00:00
var filePath = appDir + file . url ;
2022-01-19 11:30:18 +00:00
var shortFilePath = "apps/" + app . id + "/" + file . url ;
2022-01-19 11:09:12 +00:00
var fileContents = fs . readFileSync ( filePath ) . toString ( ) ;
2021-12-14 10:57:48 +00:00
var lex = Espruino . Core . Utils . getLexer ( fileContents ) ;
2022-01-18 16:21:30 +00:00
var lastIdx = 0 ;
2022-01-19 11:30:18 +00:00
var wasFnCall = undefined ; // set to 'setFont' if we're at something like setFont(".."
var wasArrayAccess = undefined ; // set to 'WIDGETS' if we're at something like WIDGETS[".."
2021-12-14 10:57:48 +00:00
var tok = lex . next ( ) ;
while ( tok !== undefined ) {
2022-01-18 16:21:30 +00:00
var previousString = fileContents . substring ( lastIdx , tok . startIdx ) ;
2021-12-14 10:57:48 +00:00
if ( tok . type == "STRING" ) {
2022-01-18 16:21:30 +00:00
if ( previousString . includes ( "/*LANG*/" ) ) { // translated!
2022-01-19 11:30:18 +00:00
addString ( translatedStrings , tok . value , shortFilePath ) ;
2022-01-18 16:21:30 +00:00
} else { // untranslated - potential to translate?
2022-02-09 23:52:49 +00:00
// filter out numbers
2022-01-19 11:30:18 +00:00
if ( ! isNotString ( tok . value , wasFnCall , wasArrayAccess ) ) {
addString ( untranslatedStrings , tok . value , shortFilePath ) ;
2022-01-18 16:21:30 +00:00
}
2021-12-14 10:57:48 +00:00
}
2022-01-19 11:30:18 +00:00
} else {
if ( tok . value != "(" ) wasFnCall = undefined ;
if ( tok . value != "[" ) wasArrayAccess = undefined ;
}
//console.log(wasFnCall,tok.type,tok.value);
if ( tok . type == "ID" ) {
wasFnCall = tok . value ;
wasArrayAccess = tok . value ;
}
2022-01-18 16:21:30 +00:00
lastIdx = tok . endIdx ;
2021-12-14 10:57:48 +00:00
tok = lex . next ( ) ;
}
} ) ;
} ) ;
2022-01-19 11:09:12 +00:00
untranslatedStrings . sort ( ( a , b ) => a . uses - b . uses ) ;
translatedStrings . sort ( ( a , b ) => a . uses - b . uses ) ;
2022-01-19 11:30:18 +00:00
2022-01-18 16:21:30 +00:00
2022-02-09 23:52:49 +00:00
/ *
* @ description Add lang to start of string
* @ param str string to add LANG to
* @ param file file that string is found
* @ returns void
* /
2022-02-10 01:12:55 +00:00
//TODO fix settings bug
2022-02-09 23:52:49 +00:00
function applyLANG ( str , file ) {
fs . readFile ( file , 'utf8' , function ( err , data ) {
if ( err ) {
return console . log ( err ) ;
}
const regex = new RegExp ( ` (.*)((?<! \/ \* LANG \* \/ )["|'] ${ str } [^A-Z].*) ` , 'gi' ) ;
const result = data . replace ( regex , '$1/*LANG*/$2' ) ;
console . log ( str , file ) ;
fs . writeFile ( file , result , 'utf8' , function ( err ) {
if ( err ) return console . log ( err ) ;
} ) ;
} ) ;
}
2022-01-18 16:21:30 +00:00
var report = "" ;
2022-01-19 11:30:18 +00:00
log ( "Translated Strings that are not tagged with LANG" ) ;
log ( "=================================================================" ) ;
log ( "" ) ;
log ( "Maybe we should add /*LANG*/ to these automatically?" ) ;
log ( "" ) ;
2022-02-09 23:52:49 +00:00
const wordsToAdd = untranslatedStrings . filter ( e => translatedStrings . find ( t => t . str == e . str ) ) ;
2022-02-10 13:30:00 +00:00
2022-02-09 23:52:49 +00:00
// Uncomment to add LANG to all strings
2022-02-10 13:30:00 +00:00
// THIS IS EXPERIMENTAL
2022-02-09 23:52:49 +00:00
//wordsToAdd.forEach(e => e.files.forEach(a => applyLANG(e.str, a)));
2022-02-10 13:30:00 +00:00
2022-02-09 23:52:49 +00:00
log ( wordsToAdd . map ( e => ` ${ JSON . stringify ( e . str ) } ( ${ e . uses } uses) ` ) . join ( "\n" ) ) ;
2022-01-19 11:30:18 +00:00
log ( "" ) ;
2022-02-09 23:52:49 +00:00
2022-01-19 11:30:18 +00:00
//process.exit(1);
2022-01-18 16:21:30 +00:00
log ( "Possible English Strings that could be translated" ) ;
log ( "=================================================================" ) ;
log ( "" ) ;
2022-01-19 11:30:18 +00:00
log ( "Add these to IGNORE_STRINGS if they don't make sense..." ) ;
2022-01-18 16:21:30 +00:00
log ( "" ) ;
2022-01-19 11:30:18 +00:00
// ignore ones only used once or twice
log ( untranslatedStrings . filter ( e => e . uses > 2 ) . filter ( e => ! translatedStrings . find ( t => t . str == e . str ) ) . map ( e => ` ${ JSON . stringify ( e . str ) } ( ${ e . uses } uses) ` ) . join ( "\n" ) ) ;
2022-01-18 16:21:30 +00:00
log ( "" ) ;
2022-01-19 11:30:18 +00:00
//process.exit(1);
2022-01-18 16:21:30 +00:00
2022-02-07 21:28:02 +00:00
let languages = JSON . parse ( fs . readFileSync ( ` ${ BASEDIR } /lang/index.json ` ) . toString ( ) ) ;
2022-02-09 23:52:49 +00:00
for ( let language of languages ) {
2022-02-07 21:28:02 +00:00
if ( language . code == "en_GB" ) {
console . log ( ` Ignoring ${ language . code } ` ) ;
2022-02-09 23:52:49 +00:00
continue ;
2022-01-19 11:09:12 +00:00
}
2022-02-07 21:28:02 +00:00
console . log ( ` Scanning ${ language . code } ` ) ;
2022-01-18 16:21:30 +00:00
log ( language . code ) ;
log ( "==========" ) ;
2022-02-07 21:28:02 +00:00
let translations = JSON . parse ( fs . readFileSync ( ` ${ BASEDIR } /lang/ ${ language . url } ` ) . toString ( ) ) ;
2022-02-09 23:52:49 +00:00
let translationPromises = [ ] ;
2022-02-07 21:28:02 +00:00
translatedStrings . forEach ( translationItem => {
if ( ! translations . GLOBAL [ translationItem . str ] ) {
console . log ( ` Missing GLOBAL translation for ${ JSON . stringify ( translationItem ) } ` ) ;
translationItem . files . forEach ( file => {
let m = file . match ( /\/([a-zA-Z0-9_-]*)\//g ) ;
if ( m && m [ 0 ] ) {
let appName = m [ 0 ] . replaceAll ( "/" , "" ) ;
if ( translations [ appName ] && translations [ appName ] [ translationItem . str ] ) {
console . log ( ` but LOCAL translation found in \" ${ appName } \" ` ) ;
2022-02-10 13:30:00 +00:00
} else if ( translate && language . code !== "tr_TR" ) { // Auto Translate
2022-02-09 23:52:49 +00:00
translationPromises . push ( new Promise ( async ( resolve ) => {
const translation = await translate ( translationItem . str , language . code . split ( "_" ) [ 0 ] ) ;
console . log ( "Translating:" , translationItem . str , translation ) ;
translations . GLOBAL [ translationItem . str ] = translation ;
resolve ( )
} ) )
2022-04-10 10:17:14 +00:00
} else if ( refresh && ! translate ) {
2022-04-08 14:50:41 +00:00
translationPromises . push ( new Promise ( async ( resolve ) => {
translations . GLOBAL [ translationItem . str ] = translationItem . str ;
resolve ( )
} ) )
2022-02-07 21:28:02 +00:00
}
}
} ) ;
}
2022-01-18 16:21:30 +00:00
} ) ;
2022-02-09 23:52:49 +00:00
Promise . all ( translationPromises ) . then ( ( ) => {
fs . writeFileSync ( ` ${ BASEDIR } /lang/ ${ language . url } ` , JSON . stringify ( translations , null , 4 ) )
} ) ;
2022-01-18 16:21:30 +00:00
log ( "" ) ;
2022-02-09 23:52:49 +00:00
}
2022-01-18 16:21:30 +00:00
console . log ( "Done." ) ;