2022-09-08 21:40:48 +02:00
import * as fs from "fs"
2023-05-03 00:57:15 +02:00
import { existsSync , mkdirSync , readFileSync , writeFileSync } from "fs"
import { Utils } from "../Utils"
2022-09-08 21:40:48 +02:00
import ScriptUtils from "./ScriptUtils"
2021-05-19 20:47:41 +02:00
2022-09-08 21:40:48 +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 {
2022-09-08 21:40:48 +02:00
const files = ScriptUtils . readDirRecSync ( path , 1 ) . filter ( ( file ) = > file . endsWith ( ".json" ) )
2022-07-02 01:59:26 +02:00
const rootTranslation = new TranslationPart ( )
for ( const file of files ) {
2023-05-03 00:57:15 +02:00
const content = JSON . parse ( readFileSync ( file , { encoding : "utf8" } ) )
2022-07-02 01:59:26 +02:00
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-09-08 21:40:48 +02:00
return
2022-06-24 16:49:03 +02:00
}
2021-05-19 20:47:41 +02:00
for ( const translationsKey in translations ) {
if ( ! translations . hasOwnProperty ( translationsKey ) ) {
2022-09-08 21:40:48 +02:00
continue
2021-05-19 20:47:41 +02:00
}
const v = translations [ translationsKey ]
2022-09-08 21:40:48 +02:00
if ( typeof v != "string" ) {
console . error (
` Non-string object at ${ context } in translation while trying to add the translation ` +
2023-05-03 00:57:15 +02:00
JSON . stringify ( v ) +
` to ' ` +
translationsKey +
"'. The offending object which _should_ be a translation is: " ,
2022-09-08 21:40:48 +02:00
v ,
"\n\nThe current object is (only showing en):" ,
this . toJson ( ) ,
"and has translations for" ,
Array . from ( this . contents . keys ( ) )
)
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"
)
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-09-08 21:40:48 +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 )
2022-09-08 21:40:48 +02:00
return
2021-05-19 20:47:41 +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 ) {
2022-09-08 21:40:48 +02:00
console . log (
"Ignoring some translations for " +
2023-05-03 00:57: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 ) {
2021-05-19 20:47:41 +02:00
if ( ! object . hasOwnProperty ( key ) ) {
2022-09-08 21:40:48 +02:00
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" ) {
2022-09-08 21:40:48 +02:00
continue
2022-08-05 13:11:50 +02:00
}
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 {
2022-09-08 21:40:48 +02:00
throw (
"At " +
context +
": every object within a tagRenderings-list should have an id. " +
JSON . stringify ( v ) +
" has no id"
)
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 ( ) )
}
2022-09-08 21:40:48 +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 ( ) ) ) {
2022-09-08 21:40:48 +02:00
const value = this . contents . get ( key )
2021-05-19 16:15:12 +02:00
2021-05-19 20:47:41 +02:00
if ( typeof value === "string" ) {
2021-05-20 00:10:38 +02:00
if ( key === "#" ) {
2022-09-08 21:40:48 +02:00
continue
2021-05-20 00:10:38 +02:00
}
2021-05-19 20:47:41 +02:00
languages . push ( key )
} else {
languages . push ( . . . ( value as TranslationPart ) . knownLanguages ( ) )
}
}
2022-09-08 21:40:48 +02:00
return Utils . Dedup ( languages )
2021-05-19 20:47:41 +02:00
}
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 ) {
2022-09-08 21:40:48 +02:00
let value = this . contents . get ( key )
2021-05-19 20:47:41 +02:00
if ( typeof value === "string" ) {
2022-09-08 21:40:48 +02:00
value = value . replace ( /"/g , '\\"' ) . 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
}
} else {
const sub = ( value as TranslationPart ) . toJson ( neededLanguage )
if ( sub !== "" ) {
2022-09-08 21:40:48 +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 ) {
2022-09-08 21:40:48 +02:00
return ""
2021-05-19 20:47:41 +02:00
}
2022-09-08 21:40:48 +02:00
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-09-08 21:40:48 +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-09-08 21:40:48 +02:00
validate ( path = [ ] ) : { error : string ; path : string [ ] } [ ] {
const errors : { error : string ; path : string [ ] } [ ] = [ ]
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 )
2022-09-08 21:40:48 +02:00
return
2022-04-09 19:52:25 +02:00
}
if ( isLeaf === undefined ) {
isLeaf = true
} else if ( ! isLeaf ) {
2022-09-08 21:40:48 +02:00
errors . push ( {
error : "Mixed node: non-leaf node has translation strings" ,
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-09-08 21:40:48 +02:00
subparts = subparts . map ( ( p ) = > p . split ( /\(.*\)/ ) [ 0 ] )
2022-04-09 19:52:25 +02:00
for ( const subpart of subparts ) {
2023-05-03 00:57:15 +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-05-03 00:57:15 +02:00
neededSubparts . forEach ( ( { part , usedByLanguage } ) = > {
2022-04-09 19:52:25 +02:00
if ( typeof value !== "string" ) {
2022-09-08 21:40:48 +02:00
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-09-08 21:40:48 +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-09-08 21:40:48 +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 ,
path : path ,
2022-04-09 19:52:25 +02:00
} )
2022-02-16 03:22:16 +01:00
}
return
}
2022-09-08 21:40:48 +02:00
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-09-08 21:40:48 +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-09-08 21:40:48 +02:00
if ( v === "" ) {
2022-08-24 04:05:49 +02:00
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" ) {
2022-09-08 21:40:48 +02:00
return false
2020-11-17 02:22:48 +01:00
}
}
2022-09-08 21:40:48 +02:00
return true
2020-11-17 02:22:48 +01: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-09-08 21:40:48 +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 ) } ) `
}
2023-01-11 01:47:23 +01:00
let values : string [ ] = [ ]
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 === "#" ) {
2022-09-08 21:40:48 +02:00
continue
2021-01-18 02:51:42 +01:00
}
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 ]
}
2022-09-08 21:40:48 +02:00
value = nv
2022-04-01 12:51:55 +02:00
}
2022-04-13 01:19:28 +02:00
2022-07-02 01:59:26 +02:00
if ( value [ "en" ] === undefined ) {
2022-09-08 21:40:48 +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-09-08 21:40:48 +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-09-08 21:40:48 +02:00
const types = Utils . Dedup ( subParts . map ( ( tp ) = > tp . substring ( 1 , tp . length - 1 ) ) )
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-09-08 21:40:48 +02:00
throw ` At ${ path . join (
"."
) } : A subpart contains invalid characters : $ { subParts . join ( ", " ) } `
2022-06-05 03:41:53 +02:00
}
2022-09-08 21:40:48 +02:00
expr = ` return new TypedTranslation<{ ${ types . join ( ", " ) } }>( ${ JSON . stringify (
value
) } , "core:${path.join(" . ")}.${key}" ) `
2022-04-13 01:19:28 +02:00
}
2022-07-02 01:59:26 +02:00
2023-01-11 01:47:23 +01:00
values . push ( ` ${ spaces } get ${ key } () { ${ 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
}
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 ) {
2022-09-08 21:40:48 +02:00
if ( object [ k ] === "" ) {
2022-08-24 04:05:49 +02:00
delete object [ k ]
continue
}
2022-09-08 21:40:48 +02:00
if ( typeof object [ k ] === "object" ) {
2022-08-24 04:05:49 +02:00
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 )
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() {
2022-09-08 21:40:48 +02:00
const translations = JSON . parse (
fs . readFileSync ( "./assets/generated/translations.json" , "utf-8" )
)
const transformed = transformTranslation ( translations )
2020-11-17 02:22:48 +01:00
2022-09-08 21:40:48 +02:00
let module = ` import {Translation, TypedTranslation} from "../../UI/i18n/Translation" \ n \ nexport default class CompiledTranslations { \ n \ n `
module += " public static t = " + transformed
2022-04-01 12:51:55 +02:00
module += "\n }"
2020-11-17 02:22:48 +01:00
2022-09-08 21:40:48 +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() {
2022-09-08 21:40:48 +02:00
const translations = ScriptUtils . readDirRecSync ( "./langs" , 1 ) . filter (
( path ) = > path . indexOf ( ".json" ) > 0
)
2021-05-19 16:15:12 +02:00
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 {
2022-09-08 21:40:48 +02:00
const contents = JSON . parse ( readFileSync ( translationFile , "utf-8" ) )
2022-01-26 21:40:38 +01:00
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
}
2022-09-08 21:40:48 +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-09-08 21:40:48 +02:00
function generateTranslationsObjectFrom (
objects : { path : string ; parsed : { id : string } } [ ] ,
target : string
) : string [ ] {
const tr = new TranslationPart ( )
2021-05-19 20:47:41 +02:00
2021-05-19 23:31:00 +02:00
for ( const layerFile of objects ) {
2022-09-08 21:40:48 +02:00
const config : { id : string } = layerFile . parsed
const layerTr = new TranslationPart ( )
2021-05-19 23:40:55 +02:00
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 )
}
2022-09-08 21:40:48 +02:00
const langs = tr . knownLanguages ( )
2021-05-19 20:47:41 +02:00
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
2022-09-08 21:40:48 +02:00
continue
2021-05-19 23:40:55 +02:00
}
2021-05-19 20:47:41 +02:00
let json = tr . toJson ( lang )
2021-05-19 22:38:05 +02:00
try {
2022-09-08 21:40:48 +02: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 ) {
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
2022-09-08 21:40:48 +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" ) {
2022-09-08 21:40:48 +02:00
throw (
"Trying to merge a translation into a fixed string at " +
context +
" for key " +
key
)
2021-07-18 18:02:17 +02:00
}
2022-09-08 21:40:48 +02:00
target [ key ] = source [ key ]
continue
2021-06-21 00:02:45 +02:00
}
2021-09-09 00:05:51 +02:00
2021-05-19 22:38:05 +02:00
if ( targetV [ language ] === sourceV ) {
// Already the same
2022-09-08 21:40:48 +02:00
continue
2021-05-19 22:38:05 +02:00
}
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" ) {
2022-09-08 21:40:48 +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
}
2022-09-08 21:40:48 +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 {
2022-09-08 21:40:48 +02:00
target [ language ] = sourceV
2021-10-25 21:50:38 +02:00
} 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 {
2022-09-08 21:40:48 +02:00
MergeTranslation ( sourceV , targetV , language , context + "." + key )
2021-05-19 22:38:05 +02:00
}
2022-09-08 21:40:48 +02:00
continue
2021-05-19 22:38:05 +02:00
}
throw "Case fallthrough"
}
2022-09-08 21:40:48 +02:00
return target
2021-05-19 22:38:05 +02:00
}
2022-09-08 21:40:48 +02:00
function mergeLayerTranslation (
layerConfig : { id : string } ,
path : string ,
translationFiles : Map < string , any >
) {
const id = layerConfig . id
2021-05-19 22:38:05 +02:00
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 > {
2022-09-08 21:40:48 +02:00
const translationFilePaths = ScriptUtils . readDirRecSync ( "./langs/" + target ) . filter ( ( path ) = >
path . endsWith ( ".json" )
)
2021-05-19 22:38:05 +02:00
2022-09-08 21:40:48 +02:00
const translationFiles = new Map < string , any > ( )
2021-05-19 22:38:05 +02:00
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 )
2022-09-08 21:40:48 +02:00
throw e
2021-09-22 16:26:34 +02:00
}
2021-05-19 22:38:05 +02:00
}
2022-09-08 21:40:48 +02:00
return translationFiles
2021-05-20 00:10:38 +02:00
}
/ * *
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() {
2022-09-08 21:40:48 +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-05-03 00:57:15 +02:00
const endsWithNewline = readFileSync ( layerFile . path , { encoding : "utf8" } ) ? . endsWith ( "\n" ) ? ? true
writeFileSync ( layerFile . path , JSON . stringify ( layerFile . parsed , null , " " ) + ( endsWithNewline ? "\n" : "" ) ) // 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() {
2022-09-08 21:40:48 +02:00
const themeFiles = ScriptUtils . getThemeFiles ( )
2021-05-20 00:10:38 +02:00
for ( const themeFile of themeFiles ) {
2022-09-08 21:40:48 +02:00
const config = themeFile . parsed
2021-05-20 00:10:38 +02:00
mergeLayerTranslation ( config , themeFile . path , loadTranslationFilesFrom ( "themes" ) )
2022-09-08 21:40:48 +02:00
const allTranslations = new TranslationPart ( )
2021-07-29 01:57:45 +02:00
allTranslations . recursiveAdd ( config , themeFile . path )
2023-05-03 00:57:15 +02:00
const endsWithNewline = readFileSync ( themeFile . path , { encoding : "utf8" } ) ? . endsWith ( "\n" ) ? ? true
writeFileSync ( themeFile . path , JSON . stringify ( config , null , " " ) + ( endsWithNewline ? "\n" : "" ) ) // 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"
2022-09-08 21:40:48 +02:00
const questionsParsed = JSON . parse ( readFileSync ( questionsPath , "utf8" ) )
2021-06-08 19:08:19 +02:00
if ( ! themeOverwritesWeblate ) {
2022-09-08 21:40:48 +02:00
mergeLayerTranslations ( )
mergeThemeTranslations ( )
mergeLayerTranslation (
questionsParsed ,
questionsPath ,
loadTranslationFilesFrom ( "shared-questions" )
)
2023-05-03 00:57:15 +02:00
const endsWithNewline = readFileSync ( questionsPath , { encoding : "utf8" } ) . endsWith ( "\n" )
writeFileSync ( questionsPath , JSON . stringify ( questionsParsed , null , " " ) + ( endsWithNewline ? "\n" : "" ) )
2021-06-08 19:08:19 +02:00
} 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" )
2022-09-08 21:40:48 +02:00
const l2 = generateTranslationsObjectFrom (
ScriptUtils . getThemeFiles ( ) . filter ( ( th ) = > th . parsed . mustHaveLanguage === undefined ) ,
"themes"
)
const l3 = generateTranslationsObjectFrom (
2023-05-03 00:57:15 +02:00
[ { path : questionsPath , parsed : questionsParsed } ] ,
2022-09-08 21:40:48 +02:00
"shared-questions"
)
const usedLanguages : string [ ] = Utils . Dedup ( l1 . concat ( l2 ) . concat ( l3 ) ) . filter ( ( v ) = > v !== "*" )
2022-01-29 02:45:59 +01:00
usedLanguages . sort ( )
2023-05-03 00:57:15 +02:00
fs . writeFileSync ( "./assets/used_languages.json" , JSON . stringify ( { languages : usedLanguages } ) )
2021-06-08 19:08:19 +02:00
if ( ! themeOverwritesWeblate ) {
2022-09-08 21:40:48 +02:00
// Generates the core translations
compileTranslationsFromWeblate ( )
2021-05-31 12:51:29 +02:00
}
2022-02-14 20:09:17 +01:00
genTranslations ( )
2022-09-08 21:40:48 +02: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!" )