File

lib/common/src/translations/nong/translators/string.translator.ts

Description

Replaces translation keys with prior provided translations for a given language.

Extends

Translator

Index

Properties
Methods
Accessors

Constructor

constructor(scopedTranslationMaps: ScopedTranslationMap[])
Parameters :
Name Type Optional
scopedTranslationMaps ScopedTranslationMap[] No

Properties

translationMaps
Type : literal type
Default value : {}
Abstract compatibleLanguages
Type : string[] | null
Inherited from Translator
Defined in Translator:24

Indicates for which languages this Translator will work, Setting this value to an empty array [] means that this translator will not work for any language; TranslationService will ignore Translators like this and log a warning. Setting it to null though means, that it works for all languages or language independent (e.g. makes use of the browser's toLocaleString() ).

Be careful not to add a translator that can only handle a few languages, because TranslationService determines the overall available languages as those that are supported by all translators. Having translatorA.compatibleLanguages = ['de'] and translatorB.compatibleLanguages = ['en'] will result in no language to be available overall.

Can be implemented as a getter if need be.

Methods

addTranslation
addTranslation(translationMap: TranslationMap, language: string, scope: string)

Add new translations to the central dictionary for a specific language. Specifiying a scope will prepend the scope to all translation keys.

Parameters :
Name Type Optional Default value
translationMap TranslationMap No
language string No
scope string No ''
Returns : void
deflateMap
deflateMap(key: TranslationKey, translationMap: TranslationMap)

Splits the key into its parts and feeds them recursively into the tanslationMap returning a sub map, a translation or null if no matching sub map or translation exist.

    let key = 'SOME_SCOPE.SECTION.MY_KEY'
    let stringTranslator = new StringTranslator()


    stringTranslator.deflateMap(key, translationMap)
    // yields: translationMap['SOME_SCOPE']['SECTION']['MY_KEY']
Parameters :
Name Type Optional
key TranslationKey No
translationMap TranslationMap No
inflateMap
inflateMap(key: TranslationKey, value: string | TranslationMap)

Splits the key into its parts and recursively prepends them to the given translation map or string.

    let key = 'SOME_SCOPE.SECTION.MY_KEY'

    stringTranslator.inflateMap(key, 'Hello World!')
    // yields: { 'SOME_SCOPE': {'SECTION': { 'MY_KEY': 'Hello World"'} } } }

    stringTranslator.inflateMap(key, {'GREETING':Hello World!'} )
    // yields: { 'SOME_SCOPE': {'SECTION': { 'MY_KEY': {'GREETING':Hello World!'}} } } }
Parameters :
Name Type Optional
key TranslationKey No
value string | TranslationMap No
Returns : TranslationMap
match
match(x)
Inherited from Translator
Defined in Translator:177
Parameters :
Name Optional
x No
Returns : number
translate
translate(input: string, language: string, param?: Record, recursionCount: number)
Inherited from Translator
Defined in Translator:210

Looks up the entry for key and language in the central dictionary and returns it. A translation may have replaceable substrings. Those substrings can be a property name {{somePropertyName}} or another translation key {{SOME_SCOPE.MY_KEY}}. If such a substring matches a property of param, the substring will be replaced by that property. If a substring matches a translation key, the substring will be replaced by the respective translation. Params take prescedence.

    let stringTranslator = new StringTranslator()

    stringTranslator.translate('SOME_SCOPE.GREETING', 'en')
    // May give something like 'Hello {{address}}!'

    stringTranslator.translate('SOME_SCOPE.GREETING', 'en', {address:'world'})
    // Will then give 'Hello world!'

Parameters :
Name Type Optional Default value
input string No
language string No
param Record<string | > Yes
recursionCount number No 0
Returns : TranslationResult
Public hash
hash(input, param?: Record)
Inherited from Translator
Defined in Translator:70

Creates a string value for any given input. Two inputs that have different translations are expected to have different hashes. This way we can check if a translation needs to be updated without repeating the whole translation process.

Parameters :
Name Type Optional
input No
param Record<string | > Yes
Returns : string | null

Accessors

compatibleLanguages
getcompatibleLanguages()
import	{	
			deflateObject,
			inflateObject,	
			NestedRecord,
			mergeObjects,
			isNestedRecord,
			assert,
			camel2Underscore
		}								from '@rcc/core'

import	{ 	Translator 				} 	from '../translator.class'

import	{ 	
			TranslationResult,
			isFinal		
		}								from '../interfaces'




// ### Interfaces and ultilities:


export type TranslationMap  = NestedRecord<string>

export function isTranslationMap(x:unknown): x is TranslationMap {
	return isNestedRecord(x, 'string')
}


export interface ScopedTranslationMap {
	scope: 			string | null
	translationMap:	{ [lang: string] : TranslationMap }
}


export type TranslationKey 		= string & { __isTranslationKey?: true}
export type TranslationTemplate	= string & { __isTranslationTemplate?: true}




export function isTranslationKey(key: string): key is TranslationKey {

	if(typeof key != 'string') 		return false

	if(key == '')					return false

	if(key.match(/^[A-Z0-9_.-]+$/))	return true

	return false
}



export function isTranslationTemplate(str: string): str is TranslationTemplate {
	
	if(typeof str != 'string') 		return false

	if(str == '')					return false	

	if(str.match(/{{[^{}]+}}/))		return true

	return false
}


/**
 * Strips everything, that is not a capital letter, underscore, dot,  a digit or a curley bracket, off a given string.
 * If the result would be an empty string (thus no translation key) '_EMPTY_KEY' is returned instead and a warning logged.
 **/
export function normalizeTranslationKey(str:string) : TranslationKey {

	const normalized_str = camel2Underscore(str).toUpperCase().replace(/[^A-Z0-9_.]/g,"")

	if(isTranslationKey(normalized_str)) return normalized_str

	console.warn(`normalizeTranslationKey() results in empty key for '${str}'.`)	

	return 	'_EMPTY_KEY'
}




/**
 * This function is meant for basic replacements in translation strings. 
 * Occurences of %_n_ , with _n_ being some integer, will be replaced by the _n_-th paramter, starting at 1. 
 * Occurences of %_s_ with _s_ being a single lowercase letter, will be replaced by the parameters in the order of their appearance; 
 * same lower case letters will be replaced by different values. 
 * The replacement text will only include capital letters, digits, dots and underscores, everything else will be stripped off:
 * ```
 * 	extendTranslationKey('SOME_SCOPE.%s' | fill: paused ? 'player_running':'player_paused') == 'SOME_SCOPE.PLAYER_PAUSED' //true iff pause is truthy.
 * 
 *	extendTranslationKey('SOME_SCOPE.GROUP_%2.LABEL_%1' | fill :  'long', 1) == 'SOME_SCOPE.GROUP_1.LABEL_LONG' //true
 * 
 * 	extendTranslationKey('SOME_SCOPE.GROUP_%s.LABEL_%t' | fill :  'long', 1) == 'SOME_SCOPE.GROUP_LONG.LABEL_1' //true
 * 
 * ```
 **/

export function extendTranslationKey(key: TranslationKey, ...content: (string|number)[] ): TranslationKey {

	if(!key) return ''

	let pos = 0

	key = key.replace(/%[a-z]+/g, () => String(content[pos++] || '') )

	content.forEach( (item, i) => {
		key = key.replace(new RegExp(`%${i+1}`,'g'), String(item))
	})

	return normalizeTranslationKey(key)
}








// ### Translator class:




/**
 * Replaces translation keys with prior provided translations for a given language.
 **/

export class StringTranslator extends Translator {


	translationMaps : { [index:string] : TranslationMap } = {}


	constructor(
		scopedTranslationMaps : ScopedTranslationMap[],
	){
		super()



		// Without any translation maps this translator cannot do anything.
		assert(scopedTranslationMaps, "StringTranslator.constructor(): missing scopedTranslationsMaps.")


		// Setup the central dictionary for all string translations:
		scopedTranslationMaps.forEach( scopedTranslationMap => {

			const { scope, translationMap } = scopedTranslationMap						

			for(const language in translationMap ){ 
				this.addTranslation(translationMap[language], language, scope) 
			} 

		})

	}

	get compatibleLanguages() : string[] {

		const languages = Object.keys(this.translationMaps)
		

		return 	languages.length == 0
				// This can happens if no translationMap was ever provided,
				// so nothing needs to be translated and this translator is
				// compatible with all langauges. -.OO.-
				?	null
				:	languages

	}

	match(x: unknown): number {

		if(typeof x != 'string') 		return -1
		if(x == '')						return -1


		if(isTranslationKey(x)) 		return 1
		if(isTranslationTemplate(x))	return 1

		return	0

	}

	/**
	 * Looks up the entry for key and language in the central dictionary and returns it.
	 * A translation may have replaceable substrings. Those substrings can be a property name 
	 * `{{somePropertyName}}` or another translation key `{{SOME_SCOPE.MY_KEY}}`.
	 * If such a substring matches a property of param, the substring will be replaced by that property.
	 * If a substring matches a translation key, the substring will be replaced by the respective translation.
	 * Params take prescedence.
	 * 
	 * ```js
	 * 	let stringTranslator = new StringTranslator()
	 * 
	 * 	stringTranslator.translate('SOME_SCOPE.GREETING', 'en') 					
	 *	// May give something like 'Hello {{address}}!'
	 * 
	 * 	stringTranslator.translate('SOME_SCOPE.GREETING', 'en', {address:'world'}) 	
	 *	// Will then give 'Hello world!'
	 *	
	 * 
	 * ```
	 **/
	translate(input: string, language: string, param?: Record<string, unknown>, recursionCount = 0): TranslationResult {

		//recursive

		// If we can't handle the input with this translator:
		if(this.match(input) < 0) return null

		
		//TODO: maybe look for a way not to call match on every recursion call

		if(recursionCount > 20) {
			console.warn('StringTranslator.translate() recursionCount greater than 20; stopping recursion: '+input)
			return null
		}

		// Check if input is a translation key, that is registered in the central dictionary:
		if(isTranslationKey(input)) {

			const preliminary_translation = this.deflateMap(input, this.translationMaps[language])

			if(typeof preliminary_translation != 'string') return null //no matching translation found

			return this.translate(preliminary_translation, language, param, recursionCount++) //maybe the preliminary translation includes new translation keys...

		}


		// Check if the string is a translation template. (if so it probably comes from a recursion step)
		if(isTranslationTemplate(input)){

			const translation  =	input.replace(

										/{{([^}]+)}}/g, 

										(match: string, sub_match: string) =>	{

											sub_match 				= 	sub_match.trim()

											const param_value 		= 	deflateObject(param, sub_match)

											const sub_value 		= 	param_value !== undefined
																		?	String(param_value)	// use params to fill in variables
																		:	sub_match			// use dictionary to fill in variables
																	
											const sub_translation	= 	this.translate(sub_value, language, param, recursionCount++)

											return	isFinal(sub_translation)
													?	sub_translation.final
													:	sub_value								
										}
									)

			return	{ final: translation }
					

		}

		return { final: input }
	}


	/**
	 * Add new translations to the central dictionary for a specific language.
	 * Specifiying a scope will prepend the scope to all translation keys.
	 **/
	addTranslation(translationMap : TranslationMap, language: string, scope : string = '') : void {

		if(!scope && typeof translationMap == 'string'){
			console.warn('StringTranslator.addTranslation() scope must not be empty, if translationMap is a string;', translationMap, language, scope)
			return;
		}

		const root_table	= 	scope
								?	this.inflateMap(scope, translationMap)
								:	translationMap

		const mergedTable 	=	mergeObjects(this.translationMaps[language] || {}, root_table)

		this.translationMaps[language] = mergedTable
	}

	
	/**
	 * Splits the key into its parts and feeds them recursively into the
	 * tanslationMap returning a sub map, a translation or null if no matching sub map or translation exist.
	 * ```js
	 * 	let key = 'SOME_SCOPE.SECTION.MY_KEY'	
	 * 	let stringTranslator = new StringTranslator()
	 * 		
	 * 
	 * 	stringTranslator.deflateMap(key, translationMap) 
	 *	// yields: translationMap['SOME_SCOPE']['SECTION']['MY_KEY']
	 * ```
	 **/
	deflateMap(key: TranslationKey, translationMap: TranslationMap): string | TranslationMap | null {
		
		return deflateObject(translationMap, key) || null

	}

	/**
	 * Splits the key into its parts and recursively prepends them to the given translation map or string.
	 * 
	 * ```js
	 * 	let key = 'SOME_SCOPE.SECTION.MY_KEY'	
	 * 	
	 * 	stringTranslator.inflateMap(key, 'Hello World!') 
	 * 	// yields: { 'SOME_SCOPE': {'SECTION': { 'MY_KEY': 'Hello World"'} } } }
	 * 
	 * 	stringTranslator.inflateMap(key, {'GREETING':Hello World!'} ) 
	 * 	// yields: { 'SOME_SCOPE': {'SECTION': { 'MY_KEY': {'GREETING':Hello World!'}} } } }
	 * ```
	 **/

	inflateMap(key: TranslationKey, value: string | TranslationMap): TranslationMap {
		
		return 	inflateObject(value, key)
				
	}


}

results matching ""

    No results matching ""