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 > ( )
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" ) {
2021-10-25 21:50:38 +02:00
console . error ( "Non-string object 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
}
}
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 } } ` ;
}
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 ( )
for ( const translationFile of translations ) {
const contents = JSON . parse ( readFileSync ( translationFile , "utf-8" ) ) ;
let language = translationFile . substring ( translationFile . lastIndexOf ( "/" ) + 1 )
2021-05-19 20:47:41 +02:00
language = language . substring ( 0 , language . length - 5 )
2021-05-19 16:15:12 +02:00
allTranslations . add ( language , contents )
}
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
* /
2021-05-19 23:40:55 +02:00
function generateTranslationsObjectFrom ( objects : { path : string , parsed : { id : string } } [ ] , target : 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 02:59:33 +01:00
json = JSON . stringify ( JSON . parse ( json ) , null , " " ) ;
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
}
}
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 oldLanguages = config . language ;
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
const newLanguages = allTranslations . knownLanguages ( )
const languageDiff = newLanguages . filter ( l = > oldLanguages . indexOf ( l ) < 0 ) . join ( ", " )
if ( languageDiff !== "" ) {
config . language = newLanguages ;
console . log ( " :hooray: Got a new language for theme" , config . id , ":" , languageDiff )
}
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 23:31:00 +02:00
generateTranslationsObjectFrom ( ScriptUtils . getLayerFiles ( ) , "layers" )
generateTranslationsObjectFrom ( ScriptUtils . getThemeFiles ( ) , "themes" )
2021-05-19 22:40:25 +02:00
2021-06-08 19:08:19 +02:00
generateTranslationsObjectFrom ( [ { path : questionsPath , parsed : questionsParsed } ] , "shared-questions" )
if ( ! themeOverwritesWeblate ) {
2021-05-31 12:51:29 +02:00
// Generates the core translations
compileTranslationsFromWeblate ( ) ;
}
2020-11-17 02:22:48 +01:00
genTranslations ( )