All files / utils/translation-query translation-query.util.js

100% Statements 218/218
100% Branches 27/27
100% Functions 9/9
100% Lines 218/218

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 2191x 1x 1x 1x 18x 18x 18x 18x 18x 1x 1x 1x 1x 1x 1x 18x 18x 18x 18x 18x 71x 71x 56x 56x 56x 15x 15x 15x 15x 15x 15x 15x 15x 15x 15x 18x 18x 18x 18x 18x 1x 1x 1x 1x 1x 1x 1x 1x 24x 24x 24x 18x 18x 18x 18x 18x 18x 24x 24x 1x 1x 1x 1x 1x 1x 1x 6x 6x 6x 6x 6x 6x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 17x 17x 17x 17x 17x 17x 1x 1x 1x 1x 1x 1x 14x 14x 14x 11x 11x 11x 8x 8x 8x 8x 8x 11x 6x 6x 1x 1x 1x 1x 1x 1x 1x 1x 24x 24x 24x 23x 24x 9x 9x 9x 9x 14x 24x 8x 8x 8x 8x 6x 6x 6x 1x 1x 1x 1x 1x 1x 1x 1x 17x 17x 17x 17x 17x 15x 15x 15x 17x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
import { parseKey } from '../../key-parser/key-parser.util.js'
import { parseValue } from '../../key-parser/value-parser.util.js'
 
const initOptimizedTranslationsObject = () => /** @type {OptimizedTranslations} */ ({
  literalKeys: {},
  templateKeys: {},
  sortedTemplateKeys: [],
  prefixTemplateSearchByWords: {},
})
 
/**
 * Creates an {@link OptimizedTranslations} based on target {@link Translations}
 * @param {Translations} translations - target translations
 * @returns {OptimizedTranslations} resulting optimized translation
 */
function optimizeTranslationForQueries (translations) {
  const result = initOptimizedTranslationsObject()
  const { literalKeys, templateKeys, sortedTemplateKeys, prefixTemplateSearchByWords } = result
 
  for (const [key, value] of Object.entries(translations)) {
    const parsedKey = parseKey(key)
    if (parsedKey.priority[0] <= 0) {
      literalKeys[key] = value
      continue
    }
 
    const optimizedTemplateKey = { key, parsedKey, value }
 
    templateKeys[key] = optimizedTemplateKey
    sortedTemplateKeys.push(optimizedTemplateKey)
    const prefix = parsedKey.ast.tokens[0].text
 
    prefixTemplateSearchByWords[prefix] ||= {}
    prefixTemplateSearchByWords[prefix][key] = value
  }
 
  sortedTemplateKeys.sort((a, b) => b.parsedKey.priorityAsNumber - a.parsedKey.priorityAsNumber)
 
  return result
}
 
/** @type {WeakMap<Translations, TranslationQueryOptimization>} */
const translationOptimizations = new WeakMap()
 
/**
 * @param {Translations} translations - target translations
 * @returns {TranslationQueryOptimization} translation query optimization object
 */
function getOrInitOptimizations (translations) {
  let optimization = translationOptimizations.get(translations)
  if (!optimization) {
    optimization = {
      cache: {},
      optimizedMap: optimizeTranslationForQueries(translations),
    }
    translationOptimizations.set(translations, optimization)
  }
  return optimization
}
 
/**
 * @param {string} key - target key used in query
 * @param {Translations} translations - translation object used to query
 * @returns {QueryResult} built not found query result
 */
const notFoundQueryResult = (key, translations) => ({
  targetKey: key,
  translations,
  found: false,
  valueTemplate: '',
  translate: () => key,
})
 
/**
 *
 * @param {string} key - target key used in query
 * @param {Translations} translations - translation object used to query
 * @param {string} valueTemplate - found value template
 * @param {MatchResult} [matchResult] - match result data, optional
 * @returns {QueryResult} built found query result
 */
const foundQueryResult = (key, translations, valueTemplate, matchResult) => ({
  targetKey: key,
  translations,
  found: true,
  valueTemplate,
  translate: translatorFromValue(valueTemplate, matchResult),
})
 
/**
 * @param {string} key - target key
 * @param {OptimizedTranslations} optimizedMap - target optimized translation map
 * @returns {{templateKey: OptimizedTemplateKey, matchResult: MatchResult} | null} matching template key or null if not found
 */
function findMatchingTemplateKey (key, optimizedMap) {
  const { templateKeys } = optimizedMap
  for (const { key: templateKeyStr } of optimizedMap.sortedTemplateKeys) {
    const templateKey = templateKeys[templateKeyStr]
    const matchResult = templateKey.parsedKey.match(key)
    if (matchResult.isMatch) {
      return {
        templateKey,
        matchResult,
      }
    }
  }
  return null
}
 
/**
 * Queries translation value from a {@link Translations} object
 * @param {string}       key          - target key
 * @param {Translations} translations - target {@link Translations} object to search
 * @returns {QueryResult} result of the query
 */
export function queryFromTranslations (key, translations) {
  const { cache, optimizedMap } = getOrInitOptimizations(translations)
 
  if (cache[key] != null) { return cache[key] }
 
  if (optimizedMap.literalKeys[key] != null) {
    const valueTemplate = optimizedMap.literalKeys[key]
    cache[key] = foundQueryResult(key, translations, valueTemplate)
    return cache[key]
  }
  const matchingTemplateKey = findMatchingTemplateKey(key, optimizedMap)
  if (matchingTemplateKey) {
    const valueTemplate = matchingTemplateKey.templateKey.value
    cache[key] = foundQueryResult(key, translations, valueTemplate, matchingTemplateKey.matchResult)
    return cache[key]
  }
 
  return notFoundQueryResult(key, translations)
}
 
/**
 * Gets the translate function from value template and match result, in case match Result is undefined
 * it will assume it came from a literal key match
 * @param {string} valueTemplate target match result
 * @param {MatchResult} [match] target match result
 * @returns {TranslateFunction} translate function from targetMatch
 */
function translatorFromValue (valueTemplate, match) {
  const parameters = match?.parameters ?? []
  const defaultFormatters = match?.defaultFormatters ?? []
  let value
  return (locale) => {
    value ??= parseValue(valueTemplate)
    return value.format(parameters, locale, defaultFormatters)
  }
}
 
/**
 * @typedef {string} TranslationValue
 *
 * Translation value, it is a separate type since it is expected to change.
 *
 * The current plan is in the future to chang to {
 *    value: string,
 *    kind: "raw" | "template" | "import" | "import template"
 * }
 */
 
/**
 * @typedef {ReturnType<ReturnType<typeof parseKey>["match"]>} MatchResult
 */
 
/**
 * @typedef {{[key: string]: TranslationValue}} Translations
 *
 * Translation map
 */
 
/**
 * @typedef {object} TranslationQueryOptimization
 *
 * An object used to optimize queries from a {@link Translations} object, it is generated the fist time it is called
 * {@link queryFromTranslations} for each new {@link Translations} object
 * @property {{[key: string]: QueryResult}}  cache        - query result cache map used for memoization
 * @property {OptimizedTranslations}        optimizedMap - optimized translation map @see OptimizedTranslations
 */
 
/**
 * @typedef {object} QueryResult
 *
 * Result of queryFromTranslations
 * @property {string}             targetKey      - key used to search translation
 * @property {Translations}       translations   - translation map used for search
 * @property {boolean}            found          - boolean that tells whether the key was found
 * @property {string}             valueTemplate  - template of found value from query, empty string if not found
 * @property {TranslateFunction}  translate      - translate function based on locale, returns target key if not found
 */
 
/**
 * @callback TranslateFunction
 * @param {Intl.Locale} locale - locale used to translate
 * @returns {string} translated content
 */
 
/**
 * @typedef {object} OptimizedTranslations
 *
 *   A {@link Translations} object adapted to improve query speed
 * @property {Translations}                          literalKeys                 -   It contains only non-template keys, since they have the highest
 *  priority it will be use for a quick search before searching the remaining keys, which all are template keys
 * @property {{[key: string]: OptimizedTemplateKey}} templateKeys   - A map of "template key" to "optimized template info" with already computed information
 * @property {OptimizedTemplateKey[]}           sortedTemplateKeys -  A list of of template keys sorted by priority
 * @property {{[prefix: string]: Translations}}    prefixTemplateSearchByWords - A map of translations by prefix, unused, @todo use it
 */
 
/**
 * @typedef {object} OptimizedTemplateKey
 *
 * An optimized template key entry with already parsed key as to avoid parsing it again every query
 * @property {string}                      key       - target translation key
 * @property {ReturnType<typeof parseKey>} parsedKey - parsed target translation key information for faster matches
 * @property {string}                      value     - respective value of translation key
 */