2020-11-17 02:22:48 +01:00
import * as fs from "fs"
2023-06-14 20:39:36 +02:00
import { existsSync , mkdirSync , readFileSync , writeFileSync } from "fs"
2023-07-15 18:04:30 +02:00
import { Utils } from "../src/Utils"
2021-05-19 16:15:12 +02:00
import ScriptUtils from "./ScriptUtils"
2023-10-30 20:06:31 +01:00
import Script from "./Script"
2021-05-19 20:47:41 +02:00
const knownLanguages = [ "en" , "nl" , "de" , "fr" , "es" , "gl" , "ca" ]
2024-02-22 10:14:01 +01:00
const ignoreTerms = [ "searchTerms" ]
2024-08-29 21:49:11 +02:00
2021-05-19 16:15:12 +02:00
class TranslationPart {
contents : Map < string , TranslationPart | string > = new Map < string , TranslationPart | string > ( )
2022-07-02 01:59:26 +02:00
static fromDirectory ( path ) : TranslationPart {
const files = ScriptUtils . readDirRecSync ( path , 1 ) . filter ( ( file ) = > file . endsWith ( ".json" ) )
const rootTranslation = new TranslationPart ( )
for ( const file of files ) {
2023-06-14 20:39:36 +02:00
const content = JSON . parse ( readFileSync ( file , { encoding : "utf8" } ) )
2024-08-29 21:49:11 +02:00
const language = file . substr ( 0 , file . length - ".json" . length )
rootTranslation . addTranslation ( language , content )
2022-07-02 01:59:26 +02:00
}
return rootTranslation
}
2022-02-16 03:22:16 +01:00
/ * *
* Add a leaf object
* @param language
* @param obj
* /
2021-05-19 20:47:41 +02:00
add ( language : string , obj : any ) {
2021-05-19 16:15:12 +02:00
for ( const key in obj ) {
const v = obj [ key ]
2021-05-19 20:47:41 +02:00
if ( ! this . contents . has ( key ) ) {
2021-05-19 16:15:12 +02:00
this . contents . set ( key , new TranslationPart ( ) )
}
const subpart = this . contents . get ( key ) as TranslationPart
2021-05-19 20:47:41 +02:00
if ( typeof v === "string" ) {
2021-05-19 16:15:12 +02:00
subpart . contents . set ( language , v )
2021-05-19 20:47:41 +02:00
} else {
2021-05-19 16:15:12 +02:00
subpart . add ( language , v )
}
}
}
2021-05-19 20:47:41 +02:00
addTranslationObject ( translations : any , context? : string ) {
2022-07-02 01:59:26 +02:00
if ( translations [ "#" ] === "no-translations" ) {
console . log ( "Ignoring object at " , context , "which has '#':'no-translations'" )
2022-06-24 16:49:03 +02:00
return
}
2021-05-19 20:47:41 +02:00
for ( const translationsKey in translations ) {
const v = translations [ translationsKey ]
2024-10-19 14:44:55 +02:00
if ( Array . isArray ( v ) && context . endsWith ( "keywords" ) ) {
2024-10-08 19:02:19 +02:00
continue
}
2021-05-19 20:47:41 +02:00
if ( typeof v != "string" ) {
2022-08-22 22:31:05 +02:00
console . error (
` Non-string object at ${ context } in translation while trying to add the translation ` +
2024-09-02 12:48:15 +02:00
JSON . stringify ( v ) +
` to ' ` +
translationsKey +
"'. The offending object which _should_ be a translation is: " ,
2022-08-22 22:31:05 +02:00
v ,
"\n\nThe current object is (only showing en):" ,
this . toJson ( ) ,
"and has translations for" ,
Array . from ( this . contents . keys ( ) )
2022-09-08 21:40:48 +02:00
)
2021-05-19 20:47:41 +02:00
throw (
"Error in an object depicting a translation: a non-string object was found. (" +
context +
")\n You probably put some other section accidentally in the translation"
2022-09-08 21:40:48 +02:00
)
2021-05-19 20:47:41 +02:00
}
this . contents . set ( translationsKey , v )
}
}
2021-05-20 00:10:38 +02:00
2021-07-29 01:57:45 +02:00
recursiveAdd ( object : any , context : string ) {
2022-08-22 22:31:05 +02:00
const isProbablyTranslationObject = knownLanguages . some ( ( l ) = > object . hasOwnProperty ( l ) ) // TODO FIXME ID
2021-05-19 20:47:41 +02:00
if ( isProbablyTranslationObject ) {
2021-07-29 01:57:45 +02:00
this . addTranslationObject ( object , context )
2021-05-19 20:47:41 +02:00
return
}
2022-07-02 01:59:26 +02:00
let dontTranslateKeys : string [ ] = undefined
{
const noTranslate = < string | string [ ] > object [ "#dont-translate" ]
if ( noTranslate === "*" ) {
console . log ( "Ignoring translations for " + context )
return
} else if ( typeof noTranslate === "string" ) {
dontTranslateKeys = [ noTranslate ]
} else {
dontTranslateKeys = noTranslate
}
if ( noTranslate !== undefined ) {
console . log (
"Ignoring some translations for " +
2024-09-02 12:48:15 +02:00
context +
": " +
dontTranslateKeys . join ( ", " )
2022-09-08 21:40:48 +02:00
)
2022-07-02 01:59:26 +02:00
}
}
2021-09-26 20:15:25 +02:00
for ( let key in object ) {
2024-02-22 10:14:01 +01:00
if ( ignoreTerms . indexOf ( key ) >= 0 ) {
continue
}
2021-05-19 20:47:41 +02:00
2022-07-02 01:59:26 +02:00
if ( dontTranslateKeys ? . indexOf ( key ) >= 0 ) {
continue
}
2021-05-19 20:47:41 +02:00
const v = object [ key ]
2021-09-26 20:32:28 +02:00
2021-05-19 20:47:41 +02:00
if ( v == null ) {
continue
}
2022-08-05 13:11:50 +02:00
if ( typeof v !== "object" ) {
continue
}
2021-05-19 20:47:41 +02:00
2022-08-05 13:11:50 +02:00
if ( context . endsWith ( ".tagRenderings" ) ) {
if ( v [ "id" ] === undefined ) {
if ( v [ "builtin" ] !== undefined && typeof v [ "builtin" ] === "string" ) {
key = v [ "builtin" ]
} else {
throw (
"At " +
context +
": every object within a tagRenderings-list should have an id. " +
JSON . stringify ( v ) +
" has no id"
2022-09-08 21:40:48 +02:00
)
2022-08-05 13:11:50 +02:00
}
2021-09-26 20:32:28 +02:00
} else {
2022-08-05 13:11:50 +02:00
// We use the embedded id as key instead of the index as this is more stable
// Note: indonesian is shortened as 'id' as well!
if ( v [ "en" ] !== undefined || v [ "nl" ] !== undefined ) {
// This is probably a translation already!
// pass
} else {
key = v [ "id" ]
if ( typeof key !== "string" ) {
throw "Panic: found a non-string ID at" + context
}
2021-09-26 20:32:28 +02:00
}
}
}
2021-05-19 20:47:41 +02:00
if ( ! this . contents . get ( key ) ) {
this . contents . set ( key , new TranslationPart ( ) )
}
2024-09-02 12:48:15 +02:00
; ( this . contents . get ( key ) as TranslationPart ) . recursiveAdd ( v , context + "." + key )
2021-05-19 20:47:41 +02:00
}
}
knownLanguages ( ) : string [ ] {
const languages = [ ]
2024-08-29 21:49:11 +02:00
for ( const key of Array . from ( this . contents . keys ( ) ) ) {
2021-05-19 16:15:12 +02:00
const value = this . contents . get ( key )
2021-05-19 20:47:41 +02:00
if ( typeof value === "string" ) {
2021-05-20 00:10:38 +02:00
if ( key === "#" ) {
continue
}
2021-05-19 20:47:41 +02:00
languages . push ( key )
} else {
languages . push ( . . . ( value as TranslationPart ) . knownLanguages ( ) )
}
}
return Utils . Dedup ( languages )
}
toJson ( neededLanguage? : string ) : string {
const parts = [ ]
2021-09-14 18:20:25 +02:00
let keys = Array . from ( this . contents . keys ( ) )
keys = keys . sort ( )
2024-08-29 21:49:11 +02:00
for ( const key of keys ) {
2021-05-19 20:47:41 +02:00
let value = this . contents . get ( key )
if ( typeof value === "string" ) {
2024-09-02 12:48:15 +02:00
value = value . replace ( /"/g , '\\"' ) . replace ( /\n/g , "\\n" )
2021-05-19 22:38:05 +02:00
if ( neededLanguage === undefined ) {
2024-08-29 21:49:11 +02:00
parts . push ( ` " ${ key } ": " ${ value } " ` )
2021-05-19 22:38:05 +02:00
} else if ( key === neededLanguage ) {
return ` " ${ value } " `
2021-05-19 20:47:41 +02:00
}
} else {
const sub = ( value as TranslationPart ) . toJson ( neededLanguage )
if ( sub !== "" ) {
2024-08-29 21:49:11 +02:00
parts . push ( ` " ${ key } ": ${ sub } ` )
2021-05-19 20:47:41 +02:00
}
2021-05-19 16:15:12 +02:00
}
}
2021-05-19 20:47:41 +02:00
if ( parts . length === 0 ) {
return ""
}
return ` { ${ parts . join ( "," ) } } `
2021-05-19 16:15:12 +02:00
}
2022-02-16 03:22:16 +01:00
2022-07-02 01:59:26 +02:00
validateStrict ( ctx? : string ) : void {
const errors = this . validate ( )
2022-02-16 03:22:16 +01:00
for ( const err of errors ) {
2022-07-02 01:59:26 +02:00
console . error (
"ERROR in " + ( ctx ? ? "" ) + " " + err . path . join ( "." ) + "\n " + err . error
)
2022-02-16 03:22:16 +01:00
}
2022-07-02 01:59:26 +02:00
if ( errors . length > 0 ) {
throw ctx + " has " + errors . length + " inconsistencies in the translation"
2022-02-16 03:22:16 +01:00
}
}
2022-07-02 01:59:26 +02:00
2022-02-16 03:22:16 +01:00
/ * *
* Checks the leaf objects : special values must be present and identical in every leaf
* /
2022-07-02 01:59:26 +02:00
validate ( path = [ ] ) : { error : string ; path : string [ ] } [ ] {
const errors : { error : string ; path : string [ ] } [ ] = [ ]
2022-04-27 11:35:47 +02:00
const neededSubparts = new Set < { part : string ; usedByLanguage : string } > ( )
2022-07-02 01:59:26 +02:00
let isLeaf : boolean = undefined
2022-02-16 03:22:16 +01:00
this . contents . forEach ( ( value , key ) = > {
2022-04-09 19:52:25 +02:00
if ( typeof value !== "string" ) {
const recErrors = value . validate ( [ . . . path , key ] )
errors . push ( . . . recErrors )
return
}
if ( isLeaf === undefined ) {
isLeaf = true
} else if ( ! isLeaf ) {
errors . push ( {
error : "Mixed node: non-leaf node has translation strings" ,
2024-09-02 12:48:15 +02:00
path : path ,
2022-04-09 19:52:25 +02:00
} )
}
let subparts : string [ ] = value . match ( /{[^}]*}/g )
if ( subparts !== null ) {
2022-04-27 11:35:47 +02:00
let [ _ , __ , weblatepart , lang ] = key . split ( "/" )
if ( lang === undefined ) {
// This is a core translation, it has one less path segment
lang = weblatepart
}
2022-04-09 19:52:25 +02:00
subparts = subparts . map ( ( p ) = > p . split ( /\(.*\)/ ) [ 0 ] )
for ( const subpart of subparts ) {
2023-06-14 20:39:36 +02:00
neededSubparts . add ( { part : subpart , usedByLanguage : lang } )
2022-04-09 19:52:25 +02:00
}
}
} )
// Actually check for the needed sub-parts, e.g. that {key} isn't translated into {sleutel}
this . contents . forEach ( ( value , key ) = > {
2023-06-14 20:39:36 +02:00
neededSubparts . forEach ( ( { part , usedByLanguage } ) = > {
2022-04-09 19:52:25 +02:00
if ( typeof value !== "string" ) {
return
2022-02-16 03:22:16 +01:00
}
2022-05-20 12:38:43 +02:00
let [ _ , __ , weblatepart , lang ] = key . split ( "/" )
if ( lang === undefined ) {
// This is a core translation, it has one less path segment
lang = weblatepart
weblatepart = "core"
}
2022-07-02 01:59:26 +02:00
const fixLink = ` Fix it on https://hosted.weblate.org/translate/mapcomplete/ ${ weblatepart } / ${ lang } /?offset=1&q=context%3A%3D%22 ${ encodeURIComponent (
path . join ( "." )
) } % 22 `
2022-02-16 03:22:16 +01:00
let subparts : string [ ] = value . match ( /{[^}]*}/g )
2022-04-09 19:52:25 +02:00
if ( subparts === null ) {
if ( neededSubparts . size > 0 ) {
errors . push ( {
2022-07-02 01:59:26 +02:00
error :
"The translation for " +
key +
" does not have any subparts, but expected " +
Array . from ( neededSubparts )
. map (
( part ) = >
part . part + " (used in " + part . usedByLanguage + ")"
2022-09-08 21:40:48 +02:00
)
2022-07-02 01:59:26 +02:00
. join ( "," ) +
" . The full translation is " +
value +
"\n" +
fixLink ,
2024-09-02 12:48:15 +02:00
path : path ,
2022-04-09 19:52:25 +02:00
} )
2022-02-16 03:22:16 +01:00
}
return
}
subparts = subparts . map ( ( p ) = > p . split ( /\(.*\)/ ) [ 0 ] )
2022-04-09 19:52:25 +02:00
if ( subparts . indexOf ( part ) < 0 ) {
2022-07-02 01:59:26 +02:00
if ( lang === "en" || usedByLanguage === "en" ) {
2022-04-27 11:35:47 +02:00
errors . push ( {
2022-07-08 03:14:55 +02:00
error : ` The translation for ${ key } does not have the required subpart ${ part } (in ${ usedByLanguage } ).
2022-04-27 11:35:47 +02:00
\ tThe full translation is $ { value }
2022-05-20 12:38:43 +02:00
\ t $ { fixLink } ` ,
2024-09-02 12:48:15 +02:00
path : path ,
2022-04-27 11:35:47 +02:00
} )
}
2022-02-16 03:22:16 +01:00
}
2022-04-09 19:52:25 +02:00
} )
2022-02-16 03:22:16 +01:00
} )
2022-04-09 19:52:25 +02:00
2022-07-02 01:59:26 +02:00
return errors
2022-02-16 03:22:16 +01:00
}
2022-07-02 01:59:26 +02:00
/ * *
* Recursively adds a translation object , the inverse of 'toJson'
* @param language
* @param object
* @private
* /
private addTranslation ( language : string , object : any ) {
for ( const key in object ) {
const v = object [ key ]
2022-08-24 04:05:49 +02:00
if ( v === "" ) {
delete object [ key ]
continue
}
2022-07-02 01:59:26 +02:00
let subpart = < TranslationPart > this . contents . get ( key )
if ( subpart === undefined ) {
subpart = new TranslationPart ( )
this . contents . set ( key , subpart )
}
if ( typeof v === "string" ) {
subpart . contents . set ( language , v )
} else {
subpart . addTranslation ( language , v )
}
}
}
2021-05-19 16:15:12 +02:00
}
2021-09-26 20:15:25 +02:00
/ * *
2022-04-01 12:51:55 +02:00
* Converts a translation object into something that can be added to the 'generated translations' .
2022-07-02 01:59:26 +02:00
*
2022-04-01 12:51:55 +02:00
* To debug the 'compiledTranslations' , add a languageWhiteList to only generate a single language
2021-09-26 20:15:25 +02:00
* /
2022-07-02 01:59:26 +02:00
function transformTranslation (
obj : any ,
path : string [ ] = [ ] ,
2024-08-29 21:49:11 +02:00
languageWhitelist : string [ ] = undefined ,
shortNotation = false
2022-07-02 01:59:26 +02:00
) {
2024-08-29 21:49:11 +02:00
if ( GenerateTranslations . isTranslation ( obj ) ) {
2020-11-17 02:22:48 +01:00
return ` new Translation( ${ JSON . stringify ( obj ) } ) `
}
2024-06-11 02:59:23 +02:00
const values : string [ ] = [ ]
2023-01-11 01:47:23 +01:00
const spaces = Utils . Times ( ( _ ) = > " " , path . length + 1 )
2020-11-17 02:22:48 +01:00
for ( const key in obj ) {
2021-05-19 20:47:41 +02:00
if ( key === "#" ) {
2021-01-18 02:51:42 +01:00
continue
}
2022-04-01 12:51:55 +02:00
2021-05-19 20:47:41 +02:00
if ( key . match ( "^[a-zA-Z0-9_]*$" ) === null ) {
throw "Invalid character in key: " + key
2021-01-18 02:51:42 +01:00
}
2022-04-01 12:51:55 +02:00
let value = obj [ key ]
2021-10-25 21:50:38 +02:00
2024-08-29 21:49:11 +02:00
if ( GenerateTranslations . isTranslation ( value ) ) {
2022-07-02 01:59:26 +02:00
if ( languageWhitelist !== undefined ) {
2022-04-01 12:51:55 +02:00
const nv = { }
for ( const ln of languageWhitelist ) {
nv [ ln ] = value [ ln ]
}
value = nv
}
2022-04-13 01:19:28 +02:00
2022-07-02 01:59:26 +02:00
if ( value [ "en" ] === undefined ) {
2022-04-18 01:06:22 +02:00
throw ` At ${ path . join ( "." ) } : Missing 'en' translation at path ${ path . join (
"."
) } . $ { key } \ n \ tThe translations in other languages are $ { JSON . stringify ( value ) } `
2022-04-13 01:19:28 +02:00
}
2022-07-02 01:59:26 +02:00
const subParts : string [ ] = value [ "en" ] . match ( /{[^}]*}/g )
2024-09-02 12:48:15 +02:00
let expr = ` new Translation( ${ JSON . stringify ( value ) } , "core: ${ path . join ( "." ) } . ${ key } ") `
2022-07-02 01:59:26 +02:00
if ( subParts !== null ) {
2022-04-13 01:19:28 +02:00
// convert '{to_substitute}' into 'to_substitute'
2022-07-02 01:59:26 +02:00
const types = Utils . Dedup ( subParts . map ( ( tp ) = > tp . substring ( 1 , tp . length - 1 ) ) )
2022-06-05 03:41:53 +02:00
const invalid = types . filter (
( part ) = > part . match ( /^[a-z0-9A-Z_]+(\(.*\))?$/ ) == null
2022-09-08 21:40:48 +02:00
)
2022-07-02 01:59:26 +02:00
if ( invalid . length > 0 ) {
2022-06-05 03:41:53 +02:00
throw ` At ${ path . join (
"."
) } : A subpart contains invalid characters : $ { subParts . join ( ", " ) } `
}
2024-08-29 21:49:11 +02:00
expr = ` new TypedTranslation<{ ${ types . join ( ", " ) } }>( ${ JSON . stringify (
2022-04-13 01:19:28 +02:00
value
) } , "core:${path.join(" . ")}.${key}" ) `
}
2024-08-29 21:49:11 +02:00
if ( shortNotation ) {
values . push ( ` ${ spaces } ${ key } : ${ expr } ` )
} else {
values . push ( ` ${ spaces } get ${ key } () { return ${ expr } } ` )
}
2021-10-25 21:50:38 +02:00
} else {
2023-01-11 01:47:23 +01:00
values . push (
spaces + key + ": " + transformTranslation ( value , [ . . . path , key ] , languageWhitelist )
)
2021-10-25 21:50:38 +02:00
}
2020-11-17 02:22:48 +01:00
}
2023-01-11 01:47:23 +01:00
return ` { ${ values . join ( ",\n" ) } } `
2020-11-17 02:22:48 +01:00
}
2024-06-11 02:59:23 +02:00
/ * *
*
2024-08-25 00:41:58 +02:00
* const result = stringifySorted ( { "b" : 43 , "a" : 42 } )
* result // => '{"a": 42,"b": 43}'
*
* // Should have correct newlines
* const result = stringifySorted ( { "b" : { "x" : "y" } , "a" : 42 } , " " )
* result // => '{\n "a": 42,\n "b": {\n "x": "y"\n }\n}'
*
* // Should sort like weblate does
* const result = stringifySorted ( { "1" : "abc" , "2" : "def" , "9" : "ghi" , "10" : "xyz" , "11" : "uvw" } )
2024-08-25 10:34:55 +02:00
* result // => '{"1": "abc","10": "xyz","11": "uvw","2": "def","9": "ghi"}'
2024-06-11 02:59:23 +02:00
* /
2024-08-25 00:41:58 +02:00
function stringifySorted ( o : object , space : string = undefined , depth = 0 ) : string {
2022-02-18 03:51:52 +01:00
const keys = Object . keys ( o )
2024-08-25 00:41:58 +02:00
let obj = "{"
2024-09-02 12:48:15 +02:00
obj += keys
. sort ( )
. map ( ( key ) = > {
const v = o [ key ]
let r = ""
if ( space !== undefined ) {
r += "\n"
for ( let i = 0 ; i <= depth ; i ++ ) {
r += space
}
2024-08-25 00:41:58 +02:00
}
2024-09-02 12:48:15 +02:00
r += JSON . stringify ( "" + key ) + ": "
if ( typeof v === "object" ) {
r += stringifySorted ( v , space , depth + 1 )
} else if ( Array . isArray ( v ) ) {
r += "[" + v . map ( ( v_ ) = > stringifySorted ( v_ , space , depth + 1 ) ) . join ( "," ) + "]"
} else {
r += JSON . stringify ( v )
}
return r
} )
. join ( "," )
2024-08-25 00:41:58 +02:00
if ( space !== undefined ) {
obj += "\n"
for ( let i = 0 ; i < depth ; i ++ ) {
obj += space
2022-02-18 03:51:52 +01:00
}
}
2024-08-25 00:41:58 +02:00
obj += "}"
return obj
2022-02-18 03:51:52 +01:00
}
2022-08-24 04:05:49 +02:00
function removeEmptyString ( object : object ) {
for ( const k in object ) {
if ( object [ k ] === "" ) {
delete object [ k ]
continue
}
if ( typeof object [ k ] === "object" ) {
removeEmptyString ( object [ k ] )
}
}
return object
}
2023-05-03 00:57:15 +02:00
2022-02-16 03:22:16 +01:00
/ * *
* Formats the specified file , helps to prevent merge conflicts
* * /
2022-02-14 20:09:17 +01:00
function formatFile ( path ) {
2022-03-31 02:58:31 +02:00
const original = readFileSync ( path , "utf8" )
let contents = JSON . parse ( original )
2022-08-24 04:05:49 +02:00
contents = removeEmptyString ( contents )
2024-08-25 00:41:58 +02:00
contents = stringifySorted ( contents , " " )
writeFileSync ( path , contents )
2022-02-14 20:09:17 +01:00
}
2021-09-26 20:15:25 +02:00
/ * *
* Reads 'lang/*.json' , writes them into to 'assets/generated/translations.json' .
* This is only for the core translations
* /
2021-05-19 16:15:12 +02:00
2021-09-26 20:15:25 +02:00
/ * *
* Get all the strings out of the layers ; writes them onto the weblate paths
* @param objects
* @param target
* /
2022-01-29 02:45:59 +01:00
function generateTranslationsObjectFrom (
objects : { path : string ; parsed : { id : string } } [ ] ,
target : string
) : string [ ] {
2021-05-19 20:47:41 +02:00
const tr = new TranslationPart ( )
2021-05-19 23:31:00 +02:00
for ( const layerFile of objects ) {
2021-05-19 23:40:55 +02:00
const config : { id : string } = layerFile . parsed
const layerTr = new TranslationPart ( )
if ( config === undefined ) {
throw "Got something not parsed! Path is " + layerFile . path
}
2021-07-29 01:57:45 +02:00
layerTr . recursiveAdd ( config , layerFile . path )
2021-05-19 20:47:41 +02:00
tr . contents . set ( config . id , layerTr )
}
const langs = tr . knownLanguages ( )
for ( const lang of langs ) {
2021-06-24 01:56:10 +02:00
if ( lang === "#" || lang === "*" ) {
// Lets not export our comments or non-translated stuff
2021-05-19 23:40:55 +02:00
continue
}
2021-05-19 20:47:41 +02:00
let json = tr . toJson ( lang )
2021-05-19 22:38:05 +02:00
try {
2021-11-16 03:05:19 +01:00
json = JSON . stringify ( JSON . parse ( json ) , null , " " ) // MUST BE FOUR SPACES
2021-05-19 22:38:05 +02:00
} catch ( e ) {
2021-05-19 20:47:41 +02:00
console . error ( e )
}
2021-05-19 22:38:05 +02:00
2021-05-19 23:31:00 +02:00
writeFileSync ( ` langs/ ${ target } / ${ lang } .json ` , json )
2021-05-19 20:47:41 +02:00
}
2022-01-29 02:45:59 +01:00
return langs
2021-05-19 20:47:41 +02:00
}
2021-09-26 20:58:10 +02:00
/ * *
* Merge two objects together
2022-04-09 19:52:25 +02:00
* @param source : where the translations come from
2021-09-26 20:58:10 +02:00
* @param target : the object in which the translations should be merged
* @param language : the language code
* @param context : context for error handling
* @constructor
* /
2021-05-19 22:38:05 +02:00
function MergeTranslation ( source : any , target : any , language : string , context : string = "" ) {
2021-09-26 20:58:10 +02:00
let keyRemapping : Map < string , string > = undefined
if ( context . endsWith ( ".tagRenderings" ) ) {
keyRemapping = new Map < string , string > ( )
for ( const key in target ) {
2023-07-28 14:34:40 +02:00
keyRemapping . set ( target [ key ] . id ? ? target [ key ] . builtin , key )
2021-09-26 20:58:10 +02:00
}
}
2021-05-19 22:38:05 +02:00
for ( const key in source ) {
const sourceV = source [ key ]
2021-09-26 20:58:10 +02:00
const targetV = target [ keyRemapping ? . get ( key ) ? ? key ]
2021-05-19 22:38:05 +02:00
if ( typeof sourceV === "string" ) {
2021-09-26 20:58:10 +02:00
// Add the translation
2021-09-09 00:05:51 +02:00
if ( targetV === undefined ) {
if ( typeof target === "string" ) {
2024-06-16 16:06:26 +02:00
throw ` Trying to merge a translation for ${ language } into a fixed string at ${ context } for key ${ key } `
2021-07-18 18:02:17 +02:00
}
2021-06-21 00:02:45 +02:00
target [ key ] = source [ key ]
continue
}
2021-09-09 00:05:51 +02:00
2021-05-19 22:38:05 +02:00
if ( targetV [ language ] === sourceV ) {
// Already the same
continue
}
2021-05-19 23:40:55 +02:00
2022-10-27 01:50:01 +02:00
if ( sourceV === "" ) {
2022-09-18 20:28:41 +02:00
console . log ( "Ignoring empty string in the translations" )
}
2021-05-19 23:40:55 +02:00
if ( typeof targetV === "string" ) {
2021-09-04 18:59:51 +02:00
throw ` At context ${ context } : Could not add a translation in language ${ language } . The target object has a string at the given path, whereas the translation contains an object. \ n String at target: ${ targetV } \ n Object at translation source: ${ JSON . stringify (
sourceV
) } `
2021-05-19 22:38:05 +02:00
}
targetV [ language ] = sourceV
2021-05-20 00:10:38 +02:00
let was = ""
2021-06-08 19:08:19 +02:00
if ( targetV [ language ] !== undefined && targetV [ language ] !== sourceV ) {
was = " (overwritten " + targetV [ language ] + ")"
2021-05-20 00:10:38 +02:00
}
2024-08-29 21:49:11 +02:00
// console.log(" + ", context + "." + language, "-->", sourceV, was)
2021-05-19 22:38:05 +02:00
continue
}
if ( typeof sourceV === "object" ) {
if ( targetV === undefined ) {
2021-10-25 21:50:38 +02:00
try {
target [ language ] = sourceV
} catch ( e ) {
2021-09-22 16:31:50 +02:00
throw ` At context ${ context } : Could not add a translation in language ${ language } due to ${ e } `
}
2021-05-19 22:38:05 +02:00
} else {
MergeTranslation ( sourceV , targetV , language , context + "." + key )
}
continue
}
throw "Case fallthrough"
}
return target
}
2021-05-20 00:10:38 +02:00
function mergeLayerTranslation (
layerConfig : { id : string } ,
path : string ,
translationFiles : Map < string , any >
) {
2021-05-19 22:38:05 +02:00
const id = layerConfig . id
translationFiles . forEach ( ( translations , lang ) = > {
const translationsForLayer = translations [ id ]
2021-09-09 00:05:51 +02:00
MergeTranslation ( translationsForLayer , layerConfig , lang , path + ":" + id )
2021-05-19 22:38:05 +02:00
} )
2021-05-20 00:10:38 +02:00
}
2021-05-19 22:38:05 +02:00
2021-05-20 00:10:38 +02:00
function loadTranslationFilesFrom ( target : string ) : Map < string , any > {
const translationFilePaths = ScriptUtils . readDirRecSync ( "./langs/" + target ) . filter ( ( path ) = >
2021-05-19 22:38:05 +02:00
path . endsWith ( ".json" )
2022-09-08 21:40:48 +02:00
)
2021-05-19 22:38:05 +02:00
const translationFiles = new Map < string , any > ( )
for ( const translationFilePath of translationFilePaths ) {
let language = translationFilePath . substr ( translationFilePath . lastIndexOf ( "/" ) + 1 )
language = language . substr ( 0 , language . length - 5 )
2021-09-26 20:32:28 +02:00
try {
2021-09-22 16:26:34 +02:00
translationFiles . set ( language , JSON . parse ( readFileSync ( translationFilePath , "utf8" ) ) )
2021-09-26 20:32:28 +02:00
} catch ( e ) {
2021-09-22 16:26:34 +02:00
console . error ( "Invalid JSON file or file does not exist" , translationFilePath )
throw e
}
2021-05-19 22:38:05 +02:00
}
2021-05-20 00:10:38 +02:00
return translationFiles
}
/ * *
2021-05-31 12:51:29 +02:00
* Load the translations from the weblate files back into the layers
2021-05-20 00:10:38 +02:00
* /
2023-10-30 20:06:31 +01:00
function mergeLayerTranslations ( englishOnly : boolean = false ) {
2021-05-19 23:31:00 +02:00
const layerFiles = ScriptUtils . getLayerFiles ( )
2021-05-19 22:38:05 +02:00
for ( const layerFile of layerFiles ) {
2021-05-20 00:10:38 +02:00
mergeLayerTranslation ( layerFile . parsed , layerFile . path , loadTranslationFilesFrom ( "layers" ) )
2023-06-14 20:39:36 +02:00
const endsWithNewline =
readFileSync ( layerFile . path , { encoding : "utf8" } ) ? . endsWith ( "\n" ) ? ? true
2023-10-30 20:06:31 +01:00
let config = layerFile . parsed
if ( englishOnly ) {
config = Utils . Clone ( config )
removeNonEnglishTranslations ( config )
}
2023-06-14 20:39:36 +02:00
writeFileSync (
layerFile . path ,
2023-10-30 20:06:31 +01:00
JSON . stringify ( config , null , " " ) + ( endsWithNewline ? "\n" : "" )
2023-06-14 20:39:36 +02:00
) // layers use 2 spaces
2021-05-19 22:38:05 +02:00
}
}
2021-05-19 23:40:55 +02:00
2023-10-30 20:06:31 +01:00
function removeNonEnglishTranslations ( object : any ) {
Utils . WalkObject (
object ,
( leaf : any ) = > {
const en = leaf [ "en" ]
if ( ! en ) {
return
}
for ( const key in leaf ) {
if ( key . startsWith ( "#" ) ) {
continue
}
delete leaf [ key ]
}
leaf [ "en" ] = en
} ,
( possibleLeaf ) = >
2024-09-02 12:48:15 +02:00
possibleLeaf !== null &&
typeof possibleLeaf === "object" &&
GenerateTranslations . isTranslation ( possibleLeaf )
2023-10-30 20:06:31 +01:00
)
}
2021-09-26 20:58:10 +02:00
/ * *
* Load the translations into the theme files
* /
2023-10-30 20:06:31 +01:00
function mergeThemeTranslations ( englishOnly : boolean = false ) {
2024-09-15 00:57:38 +02:00
const themeFiles = ScriptUtils . getThemeFiles ( true )
2021-05-20 00:10:38 +02:00
for ( const themeFile of themeFiles ) {
2023-10-30 20:06:31 +01:00
let config = themeFile . parsed
2021-05-20 00:10:38 +02:00
mergeLayerTranslation ( config , themeFile . path , loadTranslationFilesFrom ( "themes" ) )
const allTranslations = new TranslationPart ( )
2021-07-29 01:57:45 +02:00
allTranslations . recursiveAdd ( config , themeFile . path )
2023-06-14 20:39:36 +02:00
const endsWithNewline =
readFileSync ( themeFile . path , { encoding : "utf8" } ) ? . endsWith ( "\n" ) ? ? true
2023-10-30 20:06:31 +01:00
if ( englishOnly ) {
config = Utils . Clone ( config )
removeNonEnglishTranslations ( config )
}
2023-06-14 20:39:36 +02:00
writeFileSync (
themeFile . path ,
JSON . stringify ( config , null , " " ) + ( endsWithNewline ? "\n" : "" )
) // Themefiles use 2 spaces
2021-05-20 00:10:38 +02:00
}
}
2023-10-30 20:06:31 +01:00
class GenerateTranslations extends Script {
constructor ( ) {
super ( "Syncs translations from/to the theme and layer files" )
}
2021-05-19 22:40:25 +02:00
2024-08-29 21:49:11 +02:00
/ * *
* Checks that the given object only contains string - values
* @param tr
* /
static isTranslation ( tr : Record < string , string | object > ) : boolean {
if ( tr [ "#" ] === "no-translations" ) {
return false
}
if ( tr [ "special" ] ) {
return false
}
for ( const key in tr ) {
if ( typeof tr [ key ] !== "string" ) {
return false
}
}
return true
}
2023-10-30 20:06:31 +01:00
/ * *
* OUtputs the 'used_languages.json' - file
* /
detectUsedLanguages() {
{
const l1 = generateTranslationsObjectFrom ( ScriptUtils . getLayerFiles ( ) , "layers" )
const l2 = generateTranslationsObjectFrom (
2024-09-15 00:57:38 +02:00
ScriptUtils . getThemeFiles ( true ) . filter (
2023-10-30 20:06:31 +01:00
( th ) = > th . parsed . mustHaveLanguage === undefined
) ,
"themes"
)
2022-09-08 21:40:48 +02:00
2023-10-30 20:06:31 +01:00
const usedLanguages : string [ ] = Utils . Dedup ( l1 . concat ( l2 ) ) . filter ( ( v ) = > v !== "*" )
usedLanguages . sort ( )
fs . writeFileSync (
"./src/assets/used_languages.json" ,
JSON . stringify ( { languages : usedLanguages } )
)
}
}
2021-06-08 19:08:19 +02:00
2024-08-29 21:49:11 +02:00
/ * *
* Generates the big compiledTranslations file based on 'translations.json'
* /
genTranslations ( englishOnly? : boolean ) {
if ( ! fs . existsSync ( "./src/assets/generated/" ) ) {
fs . mkdirSync ( "./src/assets/generated/" )
}
const translations = JSON . parse (
fs . readFileSync ( "./src/assets/generated/translations.json" , "utf-8" )
)
2024-09-02 12:48:15 +02:00
const transformed = transformTranslation (
translations ,
undefined ,
englishOnly ? [ "en" ] : undefined ,
englishOnly
)
2024-08-29 21:49:11 +02:00
let module = ` import {Translation, TypedTranslation} from "../../UI/i18n/Translation" \ n \ nexport default class CompiledTranslations { \ n \ n `
module += " public static t = " + transformed
module += "\n }"
fs . writeFileSync ( "./src/assets/generated/CompiledTranslations.ts" , module )
}
compileTranslationsFromWeblate ( englishOnly : boolean ) {
const translations = ScriptUtils . readDirRecSync ( "./langs" , 1 ) . filter (
( path ) = > path . indexOf ( ".json" ) > 0
)
const allTranslations = new TranslationPart ( )
allTranslations . validateStrict ( )
for ( const translationFile of translations ) {
try {
const contents = JSON . parse ( readFileSync ( translationFile , "utf-8" ) )
let language = translationFile . substring ( translationFile . lastIndexOf ( "/" ) + 1 )
language = language . substring ( 0 , language . length - 5 )
if ( englishOnly && language !== "en" ) {
continue
}
allTranslations . add ( language , contents )
} catch ( e ) {
throw "Could not read file " + translationFile + " due to " + e
}
}
writeFileSync (
"./src/assets/generated/translations.json" ,
JSON . stringify ( JSON . parse ( allTranslations . toJson ( ) ) , null , " " )
)
}
2023-10-30 20:06:31 +01:00
async main ( args : string [ ] ) : Promise < void > {
if ( ! existsSync ( "./langs/themes" ) ) {
mkdirSync ( "./langs/themes" )
}
const themeOverwritesWeblate = args [ 0 ] === "--ignore-weblate"
const englishOnly = args [ 0 ] === "--english-only"
2024-08-29 21:49:11 +02:00
if ( englishOnly ) {
console . log ( "ENGLISH ONLY" )
}
2023-10-30 20:06:31 +01:00
if ( ! themeOverwritesWeblate ) {
2024-08-29 21:49:11 +02:00
mergeLayerTranslations ( englishOnly )
mergeThemeTranslations ( englishOnly )
this . compileTranslationsFromWeblate ( englishOnly )
2023-10-30 20:06:31 +01:00
} else {
console . log ( "Ignore weblate" )
}
this . detectUsedLanguages ( )
2024-08-29 21:49:11 +02:00
this . genTranslations ( englishOnly )
2023-10-30 20:06:31 +01:00
{
const allTranslationFiles = ScriptUtils . readDirRecSync ( "langs" ) . filter ( ( path ) = >
path . endsWith ( ".json" )
)
for ( const path of allTranslationFiles ) {
formatFile ( path )
}
}
// Some validation
TranslationPart . fromDirectory ( "./langs" ) . validateStrict ( "./langs" )
TranslationPart . fromDirectory ( "./langs/layers" ) . validateStrict ( "layers" )
TranslationPart . fromDirectory ( "./langs/themes" ) . validateStrict ( "themes" )
if ( englishOnly ) {
mergeLayerTranslations ( true )
mergeThemeTranslations ( true )
}
console . log ( "All done!" )
}
2022-02-18 03:45:03 +01:00
}
2022-02-18 03:37:17 +01:00
2023-10-30 20:06:31 +01:00
new GenerateTranslations ( ) . run ( )