Skip to content

Commit

Permalink
add resetDefaultValueLocale option
Browse files Browse the repository at this point in the history
  • Loading branch information
Lars Mortenson committed Nov 1, 2021
1 parent dc2ee33 commit f836445
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 18 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ module.exports = {
failOnWarnings: false,
// Exit with an exit code of 1 on warnings

customValueTemplate: null
customValueTemplate: null,
// If you wish to customize the value output the value as an object, you can set your own format.
// ${defaultValue} is the default value you set in your translation function.
// Any other custom property will be automatically extracted.
Expand All @@ -206,6 +206,12 @@ module.exports = {
// message: "${defaultValue}",
// description: "${maxLength}", // t('my-key', {maxLength: 150})
// }

resetDefaultValueLocale: null
// The locale to compare with default values to determine whether a default value has been changed.
// If this is set and a default value differs from a translation in the specified locale, all entries
// for that key across locales are reset to the default value, and existing translations are moved to
// the `_old` file.
}
```

Expand Down
57 changes: 43 additions & 14 deletions src/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,22 @@ function dotPathToHash(entry, target = {}, options = {}) {
* 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 `options.keepRemoved` is true).
* @returns An `{ old, new, mergeCount, pullCount, oldCount }` object.
* @returns An `{ old, new, mergeCount, pullCount, oldCount, reset, resetCount }` 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 `options.keepRemoved` is true and `target` didn't have the corresponding
* key).
* key) and `reset` is the keys that were reset due to not matching default values,
* and `resetCount` which is the number of keys reset.
*/
function mergeHashes(source, target, options = {}) {
function mergeHashes(source, target, options = {}, resetValues = {}) {
let old = {}
let reset = {}
let mergeCount = 0
let pullCount = 0
let oldCount = 0
let resetCount = 0

const keepRemoved = options.keepRemoved || false
const pluralSeparator = options.pluralSeparator || '_'
Expand All @@ -148,12 +151,25 @@ function mergeHashes(source, target, options = {}) {
old[key] = nested.old
}
} else if (target[key] !== undefined) {
if (typeof source[key] === 'string' || Array.isArray(source[key])) {
target[key] = source[key]
mergeCount += 1
} else {
if (typeof source[key] !== 'string' && !Array.isArray(source[key])) {
old[key] = source[key]
oldCount += 1
} else {
if (
(options.resetAndFlag &&
!isPlural(key) &&
typeof source[key] === 'string' &&
source[key] !== target[key]) ||
resetValues[key]
) {
old[key] = source[key]
oldCount += 1
reset[key] = true
resetCount += 1
} else {
target[key] = source[key]
mergeCount += 1
}
}
} else {
// support for plural in keys
Expand Down Expand Up @@ -183,7 +199,15 @@ function mergeHashes(source, target, options = {}) {
}
}

return { old, new: target, mergeCount, pullCount, oldCount }
return {
old,
new: target,
mergeCount,
pullCount,
oldCount,
reset,
resetCount,
}
}

/**
Expand All @@ -205,9 +229,16 @@ function transferValues(source, target) {
}
}

const pluralSuffixes = ['zero', 'one', 'two', 'few', 'many', 'other']

function isPlural(key) {
return pluralSuffixes.some((suffix) => key.endsWith(suffix))
}

function hasRelatedPluralKey(rawKey, source) {
const suffixes = ['zero', 'one', 'two', 'few', 'many', 'other']
return suffixes.some((suffix) => source[`${rawKey}${suffix}`] !== undefined)
return pluralSuffixes.some(
(suffix) => source[`${rawKey}${suffix}`] !== undefined
)
}

function getSingularForm(key, pluralSeparator) {
Expand All @@ -219,10 +250,8 @@ function getSingularForm(key, pluralSeparator) {
}

function getPluralSuffixPosition(key) {
const suffixes = ['zero', 'one', 'two', 'few', 'many', 'other']

for (let i = 0, len = suffixes.length; i < len; i++) {
if (key.endsWith(suffixes[i])) return i
for (let i = 0, len = pluralSuffixes.length; i < len; i++) {
if (key.endsWith(pluralSuffixes[i])) return i
}

return -1
Expand Down
36 changes: 34 additions & 2 deletions src/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default class i18nTransform extends Transform {
namespaceSeparator: ':',
pluralSeparator: '_',
output: 'locales/$LOCALE/$NAMESPACE.json',
resetDefaultValueLocale: null,
sort: false,
useKeysAsDefaultValue: false,
verbose: false,
Expand Down Expand Up @@ -125,8 +126,20 @@ export default class i18nTransform extends Transform {
}

_flush(done) {
for (const locale of this.options.locales) {
let maybeSortedLocales = this.options.locales
if (this.options.resetDefaultValueLocale) {
// ensure we process the reset locale first
maybeSortedLocales.sort((a) =>
a === this.options.resetDefaultValueLocale ? -1 : 1
)
}

// Tracks keys to reset by namespace
let resetValues = {}

for (const locale of maybeSortedLocales) {
const catalog = {}
const resetAndFlag = this.options.resetDefaultValueLocale === locale

let countWithPlurals = 0
let uniqueCount = this.entries.length
Expand Down Expand Up @@ -193,7 +206,23 @@ export default class i18nTransform extends Transform {
old: oldKeys,
mergeCount,
oldCount,
} = mergeHashes(existingCatalog, catalog[namespace], this.options)
reset: resetFlags,
resetCount,
} = mergeHashes(
existingCatalog,
catalog[namespace],
{
...this.options,
resetAndFlag,
},
resetValues[namespace]
)

// record values to be reset
// assumes that the 'default' namespace is processed first
if (resetAndFlag && !resetValues[namespace]) {
resetValues[namespace] = resetFlags
}

// restore old translations
const { old: oldCatalog, mergeCount: restoreCount } = mergeHashes(
Expand All @@ -218,6 +247,9 @@ export default class i18nTransform extends Transform {
} else {
console.log(`Removed keys: ${oldCount}`)
}
if (this.options.resetDefaultValueLocale) {
console.log(`Reset keys: ${resetCount}`)
}
console.log()
}

Expand Down
36 changes: 36 additions & 0 deletions test/helpers/mergeHashes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,40 @@ describe('mergeHashes helper function', () => {
assert.strictEqual(res.oldCount, 0)
done()
})

it('resets keys to the target value if they are flagged in the resetKeys object', (done) => {
const source = { key1: 'key1', key2: 'key2' }
const target = { key1: 'changedKey1', key2: 'changedKey2' }
const res = mergeHashes(source, target, {}, { key1: true })

assert.deepEqual(res.new, { key1: 'changedKey1', key2: 'key2' })
assert.deepEqual(res.old, { key1: 'key1' })
assert.strictEqual(res.resetCount, 1)
done()
})

it('ignores keys if they are plurals', (done) => {
const source = { key1_one: 'key1', key2: 'key2' }
const target = { key1_one: 'changedKey1', key2: 'changedKey2' }
const res = mergeHashes(source, target, {}, { key1: true })

assert.deepEqual(res.new, { key1_one: 'key1', key2: 'key2' })
assert.deepEqual(res.old, {})
assert.strictEqual(res.resetCount, 0)
done()
})

it('resets and flags keys if the resetAndFlag value is set', (done) => {
const source = { key1: 'key1', key2: 'key2' }
const target = { key1: 'changedKey1', key2: 'key2' }
const res = mergeHashes(source, target, {
resetAndFlag: true,
})

assert.deepEqual(res.new, { key1: 'changedKey1', key2: 'key2' })
assert.deepEqual(res.old, { key1: 'key1' })
assert.deepEqual(res.reset, { key1: true })
assert.strictEqual(res.resetCount, 1)
done()
})
})
3 changes: 3 additions & 0 deletions test/locales/ar/test_reset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"key": "ar_translation"
}
3 changes: 3 additions & 0 deletions test/locales/en/test_reset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"key": "en_translation"
}
3 changes: 3 additions & 0 deletions test/locales/fr/test_reset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"key": "defaultTranslation"
}
126 changes: 125 additions & 1 deletion test/parser.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { assert } from 'chai'
import { assert, expect } from 'chai'
import Vinyl from 'vinyl'
import fs from 'fs'
import i18nTransform from '../src/transform'
import path from 'path'
import sinon from 'sinon'

const enLibraryPath = path.normalize('en/translation.json')
const arLibraryPath = path.normalize('ar/translation.json')
Expand Down Expand Up @@ -661,6 +662,129 @@ describe('parser', () => {
})

describe('options', () => {
describe('resetDefaultValueLocale', () => {
it('will not reset a key if a default value has not changed in the locale', (done) => {
const i18nextParser = new i18nTransform({
resetDefaultValueLocale: 'fr',
output: 'test/locales/$LOCALE/$NAMESPACE.json',
locales: ['en', 'ar', 'fr'],
})

const fakeFile = new Vinyl({
contents: Buffer.from(
`t('test_reset:key', {
defaultValue: 'defaultTranslation',
})`
),
path: 'file.js',
})

let enResult, arResult
i18nextParser.on('data', (file) => {
if (file.relative.endsWith(path.normalize('en/test_reset.json'))) {
enResult = JSON.parse(file.contents)
} else if (
file.relative.endsWith(path.normalize('ar/test_reset.json'))
) {
arResult = JSON.parse(file.contents)
}
})
i18nextParser.once('end', () => {
assert.deepEqual(enResult, {
key: 'en_translation',
})
assert.deepEqual(arResult, {
key: 'ar_translation',
})
done()
})

i18nextParser.end(fakeFile)
})

it('will reset a key if a default value has changed in the locale', (done) => {
const i18nextParser = new i18nTransform({
resetDefaultValueLocale: 'fr',
output: 'test/locales/$LOCALE/$NAMESPACE.json',
locales: ['en', 'ar', 'fr'],
})

const newString = 'newTranslation'

const fakeFile = new Vinyl({
contents: Buffer.from(
`t('test_reset:key', {
defaultValue: '${newString}',
})`
),
path: 'file.js',
})

let enResult, arResult
i18nextParser.on('data', (file) => {
if (file.relative.endsWith(path.normalize('en/test_reset.json'))) {
enResult = JSON.parse(file.contents)
} else if (
file.relative.endsWith(path.normalize('ar/test_reset.json'))
) {
arResult = JSON.parse(file.contents)
}
})
i18nextParser.once('end', () => {
assert.deepEqual(enResult, {
key: newString,
})
assert.deepEqual(arResult, {
key: newString,
})
done()
})

i18nextParser.end(fakeFile)
})
})

describe('verbose', () => {
beforeEach(() => {
sinon.spy(console, 'log')
})

afterEach(() => {
console.log.restore();
})

describe('with defaultResetLocale', () => {
it('logs the number of values reset', (done) => {
const i18nextParser = new i18nTransform({
verbose: true,
resetDefaultValueLocale: 'fr',
output: 'test/locales/$LOCALE/$NAMESPACE.json',
locales: ['en', 'fr'],
})

const newString = 'newTranslation'

const fakeFile = new Vinyl({
contents: Buffer.from(
`t('test_reset:key', {
defaultValue: '${newString}',
})`
),
path: 'file.js',
})

i18nextParser.on('data', () => {})

i18nextParser.once('end', () => {
assert(console.log.calledWith('Reset keys: 1'))
done()
})

i18nextParser.end(fakeFile)
})
})
})

it('handles output with $LOCALE and $NAMESPACE var', (done) => {
let result
const i18nextParser = new i18nTransform({
Expand Down

0 comments on commit f836445

Please sign in to comment.