2020-11-17 02:22:48 +01:00
import * as fs from "fs" ;
2021-05-19 23:31:00 +02:00
import { 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-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 ) {
for ( const translationsKey in translations ) {
if ( ! translations . hasOwnProperty ( translationsKey ) ) {
continue ;
}
2021-11-07 16:34:51 +01:00
if ( translationsKey == "then" ) {
throw "Suspicious translation at " + context
2021-10-29 01:41:37 +02:00
}
2021-05-19 20:47:41 +02:00
const v = translations [ translationsKey ]
if ( typeof ( v ) != "string" ) {
2022-01-06 15:46:21 +01:00
console . error ( ` Non-string object at ${ context } in translation while trying to add more translations to ' ` + translationsKey + "': " , v )
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 ) {
2021-09-26 17:36:39 +02:00
const isProbablyTranslationObject = knownLanguages . some ( l = > object . hasOwnProperty ( l ) ) ;
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 ;
}
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 ;
}
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
}
2021-09-26 20:58:10 +02:00
if ( v [ "id" ] !== undefined && context . endsWith ( "tagRenderings" ) ) {
2021-09-26 20:32:28 +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 {
2021-09-26 20:58:10 +02:00
2021-09-26 20:32:28 +02:00
key = v [ "id" ]
if ( typeof key !== "string" ) {
throw "Panic: found a non-string ID at" + context
}
}
}
2021-05-19 20:47:41 +02:00
if ( typeof v !== "object" ) {
continue ;
}
if ( ! this . contents . get ( key ) ) {
this . contents . set ( key , new TranslationPart ( ) )
}
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
/ * *
* 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 ]
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 )
}
}
}
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
}
validateStrict ( ctx? :string ) : void {
const errors = this . validate ( )
for ( const err of errors ) {
console . error ( "ERROR in " + ( ctx ? ? "" ) + " " + err . path . join ( "." ) + "\n " + err . error )
}
if ( errors . length > 0 ) {
throw ctx + " has " + errors . length + " inconsistencies in the translation"
}
}
/ * *
* Checks the leaf objects : special values must be present and identical in every leaf
* /
validate ( path = [ ] ) : { error : string , path : string [ ] } [ ] {
const errors : { error : string , path : string [ ] } [ ] = [ ]
const neededSubparts = new Set < string > ( )
let isLeaf : boolean = undefined
this . contents . forEach ( ( value , key ) = > {
if ( typeof value === "string" ) {
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 ) {
if ( neededSubparts . size > 0 ) {
errors . push ( { error : "The translation for " + key + " does not have any subparts, but expected " + Array . from ( neededSubparts ) . join ( "," ) + " . The full translation is " + value , path : path } )
}
return
}
subparts = subparts . map ( p = > p . split ( /\(.*\)/ ) [ 0 ] )
neededSubparts . forEach ( part = > {
if ( subparts . indexOf ( part ) < 0 ) {
errors . push ( { error : "The translation for " + key + " does not have the required subpart " + part + ". The full translation is " + value , path : path } )
}
} )
for ( const subpart of subparts ) {
neededSubparts . add ( subpart )
}
} else {
const recErrors = value . validate ( [ . . . path , key ] )
errors . push ( . . . recErrors )
}
} )
return errors
}
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 {
for ( const key in tr ) {
if ( typeof tr [ key ] !== "string" ) {
return false ;
}
}
return true ;
}
2021-09-26 20:15:25 +02:00
/ * *
* Converts a translation object into something that can be added to the 'generated translations'
* @param obj
* @param depth
* /
2020-11-17 02:22:48 +01:00
function transformTranslation ( obj : any , depth = 1 ) {
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 ;
}
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
}
2021-10-25 21:50:38 +02:00
const value = obj [ key ]
if ( isTranslation ( value ) ) {
values += ( Utils . Times ( ( _ ) = > " " , depth ) ) + "get " + key + "() { return new Translation(" + JSON . stringify ( value ) + ") }" + ",\n"
} else {
values += ( Utils . Times ( ( _ ) = > " " , depth ) ) + key + ": " + transformTranslation ( value , depth + 1 ) + ",\n"
}
2020-11-17 02:22:48 +01:00
}
return ` { ${ values } } ` ;
}
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 ) {
const contents = JSON . parse ( readFileSync ( path , "utf8" ) )
writeFileSync ( path , JSON . stringify ( contents , null , " " ) )
}
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" ) )
2020-11-17 02:22:48 +01:00
const transformed = transformTranslation ( translations ) ;
2021-05-10 23:43:30 +02:00
let module = ` import {Translation} from "../../UI/i18n/Translation" \ n \ nexport default class CompiledTranslations { \ n \ n ` ;
2020-11-17 02:22:48 +01:00
module += " public static t = " + transformed ;
module += "}"
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-02-16 03:22:16 +01: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
* @param source : where the tranlations come from
* @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" ) )
2021-11-07 14:37:21 +01:00
writeFileSync ( layerFile . path , JSON . stringify ( layerFile . parsed , null , " " ) )
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 )
2021-05-20 00:10:38 +02:00
writeFileSync ( themeFile . path , JSON . stringify ( config , null , " " ) )
}
}
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-01-29 02:45:59 +01:00
const usedLanguages = Utils . Dedup ( l1 . concat ( l2 ) . concat ( l3 ) ) . filter ( v = > v !== "*" )
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:37:17 +01:00
formatFile ( "./langs/themes/nl.json" )
/ * c o n s t a l l T r a n s l a t i o n F i l e s = S c r i p t U t i l s . r e a d D i r R e c S y n c ( " l a n g s " ) . f i l t e r ( p a t h = > p a t h . e n d s W i t h ( " . j s o n " ) )
for ( const path of allTranslationFiles ) {
console . log ( "Formatting " , path )
formatFile ( path )
} * /
2022-02-16 03:22:16 +01:00
// SOme validation
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" )