2020-11-17 02:22:48 +01:00
import * as fs from "fs" ;
2022-07-18 16:03:56 +02:00
import { existsSync , mkdirSync , readFileSync , writeFileSync } from "fs" ;
2020-11-17 02:22:48 +01:00
import { Utils } from "../Utils" ;
2021-05-19 16:15:12 +02:00
import ScriptUtils from "./ScriptUtils" ;
2021-05-19 20:47:41 +02:00
const knownLanguages = [ "en" , "nl" , "de" , "fr" , "es" , "gl" , "ca" ] ;
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 ) {
const content = JSON . parse ( readFileSync ( file , "UTF8" ) )
rootTranslation . addTranslation ( file . substr ( 0 , file . length - ".json" . length ) , content )
}
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 ) {
if ( ! translations . hasOwnProperty ( translationsKey ) ) {
continue ;
}
const v = translations [ translationsKey ]
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 ` + JSON . stringify ( v ) + ` to ' ` + translationsKey + "'. The offending object which _should_ be a translation is: " , v , "\n\nThe current object is (only showing en):" , this . toJson ( ) , "and has translations for" , Array . from ( this . contents . keys ( ) ) )
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"
}
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
if ( context . indexOf ( "mapcomplete" ) >= 0 && context . indexOf ( "tagRenderings" ) >= 0 ) {
console . log ( "RA: CONTEXT" , context , "\n" , this . toJson ( ) . replace ( /\n/g , "\n>> " ) )
}
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-08-22 22:31:05 +02:00
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 " + context + ": " + dontTranslateKeys . join ( ", " ) )
}
}
2021-09-26 20:15:25 +02:00
for ( let key in object ) {
2021-05-19 20:47:41 +02:00
if ( ! object . hasOwnProperty ( key ) ) {
continue ;
}
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 ) {
console . warn ( "Got a null value for key " , key )
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"
}
2021-09-26 20:32:28 +02:00
} else {
2021-09-26 20:58:10 +02:00
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 ( ) )
}
2022-08-22 22:31:05 +02:00
2021-07-29 01:57:45 +02:00
( this . contents . get ( key ) as TranslationPart ) . recursiveAdd ( v , context + "." + key ) ;
2021-05-19 20:47:41 +02:00
}
}
knownLanguages ( ) : string [ ] {
const languages = [ ]
for ( let 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 ( )
for ( let key of keys ) {
2021-05-19 20:47:41 +02:00
let value = this . contents . get ( key ) ;
if ( typeof value === "string" ) {
value = value . replace ( /"/g , "\\\"" )
2021-05-19 23:40:55 +02:00
. replace ( /\n/g , "\\n" )
2021-05-19 22:38:05 +02:00
if ( neededLanguage === undefined ) {
2021-05-19 20:47:41 +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
}
2021-05-19 22:38:05 +02:00
2021-05-19 20:47:41 +02:00
} else {
const sub = ( value as TranslationPart ) . toJson ( neededLanguage )
if ( sub !== "" ) {
parts . push ( ` \ " ${ key } \ ": ${ sub } ` ) ;
}
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" , path : path } )
}
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 ) {
2022-04-27 11:35:47 +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 ) = > {
2022-04-27 11:35:47 +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 + ")" ) . join ( "," ) + " . The full translation is " + value + "\n" + fixLink ,
2022-04-09 19:52:25 +02:00
path : path
} )
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 } ` ,
2022-04-27 11:35:47 +02:00
path : path
} )
}
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
/ * *
* Checks that the given object only contains string - values
* @param tr
* /
2020-11-17 02:22:48 +01:00
function isTranslation ( tr : any ) : boolean {
2022-07-02 01:59:26 +02:00
if ( tr [ "#" ] === "no-translations" ) {
2022-06-24 16:49:03 +02:00
return false
}
2020-11-17 02:22:48 +01:00
for ( const key in tr ) {
if ( typeof tr [ key ] !== "string" ) {
return false ;
}
}
return true ;
}
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 [ ] = [ ] , languageWhitelist : string [ ] = undefined ) {
2020-11-17 02:22:48 +01:00
if ( isTranslation ( obj ) ) {
return ` new Translation( ${ JSON . stringify ( obj ) } ) `
}
let values = ""
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
if ( 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 )
2022-04-13 01:19:28 +02:00
let expr = ` return 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-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 ( ', ' ) } `
}
2022-04-13 01:19:28 +02:00
expr = ` return new TypedTranslation<{ ${ types . join ( ", " ) } }>( ${ JSON . stringify ( value ) } , "core: ${ path . join ( "." ) } . ${ key } ") `
}
2022-07-02 01:59:26 +02:00
2022-04-13 01:19:28 +02:00
values += ` ${ Utils . Times ( ( _ ) = > " " , path . length + 1 ) } get ${ key } () { ${ expr } },
2022-04-01 12:51:55 +02:00
`
2021-10-25 21:50:38 +02:00
} else {
2022-04-01 12:51:55 +02:00
values += ( Utils . Times ( ( _ ) = > " " , path . length + 1 ) ) + key + ": " + transformTranslation ( value , [ . . . path , key ] , languageWhitelist ) + ",\n"
2021-10-25 21:50:38 +02:00
}
2020-11-17 02:22:48 +01:00
}
return ` { ${ values } } ` ;
}
2022-07-02 01:59:26 +02:00
function sortKeys ( o : object ) : object {
2022-02-18 03:51:52 +01:00
const keys = Object . keys ( o )
keys . sort ( )
const nw = { }
for ( const key of keys ) {
const v = o [ key ]
2022-07-02 01:59:26 +02:00
if ( typeof v === "object" ) {
2022-02-18 03:51:52 +01:00
nw [ key ] = sortKeys ( v )
2022-07-02 01:59:26 +02:00
} else {
2022-02-18 03:51:52 +01:00
nw [ key ] = v
}
}
return nw
}
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
}
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 )
2022-02-18 03:51:52 +01:00
contents = sortKeys ( contents )
2022-03-31 02:58:31 +02:00
const endsWithNewline = original . endsWith ( "\n" )
writeFileSync ( path , JSON . stringify ( contents , null , " " ) + ( endsWithNewline ? "\n" : "" ) )
2022-02-14 20:09:17 +01:00
}
2021-09-26 20:15:25 +02:00
/ * *
* Generates the big compiledTranslations file
* /
2020-11-17 02:22:48 +01:00
function genTranslations() {
2021-05-19 16:15:12 +02:00
const translations = JSON . parse ( fs . readFileSync ( "./assets/generated/translations.json" , "utf-8" ) )
2022-07-02 01:59:26 +02:00
const transformed = transformTranslation ( translations ) ;
2020-11-17 02:22:48 +01:00
2022-04-13 01:19:28 +02:00
let module = ` import {Translation, TypedTranslation} from "../../UI/i18n/Translation" \ n \ nexport default class CompiledTranslations { \ n \ n ` ;
2020-11-17 02:22:48 +01:00
module += " public static t = " + transformed ;
2022-04-01 12:51:55 +02:00
module += "\n }"
2020-11-17 02:22:48 +01:00
2021-05-10 23:43:30 +02:00
fs . writeFileSync ( "./assets/generated/CompiledTranslations.ts" , module ) ;
2020-11-17 02:22:48 +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 20:47:41 +02:00
function compileTranslationsFromWeblate() {
2021-05-20 12:27:33 +02:00
const translations = ScriptUtils . readDirRecSync ( "./langs" , 1 )
2021-05-19 16:15:12 +02:00
. filter ( path = > path . indexOf ( ".json" ) > 0 )
const allTranslations = new TranslationPart ( )
2022-07-02 01:59:26 +02:00
allTranslations . validateStrict ( )
2021-05-19 16:15:12 +02:00
for ( const translationFile of translations ) {
2022-01-26 21:40:38 +01:00
try {
const contents = JSON . parse ( readFileSync ( translationFile , "utf-8" ) ) ;
let language = translationFile . substring ( translationFile . lastIndexOf ( "/" ) + 1 )
language = language . substring ( 0 , language . length - 5 )
allTranslations . add ( language , contents )
} catch ( e ) {
throw "Could not read file " + translationFile + " due to " + e
2021-11-16 04:16:51 +01:00
}
2021-05-19 16:15:12 +02:00
}
2021-05-19 20:47:41 +02:00
writeFileSync ( "./assets/generated/translations.json" , JSON . stringify ( JSON . parse ( allTranslations . toJson ( ) ) , null , " " ) )
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-09-26 20:32:28 +02:00
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-09 00:05:51 +02:00
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 ) {
keyRemapping . set ( target [ key ] . id , key )
}
}
2021-05-19 22:38:05 +02:00
for ( const key in source ) {
if ( ! source . hasOwnProperty ( key ) ) {
continue
}
2021-09-26 20:32:28 +02:00
2021-05-19 22:38:05 +02:00
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" ) {
throw "Trying to merge a translation 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
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
}
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 )
2021-05-19 22:38:05 +02:00
. filter ( path = > path . endsWith ( ".json" ) )
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
* /
function mergeLayerTranslations() {
2021-05-19 22:38:05 +02:00
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" ) )
2022-02-18 03:45:03 +01:00
writeFileSync ( layerFile . path , JSON . stringify ( layerFile . parsed , null , " " ) ) // layers use 2 spaces
2021-05-19 22:38:05 +02:00
}
}
2021-05-19 23:40:55 +02:00
2021-09-26 20:58:10 +02:00
/ * *
* Load the translations into the theme files
* /
2021-05-20 00:10:38 +02:00
function mergeThemeTranslations() {
const themeFiles = ScriptUtils . getThemeFiles ( ) ;
for ( const themeFile of themeFiles ) {
const config = themeFile . parsed ;
mergeLayerTranslation ( config , themeFile . path , loadTranslationFilesFrom ( "themes" ) )
const allTranslations = new TranslationPart ( ) ;
2021-07-29 01:57:45 +02:00
allTranslations . recursiveAdd ( config , themeFile . path )
2022-02-18 03:45:03 +01:00
writeFileSync ( themeFile . path , JSON . stringify ( config , null , " " ) ) // Themefiles use 2 spaces
2021-05-20 00:10:38 +02:00
}
}
2022-08-05 13:11:50 +02:00
if ( ! existsSync ( "./langs/themes" ) ) {
2022-07-18 16:03:56 +02:00
mkdirSync ( "./langs/themes" )
}
2021-05-31 12:58:49 +02:00
const themeOverwritesWeblate = process . argv [ 2 ] === "--ignore-weblate"
2021-06-08 19:08:19 +02:00
const questionsPath = "assets/tagRenderings/questions.json"
const questionsParsed = JSON . parse ( readFileSync ( questionsPath , 'utf8' ) )
if ( ! themeOverwritesWeblate ) {
2021-05-31 12:51:29 +02:00
mergeLayerTranslations ( ) ;
mergeThemeTranslations ( ) ;
2021-06-08 19:08:19 +02:00
mergeLayerTranslation ( questionsParsed , questionsPath , loadTranslationFilesFrom ( "shared-questions" ) )
writeFileSync ( questionsPath , JSON . stringify ( questionsParsed , null , " " ) )
} else {
2021-05-31 12:58:49 +02:00
console . log ( "Ignore weblate" )
2021-05-31 12:51:29 +02:00
}
2021-05-19 22:40:25 +02:00
2022-01-29 02:45:59 +01:00
const l1 = generateTranslationsObjectFrom ( ScriptUtils . getLayerFiles ( ) , "layers" )
const l2 = generateTranslationsObjectFrom ( ScriptUtils . getThemeFiles ( ) . filter ( th = > th . parsed . mustHaveLanguage === undefined ) , "themes" )
const l3 = generateTranslationsObjectFrom ( [ { path : questionsPath , parsed : questionsParsed } ] , "shared-questions" )
2021-06-08 19:08:19 +02:00
2022-04-01 21:17:27 +02:00
const usedLanguages : string [ ] = Utils . Dedup ( l1 . concat ( l2 ) . concat ( l3 ) ) . filter ( v = > v !== "*" )
2022-01-29 02:45:59 +01:00
usedLanguages . sort ( )
fs . writeFileSync ( "./assets/generated/used_languages.json" , JSON . stringify ( { languages : usedLanguages } ) )
2021-06-08 19:08:19 +02:00
if ( ! themeOverwritesWeblate ) {
2021-05-31 12:51:29 +02:00
// Generates the core translations
compileTranslationsFromWeblate ( ) ;
}
2022-02-14 20:09:17 +01:00
genTranslations ( )
2022-02-18 03:45:03 +01:00
const allTranslationFiles = ScriptUtils . readDirRecSync ( "langs" ) . filter ( path = > path . endsWith ( ".json" ) )
2022-02-18 03:37:17 +01:00
for ( const path of allTranslationFiles ) {
formatFile ( path )
2022-02-18 03:45:03 +01:00
}
2022-02-18 03:37:17 +01:00
2022-04-01 12:51:55 +02:00
// Some validation
2022-02-16 03:22:16 +01:00
TranslationPart . fromDirectory ( "./langs" ) . validateStrict ( "./langs" )
TranslationPart . fromDirectory ( "./langs/layers" ) . validateStrict ( "layers" )
TranslationPart . fromDirectory ( "./langs/themes" ) . validateStrict ( "themes" )
TranslationPart . fromDirectory ( "./langs/shared-questions" ) . validateStrict ( "shared-questions" )
2022-04-22 02:06:03 +02:00
console . log ( "All done!" )