/** * Take an entry for the Parser and turn it into a hash, * turning the key path 'foo.bar' into an hash {foo: {bar: ""}} * The generated hash can be merged with an optional `target`. * @returns An `{ target, duplicate, conflict }` object. `target` is the hash * that was passed as an argument or a new hash if none was passed. `duplicate` * indicates whether the entry already existed in the `target` hash. `conflict` * is `"key"` if a parent of the key was already mapped to a string (e.g. when * merging entry {one: {two: "bla"}} with target {one: "bla"}) or the key was * already mapped to a map (e.g. when merging entry {one: "bla"} with target * {one: {two: "bla"}}), `"value"` if the same key already exists swith a * different value, or `false`. */ function dotPathToHash(entry, target = {}, options = {}) { let path = entry.keyWithNamespace if (options.suffix || options.suffix === 0) { path += `${options.pluralSeparator}${options.suffix}` } const separator = options.separator || '.' const key = entry.keyWithNamespace.substring( entry.keyWithNamespace.indexOf(separator) + separator.length, entry.keyWithNamespace.length ) const defaultValue = typeof options.value === 'function' ? options.value(options.locale, entry.namespace, key) : options.value const skipDefaultValues = typeof options.skipDefaultValues === 'function' ? options.skipDefaultValues(options.locale, entry.namespace) : options.skipDefaultValues const useKeysAsDefaultValue = typeof options.useKeysAsDefaultValue === 'function' ? options.useKeysAsDefaultValue(options.locale, entry.namespace) : options.useKeysAsDefaultValue let newValue = entry[`defaultValue_${options.suffix}`] || entry.defaultValue || defaultValue || '' if (skipDefaultValues) { newValue = '' } if (useKeysAsDefaultValue) { newValue = key } if (path.endsWith(separator)) { path = path.slice(0, -separator.length) } const segments = path.split(separator) let inner = target let conflict = false for (let i = 0; i < segments.length - 1; i += 1) { const segment = segments[i] if (segment) { if (typeof inner[segment] === 'string') { conflict = 'key' } if (inner[segment] === undefined || conflict) { inner[segment] = {} } inner = inner[segment] } } const lastSegment = segments[segments.length - 1] const oldValue = inner[lastSegment] if (oldValue !== undefined && oldValue !== newValue) { conflict = typeof oldValue !== typeof newValue ? 'key' : 'value' } const duplicate = oldValue !== undefined || conflict !== false if (options.customValueTemplate) { inner[lastSegment] = {} const entries = Object.entries(options.customValueTemplate) entries.forEach((valueEntry) => { if (valueEntry[1] === '${defaultValue}') { inner[lastSegment][valueEntry[0]] = newValue } else { inner[lastSegment][valueEntry[0]] = entry[valueEntry[1].replace(/\${(\w+)}/, '$1')] || '' } }) } else { inner[lastSegment] = newValue } return { target, duplicate, conflict } } /** * Takes a `source` hash and makes sure its value * is pasted in the `target` hash, if the target * hash has the corresponding key (or if `keepRemoved` is true). * @returns An `{ old, new, mergeCount, pullCount, oldCount }` object. * `old` is a hash of values that have not been merged into `target`. * `new` is `target`. `mergeCount` is the number of keys merged into * `new`, `pullCount` is the number of context and plural keys added to * `new` and `oldCount` is the number of keys that were either added to `old` or * `new` (if `keepRemoved` is true and `target` didn't have the corresponding * key). */ function mergeHashes(source, target, keepRemoved = false) { let old = {} let mergeCount = 0 let pullCount = 0 let oldCount = 0 for (const key in source) { const hasNestedEntries = typeof target[key] === 'object' && !Array.isArray(target[key]) if (hasNestedEntries) { const nested = mergeHashes(source[key], target[key], keepRemoved) mergeCount += nested.mergeCount pullCount += nested.pullCount if (Object.keys(nested.old).length) { old[key] = nested.old oldCount += nested.oldCount } } else { if (target[key] !== undefined) { if (typeof source[key] === 'string' || Array.isArray(source[key])) { target[key] = source[key] mergeCount += 1 } else { old[key] = source[key] oldCount += 1 } } else { // support for plural in keys const pluralRegex = /(_plural)|(_\d+)$/ const pluralMatch = pluralRegex.test(key) const singularKey = key.replace(pluralRegex, '') // support for context in keys const contextRegex = /_([^_]+)?$/ const contextMatch = contextRegex.test(singularKey) const rawKey = singularKey.replace(contextRegex, '') if ( (contextMatch && target[rawKey] !== undefined) || (pluralMatch && target[singularKey] !== undefined) ) { target[key] = source[key] pullCount += 1 } else { if (keepRemoved) { target[key] = source[key] } else { old[key] = source[key] } oldCount += 1 } } } } return { old, new: target, mergeCount, pullCount, oldCount } } /** * Merge `source` into `target` by merging nested dictionaries. */ function transferValues(source, target) { for (const key in source) { const sourceValue = source[key] const targetValue = target[key] if ( typeof sourceValue === 'object' && typeof targetValue === 'object' && !Array.isArray(sourceValue) ) { transferValues(sourceValue, targetValue) } else { target[key] = sourceValue } } } export { dotPathToHash, mergeHashes, transferValues }