lib/common/src/translations/nong/translation-service.class.ts
This service manages all translation related tasks. It is rather basic, but extendenable. Heavy lifting is done in StringTranslator.
In order to extend its capabilities add custom extensions of Translator to the constructor call.
TODO: default Language hardcoded
Properties |
|
Methods |
Accessors |
constructor(translators: Translator[], defaultLanguage?: string)
|
|||||||||
Parameters :
|
Public defaultLanguage |
Type : string
|
Default value : 'en'
|
This language will be used as initial active language. |
Public translators |
Type : Translator[]
|
Default value : []
|
Service instances of all registred Translators. |
Public addTranslator | ||||||
addTranslator(translator: Translator)
|
||||||
Adds a translator to the pool of translators. Checks if the translator is compatible with at least one language.
Parameters :
Returns :
void
|
getMatchingTranslator | ||||
getMatchingTranslator(input)
|
||||
Find all matching translators for the given input and returns them in order of their matching score (best first).
Parameters :
Returns :
Translator[]
|
getTranslationData | ||||||||||||||||||||
getTranslationData(input, param?: Record
|
||||||||||||||||||||
Tries to translate any given input, by finding a matching translator and returing that translator's translation. Returns the translation result and the associated Translator.
Parameters :
|
translate | ||||||||||||||||
translate(input, param?: Record
|
||||||||||||||||
Tries to translate any given input, by finding a matching translator and returing that translator's translation. Returns null, if the input cannot be handled by any translator.
Parameters :
Returns :
string | null
|
activeLanguage | ||||||
getactiveLanguage()
|
||||||
setactiveLanguage(lang: string)
|
||||||
Parameters :
Returns :
void
|
import {
BehaviorSubject,
} from 'rxjs'
import {
throwError
} from '@rcc/core'
import {
Translator,
} from './translator.class'
import {
isFinal,
isIntermediate
} from './interfaces'
//TODO settings
/**
* This service manages all translation related tasks. It is rather basic, but extendenable.
* Heavy lifting is done in {@link StringTranslator}.
*
* In order to extend its capabilities add custom extensions of {@link Translator} to the constructor call.
*
* TODO: default Language hardcoded
*
*/
export class TranslationService {
/**
* The currently set language; all translations will default to this language,
* when no other language is specified.
* Languages are denoted by language code (i.e. 'en', 'de', 'fr', ...)
*
* Emits on subscribe and when the active language is switched to a new one.
*/
public activeLanguageChange$ : BehaviorSubject<string>
/**
* Service instances of all registred Translators.
*/
public translators : Translator[] = []
/**
* This language will be used as initial active language.
*/
public defaultLanguage : string = 'en'
constructor(
translators : Translator[],
defaultLanguage? : string
){
this.defaultLanguage = defaultLanguage || this.defaultLanguage
this.activeLanguageChange$ = new BehaviorSubject<string>(this.defaultLanguage)
// Without any translators, this service can't do anything:
translators || throwError('RccTranslationService.constructor() missing translators.')
translators.length || throwError('RccTranslationService.constructor() translators empyt or not an array.')
translators.forEach( translator => this.addTranslator(translator) )
}
set activeLanguage( lang: string ){
this.activeLanguageChange$.next(lang)
}
get activeLanguage(): string {
return this.activeLanguageChange$.getValue()
}
/**
* Adds a translator to the pool of translators. Checks if the translator is compatible with at least one language.
*/
public addTranslator( translator: Translator): void {
const className = translator.constructor.name
// Translators with no language restrictions will be added without further checks:
if(translator.compatibleLanguages === null) { this.translators.push(translator); return }
// Translators with bad compatibleLanguages property:
Array.isArray(translator.compatibleLanguages) || throwError(`RccTranslationService.addTranslator(): ${className}.compatibleLanguages must either be null or an array.`, translator)
translator.compatibleLanguages.length > 0 || throwError(`RccTranslationService.addTranslator(): ${className}.compatibleLanguages must not be empty.`, translator)
// For translators restricted to some languages:
this.translators.push(translator)
// Everything still works for *all* languages:
if(this.availableLanguages == null) return;
// After adding the Translator, no more languages are available:
if(this.availableLanguages.length == 0) throwError(`RccTranslationService.addTranslator(): no availble languages after adding ${className}.`, this.translators)
// At this point the TranslationService is restricted to some languages,
// but can hanlde at least one. This should be the most common case.
}
/**
* Checks all translators for compatible languages. Only those languages
* that are compatible with all translators will be recognized as overall available.
*/
get availableLanguages(): null | string[] {
// All translators, that only work for some languages:
const restrictedTranslators = this.translators
.filter( translator => Array.isArray(translator.compatibleLanguages) )
// Languages some translators are restricted to:
const conspicuousLanguages = restrictedTranslators
.map( translator => translator.compatibleLanguages || [] )
.flat()
.filter( (x,i,a) => a.indexOf(x) == i)
// This is a bit silly, it only happens when all translators are working with all languages
// That's usually not the case; StringTranslator will allways be restricted to some languages
// and that's the most prominent translator.
if(conspicuousLanguages.length == 0) return null
const availableLanguages = conspicuousLanguages
.filter( language =>
restrictedTranslators
.every( translator =>
translator.compatibleLanguages?.includes(language)
)
)
return availableLanguages
}
/**
* Find all matching translators for the given input and returns them in order
* of their matching score (best first).
*/
getMatchingTranslator(input: unknown): Translator[] {
const translators_with_priority = this.translators
.map( translator => ({
translator,
priority: translator.match(input)
}))
const ranked_matching_translators = translators_with_priority
.filter( ({priority}) => priority >= 0)
.sort( ( {priority : p1}, {priority : p2} ) => {
if(p1 < p2) return 1
if(p1 > p2) return -1
return 0
})
.map( ({translator}) => translator )
return ranked_matching_translators
}
/**
* Tries to translate any given input, by finding a matching translator and returing that translator's translation.
*
* Returns the translation result and the associated {@link Translator}.
**/
getTranslationData(input: unknown, param?: Record<string,unknown>, language: string = this.activeLanguage, recursionCount = 0) : [string, Translator] {
if(recursionCount > 20) {
console.warn('TranslationService.translate() recursionCount > 20.')
return null
}
const translators = this.getMatchingTranslator(input)
let translation = null
// Try translators one by one starting with the best match:
for(const translator of translators){
translation = translator.translate(input, language, param)
if( isFinal(translation) ) return [ translation.final, translator]
if( isIntermediate(translation) ) return [
this.getTranslationData(translation.intermediate, param, language)[0],
translator
]
}
return [String(input), null]
}
/**
* Tries to translate any given input, by finding a matching translator and returing that translator's translation.
*
* Returns null, if the input cannot be handled by any translator.
**/
translate(input: unknown, param?: Record<string,unknown>, language: string = this.activeLanguage): string | null {
const [text] = this.getTranslationData(input, param, language)
return text
}
}