2022-09-08 21:40:48 +02:00
import Locale from "./Locale"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import LinkToWeblate from "../Base/LinkToWeblate"
2023-12-12 19:18:50 +01:00
import { Store } from "../../Logic/UIEventSource"
2020-11-06 01:58:26 +01:00
2021-06-10 01:36:20 +02:00
export class Translation extends BaseUIElement {
2022-09-08 21:40:48 +02:00
public static forcedLanguage = undefined
2020-11-06 01:58:26 +01:00
2022-06-24 16:47:00 +02:00
public readonly translations : Record < string , string >
2023-04-07 02:45:34 +02:00
public readonly context? : string
2023-12-12 19:18:50 +01:00
private onDestroy : ( ) = > void
2024-05-26 22:48:59 +02:00
/ * *
* If a text is needed to display and is not available in the requested language ,
* it will default to english and - if this is not available - give any language it has available .
*
* If strictLanguages is set , it ' ll return undefined instead
* @private
* /
private _strictLanguages : boolean
constructor ( translations : string | Record < string , string > , context? : string , strictLanguages? : boolean ) {
2021-06-10 01:36:20 +02:00
super ( )
2024-05-26 22:48:59 +02:00
this . _strictLanguages = strictLanguages
if ( strictLanguages ) {
console . log ( ">>> strict:" , translations )
}
2021-01-14 22:25:11 +01:00
if ( translations === undefined ) {
2022-09-08 21:40:48 +02:00
console . error ( "Translation without content at " + context )
2020-11-11 16:23:49 +01:00
throw ` Translation without content ( ${ context } ) `
}
2022-09-08 21:40:48 +02:00
this . context = translations [ "_context" ] ? ? context
2023-05-30 23:45:30 +02:00
2022-01-26 21:40:38 +01:00
if ( typeof translations === "string" ) {
2022-09-08 21:40:48 +02:00
translations = { "*" : translations }
2021-12-21 18:35:31 +01:00
}
2023-05-30 23:45:30 +02:00
2022-09-08 21:40:48 +02:00
let count = 0
2020-11-11 16:23:49 +01:00
for ( const translationsKey in translations ) {
2021-06-10 01:36:20 +02:00
if ( ! translations . hasOwnProperty ( translationsKey ) ) {
2021-06-01 21:24:35 +02:00
continue
}
2023-06-20 03:49:12 +02:00
if (
translationsKey === "_context" ||
translationsKey === "_meta" ||
translationsKey === "special"
) {
2022-04-06 17:28:51 +02:00
continue
}
2023-06-20 03:49:12 +02:00
2022-09-08 21:40:48 +02:00
count ++
if ( typeof translations [ translationsKey ] != "string" ) {
2023-04-15 02:28:24 +02:00
console . error (
"Non-string object at" ,
context ,
2024-01-25 03:13:18 +01:00
"of type" ,
typeof translations [ translationsKey ] ,
2023-06-20 03:14:18 +02:00
` for language ` ,
translationsKey ,
2024-01-25 03:13:18 +01:00
` . The offending object is: ` ,
2023-04-15 02:28:24 +02:00
translations [ translationsKey ] ,
"\n current translations are: " ,
translations
)
2022-09-08 21:40:48 +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"
)
2021-04-10 03:50:44 +02:00
}
2020-11-11 16:23:49 +01:00
}
2022-09-08 21:40:48 +02:00
this . translations = translations
2021-01-14 22:25:11 +01:00
if ( count === 0 ) {
2022-09-08 21:40:48 +02:00
console . error (
"Constructing a translation, but the object containing translations is empty " +
2023-08-23 11:11:53 +02:00
( context ? ? "No context given" )
2022-09-08 21:40:48 +02:00
)
2020-11-11 16:23:49 +01:00
}
}
2024-01-25 03:13:18 +01:00
private _current : Store < string >
2024-05-26 22:48:59 +02:00
private _currentLanguage : Store < string >
/ * *
* Indicates what language is effectively returned by ` current ` .
* In most cases , this will be the language of choice , but if no translation is available , this will probably be ` en `
* /
get currentLang ( ) : Store < string > {
if ( ! this . _currentLanguage ) {
this . _currentLanguage = Locale . language . map (
( l ) = > this . actualLanguage ( l ) ,
2023-12-12 19:18:50 +01:00
[ ] ,
( f ) = > {
this . onDestroy = f
}
)
}
2024-05-26 22:48:59 +02:00
return this . _currentLanguage
}
get current ( ) : Store < string > {
if ( ! this . _current ) {
this . _current = this . currentLang . map ( l = > this . translations [ l ] )
}
2023-12-12 19:18:50 +01:00
return this . _current
}
2024-01-25 03:13:18 +01:00
get txt ( ) : string {
return this . textFor ( Translation . forcedLanguage ? ? Locale . language . data )
}
2022-09-08 21:40:48 +02:00
static ExtractAllTranslationsFrom (
object : any ,
context = ""
) : { context : string ; tr : Translation } [ ] {
const allTranslations : { context : string ; tr : Translation } [ ] = [ ]
2021-11-07 16:34:51 +01:00
for ( const key in object ) {
const v = object [ key ]
if ( v === undefined || v === null ) {
continue
}
if ( v instanceof Translation ) {
2022-09-08 21:40:48 +02:00
allTranslations . push ( { context : context + "." + key , tr : v } )
2021-11-07 16:34:51 +01:00
continue
}
if ( typeof v === "object" ) {
2022-09-08 21:40:48 +02:00
allTranslations . push (
. . . Translation . ExtractAllTranslationsFrom ( v , context + "." + key )
)
2021-11-07 16:34:51 +01:00
}
}
return allTranslations
}
2024-05-26 22:48:59 +02:00
static fromMap ( transl : Map < string , string > , strictLanguages : boolean = false ) {
2021-11-07 16:34:51 +01:00
const translations = { }
2024-05-26 22:48:59 +02:00
console . log ( "Strict:" , strictLanguages )
2022-09-08 21:40:48 +02:00
let hasTranslation = false
2021-11-07 16:34:51 +01:00
transl ? . forEach ( ( value , key ) = > {
translations [ key ] = value
hasTranslation = true
} )
if ( ! hasTranslation ) {
return undefined
}
2024-05-26 22:48:59 +02:00
return new Translation ( translations , undefined , strictLanguages )
2021-11-07 16:34:51 +01:00
}
2023-03-29 17:21:20 +02:00
public toString() {
return this . txt
}
2022-01-26 21:40:38 +01:00
Destroy() {
2022-09-08 21:40:48 +02:00
super . Destroy ( )
2023-12-12 19:18:50 +01:00
this . onDestroy ( )
2022-09-08 21:40:48 +02:00
this . isDestroyed = true
2022-01-26 21:40:38 +01:00
}
2024-05-26 22:48:59 +02:00
/ * *
* Which language will be effectively used for the given language of choice ?
* /
public actualLanguage ( language : string ) : "*" | string | undefined {
2021-01-14 22:25:11 +01:00
if ( this . translations [ "*" ] ) {
2024-05-26 22:48:59 +02:00
return "*"
2021-01-14 22:25:11 +01:00
}
2022-09-08 21:40:48 +02:00
const txt = this . translations [ language ]
2024-05-26 22:48:59 +02:00
if ( txt === undefined && this . _strictLanguages ) {
return undefined
}
if ( txt !== undefined ) {
return language
2021-01-14 22:25:11 +01:00
}
2024-05-26 22:48:59 +02:00
if ( this . translations [ "en" ] !== undefined ) {
return "en"
2021-01-14 22:25:11 +01:00
}
for ( const i in this . translations ) {
2022-09-08 21:40:48 +02:00
return this . translations [ i ] // Return a random language
2021-01-14 22:25:11 +01:00
}
console . error ( "Missing language " , Locale . language . data , "for" , this . translations )
2022-09-08 21:40:48 +02:00
return ""
2021-01-14 22:25:11 +01:00
}
2024-05-26 22:48:59 +02:00
public textFor ( language : string ) : string | undefined {
return this . translations [ this . actualLanguage ( language ) ]
}
2021-09-09 00:05:51 +02:00
2021-06-10 01:36:20 +02:00
InnerConstructElement ( ) : HTMLElement {
const el = document . createElement ( "span" )
2022-01-06 18:51:52 +01:00
const self = this
2024-05-26 22:48:59 +02:00
if ( self . txt ) {
el . innerHTML = self . txt
}
2022-06-30 03:07:54 +02:00
if ( self . translations [ "*" ] !== undefined ) {
2022-09-08 21:40:48 +02:00
return el
2022-06-30 03:07:54 +02:00
}
2022-09-08 21:40:48 +02:00
Locale . language . addCallback ( ( _ ) = > {
2022-01-26 21:40:38 +01:00
if ( self . isDestroyed ) {
2022-01-06 18:51:52 +01:00
return true
}
2024-05-26 22:48:59 +02:00
if ( self . txt === undefined ) {
el . innerHTML = ""
} else {
el . innerHTML = self . txt
}
2021-06-10 01:36:20 +02:00
} )
2022-09-08 21:40:48 +02:00
if ( self . context === undefined || self . context ? . indexOf ( ":" ) < 0 ) {
return el
2022-04-01 12:51:55 +02:00
}
2022-06-30 03:07:54 +02:00
2022-04-01 12:51:55 +02:00
const wrapper = document . createElement ( "span" )
wrapper . appendChild ( el )
2022-09-08 21:40:48 +02:00
Locale . showLinkToWeblate . addCallbackAndRun ( ( doShow ) = > {
2022-04-01 12:51:55 +02:00
if ( ! doShow ) {
2022-09-08 21:40:48 +02:00
return
2022-04-01 12:51:55 +02:00
}
2023-01-13 02:48:48 +01:00
const linkToWeblate = new LinkToWeblate ( self . context , self . translations )
2022-04-01 12:51:55 +02:00
wrapper . appendChild ( linkToWeblate . ConstructElement ( ) )
2022-09-08 21:40:48 +02:00
return true
2022-04-01 12:51:55 +02:00
} )
2022-09-08 21:40:48 +02:00
return wrapper
2021-06-10 01:36:20 +02:00
}
2021-01-14 22:25:11 +01:00
public SupportedLanguages ( ) : string [ ] {
const langs = [ ]
for ( const translationsKey in this . translations ) {
2021-06-10 01:36:20 +02:00
if ( ! this . translations . hasOwnProperty ( translationsKey ) ) {
2022-09-08 21:40:48 +02:00
continue
2021-06-10 01:36:20 +02:00
}
2021-04-09 02:57:06 +02:00
if ( translationsKey === "#" ) {
2022-09-08 21:40:48 +02:00
continue
2021-01-14 22:25:11 +01:00
}
2021-09-09 00:05:51 +02:00
if ( ! this . translations . hasOwnProperty ( translationsKey ) ) {
2021-06-08 18:54:29 +02:00
continue
}
2021-01-14 22:25:11 +01:00
langs . push ( translationsKey )
}
2022-09-08 21:40:48 +02:00
return langs
2021-01-14 22:25:11 +01:00
}
2022-01-26 21:40:38 +01:00
public AllValues ( ) : string [ ] {
2022-09-08 21:40:48 +02:00
return this . SupportedLanguages ( ) . map ( ( lng ) = > this . translations [ lng ] )
2021-12-05 02:06:14 +01:00
}
2022-06-30 03:07:54 +02:00
/ * *
* Constructs a new Translation where every contained string has been modified
* /
2022-09-08 21:40:48 +02:00
public OnEveryLanguage (
f : ( s : string , language : string ) = > string ,
context? : string
) : Translation {
const newTranslations = { }
2020-11-06 01:58:26 +01:00
for ( const lang in this . translations ) {
2021-06-10 01:36:20 +02:00
if ( ! this . translations . hasOwnProperty ( lang ) ) {
2022-09-08 21:40:48 +02:00
continue
2021-06-10 01:36:20 +02:00
}
2022-09-08 21:40:48 +02:00
newTranslations [ lang ] = f ( this . translations [ lang ] , lang )
2020-11-06 01:58:26 +01:00
}
2022-09-08 21:40:48 +02:00
return new Translation ( newTranslations , context ? ? this . context )
2020-11-06 01:58:26 +01:00
}
2022-09-08 21:40:48 +02:00
2022-03-15 01:42:38 +01:00
/ * *
* Replaces the given string with the given text in the language .
* Other substitutions are left in place
2022-09-08 21:40:48 +02:00
*
2022-03-15 01:42:38 +01:00
* const tr = new Translation (
2022-09-08 21:40:48 +02:00
* { "nl" : "Een voorbeeldtekst met {key} en {key1}, en nogmaals {key}" ,
2022-03-15 01:42:38 +01:00
* "en" : "Just a single {key}" } )
* const r = tr . replace ( "{key}" , "value" )
* r . textFor ( "nl" ) // => "Een voorbeeldtekst met value en {key1}, en nogmaals value"
* r . textFor ( "en" ) // => "Just a single value"
2022-09-08 21:40:48 +02:00
*
2022-03-15 01:42:38 +01:00
* /
2020-11-06 01:58:26 +01:00
public replace ( a : string , b : string ) {
2022-09-08 21:40:48 +02:00
return this . OnEveryLanguage ( ( str ) = > str . replace ( new RegExp ( a , "g" ) , b ) )
2020-11-06 01:58:26 +01:00
}
public Clone() {
2022-04-01 12:51:55 +02:00
return new Translation ( this . translations , this . context )
2020-11-06 01:58:26 +01:00
}
2023-09-02 00:46:17 +02:00
/ * *
* Build a new translation which only contains the first sentence of every language
* A sentence stops at either a dot ( ` . ` ) or a HTML - break ( '<br/>' ) .
* The dot or linebreak are _not_ returned .
*
* new Translation ( { "en" : "This is a sentence. This is another sentence" } ) . FirstSentence ( ) . textFor ( "en" ) // "This is a sentence"
* new Translation ( { "en" : "This is a sentence <br/> This is another sentence" } ) . FirstSentence ( ) . textFor ( "en" ) // "This is a sentence"
2023-09-02 02:04:59 +02:00
* new Translation ( { "en" : "This is a sentence <br> This is another sentence" } ) . FirstSentence ( ) . textFor ( "en" ) // "This is a sentence"
2023-09-02 00:46:17 +02:00
* new Translation ( { "en" : "This is a sentence with a <b>bold</b> word. This is another sentence" } ) . FirstSentence ( ) . textFor ( "en" ) // "This is a sentence with a <b>bold</b> word"
* @constructor
* /
public FirstSentence ( ) : Translation {
2022-09-08 21:40:48 +02:00
const tr = { }
2020-11-06 01:58:26 +01:00
for ( const lng in this . translations ) {
2021-06-10 01:36:20 +02:00
if ( ! this . translations . hasOwnProperty ( lng ) ) {
continue
}
2022-09-08 21:40:48 +02:00
let txt = this . translations [ lng ]
2023-09-26 01:25:28 +02:00
txt = txt . replace ( /(\.|<br\/>|<br>|。).*/ , "" )
2022-09-08 21:40:48 +02:00
txt = Utils . EllipsesAfter ( txt , 255 )
2023-09-02 00:46:17 +02:00
tr [ lng ] = txt . trim ( )
2020-11-06 01:58:26 +01:00
}
2022-09-08 21:40:48 +02:00
return new Translation ( tr )
2020-11-06 01:58:26 +01:00
}
2021-04-09 02:57:06 +02:00
2022-03-21 02:00:50 +01:00
/ * *
* Extracts all images ( including HTML - images ) from all the embedded translations
2022-09-08 21:40:48 +02:00
*
2022-03-21 02:00:50 +01:00
* // should detect sources of <img>
* const tr = new Translation ( { en : "XYZ <img src='a.svg'/> XYZ <img src=\"some image.svg\"></img> XYZ <img src=b.svg/>" } )
* new Set < string > ( tr . ExtractImages ( false ) ) // new Set(["a.svg", "b.svg", "some image.svg"])
* /
2021-04-09 02:57:06 +02:00
public ExtractImages ( isIcon = false ) : string [ ] {
const allIcons : string [ ] = [ ]
for ( const key in this . translations ) {
2021-06-10 01:36:20 +02:00
if ( ! this . translations . hasOwnProperty ( key ) ) {
2022-09-08 21:40:48 +02:00
continue
2021-06-10 01:36:20 +02:00
}
2021-04-09 02:57:06 +02:00
const render = this . translations [ key ]
if ( isIcon ) {
2022-09-08 21:40:48 +02:00
const icons = render
. split ( ";" )
. filter ( ( part ) = > part . match ( /(\.svg|\.png|\.jpg)$/ ) != null )
2021-04-09 02:57:06 +02:00
allIcons . push ( . . . icons )
2021-04-09 13:59:49 +02:00
} else if ( ! Utils . runningFromConsole ) {
2021-04-09 02:57:06 +02:00
// This might be a tagrendering containing some img as html
const htmlElement = document . createElement ( "div" )
htmlElement . innerHTML = render
2022-09-08 21:40:48 +02:00
const images = Array . from ( htmlElement . getElementsByTagName ( "img" ) ) . map (
( img ) = > img . src
)
2021-04-09 02:57:06 +02:00
allIcons . push ( . . . images )
2021-04-09 13:59:49 +02:00
} else {
// We are running this in ts-node (~= nodejs), and can not access document
// So, we fallback to simple regex
2021-04-10 03:50:44 +02:00
try {
const matches = render . match ( /<img[^>]+>/g )
if ( matches != null ) {
2022-09-08 21:40:48 +02:00
const sources = matches
. map ( ( img ) = > img . match ( /src=("[^"]+"|'[^']+'|[^/ ]+)/ ) )
. filter ( ( match ) = > match != null )
. map ( ( match ) = >
match [ 1 ] . trim ( ) . replace ( /^['"]/ , "" ) . replace ( /['"]$/ , "" )
)
2021-04-10 03:50:44 +02:00
allIcons . push ( . . . sources )
}
2021-04-11 19:21:41 +02:00
} catch ( e ) {
2021-04-10 03:50:44 +02:00
console . error ( "Could not search for images: " , render , this . txt )
throw e
2021-04-09 13:59:49 +02:00
}
2021-04-09 02:57:06 +02:00
}
}
2022-09-08 21:40:48 +02:00
return allIcons . filter ( ( icon ) = > icon != undefined )
2021-04-09 02:57:06 +02:00
}
2022-01-26 21:40:38 +01:00
2021-11-08 02:36:01 +01:00
AsMarkdown ( ) : string {
return this . txt
}
2022-04-13 01:19:28 +02:00
}
2023-03-24 19:21:15 +01:00
export class TypedTranslation < T extends Record < string , any > > extends Translation {
2022-06-24 16:47:00 +02:00
constructor ( translations : Record < string , string > , context? : string ) {
2022-09-08 21:40:48 +02:00
super ( translations , context )
2022-04-13 01:19:28 +02:00
}
2022-04-01 12:51:55 +02:00
2022-04-13 01:19:28 +02:00
/ * *
* Substitutes text in a translation .
* If a translation is passed , it ' ll be fused
*
* // Should replace simple keys
* new TypedTranslation < object > ( { "en" : "Some text {key}" } ) . Subs ( { key : "xyz" } ) . textFor ( "en" ) // => "Some text xyz"
*
* // Should fuse translations
* const subpart = new Translation ( { "en" : "subpart" , "nl" : "onderdeel" } )
* const tr = new TypedTranslation < object > ( { "en" : "Full sentence with {part}" , nl : "Volledige zin met {part}" } )
* const subbed = tr . Subs ( { part : subpart } )
* subbed . textFor ( "en" ) // => "Full sentence with subpart"
* subbed . textFor ( "nl" ) // => "Volledige zin met onderdeel"
2022-09-08 21:40:48 +02:00
*
2022-04-13 01:19:28 +02:00
* /
Subs ( text : T , context? : string ) : Translation {
2022-05-01 22:58:59 +02:00
return this . OnEveryLanguage ( ( template , lang ) = > {
2022-09-08 21:40:48 +02:00
if ( lang === "_context" ) {
2022-05-01 22:58:59 +02:00
return template
}
2022-09-08 21:40:48 +02:00
return Utils . SubstituteKeys ( template , text , lang )
2022-05-01 22:58:59 +02:00
} , context )
2022-04-13 01:19:28 +02:00
}
2022-07-25 16:55:44 +02:00
2022-09-08 21:40:48 +02:00
PartialSubs < X extends string > (
text : Partial < T > & Record < X , string >
) : TypedTranslation < Omit < T , X > > {
const newTranslations : Record < string , string > = { }
2022-07-25 16:55:44 +02:00
for ( const lang in this . translations ) {
const template = this . translations [ lang ]
2022-09-08 21:40:48 +02:00
if ( lang === "_context" ) {
newTranslations [ lang ] = template
2022-07-25 16:55:44 +02:00
continue
}
newTranslations [ lang ] = Utils . SubstituteKeys ( template , text , lang )
}
2022-09-08 21:40:48 +02:00
2022-07-25 16:55:44 +02:00
return new TypedTranslation < Omit < T , X > > ( newTranslations , this . context )
}
2024-01-25 03:13:18 +01:00
PartialSubsTr < K extends string > (
key : string ,
replaceWith : Translation
) : TypedTranslation < Omit < T , K > > {
const newTranslations : Record < string , string > = { }
const toSearch = "{" + key + "}"
const missingLanguages = new Set < string > ( Object . keys ( this . translations ) )
for ( const lang in this . translations ) {
missingLanguages . delete ( lang )
const template = this . translations [ lang ]
if ( lang === "_context" ) {
newTranslations [ lang ] = template
continue
}
const v = replaceWith . textFor ( lang )
newTranslations [ lang ] = template . replaceAll ( toSearch , v )
}
const baseTemplate = this . textFor ( "en" )
for ( const missingLanguage of missingLanguages ) {
newTranslations [ missingLanguage ] = baseTemplate . replaceAll (
toSearch ,
replaceWith . textFor ( missingLanguage )
)
}
return new TypedTranslation < Omit < T , K > > ( newTranslations , this . context )
}
2022-09-08 21:40:48 +02:00
}